From a4b7ff59fd51a9a47280711fae46450da70dec9f Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Sat, 4 Jan 2025 17:46:15 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[PC-292]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_google_login.xml | 26 ++++ .../src/main/res/drawable/ic_kakao_login.xml | 10 ++ .../puzzle/auth/graph/login/LoginScreen.kt | 144 +++++++++++++++++- 3 files changed, 173 insertions(+), 7 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_google_login.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_kakao_login.xml diff --git a/core/designsystem/src/main/res/drawable/ic_google_login.xml b/core/designsystem/src/main/res/drawable/ic_google_login.xml new file mode 100644 index 00000000..110c82de --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_google_login.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_kakao_login.xml b/core/designsystem/src/main/res/drawable/ic_kakao_login.xml new file mode 100644 index 00000000..06413ea9 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_kakao_login.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt index faa84c97..017d233c 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt @@ -1,21 +1,42 @@ package com.puzzle.auth.graph.login import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.dp import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.kakao.sdk.user.UserApiClient import com.puzzle.auth.graph.login.contract.LoginIntent.Navigate import com.puzzle.auth.graph.login.contract.LoginState +import com.puzzle.designsystem.R +import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.navigation.AuthGraph import com.puzzle.navigation.AuthGraphDest @@ -48,10 +69,13 @@ fun LoginScreen( state: LoginState, loginKakao: () -> Unit, navigate: (NavigationEvent) -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() + .background(PieceTheme.colors.white) + .padding(horizontal = 20.dp) .clickable { navigate( NavigationEvent.NavigateTo( @@ -61,16 +85,122 @@ fun LoginScreen( ) }, ) { + PieceSubCloseTopBar( + title = "", + onCloseClick = { }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + ) + + Spacer(modifier = Modifier.height(20.dp)) + Text( - text = "카카오 로그인", - fontSize = 30.sp, - modifier = Modifier.clickable { loginKakao() }, + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) { + append("Piece") + } + + append("에서 마음이 통하는\n이상형을 만나보세요") + }, + style = PieceTheme.typography.headingLSB, + color = PieceTheme.colors.black, + modifier = Modifier.fillMaxWidth(), ) + Spacer(modifier = Modifier.height(12.dp)) + Text( - text = "AuthRoute", - fontSize = 30.sp, + text = "서로의 빈 곳을 채우며 맞물리는 퍼즐처럼.\n서로의 가치관과 마음이 연결되는 순간을 만들어갑니다.", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(70.dp)) + + Image( + painter = painterResource(R.drawable.ic_puzzle1), + contentDescription = null, + modifier = Modifier + .size(240.dp) + .align(Alignment.CenterHorizontally), ) + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = loginKakao, + enabled = true, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFFFFE812), + contentColor = PieceTheme.colors.white, + disabledContainerColor = PieceTheme.colors.light1, + disabledContentColor = PieceTheme.colors.white, + ), + modifier = Modifier + .height(52.dp) + .fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_kakao_login), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + + Text( + text = "카카오로 시작하기", + style = PieceTheme.typography.bodyMSB, + color = PieceTheme.colors.black, + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Button( + onClick = loginKakao, + enabled = true, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = PieceTheme.colors.white, + contentColor = PieceTheme.colors.white, + disabledContainerColor = PieceTheme.colors.light1, + disabledContentColor = PieceTheme.colors.white, + ), + modifier = Modifier + .height(52.dp) + .fillMaxWidth() + .border( + width = 1.dp, + color = PieceTheme.colors.light1, + shape = RoundedCornerShape(8.dp) + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + painter = painterResource(R.drawable.ic_google_login), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + + Text( + text = "구글로 시작하기", + style = PieceTheme.typography.bodyMSB, + color = PieceTheme.colors.black, + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) } } From a362e6528f7fe39e8d805d6c4dcf1538cf1ad002 Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Tue, 7 Jan 2025 21:45:39 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[PC-292]=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../build/logic/configure/AndroidComposes.kt | 1 + .../graph/verification/VerificationScreen.kt | 180 +++++++++++++++++- .../verification/VerificationViewModel.kt | 12 +- ...rficationState.kt => VerificationState.kt} | 2 +- gradle/libs.versions.toml | 3 + 5 files changed, 189 insertions(+), 9 deletions(-) rename feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/{VerficationState.kt => VerificationState.kt} (82%) diff --git a/build-logic/src/main/java/com/puzzle/build/logic/configure/AndroidComposes.kt b/build-logic/src/main/java/com/puzzle/build/logic/configure/AndroidComposes.kt index 722cd11a..8e2cb7c4 100644 --- a/build-logic/src/main/java/com/puzzle/build/logic/configure/AndroidComposes.kt +++ b/build-logic/src/main/java/com/puzzle/build/logic/configure/AndroidComposes.kt @@ -28,6 +28,7 @@ internal fun Project.configureAndroidCompose() { add("implementation", libs.findLibrary("androidx.compose.material3").get()) add("implementation", libs.findLibrary("androidx.compose.ui").get()) add("implementation", libs.findLibrary("androidx.compose.ui.tooling.preview").get()) + add("implementation", libs.findLibrary("androidx.compose.foundation").get()) add("debugImplementation", libs.findLibrary("androidx.compose.ui.tooling").get()) } } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index 2c365309..3d648c7c 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -1,10 +1,40 @@ package com.puzzle.auth.graph.verification +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel -import com.puzzle.auth.graph.verification.contract.VerficationState +import com.puzzle.auth.graph.verification.contract.VerificationState +import com.puzzle.designsystem.component.PieceSolidButton +import com.puzzle.designsystem.component.PieceSubCloseTopBar +import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.navigation.AuthGraph +import com.puzzle.navigation.AuthGraphDest +import com.puzzle.navigation.NavigationEvent @Composable internal fun VerificationRoute( @@ -14,12 +44,158 @@ internal fun VerificationRoute( VerificationScreen( state = state, + navigate = {}, ) } @Composable private fun VerificationScreen( - state: VerficationState, + state: VerificationState, + navigate: (NavigationEvent) -> Unit, + modifier: Modifier = Modifier, ) { + Column( + modifier = modifier + .fillMaxSize() + .background(PieceTheme.colors.white) + .padding(horizontal = 20.dp) + .clickable { + navigate( + NavigationEvent.NavigateTo( + route = AuthGraphDest.RegistrationRoute, + popUpTo = AuthGraph, + ) + ) + }, + ) { + PieceSubCloseTopBar( + title = "", + onCloseClick = { }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + ) + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) { + append("휴대폰 번호") + } + + append("로\n인증을 진행해 주세요") + }, + style = PieceTheme.typography.headingLSB, + color = PieceTheme.colors.black, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "신뢰도 높은 매칭과 안전한 커뮤니티를 위해 \n" + + "휴대폰 번호로 인증해 주세요.", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(70.dp)) + + Text( + text = "휴대폰 번호", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + var phoneNumber by rememberSaveable { mutableStateOf("") } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it }, + textStyle = PieceTheme.typography.bodyMM, + modifier = Modifier + .height(52.dp) + .clip(RoundedCornerShape(8.dp)) + .background(PieceTheme.colors.light3) + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ) + .weight(1f), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + PieceSolidButton( + label = "인증 번호 받기", + onClick = {}, + enabled = true, + ) + } + + if (true) { + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = "인증 번호", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BasicTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it }, + textStyle = PieceTheme.typography.bodyMM, + modifier = Modifier + .height(52.dp) + .clip(RoundedCornerShape(8.dp)) + .background(PieceTheme.colors.light3) + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ) + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "안전한 이용을 위해 타인과 절대 공유하지 마세요.", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + PieceSolidButton( + label = "인증 번호 받기", + onClick = {}, + enabled = true, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(10.dp)) + } +} + +@Preview +@Composable +fun PreviewVerificationScreen() { + PieceTheme { + VerificationScreen( + state = VerificationState(), + navigate = {}, + ) + } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt index 1490b861..cba3a0c4 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt @@ -4,21 +4,21 @@ import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory -import com.puzzle.auth.graph.verification.contract.VerficationState +import com.puzzle.auth.graph.verification.contract.VerificationState import com.puzzle.navigation.NavigationHelper import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject class VerificationViewModel @AssistedInject constructor( - @Assisted initialState: VerficationState, + @Assisted initialState: VerificationState, private val navigationHelper: NavigationHelper, -) : MavericksViewModel(initialState) { +) : MavericksViewModel(initialState) { @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(state: VerficationState): VerificationViewModel + interface Factory : AssistedViewModelFactory { + override fun create(state: VerificationState): VerificationViewModel } companion object : - MavericksViewModelFactory by hiltMavericksViewModelFactory() + MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerficationState.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt similarity index 82% rename from feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerficationState.kt rename to feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt index fe486ee9..3b46a13e 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerficationState.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt @@ -2,6 +2,6 @@ package com.puzzle.auth.graph.verification.contract import com.airbnb.mvrx.MavericksState -data class VerficationState( +data class VerificationState( val a: Boolean = false, ) : MavericksState \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5514cf41..cafe0638 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,8 @@ androidxSplashscreen = "1.0.1" androidxComposeBom = "2024.12.01" # https://developer.android.com/jetpack/androidx/releases/navigation androidxComposeNavigation = "2.8.4" +# https://developer.android.com/jetpack/androidx/releases/compose-foundation +androidxComposeFoundation = "1.7.6" ## Amplitude # https://amplitude.com/docs/sdks/analytics/android @@ -124,6 +126,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxComposeFoundation" } androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } From 2d69ca4e1e14a9a016417cff138d7e0623137220 Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Thu, 9 Jan 2025 22:39:16 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[PC-292]=20=EC=9D=B8=EC=A6=9D=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20ui=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graph/verification/VerificationScreen.kt | 158 ++++++++++++++++-- 1 file changed, 144 insertions(+), 14 deletions(-) diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index 3d648c7c..f19a1721 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -1,7 +1,9 @@ package com.puzzle.auth.graph.verification +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,8 +16,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField 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.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -35,6 +39,7 @@ import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.navigation.AuthGraph import com.puzzle.navigation.AuthGraphDest import com.puzzle.navigation.NavigationEvent +import kotlinx.coroutines.delay @Composable internal fun VerificationRoute( @@ -49,11 +54,28 @@ internal fun VerificationRoute( } @Composable -private fun VerificationScreen( +fun VerificationScreen( state: VerificationState, navigate: (NavigationEvent) -> Unit, modifier: Modifier = Modifier, ) { + // 전화번호, 인증번호는 회전 등에서 상태 유지를 위해 rememberSaveable 사용 + var phoneNumber by rememberSaveable { mutableStateOf("") } + var verificationNumber by rememberSaveable { mutableStateOf("") } + + // 타이머 상태를 별도 함수로 추출 + val timerState = rememberTimerState( + totalSeconds = 300, // 5분 + onTimerFinish = { + // 필요한 후처리가 있으면 여기서 처리 + } + ) + + // 인증번호 받기 버튼/재전송 버튼 텍스트 + // - 타이머가 한 번이라도 시작했으면 재전송 버튼으로, 아니면 받기 버튼으로 + // - 혹은 'isTimerRunning' 상태에 맞추어서만 바꿔도 됨 + val requestButtonLabel = if (timerState.hasStarted) "인증번호 재전송" else "인증번호 받기" + Column( modifier = modifier .fillMaxSize() @@ -70,7 +92,7 @@ private fun VerificationScreen( ) { PieceSubCloseTopBar( title = "", - onCloseClick = { }, + onCloseClick = { /* 뒤로가기 혹은 close 동작 */ }, modifier = Modifier .fillMaxWidth() .padding(vertical = 14.dp), @@ -83,7 +105,6 @@ private fun VerificationScreen( withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) { append("휴대폰 번호") } - append("로\n인증을 진행해 주세요") }, style = PieceTheme.typography.headingLSB, @@ -103,6 +124,9 @@ private fun VerificationScreen( Spacer(modifier = Modifier.height(70.dp)) + // ----------------------------- + // (1) 전화번호 입력 영역 + // ----------------------------- Text( text = "휴대폰 번호", style = PieceTheme.typography.bodySM, @@ -111,12 +135,11 @@ private fun VerificationScreen( Spacer(modifier = Modifier.height(8.dp)) - var phoneNumber by rememberSaveable { mutableStateOf("") } - Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { + // 기본 입력 필드 BasicTextField( value = phoneNumber, onValueChange = { phoneNumber = it }, @@ -134,14 +157,22 @@ private fun VerificationScreen( Spacer(modifier = Modifier.width(8.dp)) + // 인증번호 받기 / 재전송 버튼 PieceSolidButton( - label = "인증 번호 받기", - onClick = {}, - enabled = true, + label = requestButtonLabel, + onClick = { + // 인증번호 API 호출 후 타이머 시작 + timerState.resendCode() + }, + enabled = phoneNumber.isNotEmpty() ) } - if (true) { + // ----------------------------- + // (2) 인증번호 입력 & 타이머 노출 + // ----------------------------- + // 타이머가 0이 아니면 인증번호 입력 영역 노출 + if (timerState.remainingTime > 0) { Spacer(modifier = Modifier.height(32.dp)) Text( @@ -152,10 +183,27 @@ private fun VerificationScreen( Spacer(modifier = Modifier.height(8.dp)) + // 인증번호 입력 필드 + 남은 시간 BasicTextField( - value = phoneNumber, - onValueChange = { phoneNumber = it }, + value = verificationNumber, + onValueChange = { verificationNumber = it }, textStyle = PieceTheme.typography.bodyMM, + decorationBox = { innerTextField -> + // 인증번호 입력 내부에 남은 시간을 표시 + Box( + modifier = Modifier.fillMaxSize() + ) { + innerTextField() + + Text( + text = formatTime(timerState.remainingTime), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.primaryDefault, + modifier = Modifier + .align(Alignment.CenterEnd) + ) + } + }, modifier = Modifier .height(52.dp) .clip(RoundedCornerShape(8.dp)) @@ -178,10 +226,16 @@ private fun VerificationScreen( Spacer(modifier = Modifier.weight(1f)) + // ----------------------------- + // (3) 최종 인증 / 다음 버튼 + // ----------------------------- PieceSolidButton( - label = "인증 번호 받기", - onClick = {}, - enabled = true, + label = "다음", + onClick = { + // 실제 인증 로직 처리 + // 예: navigate(AuthGraphDest.SomeNextRoute) + }, + enabled = verificationNumber.isNotEmpty(), modifier = Modifier.fillMaxWidth() ) @@ -189,6 +243,82 @@ private fun VerificationScreen( } } +/** + * [totalSeconds]만큼 카운트다운을 진행하는 타이머 상태를 기억하는 Composable. + * 필요한 타이머 관련 로직을 캡슐화하여, UI 단에서는 상태만 받아서 사용. + */ +@Composable +fun rememberTimerState( + totalSeconds: Int, + onTimerFinish: () -> Unit = {}, +): TimerState { + // 남은 시간 + var remainingTime by rememberSaveable { mutableStateOf(totalSeconds) } + + // 타이머 동작 여부 + var isTimerRunning by rememberSaveable { mutableStateOf(false) } + + // 한 번이라도 타이머를 시작했는지 여부 + var hasStarted by rememberSaveable { mutableStateOf(false) } + + // 타이머가 동작 중이고, 남은 시간이 있을 때 1초마다 감소 + if (isTimerRunning && remainingTime > 0) { + LaunchedEffect(remainingTime) { + delay(1000L) + remainingTime-- + if (remainingTime <= 0) { + isTimerRunning = false + onTimerFinish() + } + } + } + + // 타이머 시작 함수 + fun startTimer() { + remainingTime = totalSeconds + isTimerRunning = true + hasStarted = true + } + + // 인증번호 재전송 함수 + fun resendCode() { + // 예: API 호출 등 인증번호 재발급 로직 + startTimer() + } + + return remember { + TimerState( + remainingTime = remainingTime, + isTimerRunning = isTimerRunning, + hasStarted = hasStarted, + startTimer = ::startTimer, + resendCode = ::resendCode, + ) + } +} + +/** + * 타이머 관련 상태값을 담고 있는 자료 구조. + * 상태는 람다로 보관해두고, 필요한 시점에만 읽기 위해 함수 형태로 둠. + */ +data class TimerState( + val remainingTime: Int, + val isTimerRunning: Boolean, + val hasStarted: Boolean, + val startTimer: () -> Unit, + val resendCode: () -> Unit, +) + +/** + * 초 단위 [seconds]를 "mm:ss" 형태 문자열로 변환하는 함수 + */ +@SuppressLint("DefaultLocale") +fun formatTime(seconds: Int): String { + val minutes = seconds / 60 + val secs = seconds % 60 + return String.format("%02d:%02d", minutes, secs) +} + @Preview @Composable fun PreviewVerificationScreen() { From eac5db670c056495a55306d03f1e05b0a431240e Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Fri, 10 Jan 2025 02:56:49 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[PC-292]=20=EB=B2=88=ED=98=B8=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20ui/ux=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../puzzle/auth/graph/login/LoginScreen.kt | 2 +- .../graph/verification/VerificationScreen.kt | 345 +++++++++--------- .../verification/VerificationViewModel.kt | 113 ++++++ .../contract/VerificationIntent.kt | 6 +- .../contract/VerificationSideEffect.kt | 6 +- .../contract/VerificationState.kt | 15 +- 6 files changed, 303 insertions(+), 184 deletions(-) diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt index 017d233c..891c158a 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginScreen.kt @@ -79,7 +79,7 @@ fun LoginScreen( .clickable { navigate( NavigationEvent.NavigateTo( - route = AuthGraphDest.RegistrationRoute, + route = AuthGraphDest.VerificationRoute, popUpTo = AuthGraph, ) ) diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index f19a1721..271ffc08 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -16,10 +16,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField 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.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -32,14 +30,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel +import com.puzzle.auth.graph.verification.contract.VerificationIntent +import com.puzzle.auth.graph.verification.contract.VerificationSideEffect import com.puzzle.auth.graph.verification.contract.VerificationState +import com.puzzle.auth.graph.verification.contract.VerificationState.VerificationCodeStatus import com.puzzle.designsystem.component.PieceSolidButton import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.navigation.AuthGraph import com.puzzle.navigation.AuthGraphDest import com.puzzle.navigation.NavigationEvent -import kotlinx.coroutines.delay @Composable internal fun VerificationRoute( @@ -49,33 +49,30 @@ internal fun VerificationRoute( VerificationScreen( state = state, - navigate = {}, + navigate = { + viewModel.onSideEffect(VerificationSideEffect.Navigate(it)) + }, + onRequestVerificationCodeClick = { + viewModel.onIntent(VerificationIntent.RequestVerificationCode) + }, + onVerifyClick = { code -> + viewModel.onIntent(VerificationIntent.VerifyCode(code)) + }, + onNextClick = { + viewModel.onIntent(VerificationIntent.CompleteVerification) + } ) } @Composable -fun VerificationScreen( +private fun VerificationScreen( state: VerificationState, + onRequestVerificationCodeClick: () -> Unit, + onVerifyClick: (String) -> Unit, + onNextClick: () -> Unit, navigate: (NavigationEvent) -> Unit, modifier: Modifier = Modifier, ) { - // 전화번호, 인증번호는 회전 등에서 상태 유지를 위해 rememberSaveable 사용 - var phoneNumber by rememberSaveable { mutableStateOf("") } - var verificationNumber by rememberSaveable { mutableStateOf("") } - - // 타이머 상태를 별도 함수로 추출 - val timerState = rememberTimerState( - totalSeconds = 300, // 5분 - onTimerFinish = { - // 필요한 후처리가 있으면 여기서 처리 - } - ) - - // 인증번호 받기 버튼/재전송 버튼 텍스트 - // - 타이머가 한 번이라도 시작했으면 재전송 버튼으로, 아니면 받기 버튼으로 - // - 혹은 'isTimerRunning' 상태에 맞추어서만 바꿔도 됨 - val requestButtonLabel = if (timerState.hasStarted) "인증번호 재전송" else "인증번호 받기" - Column( modifier = modifier .fillMaxSize() @@ -92,7 +89,9 @@ fun VerificationScreen( ) { PieceSubCloseTopBar( title = "", - onCloseClick = { /* 뒤로가기 혹은 close 동작 */ }, + onCloseClick = { + navigate(NavigationEvent.NavigateUp) + }, modifier = Modifier .fillMaxWidth() .padding(vertical = 14.dp), @@ -124,118 +123,29 @@ fun VerificationScreen( Spacer(modifier = Modifier.height(70.dp)) - // ----------------------------- - // (1) 전화번호 입력 영역 - // ----------------------------- - Text( - text = "휴대폰 번호", - style = PieceTheme.typography.bodySM, - color = PieceTheme.colors.dark3, + PhoneNumberBody( + hasStarted = state.hasStarted, + onRequestVerificationCodeClick = onRequestVerificationCodeClick ) - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - // 기본 입력 필드 - BasicTextField( - value = phoneNumber, - onValueChange = { phoneNumber = it }, - textStyle = PieceTheme.typography.bodyMM, - modifier = Modifier - .height(52.dp) - .clip(RoundedCornerShape(8.dp)) - .background(PieceTheme.colors.light3) - .padding( - horizontal = 16.dp, - vertical = 14.dp, - ) - .weight(1f), - ) - - Spacer(modifier = Modifier.width(8.dp)) - - // 인증번호 받기 / 재전송 버튼 - PieceSolidButton( - label = requestButtonLabel, - onClick = { - // 인증번호 API 호출 후 타이머 시작 - timerState.resendCode() - }, - enabled = phoneNumber.isNotEmpty() - ) - } - - // ----------------------------- - // (2) 인증번호 입력 & 타이머 노출 - // ----------------------------- - // 타이머가 0이 아니면 인증번호 입력 영역 노출 - if (timerState.remainingTime > 0) { + if (state.hasStarted) { Spacer(modifier = Modifier.height(32.dp)) - Text( - text = "인증 번호", - style = PieceTheme.typography.bodySM, - color = PieceTheme.colors.dark3, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 인증번호 입력 필드 + 남은 시간 - BasicTextField( - value = verificationNumber, - onValueChange = { verificationNumber = it }, - textStyle = PieceTheme.typography.bodyMM, - decorationBox = { innerTextField -> - // 인증번호 입력 내부에 남은 시간을 표시 - Box( - modifier = Modifier.fillMaxSize() - ) { - innerTextField() - - Text( - text = formatTime(timerState.remainingTime), - style = PieceTheme.typography.bodySM, - color = PieceTheme.colors.primaryDefault, - modifier = Modifier - .align(Alignment.CenterEnd) - ) - } - }, - modifier = Modifier - .height(52.dp) - .clip(RoundedCornerShape(8.dp)) - .background(PieceTheme.colors.light3) - .padding( - horizontal = 16.dp, - vertical = 14.dp, - ) - .fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "안전한 이용을 위해 타인과 절대 공유하지 마세요.", - style = PieceTheme.typography.bodySM, - color = PieceTheme.colors.dark3, + VerificationCodeBody( + remainingTimeInSec = state.remainingTimeInSec, + verificationCodeStatus = state.verificationCodeStatus, + onVerifyClick = onVerifyClick, ) } Spacer(modifier = Modifier.weight(1f)) - // ----------------------------- - // (3) 최종 인증 / 다음 버튼 - // ----------------------------- PieceSolidButton( label = "다음", onClick = { - // 실제 인증 로직 처리 - // 예: navigate(AuthGraphDest.SomeNextRoute) + onNextClick() }, - enabled = verificationNumber.isNotEmpty(), + enabled = state.isVerified, modifier = Modifier.fillMaxWidth() ) @@ -243,71 +153,140 @@ fun VerificationScreen( } } -/** - * [totalSeconds]만큼 카운트다운을 진행하는 타이머 상태를 기억하는 Composable. - * 필요한 타이머 관련 로직을 캡슐화하여, UI 단에서는 상태만 받아서 사용. - */ @Composable -fun rememberTimerState( - totalSeconds: Int, - onTimerFinish: () -> Unit = {}, -): TimerState { - // 남은 시간 - var remainingTime by rememberSaveable { mutableStateOf(totalSeconds) } - - // 타이머 동작 여부 - var isTimerRunning by rememberSaveable { mutableStateOf(false) } - - // 한 번이라도 타이머를 시작했는지 여부 - var hasStarted by rememberSaveable { mutableStateOf(false) } - - // 타이머가 동작 중이고, 남은 시간이 있을 때 1초마다 감소 - if (isTimerRunning && remainingTime > 0) { - LaunchedEffect(remainingTime) { - delay(1000L) - remainingTime-- - if (remainingTime <= 0) { - isTimerRunning = false - onTimerFinish() - } +private fun VerificationCodeBody( + remainingTimeInSec: Int, + verificationCodeStatus: VerificationCodeStatus, + onVerifyClick: (String) -> Unit, +) { + var verificationCode by rememberSaveable { mutableStateOf("") } + + val (verificationCodeStatusMessage, verificationCodeStatusColor) = + when (verificationCodeStatus) { + VerificationCodeStatus.DO_NOT_SHARE -> + "어떤 경우에도 타인에게 공유하지 마세요" to PieceTheme.colors.dark3 + + VerificationCodeStatus.VERIFIED -> + "전화번호 인증을 완료했어요" to PieceTheme.colors.primaryDefault + + VerificationCodeStatus.INVALID -> + "올바른 인증번호가 아니에요" to PieceTheme.colors.subDefault + + VerificationCodeStatus.TIME_EXPIRED -> + "유효시간이 지났어요! ‘인증번호 재전송’을 눌러주세요" to PieceTheme.colors.subDefault } - } - // 타이머 시작 함수 - fun startTimer() { - remainingTime = totalSeconds - isTimerRunning = true - hasStarted = true - } + val isVerifyButtonEnabled = + verificationCodeStatus == VerificationCodeStatus.DO_NOT_SHARE || + verificationCodeStatus == VerificationCodeStatus.INVALID - // 인증번호 재전송 함수 - fun resendCode() { - // 예: API 호출 등 인증번호 재발급 로직 - startTimer() - } + Text( + text = "인증 번호", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + ) + + Spacer(modifier = Modifier.height(8.dp)) - return remember { - TimerState( - remainingTime = remainingTime, - isTimerRunning = isTimerRunning, - hasStarted = hasStarted, - startTimer = ::startTimer, - resendCode = ::resendCode, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = verificationCode, + onValueChange = { verificationCode = it }, + textStyle = PieceTheme.typography.bodyMM, + decorationBox = { innerTextField -> + Box { + innerTextField() + + Text( + text = formatTime(remainingTimeInSec), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.primaryDefault, + modifier = Modifier + .align(Alignment.CenterEnd) + ) + } + }, + modifier = Modifier + .height(52.dp) + .clip(RoundedCornerShape(8.dp)) + .background(PieceTheme.colors.light3) + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ) + .weight(1f), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + PieceSolidButton( + label = "확인", + onClick = { + onVerifyClick(verificationCode) + }, + enabled = isVerifyButtonEnabled, ) } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = verificationCodeStatusMessage, + style = PieceTheme.typography.bodySM, + color = verificationCodeStatusColor, + ) } -/** - * 타이머 관련 상태값을 담고 있는 자료 구조. - * 상태는 람다로 보관해두고, 필요한 시점에만 읽기 위해 함수 형태로 둠. - */ -data class TimerState( - val remainingTime: Int, - val isTimerRunning: Boolean, - val hasStarted: Boolean, - val startTimer: () -> Unit, - val resendCode: () -> Unit, -) +@Composable +private fun PhoneNumberBody( + hasStarted: Boolean, + onRequestVerificationCodeClick: () -> Unit +) { + var phoneNumber by rememberSaveable { mutableStateOf("") } + + val requestButtonLabel = if (hasStarted) "인증번호 재전송" else "인증번호 받기" + + Text( + text = "휴대폰 번호", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark3, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it }, + textStyle = PieceTheme.typography.bodyMM, + modifier = Modifier + .height(52.dp) + .clip(RoundedCornerShape(8.dp)) + .background(PieceTheme.colors.light3) + .padding( + horizontal = 16.dp, + vertical = 14.dp, + ) + .weight(1f), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + PieceSolidButton( + label = requestButtonLabel, + onClick = { + onRequestVerificationCodeClick() + }, + enabled = phoneNumber.isNotEmpty() + ) + } +} /** * 초 단위 [seconds]를 "mm:ss" 형태 문자열로 변환하는 함수 @@ -324,8 +303,16 @@ fun formatTime(seconds: Int): String { fun PreviewVerificationScreen() { PieceTheme { VerificationScreen( - state = VerificationState(), + state = VerificationState( + hasStarted = true, + remainingTimeInSec = 299, + isVerified = true, + verificationCodeStatus = VerificationState.VerificationCodeStatus.DO_NOT_SHARE, + ), navigate = {}, + onRequestVerificationCodeClick = {}, + onVerifyClick = {}, + onNextClick = {}, ) } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt index cba3a0c4..59552e0b 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt @@ -4,11 +4,24 @@ import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.puzzle.auth.graph.verification.contract.VerificationIntent +import com.puzzle.auth.graph.verification.contract.VerificationSideEffect import com.puzzle.auth.graph.verification.contract.VerificationState +import com.puzzle.navigation.AuthGraph +import com.puzzle.navigation.AuthGraphDest +import com.puzzle.navigation.NavigationEvent import com.puzzle.navigation.NavigationHelper import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch class VerificationViewModel @AssistedInject constructor( @Assisted initialState: VerificationState, @@ -19,6 +32,106 @@ class VerificationViewModel @AssistedInject constructor( override fun create(state: VerificationState): VerificationViewModel } + private val intents = Channel(BUFFERED) + private val sideEffects = Channel(BUFFERED) + private var timerJob: Job? = null + + init { + intents.receiveAsFlow() + .onEach(::processIntent) + .launchIn(viewModelScope) + + sideEffects.receiveAsFlow() + .onEach(::handleSideEffect) + .launchIn(viewModelScope) + } + + internal fun onIntent(intent: VerificationIntent) = viewModelScope.launch { + intents.send(intent) + } + + internal fun onSideEffect(sideEffect: VerificationSideEffect) = viewModelScope.launch { + sideEffects.send(sideEffect) + } + + private fun processIntent(intent: VerificationIntent) { + when (intent) { + VerificationIntent.RequestVerificationCode -> handleRequestVerificationCode() + is VerificationIntent.VerifyCode -> handleVerificateCode(intent.code) + VerificationIntent.CompleteVerification -> handleCompleteVerification() + } + } + + private fun handleSideEffect(sideEffect: VerificationSideEffect) { + when (sideEffect) { + is VerificationSideEffect.Navigate -> navigationHelper.navigate(sideEffect.navigationEvent) + } + } + + private fun handleCompleteVerification() { + navigationHelper.navigate( + NavigationEvent.NavigateTo( + route = AuthGraphDest.RegistrationRoute, + popUpTo = AuthGraph, + ) + ) + } + + private fun handleRequestVerificationCode() { + startCodeExpiryTimer(5) + } + + private fun handleVerificateCode(code: String) { + // TODO : code 검증 api, 결과에 따라 분기 처리 + val result = true + + setState { + copy( + isVerified = result, + remainingTimeInSec = 0, + verificationCodeStatus = if (result) { + timerJob?.cancel() + VerificationState.VerificationCodeStatus.VERIFIED + } else { + VerificationState.VerificationCodeStatus.INVALID + }, + ) + } + } + + private fun startCodeExpiryTimer(durationInSec: Int = 300) { + timerJob?.cancel() + + setState { + copy( + hasStarted = true, + remainingTimeInSec = durationInSec, + verificationCodeStatus = VerificationState.VerificationCodeStatus.DO_NOT_SHARE, + ) + } + + timerJob = viewModelScope.launch { + while (true) { + delay(1000L) + withState { currentState -> + if (currentState.remainingTimeInSec <= 0) { + setState { + copy( + remainingTimeInSec = 0, + verificationCodeStatus = VerificationState.VerificationCodeStatus.TIME_EXPIRED, + ) + } + return@withState + } + + setState { + copy(remainingTimeInSec = currentState.remainingTimeInSec - 1) + } + } + } + } + } + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt index fbc0a7cc..cbbbe9d0 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt @@ -1,3 +1,7 @@ package com.puzzle.auth.graph.verification.contract -sealed class VerificationIntent \ No newline at end of file +sealed class VerificationIntent { + data object RequestVerificationCode : VerificationIntent() + data class VerifyCode(val code: String) : VerificationIntent() + data object CompleteVerification : VerificationIntent() +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationSideEffect.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationSideEffect.kt index 226c5445..e7292efb 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationSideEffect.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationSideEffect.kt @@ -1,3 +1,7 @@ package com.puzzle.auth.graph.verification.contract -sealed class VerificationSideEffect \ No newline at end of file +import com.puzzle.navigation.NavigationEvent + +sealed class VerificationSideEffect { + data class Navigate(val navigationEvent: NavigationEvent) : VerificationSideEffect() +} diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt index 3b46a13e..1d98c196 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt @@ -3,5 +3,16 @@ package com.puzzle.auth.graph.verification.contract import com.airbnb.mvrx.MavericksState data class VerificationState( - val a: Boolean = false, -) : MavericksState \ No newline at end of file + val hasStarted: Boolean = false, + val remainingTimeInSec: Int = 0, + val verificationCodeStatus: VerificationCodeStatus = VerificationCodeStatus.DO_NOT_SHARE, + val isVerified: Boolean = false, +) : MavericksState { + + enum class VerificationCodeStatus { + DO_NOT_SHARE, // "어떤 경우에도 타인에게 공유하지 마세요" + VERIFIED, // "전화번호 인증을 완료했어요" + INVALID, // "올바른 인증번호가 아니에요" + TIME_EXPIRED, // "유효시간이 지났어요! ‘인증번호 재전송’을 눌러주세요" + } +} \ No newline at end of file From 589d03767ba71e5d8e4a9df51fad3e34b7891408 Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Sat, 11 Jan 2025 15:07:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[Fix]=20string=20resource=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/puzzle/data/di/DataModule.kt | 8 ++++ .../VerificationCodeRepositoryImpl.kt | 13 +++++ .../src/main/res/values/strings.xml | 13 +++++ .../repository/VerificationCodeRepository.kt | 6 +++ .../graph/verification/VerificationScreen.kt | 48 ++++++++++--------- .../verification/VerificationViewModel.kt | 48 +++++++++++-------- .../contract/VerificationIntent.kt | 6 +-- 7 files changed, 95 insertions(+), 47 deletions(-) create mode 100644 core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt create mode 100644 core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt diff --git a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt index bba9932e..202a7abf 100644 --- a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt +++ b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt @@ -2,8 +2,10 @@ package com.puzzle.data.di import com.puzzle.data.repository.AuthRepositoryImpl import com.puzzle.data.repository.TermsRepositoryImpl +import com.puzzle.data.repository.VerificationCodeRepositoryImpl import com.puzzle.domain.repository.AuthRepository import com.puzzle.domain.repository.TermsRepository +import com.puzzle.domain.repository.VerificationCodeRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -26,4 +28,10 @@ abstract class DataModule { abstract fun bindsTermsRepository( termsRepositoryImpl: TermsRepositoryImpl, ): TermsRepository + + @Binds + @Singleton + abstract fun bindsTermsRepository( + verificationCodeRepositoryImpl: VerificationCodeRepositoryImpl, + ): VerificationCodeRepository } diff --git a/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt new file mode 100644 index 00000000..f24180b7 --- /dev/null +++ b/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt @@ -0,0 +1,13 @@ +package com.puzzle.data.repository + +import com.puzzle.domain.repository.VerificationCodeRepository + +class VerificationCodeRepositoryImpl : VerificationCodeRepository { + override suspend fun requestVerificationCode(phoneNumber: String) { + + } + + override suspend fun verify(code: String): Boolean { + return true + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index b350db63..7429112c 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -11,6 +11,19 @@ 나와 같은 가치관 매칭 수락하기 + + 신뢰도 높은 매칭과 안전한 커뮤니티를 위해\n휴대폰 번호로 인증해 주세요. + 확인 + 다음 + 어떤 경우에도 타인에게 공유하지 마세요 + 전화번호 인증을 완료했어요 + 올바른 인증번호가 아니에요 + 유효시간이 지났어요! ‘인증번호 재전송’을 눌러주세요 + 인증번호 + 인증번호 재전송 + 인증번호 받기 + 휴대폰 번호 + 가치관 Talk diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt new file mode 100644 index 00000000..f0204f6a --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt @@ -0,0 +1,6 @@ +package com.puzzle.domain.repository + +interface VerificationCodeRepository { + suspend fun requestVerificationCode(phoneNumber: String) + suspend fun verify(code: String): Boolean +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index 271ffc08..9c04b8fa 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -34,6 +35,7 @@ import com.puzzle.auth.graph.verification.contract.VerificationIntent import com.puzzle.auth.graph.verification.contract.VerificationSideEffect import com.puzzle.auth.graph.verification.contract.VerificationState import com.puzzle.auth.graph.verification.contract.VerificationState.VerificationCodeStatus +import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceSolidButton import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme @@ -52,14 +54,14 @@ internal fun VerificationRoute( navigate = { viewModel.onSideEffect(VerificationSideEffect.Navigate(it)) }, - onRequestVerificationCodeClick = { - viewModel.onIntent(VerificationIntent.RequestVerificationCode) + onRequestVerificationCodeClick = { phoneNumber -> + viewModel.onIntent(VerificationIntent.OnRequestVerificationCodeClick(phoneNumber)) }, onVerifyClick = { code -> - viewModel.onIntent(VerificationIntent.VerifyCode(code)) + viewModel.onIntent(VerificationIntent.OnVerifyClick(code)) }, onNextClick = { - viewModel.onIntent(VerificationIntent.CompleteVerification) + viewModel.onIntent(VerificationIntent.OnNextClick) } ) } @@ -67,7 +69,7 @@ internal fun VerificationRoute( @Composable private fun VerificationScreen( state: VerificationState, - onRequestVerificationCodeClick: () -> Unit, + onRequestVerificationCodeClick: (String) -> Unit, onVerifyClick: (String) -> Unit, onNextClick: () -> Unit, navigate: (NavigationEvent) -> Unit, @@ -114,8 +116,7 @@ private fun VerificationScreen( Spacer(modifier = Modifier.height(12.dp)) Text( - text = "신뢰도 높은 매칭과 안전한 커뮤니티를 위해 \n" + - "휴대폰 번호로 인증해 주세요.", + text = stringResource(R.string.verification_subtitle), style = PieceTheme.typography.bodySM, color = PieceTheme.colors.dark3, modifier = Modifier.fillMaxWidth(), @@ -125,7 +126,7 @@ private fun VerificationScreen( PhoneNumberBody( hasStarted = state.hasStarted, - onRequestVerificationCodeClick = onRequestVerificationCodeClick + onRequestVerificationCodeClick = onRequestVerificationCodeClick, ) if (state.hasStarted) { @@ -141,12 +142,12 @@ private fun VerificationScreen( Spacer(modifier = Modifier.weight(1f)) PieceSolidButton( - label = "다음", + label = stringResource(R.string.verification_submit), onClick = { onNextClick() }, enabled = state.isVerified, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(10.dp)) @@ -164,16 +165,16 @@ private fun VerificationCodeBody( val (verificationCodeStatusMessage, verificationCodeStatusColor) = when (verificationCodeStatus) { VerificationCodeStatus.DO_NOT_SHARE -> - "어떤 경우에도 타인에게 공유하지 마세요" to PieceTheme.colors.dark3 + stringResource(R.string.verification_do_not_share) to PieceTheme.colors.dark3 VerificationCodeStatus.VERIFIED -> - "전화번호 인증을 완료했어요" to PieceTheme.colors.primaryDefault + stringResource(R.string.verification_verified) to PieceTheme.colors.primaryDefault VerificationCodeStatus.INVALID -> - "올바른 인증번호가 아니에요" to PieceTheme.colors.subDefault + stringResource(R.string.verification_invalid) to PieceTheme.colors.subDefault VerificationCodeStatus.TIME_EXPIRED -> - "유효시간이 지났어요! ‘인증번호 재전송’을 눌러주세요" to PieceTheme.colors.subDefault + stringResource(R.string.verification_time_expired) to PieceTheme.colors.subDefault } val isVerifyButtonEnabled = @@ -181,7 +182,7 @@ private fun VerificationCodeBody( verificationCodeStatus == VerificationCodeStatus.INVALID Text( - text = "인증 번호", + text = stringResource(R.string.verification_verifiaction_code), style = PieceTheme.typography.bodySM, color = PieceTheme.colors.dark3, ) @@ -205,7 +206,7 @@ private fun VerificationCodeBody( style = PieceTheme.typography.bodySM, color = PieceTheme.colors.primaryDefault, modifier = Modifier - .align(Alignment.CenterEnd) + .align(Alignment.CenterEnd), ) } }, @@ -223,7 +224,7 @@ private fun VerificationCodeBody( Spacer(modifier = Modifier.width(8.dp)) PieceSolidButton( - label = "확인", + label = stringResource(R.string.verification_submit), onClick = { onVerifyClick(verificationCode) }, @@ -243,14 +244,15 @@ private fun VerificationCodeBody( @Composable private fun PhoneNumberBody( hasStarted: Boolean, - onRequestVerificationCodeClick: () -> Unit + onRequestVerificationCodeClick: (String) -> Unit ) { var phoneNumber by rememberSaveable { mutableStateOf("") } - val requestButtonLabel = if (hasStarted) "인증번호 재전송" else "인증번호 받기" + val requestButtonLabel = + if (hasStarted) stringResource(R.string.verification_resend) else stringResource(R.string.verification_request) Text( - text = "휴대폰 번호", + text = stringResource(R.string.verification_phone_number), style = PieceTheme.typography.bodySM, color = PieceTheme.colors.dark3, ) @@ -281,9 +283,9 @@ private fun PhoneNumberBody( PieceSolidButton( label = requestButtonLabel, onClick = { - onRequestVerificationCodeClick() + onRequestVerificationCodeClick(phoneNumber) }, - enabled = phoneNumber.isNotEmpty() + enabled = phoneNumber.isNotEmpty(), ) } } @@ -307,7 +309,7 @@ fun PreviewVerificationScreen() { hasStarted = true, remainingTimeInSec = 299, isVerified = true, - verificationCodeStatus = VerificationState.VerificationCodeStatus.DO_NOT_SHARE, + verificationCodeStatus = VerificationCodeStatus.DO_NOT_SHARE, ), navigate = {}, onRequestVerificationCodeClick = {}, diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt index 59552e0b..9b1ce75b 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt @@ -7,6 +7,7 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.puzzle.auth.graph.verification.contract.VerificationIntent import com.puzzle.auth.graph.verification.contract.VerificationSideEffect import com.puzzle.auth.graph.verification.contract.VerificationState +import com.puzzle.domain.repository.VerificationCodeRepository import com.puzzle.navigation.AuthGraph import com.puzzle.navigation.AuthGraphDest import com.puzzle.navigation.NavigationEvent @@ -26,6 +27,7 @@ import kotlinx.coroutines.launch class VerificationViewModel @AssistedInject constructor( @Assisted initialState: VerificationState, private val navigationHelper: NavigationHelper, + private val verificationCodeRepository: VerificationCodeRepository, ) : MavericksViewModel(initialState) { @AssistedFactory interface Factory : AssistedViewModelFactory { @@ -56,9 +58,9 @@ class VerificationViewModel @AssistedInject constructor( private fun processIntent(intent: VerificationIntent) { when (intent) { - VerificationIntent.RequestVerificationCode -> handleRequestVerificationCode() - is VerificationIntent.VerifyCode -> handleVerificateCode(intent.code) - VerificationIntent.CompleteVerification -> handleCompleteVerification() + is VerificationIntent.OnRequestVerificationCodeClick -> requestVerificationCode(intent.phoneNumber) + is VerificationIntent.OnVerifyClick -> verify(intent.code) + VerificationIntent.OnNextClick -> moveToNextPage() } } @@ -68,7 +70,7 @@ class VerificationViewModel @AssistedInject constructor( } } - private fun handleCompleteVerification() { + private fun moveToNextPage() { navigationHelper.navigate( NavigationEvent.NavigateTo( route = AuthGraphDest.RegistrationRoute, @@ -77,25 +79,29 @@ class VerificationViewModel @AssistedInject constructor( ) } - private fun handleRequestVerificationCode() { - startCodeExpiryTimer(5) - } + private fun requestVerificationCode(phoneNumber: String) { + viewModelScope.launch { + verificationCodeRepository.requestVerificationCode(phoneNumber) - private fun handleVerificateCode(code: String) { - // TODO : code 검증 api, 결과에 따라 분기 처리 - val result = true + startCodeExpiryTimer(5) + } + } - setState { - copy( - isVerified = result, - remainingTimeInSec = 0, - verificationCodeStatus = if (result) { - timerJob?.cancel() - VerificationState.VerificationCodeStatus.VERIFIED - } else { - VerificationState.VerificationCodeStatus.INVALID - }, - ) + private fun verify(code: String) { + viewModelScope.launch { + val result = verificationCodeRepository.verify(code) + setState { + copy( + isVerified = result, + remainingTimeInSec = 0, + verificationCodeStatus = if (result) { + timerJob?.cancel() + VerificationState.VerificationCodeStatus.VERIFIED + } else { + VerificationState.VerificationCodeStatus.INVALID + }, + ) + } } } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt index cbbbe9d0..2eb1ac82 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationIntent.kt @@ -1,7 +1,7 @@ package com.puzzle.auth.graph.verification.contract sealed class VerificationIntent { - data object RequestVerificationCode : VerificationIntent() - data class VerifyCode(val code: String) : VerificationIntent() - data object CompleteVerification : VerificationIntent() + data class OnRequestVerificationCodeClick(val phoneNumber: String) : VerificationIntent() + data class OnVerifyClick(val code: String) : VerificationIntent() + data object OnNextClick : VerificationIntent() } \ No newline at end of file From da6b76c0f47712512e381d1f66b973b6371e27a1 Mon Sep 17 00:00:00 2001 From: sksowk156 Date: Sat, 11 Jan 2025 15:46:13 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[PC-292]=20=ED=95=B8=EB=93=9C=ED=8F=B0=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/puzzle/data/di/DataModule.kt | 2 +- .../VerificationCodeRepositoryImpl.kt | 7 ++++--- .../com/puzzle/designsystem/component/Button.kt | 17 +++++++++++++---- .../src/main/res/values/strings.xml | 3 ++- .../repository/VerificationCodeRepository.kt | 2 +- .../graph/verification/VerificationScreen.kt | 14 +++++++++++++- .../graph/verification/VerificationViewModel.kt | 10 +++++++++- .../verification/contract/VerificationState.kt | 1 + 8 files changed, 44 insertions(+), 12 deletions(-) diff --git a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt index 202a7abf..d98c3a54 100644 --- a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt +++ b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt @@ -31,7 +31,7 @@ abstract class DataModule { @Binds @Singleton - abstract fun bindsTermsRepository( + abstract fun bindsVerificationCodeRepository( verificationCodeRepositoryImpl: VerificationCodeRepositoryImpl, ): VerificationCodeRepository } diff --git a/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt index f24180b7..d47e033c 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/VerificationCodeRepositoryImpl.kt @@ -1,10 +1,11 @@ package com.puzzle.data.repository import com.puzzle.domain.repository.VerificationCodeRepository +import javax.inject.Inject -class VerificationCodeRepositoryImpl : VerificationCodeRepository { - override suspend fun requestVerificationCode(phoneNumber: String) { - +class VerificationCodeRepositoryImpl @Inject constructor() : VerificationCodeRepository { + override suspend fun requestVerificationCode(phoneNumber: String): Boolean { + return true } override suspend fun verify(code: String): Boolean { diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt index 18eb1887..4e9e8d7b 100644 --- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -40,7 +41,9 @@ fun PieceSolidButton( disabledContainerColor = PieceTheme.colors.light1, disabledContentColor = PieceTheme.colors.white, ), - modifier = modifier.height(52.dp), + modifier = modifier + .height(52.dp) + .widthIn(min = 100.dp), ) { Text( text = label, @@ -67,7 +70,9 @@ fun PieceOutlinedButton( disabledContainerColor = PieceTheme.colors.light1, disabledContentColor = PieceTheme.colors.primaryDefault, ), - modifier = modifier.height(52.dp), + modifier = modifier + .height(52.dp) + .widthIn(min = 100.dp), ) { Text( text = label, @@ -93,7 +98,9 @@ fun PieceSubButton( disabledContainerColor = PieceTheme.colors.light3, disabledContentColor = PieceTheme.colors.dark2, ), - modifier = modifier.height(52.dp), + modifier = modifier + .height(52.dp) + .widthIn(min = 100.dp), ) { Text( text = label, @@ -120,7 +127,9 @@ fun PieceIconButton( disabledContainerColor = PieceTheme.colors.light1, disabledContentColor = PieceTheme.colors.white, ), - modifier = modifier.height(52.dp), + modifier = modifier + .height(52.dp) + .widthIn(min = 100.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 7429112c..6e915aee 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -17,12 +17,13 @@ 다음 어떤 경우에도 타인에게 공유하지 마세요 전화번호 인증을 완료했어요 - 올바른 인증번호가 아니에요 + 올바른 인증번호가 아니에요 유효시간이 지났어요! ‘인증번호 재전송’을 눌러주세요 인증번호 인증번호 재전송 인증번호 받기 휴대폰 번호 + 올바른 전화번호가 아니에요 diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt index f0204f6a..5687e017 100644 --- a/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt +++ b/core/domain/src/main/java/com/puzzle/domain/repository/VerificationCodeRepository.kt @@ -1,6 +1,6 @@ package com.puzzle.domain.repository interface VerificationCodeRepository { - suspend fun requestVerificationCode(phoneNumber: String) + suspend fun requestVerificationCode(phoneNumber: String): Boolean suspend fun verify(code: String): Boolean } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index 9c04b8fa..1932ea94 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -125,6 +125,7 @@ private fun VerificationScreen( Spacer(modifier = Modifier.height(70.dp)) PhoneNumberBody( + isValidPhoneNumber = state.isValidPhoneNumber, hasStarted = state.hasStarted, onRequestVerificationCodeClick = onRequestVerificationCodeClick, ) @@ -171,7 +172,7 @@ private fun VerificationCodeBody( stringResource(R.string.verification_verified) to PieceTheme.colors.primaryDefault VerificationCodeStatus.INVALID -> - stringResource(R.string.verification_invalid) to PieceTheme.colors.subDefault + stringResource(R.string.verification_invalid_code) to PieceTheme.colors.subDefault VerificationCodeStatus.TIME_EXPIRED -> stringResource(R.string.verification_time_expired) to PieceTheme.colors.subDefault @@ -244,6 +245,7 @@ private fun VerificationCodeBody( @Composable private fun PhoneNumberBody( hasStarted: Boolean, + isValidPhoneNumber: Boolean, onRequestVerificationCodeClick: (String) -> Unit ) { var phoneNumber by rememberSaveable { mutableStateOf("") } @@ -288,6 +290,16 @@ private fun PhoneNumberBody( enabled = phoneNumber.isNotEmpty(), ) } + + if (!isValidPhoneNumber) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.verification_invalid_phone_number), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.subDefault, + ) + } } /** diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt index 9b1ce75b..fe2eb499 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationViewModel.kt @@ -81,7 +81,15 @@ class VerificationViewModel @AssistedInject constructor( private fun requestVerificationCode(phoneNumber: String) { viewModelScope.launch { - verificationCodeRepository.requestVerificationCode(phoneNumber) + val result = verificationCodeRepository.requestVerificationCode(phoneNumber) + + setState { + copy( + isValidPhoneNumber = result + ) + } + + if (!result) return@launch startCodeExpiryTimer(5) } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt index 1d98c196..437272bf 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/contract/VerificationState.kt @@ -3,6 +3,7 @@ package com.puzzle.auth.graph.verification.contract import com.airbnb.mvrx.MavericksState data class VerificationState( + val isValidPhoneNumber: Boolean = true, val hasStarted: Boolean = false, val remainingTimeInSec: Int = 0, val verificationCodeStatus: VerificationCodeStatus = VerificationCodeStatus.DO_NOT_SHARE,