From fcde1167dd46e6b54680eef9526f33371bcade0e Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 17:58:23 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20PrezelTopAppBar=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20windowInsets=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/team/prezel/core/designsystem/component/TopAppBar.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt index e8309fd8..5163b057 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt @@ -1,6 +1,7 @@ package com.team.prezel.core.designsystem.component import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -27,6 +28,7 @@ fun PrezelTopAppBar( leadingIcon: @Composable () -> Unit = {}, trailingIcons: @Composable RowScope.() -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null, + windowInsets: WindowInsets = WindowInsets(), ) { TopAppBar( title = { @@ -38,6 +40,7 @@ fun PrezelTopAppBar( actions = trailingIcons, colors = prezelTopAppBarColors(), scrollBehavior = scrollBehavior, + windowInsets = windowInsets, modifier = modifier.testTag("PrezelTopAppBar"), ) } From e5df6f119ce54099183d96dafb9f640e05d1baad Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:35:10 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refactor:=20PrezelCheckbox=EB=A5=BC=20Pre?= =?UTF-8?q?zelTouchArea=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/PrezelCheckbox.kt | 72 +++++++++---------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt index a9b31962..5c0bd6bd 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt @@ -1,22 +1,22 @@ package com.team.prezel.core.designsystem.component -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.size -import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Icon import androidx.compose.runtime.Composable 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.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.team.prezel.core.designsystem.R +import com.team.prezel.core.designsystem.component.base.PrezelTouchArea import com.team.prezel.core.designsystem.icon.PrezelIcons import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.preview.PreviewSection @@ -34,48 +34,44 @@ fun PrezelCheckbox( checked: Boolean, modifier: Modifier = Modifier, size: CheckboxSize = CheckboxSize.REGULAR, + extraTouchPadding: PaddingValues = PaddingValues(all = PrezelTheme.spacing.V8), onCheckedChange: (Boolean) -> Unit, ) { - val checkboxSize = when (size) { - CheckboxSize.REGULAR -> 24.dp - CheckboxSize.LARGE -> 32.dp - } - - val iconRes = - if (checked) { - PrezelIcons.CheckCircleFilled - } else { - PrezelIcons.CheckCircleOutlined - } - - val iconColor = - if (checked) { - PrezelTheme.colors.feedbackGoodRegular - } else { - PrezelTheme.colors.iconDisabled - } - - Box( - modifier = modifier - .padding(all = PrezelTheme.spacing.V8) - .toggleable( - value = checked, - interactionSource = null, - indication = null, - role = Role.Checkbox, - onValueChange = onCheckedChange, - ), - contentAlignment = Alignment.Center, + PrezelTouchArea( + modifier = modifier, + onClick = { onCheckedChange(!checked) }, + isUseRipple = false, + extraTouchPadding = extraTouchPadding, ) { Icon( - painter = painterResource(id = iconRes), + painter = checkboxIconRes(checked = checked), contentDescription = stringResource(R.string.core_designsystem_checkbox_desc), - modifier = Modifier.size(checkboxSize), - tint = iconColor, + modifier = Modifier.size(size = checkboxSize(size = size)), + tint = checkboxIconTintColor(checked = checked), ) } } +private fun checkboxSize(size: CheckboxSize): Dp = + when (size) { + CheckboxSize.REGULAR -> 24.dp + CheckboxSize.LARGE -> 32.dp + } + +@Composable +private fun checkboxIconRes(checked: Boolean): Painter { + val resId = if (checked) PrezelIcons.CheckCircleFilled else PrezelIcons.CheckCircleOutlined + return painterResource(id = resId) +} + +@Composable +private fun checkboxIconTintColor(checked: Boolean): Color = + if (checked) { + PrezelTheme.colors.feedbackGoodRegular + } else { + PrezelTheme.colors.iconDisabled + } + @BasicPreview @Composable private fun PrezelCheckboxPreview() { From 25a1afad8fa11981e0e8e89f7ffbffd031ec844c Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:35:46 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=82=B4=20BuildConfig=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20=EB=B0=8F=20=EC=95=BD=EA=B4=80=20URL=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Prezel/feature/login/impl/build.gradle.kts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Prezel/feature/login/impl/build.gradle.kts b/Prezel/feature/login/impl/build.gradle.kts index 56e519c8..5a113b2e 100644 --- a/Prezel/feature/login/impl/build.gradle.kts +++ b/Prezel/feature/login/impl/build.gradle.kts @@ -1,9 +1,20 @@ +import com.team.prezel.buildlogic.convention.external.localProperty + plugins { alias(libs.plugins.prezel.android.feature.impl) } android { namespace = "com.team.prezel.feature.login.impl" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigField("String", "PRIVACY_POLICY_URL", "\"${localProperty("privacy.policy.url").get()}\"") + buildConfigField("String", "TERMS_OF_SERVICE_URL", "\"${localProperty("terms.of.service.url").get()}\"") + } } dependencies { From 13a93bac758f2aeeee58f896940e6623a952b01b Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:36:33 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=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 --- .../designsystem/component/PrezelAccordion.kt | 2 + .../login/impl/{ => landing}/LoginScreen.kt | 43 ++- .../{viewModel => landing}/LoginViewModel.kt | 35 +-- .../contract}/LoginUiEffect.kt | 8 +- .../contract}/LoginUiIntent.kt | 4 +- .../impl/landing/contract/LoginUiState.kt | 8 + .../impl/landing/model/LoginUiMessage.kt | 7 + .../login/impl/model/LoginUiMessage.kt | 7 - .../impl/navigation/LoginEntryBuilder.kt | 22 +- .../login/impl/navigation/LoginTermsNavKey.kt | 7 + .../feature/login/impl/terms/TermsScreen.kt | 274 ++++++++++++++++++ .../login/impl/terms/TermsViewModel.kt | 74 +++++ .../impl/terms/component/TermsDetailModal.kt | 107 +++++++ .../impl/terms/contract/TermsUiEffect.kt | 5 + .../impl/terms/contract/TermsUiIntent.kt | 13 + .../login/impl/terms/contract/TermsUiState.kt | 16 + .../login/impl/viewModel/LoginUiState.kt | 8 - .../impl/src/main/res/values/strings.xml | 23 +- 18 files changed, 594 insertions(+), 69 deletions(-) rename Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/{ => landing}/LoginScreen.kt (88%) rename Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/{viewModel => landing}/LoginViewModel.kt (65%) rename Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/{viewModel => landing/contract}/LoginUiEffect.kt (51%) rename Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/{viewModel => landing/contract}/LoginUiIntent.kt (73%) create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.kt delete mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/model/LoginUiMessage.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginTermsNavKey.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt create mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt delete mode 100644 Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt index 1a2f7994..2130a344 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAccordion.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -176,6 +177,7 @@ private fun PrezelAccordionPreview() { modifier = Modifier.drawDashBorder(), size = CheckboxSize.REGULAR, onCheckedChange = { state = !state }, + extraTouchPadding = PaddingValues(), ) }, trailingContent = { diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt similarity index 88% rename from Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt rename to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt index 692c020f..67a8a4c0 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.team.prezel.feature.login.impl +package com.team.prezel.feature.login.impl.landing import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope @@ -25,8 +25,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.team.prezel.core.auth.AuthManager import com.team.prezel.core.auth.model.AuthProvider import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea @@ -40,11 +42,11 @@ import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.theme.PrezelTheme import com.team.prezel.core.ui.LocalSnackbarHostState import com.team.prezel.feature.login.api.AUTH_LOGO_SHARED_ELEMENT_KEY -import com.team.prezel.feature.login.impl.model.LoginUiMessage -import com.team.prezel.feature.login.impl.viewModel.LoginUiEffect -import com.team.prezel.feature.login.impl.viewModel.LoginUiIntent -import com.team.prezel.feature.login.impl.viewModel.LoginUiState -import com.team.prezel.feature.login.impl.viewModel.LoginViewModel +import com.team.prezel.feature.login.impl.R +import com.team.prezel.feature.login.impl.landing.contract.LoginUiEffect +import com.team.prezel.feature.login.impl.landing.contract.LoginUiIntent +import com.team.prezel.feature.login.impl.landing.contract.LoginUiState +import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage import com.team.prezel.core.designsystem.R as DSR private const val AUTH_SHARED_ELEMENT_TRANSITION_DURATION = 300 @@ -52,21 +54,26 @@ private const val AUTH_SHARED_ELEMENT_TRANSITION_DELAY = 400 @Composable internal fun SharedTransitionScope.LoginScreen( - animatedVisibilityScope: AnimatedVisibilityScope, authManager: AuthManager, - navigateToHome: () -> Unit, + navigateToTerms: () -> Unit, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), ) { val context = LocalContext.current val resources = LocalResources.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = LocalSnackbarHostState.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> when (effect) { - LoginUiEffect.NavigateToHome -> navigateToHome() + is LoginUiEffect.LaunchLogin -> { + authManager.login(context = context, provider = effect.provider).also { result -> + viewModel.onIntent(LoginUiIntent.OnLoginResult(result = result)) + } + } + + LoginUiEffect.NavigateToTerms -> navigateToTerms() is LoginUiEffect.ShowMessage -> { val resId = when (effect.message) { @@ -74,16 +81,7 @@ internal fun SharedTransitionScope.LoginScreen( LoginUiMessage.LoginFailedRateLimited -> R.string.feature_login_impl_kakao_rate_limited LoginUiMessage.LoginFailedUnknown -> R.string.feature_login_impl_kakao_failure } - snackbarHostState.showPrezelSnackbar( - message = resources.getString(resId), - actionLabel = resources.getString(R.string.feature_login_impl_snackbar_confirm), - ) - } - - is LoginUiEffect.LaunchLogin -> { - authManager.login(context = context, provider = effect.provider).also { result -> - viewModel.onIntent(LoginUiIntent.OnLoginResult(result = result)) - } + snackbarHostState.showPrezelSnackbar(message = resources.getString(resId)) } } } @@ -91,7 +89,7 @@ internal fun SharedTransitionScope.LoginScreen( LoginScreen( uiState = uiState, - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = LocalNavAnimatedContentScope.current, onLogin = { viewModel.onIntent(LoginUiIntent.OnClickLogin(provider = AuthProvider.KAKAO)) }, modifier = modifier, ) @@ -153,6 +151,7 @@ private fun LoginFooter( modifier: Modifier = Modifier, ) { var isButtonVisible by remember { mutableStateOf(false) } + val startWithKakaoLabel = stringResource(R.string.feature_login_impl_start_with_kakao) val kakaoButtonConfig = PrezelButtonDefaults.getDefault( isIconOnly = false, type = ButtonType.FILLED, @@ -182,7 +181,7 @@ private fun LoginFooter( PrezelButtonArea { CustomButton( iconResId = PrezelIcons.Kakao, - label = "카카오로 시작하기", + label = startWithKakaoLabel, enabled = enabled, onClick = onLogin, config = kakaoButtonConfig, diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt similarity index 65% rename from Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt rename to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt index fe1400f5..38734fe3 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt @@ -1,9 +1,13 @@ -package com.team.prezel.feature.login.impl.viewModel +package com.team.prezel.feature.login.impl.landing import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.team.prezel.core.auth.model.AuthProvider import com.team.prezel.core.auth.model.AuthResult -import com.team.prezel.feature.login.impl.model.LoginUiMessage +import com.team.prezel.feature.login.impl.landing.contract.LoginUiEffect +import com.team.prezel.feature.login.impl.landing.contract.LoginUiIntent +import com.team.prezel.feature.login.impl.landing.contract.LoginUiState +import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -15,12 +19,12 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class LoginViewModel +internal class LoginViewModel @Inject constructor() : ViewModel() { private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow = _uiState - val currentState: LoginUiState + private val currentState: LoginUiState get() = uiState.value private val _uiEffect = Channel() @@ -28,7 +32,7 @@ class LoginViewModel fun onIntent(intent: LoginUiIntent) { when (intent) { - is LoginUiIntent.OnClickLogin -> handleClickLogin() + is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider) is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result) } } @@ -37,14 +41,13 @@ class LoginViewModel _uiState.update(reducer) } - private fun handleClickLogin() { + private fun handleClickLogin(provider: AuthProvider) { if (currentState.isLoading) return viewModelScope.launch { update { copy(isLoading = true) } - // _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) - _uiEffect.send(LoginUiEffect.NavigateToHome) + _uiEffect.send(LoginUiEffect.NavigateToTerms) } } @@ -53,22 +56,14 @@ class LoginViewModel update { copy(isLoading = false) } when (result) { - AuthResult.Success -> { - _uiEffect.send(LoginUiEffect.NavigateToHome) - } - - AuthResult.Cancelled -> { - _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) - } - - is AuthResult.Failure -> { - _uiEffect.send(LoginUiEffect.ShowMessage(result.toLoginUiMessage())) - } + AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms) + AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) + is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage())) } } } - private fun AuthResult.Failure.toLoginUiMessage(): LoginUiMessage = + private fun AuthResult.Failure.toUiMessage(): LoginUiMessage = when (this) { AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt similarity index 51% rename from Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt rename to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt index 571217c2..2176088e 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiEffect.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.kt @@ -1,14 +1,14 @@ -package com.team.prezel.feature.login.impl.viewModel +package com.team.prezel.feature.login.impl.landing.contract import com.team.prezel.core.auth.model.AuthProvider -import com.team.prezel.feature.login.impl.model.LoginUiMessage +import com.team.prezel.feature.login.impl.landing.model.LoginUiMessage -sealed interface LoginUiEffect { +internal sealed interface LoginUiEffect { data class LaunchLogin( val provider: AuthProvider, ) : LoginUiEffect - data object NavigateToHome : LoginUiEffect + data object NavigateToTerms : LoginUiEffect data class ShowMessage( val message: LoginUiMessage, diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt similarity index 73% rename from Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt rename to Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt index 2009a45f..17c4d475 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiIntent.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiIntent.kt @@ -1,9 +1,9 @@ -package com.team.prezel.feature.login.impl.viewModel +package com.team.prezel.feature.login.impl.landing.contract import com.team.prezel.core.auth.model.AuthProvider import com.team.prezel.core.auth.model.AuthResult -sealed interface LoginUiIntent { +internal sealed interface LoginUiIntent { data class OnClickLogin( val provider: AuthProvider, ) : LoginUiIntent diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt new file mode 100644 index 00000000..fbc8edc2 --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiState.kt @@ -0,0 +1,8 @@ +package com.team.prezel.feature.login.impl.landing.contract + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class LoginUiState( + val isLoading: Boolean = false, +) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.kt new file mode 100644 index 00000000..f9b4149c --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/model/LoginUiMessage.kt @@ -0,0 +1,7 @@ +package com.team.prezel.feature.login.impl.landing.model + +internal enum class LoginUiMessage { + LoginCancelled, + LoginFailedRateLimited, + LoginFailedUnknown, +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/model/LoginUiMessage.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/model/LoginUiMessage.kt deleted file mode 100644 index 7e491652..00000000 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/model/LoginUiMessage.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.team.prezel.feature.login.impl.model - -enum class LoginUiMessage { - LoginFailedUnknown, - LoginFailedRateLimited, - LoginCancelled, -} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt index 1176cb98..170e2228 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt @@ -2,13 +2,13 @@ package com.team.prezel.feature.login.impl.navigation import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.team.prezel.core.auth.AuthManager import com.team.prezel.core.navigation.LocalNavigator import com.team.prezel.core.navigation.LocalSharedTransitionScope import com.team.prezel.feature.home.api.HomeNavKey import com.team.prezel.feature.login.api.LoginNavKey -import com.team.prezel.feature.login.impl.LoginScreen +import com.team.prezel.feature.login.impl.landing.LoginScreen +import com.team.prezel.feature.login.impl.terms.TermsScreen import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,14 +21,26 @@ internal fun EntryProviderScope.featureLoginEntryBuilder(authManager: Au with(LocalSharedTransitionScope.current) { LoginScreen( - animatedVisibilityScope = LocalNavAnimatedContentScope.current, authManager = authManager, - navigateToHome = { - navigator.replaceRoot(HomeNavKey) + navigateToTerms = { + navigator.navigate(LoginTermsNavKey) }, ) } } + + entry { + val navigator = LocalNavigator.current + + TermsScreen( + navigateBack = { + navigator.goBack() + }, + navigateToHome = { + navigator.replaceRoot(HomeNavKey) + }, + ) + } } @Module diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginTermsNavKey.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginTermsNavKey.kt new file mode 100644 index 00000000..443cf480 --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginTermsNavKey.kt @@ -0,0 +1,7 @@ +package com.team.prezel.feature.login.impl.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +internal data object LoginTermsNavKey : NavKey diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt new file mode 100644 index 00000000..72185a88 --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt @@ -0,0 +1,274 @@ +package com.team.prezel.feature.login.impl.terms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.team.prezel.core.designsystem.component.CheckboxSize +import com.team.prezel.core.designsystem.component.PrezelAccordion +import com.team.prezel.core.designsystem.component.PrezelCheckbox +import com.team.prezel.core.designsystem.component.PrezelDividerType +import com.team.prezel.core.designsystem.component.PrezelHorizontalDivider +import com.team.prezel.core.designsystem.component.PrezelTopAppBar +import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea +import com.team.prezel.core.designsystem.component.actions.button.PrezelHyperlinkButton +import com.team.prezel.core.designsystem.component.list.PrezelList +import com.team.prezel.core.designsystem.component.list.PrezelListSize +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.theme.PrezelTheme +import com.team.prezel.feature.login.impl.BuildConfig +import com.team.prezel.feature.login.impl.R +import com.team.prezel.feature.login.impl.terms.component.TermsDetailModal +import com.team.prezel.feature.login.impl.terms.contract.TermsUiEffect +import com.team.prezel.feature.login.impl.terms.contract.TermsUiIntent +import com.team.prezel.feature.login.impl.terms.contract.TermsUiState + +@Composable +internal fun TermsScreen( + navigateBack: () -> Unit, + navigateToHome: () -> Unit, + modifier: Modifier = Modifier, + viewModel: TermsViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var activeDetailUrl by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + viewModel.uiEffect.collect { effect -> + when (effect) { + TermsUiEffect.NavigateToHome -> navigateToHome() + } + } + } + + activeDetailUrl?.let { url -> + TermsDetailModal( + url = url, + onDismiss = { activeDetailUrl = null }, + ) + } + + TermsScreen( + uiState = uiState, + onBack = navigateBack, + onToggleAll = { viewModel.onIntent(TermsUiIntent.OnToggleAll) }, + onToggleTermsOfService = { viewModel.onIntent(TermsUiIntent.OnToggleTermsOfService) }, + onTogglePrivacyPolicy = { viewModel.onIntent(TermsUiIntent.OnTogglePrivacyPolicy) }, + onToggleMarketingConsent = { viewModel.onIntent(TermsUiIntent.OnToggleMarketingConsent) }, + onClickTermsOfServiceDetail = { activeDetailUrl = BuildConfig.TERMS_OF_SERVICE_URL }, + onClickPrivacyPolicyDetail = { activeDetailUrl = BuildConfig.PRIVACY_POLICY_URL }, + onContinue = { viewModel.onIntent(TermsUiIntent.OnClickContinue) }, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TermsScreen( + uiState: TermsUiState, + onBack: () -> Unit, + onToggleAll: () -> Unit, + onToggleTermsOfService: () -> Unit, + onTogglePrivacyPolicy: () -> Unit, + onToggleMarketingConsent: () -> Unit, + onClickTermsOfServiceDetail: () -> Unit, + onClickPrivacyPolicyDetail: () -> Unit, + onContinue: () -> Unit, + modifier: Modifier = Modifier, +) { + val resources = LocalResources.current + + Column(modifier = modifier.fillMaxSize()) { + PrezelTopAppBar( + title = { + Text(text = stringResource(R.string.feature_login_impl_terms_title)) + }, + leadingIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(PrezelIcons.ArrowLeft), + contentDescription = stringResource(R.string.feature_login_impl_back_icon_description), + ) + } + }, + ) + + Column( + modifier = Modifier + .weight(1f) + .padding( + horizontal = PrezelTheme.spacing.V20, + vertical = PrezelTheme.spacing.V16, + ), + ) { + PrezelList( + modifier = modifier, + title = stringResource(R.string.feature_login_impl_terms_all), + size = PrezelListSize.REGULAR, + nested = true, + leadingContent = { + PrezelCheckbox( + checked = uiState.isAllChecked, + size = CheckboxSize.REGULAR, + onCheckedChange = { onToggleAll() }, + extraTouchPadding = PaddingValues( + start = PrezelTheme.spacing.V8, + top = PrezelTheme.spacing.V8, + bottom = PrezelTheme.spacing.V8, + ), + ) + }, + ) + + PrezelHorizontalDivider(type = PrezelDividerType.THICK) + + AgreementSection { + AgreementRow( + checked = uiState.isTermsOfServiceChecked, + title = stringResource(R.string.feature_login_impl_terms_of_service), + summary = stringResource(R.string.feature_login_impl_terms_of_service_summary), + isShowDetailButton = true, + onCheckedChange = { onToggleTermsOfService() }, + onClickDetail = onClickTermsOfServiceDetail, + ) + AgreementRow( + checked = uiState.isPrivacyPolicyChecked, + title = stringResource(R.string.feature_login_impl_privacy_policy), + summary = stringResource(R.string.feature_login_impl_privacy_policy_summary), + isShowDetailButton = true, + onCheckedChange = { onTogglePrivacyPolicy() }, + onClickDetail = onClickPrivacyPolicyDetail, + ) + AgreementRow( + checked = uiState.isMarketingConsentChecked, + title = stringResource(R.string.feature_login_impl_marketing_consent), + summary = stringResource(R.string.feature_login_impl_marketing_consent_summary), + isShowDetailButton = false, + onCheckedChange = { onToggleMarketingConsent() }, + onClickDetail = {}, + ) + } + } + + PrezelButtonArea { + MainButton( + label = resources.getString(R.string.feature_login_impl_terms_continue_button_text), + enabled = uiState.isRequiredChecked, + onClick = onContinue, + ) + } + } +} + +@Composable +private fun AgreementSection( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(start = PrezelTheme.spacing.V14) + .padding(vertical = PrezelTheme.spacing.V12), + verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V16), + content = content, + ) +} + +@Composable +private fun AgreementRow( + checked: Boolean, + title: String, + summary: String, + isShowDetailButton: Boolean, + onCheckedChange: (Boolean) -> Unit, + onClickDetail: () -> Unit, + modifier: Modifier = Modifier, +) { + PrezelAccordion( + modifier = modifier, + title = title, + nested = true, + leadingContent = { + PrezelCheckbox( + checked = checked, + size = CheckboxSize.REGULAR, + onCheckedChange = onCheckedChange, + extraTouchPadding = PaddingValues(), + ) + }, + trailingContent = { + if (isShowDetailButton) { + PrezelHyperlinkButton( + text = stringResource(R.string.feature_login_impl_terms_detail_button_text), + onClick = onClickDetail, + ) + } + }, + ) { + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8)) + Box( + modifier = Modifier + .fillMaxWidth() + .background(PrezelTheme.colors.bgMedium) + .padding(all = PrezelTheme.spacing.V12), + ) { + Text( + text = summary, + style = PrezelTheme.typography.caption2Regular, + color = PrezelTheme.colors.textLarge, + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun TermsScreenPreview() { + var uiState by remember { mutableStateOf(TermsUiState()) } + + PrezelTheme { + TermsScreen( + uiState = uiState, + onBack = {}, + onToggleAll = { + val checked = !uiState.isAllChecked + uiState = uiState.copy( + isTermsOfServiceChecked = checked, + isPrivacyPolicyChecked = checked, + isMarketingConsentChecked = checked, + ) + }, + onToggleTermsOfService = { uiState = uiState.copy(isTermsOfServiceChecked = !uiState.isTermsOfServiceChecked) }, + onTogglePrivacyPolicy = { uiState = uiState.copy(isPrivacyPolicyChecked = !uiState.isPrivacyPolicyChecked) }, + onToggleMarketingConsent = { uiState = uiState.copy(isMarketingConsentChecked = !uiState.isMarketingConsentChecked) }, + onClickTermsOfServiceDetail = {}, + onClickPrivacyPolicyDetail = {}, + onContinue = {}, + ) + } +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt new file mode 100644 index 00000000..13d089ac --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt @@ -0,0 +1,74 @@ +package com.team.prezel.feature.login.impl.terms + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.team.prezel.feature.login.impl.terms.contract.TermsUiEffect +import com.team.prezel.feature.login.impl.terms.contract.TermsUiIntent +import com.team.prezel.feature.login.impl.terms.contract.TermsUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class TermsViewModel + @Inject + constructor() : ViewModel() { + private val _uiState = MutableStateFlow(TermsUiState()) + val uiState: StateFlow = _uiState + private val currentState: TermsUiState + get() = uiState.value + + private val _uiEffect = Channel() + val uiEffect: Flow = _uiEffect.receiveAsFlow() + + fun onIntent(intent: TermsUiIntent) { + when (intent) { + TermsUiIntent.OnToggleAll -> toggleAll() + TermsUiIntent.OnToggleTermsOfService -> toggleTermsOfService() + TermsUiIntent.OnTogglePrivacyPolicy -> togglePrivacyPolicy() + TermsUiIntent.OnToggleMarketingConsent -> toggleMarketingConsent() + TermsUiIntent.OnClickContinue -> handleClickContinue() + } + } + + private fun update(reducer: TermsUiState.() -> TermsUiState) { + _uiState.update(reducer) + } + + private fun toggleAll() { + val newChecked = !currentState.isAllChecked + + update { + copy( + isTermsOfServiceChecked = newChecked, + isPrivacyPolicyChecked = newChecked, + isMarketingConsentChecked = newChecked, + ) + } + } + + private fun toggleTermsOfService() { + update { copy(isTermsOfServiceChecked = !isTermsOfServiceChecked) } + } + + private fun togglePrivacyPolicy() { + update { copy(isPrivacyPolicyChecked = !isPrivacyPolicyChecked) } + } + + private fun toggleMarketingConsent() { + update { copy(isMarketingConsentChecked = !isMarketingConsentChecked) } + } + + private fun handleClickContinue() { + if (!currentState.isRequiredChecked) return + viewModelScope.launch { + _uiEffect.send(TermsUiEffect.NavigateToHome) + } + } + } diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt new file mode 100644 index 00000000..07378112 --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt @@ -0,0 +1,107 @@ +package com.team.prezel.feature.login.impl.terms.component + +import android.annotation.SuppressLint +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.zIndex +import com.team.prezel.core.designsystem.component.PrezelTopAppBar +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.theme.PrezelTheme +import com.team.prezel.feature.login.impl.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TermsDetailModal( + url: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = onDismiss) + + Column( + modifier = modifier + .background(PrezelTheme.colors.bgRegular) + .zIndex(1f) + .fillMaxSize(), + ) { + PrezelTopAppBar( + trailingIcons = { + IconButton(onClick = onDismiss) { + Icon( + painter = painterResource(PrezelIcons.Cancel), + contentDescription = stringResource(R.string.feature_login_impl_cancel_icon_description), + ) + } + }, + ) + NotionWebView(url = url, modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun NotionWebView( + url: String, + modifier: Modifier = Modifier, +) { + val webView = rememberWebView(url) + + AndroidView( + modifier = modifier, + factory = { webView }, + update = { view -> + view.url?.let(view::loadUrl) + }, + ) +} + +private const val NOTION_STYLE_PATCH = """ + (function() { + var style = document.createElement('style'); + style.innerHTML = ` + html, body, * { + overflow-y: auto !important; + height: auto !important; + } + `; + document.head.appendChild(style); + })(); +""" + +@SuppressLint("SetJavaScriptEnabled") +@Composable +private fun rememberWebView(url: String): WebView { + val context = LocalContext.current + + return remember(url) { + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + webViewClient = object : WebViewClient() { + override fun onPageFinished( + view: WebView?, + loadedUrl: String?, + ) { + val targetView = view ?: return + targetView.evaluateJavascript(NOTION_STYLE_PATCH.trimIndent(), null) + } + } + + loadUrl(url) + } + } +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.kt new file mode 100644 index 00000000..69e4755e --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.kt @@ -0,0 +1,5 @@ +package com.team.prezel.feature.login.impl.terms.contract + +internal sealed interface TermsUiEffect { + data object NavigateToHome : TermsUiEffect +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt new file mode 100644 index 00000000..cc123669 --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt @@ -0,0 +1,13 @@ +package com.team.prezel.feature.login.impl.terms.contract + +internal sealed interface TermsUiIntent { + data object OnToggleAll : TermsUiIntent + + data object OnToggleTermsOfService : TermsUiIntent + + data object OnTogglePrivacyPolicy : TermsUiIntent + + data object OnToggleMarketingConsent : TermsUiIntent + + data object OnClickContinue : TermsUiIntent +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt new file mode 100644 index 00000000..1baaca6d --- /dev/null +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt @@ -0,0 +1,16 @@ +package com.team.prezel.feature.login.impl.terms.contract + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class TermsUiState( + val isTermsOfServiceChecked: Boolean = false, + val isPrivacyPolicyChecked: Boolean = false, + val isMarketingConsentChecked: Boolean = false, +) { + val isRequiredChecked: Boolean + get() = isTermsOfServiceChecked && isPrivacyPolicyChecked + + val isAllChecked: Boolean + get() = isRequiredChecked && isMarketingConsentChecked +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt deleted file mode 100644 index 3c2ab3ea..00000000 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/viewModel/LoginUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.team.prezel.feature.login.impl.viewModel - -import androidx.compose.runtime.Immutable - -@Immutable -data class LoginUiState( - val isLoading: Boolean = false, -) diff --git a/Prezel/feature/login/impl/src/main/res/values/strings.xml b/Prezel/feature/login/impl/src/main/res/values/strings.xml index 021ab975..cab90d27 100644 --- a/Prezel/feature/login/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/login/impl/src/main/res/values/strings.xml @@ -1,7 +1,28 @@ + 카카오 로그인에 문제가 발생했어요.\n잠시 후 다시 시도해 주세요. 로그인 시도가 너무 많아요.\n잠시 후 다시 시도해 주세요. 로그인이 취소되었어요. - 확인 + 카카오로 시작하기 + 뒤로가기 + 닫기 + + + 약관동의 + 전체 동의 + (필수) 이용약관 + (필수) 개인정보 정책 + (선택) 데이터 활용 동의 + 자세히 보기 + 다음 + + 본 약관은 서비스 이용과 관련한 기본적인 권리·의무 및 책임사항을 규정합니다. + + 서비스 제공을 위해 개인정보를 수집 · 이용합니다.\n + 발표 연습을 위한 음성 녹음 및 분석 데이터 처리 내용이 포함됩니다.\n\n + 수집 항목 : 계정 정보, 음성 녹음 파일, 음성 분석 결과, 발표 대본, 서비스 이용 기록 등\n\n + 수집 목적 : 발표 분석, 개인 맞춤 피드백 제공, 연습 기록 관리 + + 서비스 품질 향상 및 기능 개선을 위해 비식별 처리된 분석 데이터를 활용할 수 있습니다. From 7f9efbd7ee895f95eb4e314cb89aca5b2fd8235b Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:47:35 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:=20`TermsScreen`=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=ED=95=A8=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EB=85=BC=EB=A6=AC=EC=A0=81=20=EB=8B=A8=EC=9C=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=98=EC=97=AC=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=EA=B3=BC=20=EC=9C=A0=EC=A7=80=EB=B3=B4=EC=88=98?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/login/impl/terms/TermsScreen.kt | 177 +++++++++++------- 1 file changed, 108 insertions(+), 69 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt index 72185a88..c4c00a75 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt @@ -103,76 +103,18 @@ internal fun TermsScreen( val resources = LocalResources.current Column(modifier = modifier.fillMaxSize()) { - PrezelTopAppBar( - title = { - Text(text = stringResource(R.string.feature_login_impl_terms_title)) - }, - leadingIcon = { - IconButton(onClick = onBack) { - Icon( - painter = painterResource(PrezelIcons.ArrowLeft), - contentDescription = stringResource(R.string.feature_login_impl_back_icon_description), - ) - } - }, - ) - - Column( - modifier = Modifier - .weight(1f) - .padding( - horizontal = PrezelTheme.spacing.V20, - vertical = PrezelTheme.spacing.V16, - ), - ) { - PrezelList( - modifier = modifier, - title = stringResource(R.string.feature_login_impl_terms_all), - size = PrezelListSize.REGULAR, - nested = true, - leadingContent = { - PrezelCheckbox( - checked = uiState.isAllChecked, - size = CheckboxSize.REGULAR, - onCheckedChange = { onToggleAll() }, - extraTouchPadding = PaddingValues( - start = PrezelTheme.spacing.V8, - top = PrezelTheme.spacing.V8, - bottom = PrezelTheme.spacing.V8, - ), - ) - }, - ) + TermsScreenTopAppBar(onBack = onBack) - PrezelHorizontalDivider(type = PrezelDividerType.THICK) - - AgreementSection { - AgreementRow( - checked = uiState.isTermsOfServiceChecked, - title = stringResource(R.string.feature_login_impl_terms_of_service), - summary = stringResource(R.string.feature_login_impl_terms_of_service_summary), - isShowDetailButton = true, - onCheckedChange = { onToggleTermsOfService() }, - onClickDetail = onClickTermsOfServiceDetail, - ) - AgreementRow( - checked = uiState.isPrivacyPolicyChecked, - title = stringResource(R.string.feature_login_impl_privacy_policy), - summary = stringResource(R.string.feature_login_impl_privacy_policy_summary), - isShowDetailButton = true, - onCheckedChange = { onTogglePrivacyPolicy() }, - onClickDetail = onClickPrivacyPolicyDetail, - ) - AgreementRow( - checked = uiState.isMarketingConsentChecked, - title = stringResource(R.string.feature_login_impl_marketing_consent), - summary = stringResource(R.string.feature_login_impl_marketing_consent_summary), - isShowDetailButton = false, - onCheckedChange = { onToggleMarketingConsent() }, - onClickDetail = {}, - ) - } - } + AgreementSections( + modifier = Modifier.weight(1f), + uiState = uiState, + onToggleAll = onToggleAll, + onToggleTermsOfService = onToggleTermsOfService, + onClickTermsOfServiceDetail = onClickTermsOfServiceDetail, + onTogglePrivacyPolicy = onTogglePrivacyPolicy, + onClickPrivacyPolicyDetail = onClickPrivacyPolicyDetail, + onToggleMarketingConsent = onToggleMarketingConsent, + ) PrezelButtonArea { MainButton( @@ -184,6 +126,103 @@ internal fun TermsScreen( } } +@Composable +private fun AgreementSections( + uiState: TermsUiState, + onToggleAll: () -> Unit, + onToggleTermsOfService: () -> Unit, + onClickTermsOfServiceDetail: () -> Unit, + onTogglePrivacyPolicy: () -> Unit, + onClickPrivacyPolicyDetail: () -> Unit, + onToggleMarketingConsent: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding( + horizontal = PrezelTheme.spacing.V20, + vertical = PrezelTheme.spacing.V16, + ), + ) { + AgreementAll(isAllChecked = uiState.isAllChecked, onToggleAll = onToggleAll) + + PrezelHorizontalDivider(type = PrezelDividerType.THICK) + + AgreementSection { + AgreementRow( + checked = uiState.isTermsOfServiceChecked, + title = stringResource(R.string.feature_login_impl_terms_of_service), + summary = stringResource(R.string.feature_login_impl_terms_of_service_summary), + isShowDetailButton = true, + onCheckedChange = { onToggleTermsOfService() }, + onClickDetail = onClickTermsOfServiceDetail, + ) + AgreementRow( + checked = uiState.isPrivacyPolicyChecked, + title = stringResource(R.string.feature_login_impl_privacy_policy), + summary = stringResource(R.string.feature_login_impl_privacy_policy_summary), + isShowDetailButton = true, + onCheckedChange = { onTogglePrivacyPolicy() }, + onClickDetail = onClickPrivacyPolicyDetail, + ) + AgreementRow( + checked = uiState.isMarketingConsentChecked, + title = stringResource(R.string.feature_login_impl_marketing_consent), + summary = stringResource(R.string.feature_login_impl_marketing_consent_summary), + isShowDetailButton = false, + onCheckedChange = { onToggleMarketingConsent() }, + onClickDetail = {}, + ) + } + } +} + +@Composable +private fun AgreementAll( + isAllChecked: Boolean, + onToggleAll: () -> Unit, +) { + PrezelList( + title = stringResource(R.string.feature_login_impl_terms_all), + size = PrezelListSize.REGULAR, + nested = true, + leadingContent = { + PrezelCheckbox( + checked = isAllChecked, + size = CheckboxSize.REGULAR, + onCheckedChange = { onToggleAll() }, + extraTouchPadding = PaddingValues( + start = PrezelTheme.spacing.V8, + top = PrezelTheme.spacing.V8, + bottom = PrezelTheme.spacing.V8, + ), + ) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TermsScreenTopAppBar( + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + PrezelTopAppBar( + modifier = modifier, + title = { + Text(text = stringResource(R.string.feature_login_impl_terms_title)) + }, + leadingIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(PrezelIcons.ArrowLeft), + contentDescription = stringResource(R.string.feature_login_impl_back_icon_description), + ) + } + }, + ) +} + @Composable private fun AgreementSection( modifier: Modifier = Modifier, From 2a402e54b89a87f1cf59c8f84ada55c77aa82b90 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:55:11 +0900 Subject: [PATCH 06/14] =?UTF-8?q?style:=20TermsScreen=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/login/impl/terms/TermsScreen.kt | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt index c4c00a75..12e6b9cc 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt @@ -72,7 +72,7 @@ internal fun TermsScreen( ) } - TermsScreen( + TermsScreenScreen( uiState = uiState, onBack = navigateBack, onToggleAll = { viewModel.onIntent(TermsUiIntent.OnToggleAll) }, @@ -88,7 +88,7 @@ internal fun TermsScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun TermsScreen( +private fun TermsScreenScreen( uiState: TermsUiState, onBack: () -> Unit, onToggleAll: () -> Unit, @@ -105,7 +105,7 @@ internal fun TermsScreen( Column(modifier = modifier.fillMaxSize()) { TermsScreenTopAppBar(onBack = onBack) - AgreementSections( + TermsAgreementContent( modifier = Modifier.weight(1f), uiState = uiState, onToggleAll = onToggleAll, @@ -126,8 +126,30 @@ internal fun TermsScreen( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TermsScreenTopAppBar( + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + PrezelTopAppBar( + modifier = modifier, + title = { + Text(text = stringResource(R.string.feature_login_impl_terms_title)) + }, + leadingIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(PrezelIcons.ArrowLeft), + contentDescription = stringResource(R.string.feature_login_impl_back_icon_description), + ) + } + }, + ) +} + @Composable -private fun AgreementSections( +private fun TermsAgreementContent( uiState: TermsUiState, onToggleAll: () -> Unit, onToggleTermsOfService: () -> Unit, @@ -144,12 +166,15 @@ private fun AgreementSections( vertical = PrezelTheme.spacing.V16, ), ) { - AgreementAll(isAllChecked = uiState.isAllChecked, onToggleAll = onToggleAll) + TermsSelectAllRow( + isAllChecked = uiState.isAllChecked, + onToggleAll = onToggleAll, + ) PrezelHorizontalDivider(type = PrezelDividerType.THICK) - AgreementSection { - AgreementRow( + TermsAgreementSection { + TermsAgreementRow( checked = uiState.isTermsOfServiceChecked, title = stringResource(R.string.feature_login_impl_terms_of_service), summary = stringResource(R.string.feature_login_impl_terms_of_service_summary), @@ -157,7 +182,7 @@ private fun AgreementSections( onCheckedChange = { onToggleTermsOfService() }, onClickDetail = onClickTermsOfServiceDetail, ) - AgreementRow( + TermsAgreementRow( checked = uiState.isPrivacyPolicyChecked, title = stringResource(R.string.feature_login_impl_privacy_policy), summary = stringResource(R.string.feature_login_impl_privacy_policy_summary), @@ -165,7 +190,7 @@ private fun AgreementSections( onCheckedChange = { onTogglePrivacyPolicy() }, onClickDetail = onClickPrivacyPolicyDetail, ) - AgreementRow( + TermsAgreementRow( checked = uiState.isMarketingConsentChecked, title = stringResource(R.string.feature_login_impl_marketing_consent), summary = stringResource(R.string.feature_login_impl_marketing_consent_summary), @@ -178,11 +203,13 @@ private fun AgreementSections( } @Composable -private fun AgreementAll( +private fun TermsSelectAllRow( isAllChecked: Boolean, onToggleAll: () -> Unit, + modifier: Modifier = Modifier, ) { PrezelList( + modifier = modifier, title = stringResource(R.string.feature_login_impl_terms_all), size = PrezelListSize.REGULAR, nested = true, @@ -201,30 +228,8 @@ private fun AgreementAll( ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TermsScreenTopAppBar( - onBack: () -> Unit, - modifier: Modifier = Modifier, -) { - PrezelTopAppBar( - modifier = modifier, - title = { - Text(text = stringResource(R.string.feature_login_impl_terms_title)) - }, - leadingIcon = { - IconButton(onClick = onBack) { - Icon( - painter = painterResource(PrezelIcons.ArrowLeft), - contentDescription = stringResource(R.string.feature_login_impl_back_icon_description), - ) - } - }, - ) -} - @Composable -private fun AgreementSection( +private fun TermsAgreementSection( modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { @@ -239,7 +244,7 @@ private fun AgreementSection( } @Composable -private fun AgreementRow( +private fun TermsAgreementRow( checked: Boolean, title: String, summary: String, @@ -291,7 +296,7 @@ private fun TermsScreenPreview() { var uiState by remember { mutableStateOf(TermsUiState()) } PrezelTheme { - TermsScreen( + TermsScreenScreen( uiState = uiState, onBack = {}, onToggleAll = { From a809fbb83a8805fec05751381ea8b4d0474810f8 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:57:45 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20LoginViewModel=20=EB=82=B4=20?= =?UTF-8?q?=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=AA=A8=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login/impl/landing/LoginViewModel.kt | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt index 38734fe3..315252f1 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.team.prezel.core.auth.model.AuthProvider import com.team.prezel.core.auth.model.AuthResult +import com.team.prezel.feature.login.impl.BuildConfig import com.team.prezel.feature.login.impl.landing.contract.LoginUiEffect import com.team.prezel.feature.login.impl.landing.contract.LoginUiIntent import com.team.prezel.feature.login.impl.landing.contract.LoginUiState @@ -20,52 +21,57 @@ import javax.inject.Inject @HiltViewModel internal class LoginViewModel - @Inject - constructor() : ViewModel() { - private val _uiState = MutableStateFlow(LoginUiState()) - val uiState: StateFlow = _uiState - private val currentState: LoginUiState - get() = uiState.value +@Inject +constructor() : ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState + private val currentState: LoginUiState + get() = uiState.value - private val _uiEffect = Channel() - val uiEffect: Flow = _uiEffect.receiveAsFlow() + private val _uiEffect = Channel() + val uiEffect: Flow = _uiEffect.receiveAsFlow() - fun onIntent(intent: LoginUiIntent) { - when (intent) { - is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider) - is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result) - } + fun onIntent(intent: LoginUiIntent) { + when (intent) { + is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider) + is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result) } + } - private fun update(reducer: LoginUiState.() -> LoginUiState) { - _uiState.update(reducer) - } + private fun update(reducer: LoginUiState.() -> LoginUiState) { + _uiState.update(reducer) + } + + private fun handleClickLogin(provider: AuthProvider) { + if (currentState.isLoading) return - private fun handleClickLogin(provider: AuthProvider) { - if (currentState.isLoading) return + viewModelScope.launch { + update { copy(isLoading = true) } - viewModelScope.launch { - update { copy(isLoading = true) } -// _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) + // todo: MVP 개발 완료 후 해당 조건 제거 + if (BuildConfig.DEBUG) { _uiEffect.send(LoginUiEffect.NavigateToTerms) + } else { + _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) } } + } - private fun handleLoginResult(result: AuthResult) { - viewModelScope.launch { - update { copy(isLoading = false) } + private fun handleLoginResult(result: AuthResult) { + viewModelScope.launch { + update { copy(isLoading = false) } - when (result) { - AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms) - AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) - is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage())) - } + when (result) { + AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms) + AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) + is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage())) } } - - private fun AuthResult.Failure.toUiMessage(): LoginUiMessage = - when (this) { - AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited - AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown - } } + + private fun AuthResult.Failure.toUiMessage(): LoginUiMessage = + when (this) { + AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited + AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown + } +} From 3c92db9dfc5e1b6a0a9a4b937f36ac3d09a2709b Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 2 Apr 2026 23:58:58 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20LoginViewModel=20=EB=82=B4=20?= =?UTF-8?q?=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=AA=A8=EB=93=9C=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../login/impl/landing/LoginViewModel.kt | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt index 315252f1..d22bbc66 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt @@ -21,57 +21,57 @@ import javax.inject.Inject @HiltViewModel internal class LoginViewModel -@Inject -constructor() : ViewModel() { - private val _uiState = MutableStateFlow(LoginUiState()) - val uiState: StateFlow = _uiState - private val currentState: LoginUiState - get() = uiState.value + @Inject + constructor() : ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState + private val currentState: LoginUiState + get() = uiState.value - private val _uiEffect = Channel() - val uiEffect: Flow = _uiEffect.receiveAsFlow() + private val _uiEffect = Channel() + val uiEffect: Flow = _uiEffect.receiveAsFlow() - fun onIntent(intent: LoginUiIntent) { - when (intent) { - is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider) - is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result) + fun onIntent(intent: LoginUiIntent) { + when (intent) { + is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider) + is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result) + } } - } - private fun update(reducer: LoginUiState.() -> LoginUiState) { - _uiState.update(reducer) - } + private fun update(reducer: LoginUiState.() -> LoginUiState) { + _uiState.update(reducer) + } - private fun handleClickLogin(provider: AuthProvider) { - if (currentState.isLoading) return + private fun handleClickLogin(provider: AuthProvider) { + if (currentState.isLoading) return - viewModelScope.launch { - update { copy(isLoading = true) } + viewModelScope.launch { + update { copy(isLoading = true) } - // todo: MVP 개발 완료 후 해당 조건 제거 - if (BuildConfig.DEBUG) { - _uiEffect.send(LoginUiEffect.NavigateToTerms) - } else { - _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) + // todo: MVP 개발 완료 후 해당 조건 제거 + if (BuildConfig.DEBUG) { + _uiEffect.send(LoginUiEffect.NavigateToTerms) + } else { + _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) + } } } - } - private fun handleLoginResult(result: AuthResult) { - viewModelScope.launch { - update { copy(isLoading = false) } + private fun handleLoginResult(result: AuthResult) { + viewModelScope.launch { + update { copy(isLoading = false) } - when (result) { - AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms) - AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) - is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage())) + when (result) { + AuthResult.Success -> _uiEffect.send(LoginUiEffect.NavigateToTerms) + AuthResult.Cancelled -> _uiEffect.send(LoginUiEffect.ShowMessage(LoginUiMessage.LoginCancelled)) + is AuthResult.Failure -> _uiEffect.send(LoginUiEffect.ShowMessage(result.toUiMessage())) + } } } - } - private fun AuthResult.Failure.toUiMessage(): LoginUiMessage = - when (this) { - AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited - AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown - } -} + private fun AuthResult.Failure.toUiMessage(): LoginUiMessage = + when (this) { + AuthResult.Failure.RateLimited -> LoginUiMessage.LoginFailedRateLimited + AuthResult.Failure.Unknown -> LoginUiMessage.LoginFailedUnknown + } + } From 5a5fc922a74ab351f2d28544976ace7c2bab91f2 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Fri, 3 Apr 2026 00:20:13 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20PrezelCheckbox=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EC=8B=9C=EB=A7=A8=ED=8B=B1=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PrezelCheckbox` 컴포넌트에 접근성 지원을 위한 시맨틱 정보를 추가하였습니다. * `modifier`에 `Role.Checkbox`를 지정하여 컴포넌트의 역할을 명시했습니다. * `ToggleableState`를 통해 현재 체크 상태(`checked`)를 시맨틱 트리에 반영했습니다. --- .../core/designsystem/component/PrezelCheckbox.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt index 5c0bd6bd..487a2faf 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelCheckbox.kt @@ -13,6 +13,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.team.prezel.core.designsystem.R @@ -38,7 +43,10 @@ fun PrezelCheckbox( onCheckedChange: (Boolean) -> Unit, ) { PrezelTouchArea( - modifier = modifier, + modifier = modifier.semantics { + role = Role.Checkbox + toggleableState = ToggleableState(checked) + }, onClick = { onCheckedChange(!checked) }, isUseRipple = false, extraTouchPadding = extraTouchPadding, From 98e83b30f0a5a14b5e9c52eaf137775f47d3ef1c Mon Sep 17 00:00:00 2001 From: moondev03 Date: Fri, 3 Apr 2026 00:36:46 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20LoginViewModel=20=EB=82=B4=20?= =?UTF-8?q?=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=AA=A8=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EB=94=A9=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 디버그 환경에서 로그인 시 로딩 상태를 해제하도록 수정 MVP 개발용 디버그 로직에서 약관 화면(`NavigateToTerms`)으로 이동하기 전, `isLoading` 상태를 `false`로 업데이트하여 로딩 인디케이터가 남지 않도록 개선했습니다. --- .../com/team/prezel/feature/login/impl/landing/LoginViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt index d22bbc66..11cf9fb5 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt @@ -50,6 +50,7 @@ internal class LoginViewModel // todo: MVP 개발 완료 후 해당 조건 제거 if (BuildConfig.DEBUG) { + update { copy(isLoading = false) } _uiEffect.send(LoginUiEffect.NavigateToTerms) } else { _uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider)) From 825a86488f381b44aed8b639c008f9556ec5dd61 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Fri, 3 Apr 2026 00:43:17 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20`WebView`=EC=9D=98=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=88=84=EC=88=98=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/terms/component/TermsDetailModal.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt index 07378112..87c2b7db 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/component/TermsDetailModal.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -62,9 +63,6 @@ private fun NotionWebView( AndroidView( modifier = modifier, factory = { webView }, - update = { view -> - view.url?.let(view::loadUrl) - }, ) } @@ -86,7 +84,7 @@ private const val NOTION_STYLE_PATCH = """ private fun rememberWebView(url: String): WebView { val context = LocalContext.current - return remember(url) { + val webView = remember(url) { WebView(context).apply { settings.javaScriptEnabled = true settings.domStorageEnabled = true @@ -104,4 +102,13 @@ private fun rememberWebView(url: String): WebView { loadUrl(url) } } + + DisposableEffect(url) { + onDispose { + webView.stopLoading() + webView.destroy() + } + } + + return webView } From 52177dda4ad78db57c505d116489df35f905931f Mon Sep 17 00:00:00 2001 From: moondev03 Date: Fri, 3 Apr 2026 00:44:16 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20TermsUiState=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=83=81=ED=83=9C=20=EA=B3=84=EC=82=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/login/impl/terms/contract/TermsUiState.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt index 1baaca6d..d9bfad75 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiState.kt @@ -8,9 +8,7 @@ internal data class TermsUiState( val isPrivacyPolicyChecked: Boolean = false, val isMarketingConsentChecked: Boolean = false, ) { - val isRequiredChecked: Boolean - get() = isTermsOfServiceChecked && isPrivacyPolicyChecked + val isRequiredChecked: Boolean = isTermsOfServiceChecked && isPrivacyPolicyChecked - val isAllChecked: Boolean - get() = isRequiredChecked && isMarketingConsentChecked + val isAllChecked: Boolean = isTermsOfServiceChecked && isPrivacyPolicyChecked && isMarketingConsentChecked } From bc4ccd0a374e94efe99237378434f306aed2af53 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Fri, 3 Apr 2026 13:44:35 +0900 Subject: [PATCH 13/14] =?UTF-8?q?style:=20TermsScreen=20=EB=82=B4=20UI=20I?= =?UTF-8?q?ntent=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=ED=8F=AC=EB=A7=B7=ED=8C=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 * refactor: `TermsUiIntent` 네이밍 변경 `TermsUiIntent`의 각 항목에서 불필요한 `On` 접두사를 제거하여 네이밍을 간결하게 수정했습니다. - `OnToggleAll` -> `ToggleAll` - `OnToggleTermsOfService` -> `ToggleTermsOfService` - `OnTogglePrivacyPolicy` -> `TogglePrivacyPolicy` - `OnToggleMarketingConsent` -> `ToggleMarketingConsent` - `OnClickContinue` -> `ClickContinue` * refactor: TermsViewModel 코드 포맷팅 및 구조 정리 - `TermsViewModel` 생성자 및 내부 메서드의 들여쓰기(Indentation)를 수정하여 가독성을 개선했습니다. - 변경된 `TermsUiIntent` 명칭에 맞춰 `onIntent` 내 `when` 분기 로직을 업데이트했습니다. * refactor: TermsScreen 내 Intent 호출부 업데이트 - `TermsScreen` 컴포저블에서 `ViewModel`로 Intent를 전달하는 코드를 신규 네이밍 규칙에 맞게 수정했습니다. --- .../feature/login/impl/terms/TermsScreen.kt | 10 +-- .../login/impl/terms/TermsViewModel.kt | 84 +++++++++---------- .../impl/terms/contract/TermsUiIntent.kt | 10 +-- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt index 12e6b9cc..043f7ee1 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.kt @@ -75,13 +75,13 @@ internal fun TermsScreen( TermsScreenScreen( uiState = uiState, onBack = navigateBack, - onToggleAll = { viewModel.onIntent(TermsUiIntent.OnToggleAll) }, - onToggleTermsOfService = { viewModel.onIntent(TermsUiIntent.OnToggleTermsOfService) }, - onTogglePrivacyPolicy = { viewModel.onIntent(TermsUiIntent.OnTogglePrivacyPolicy) }, - onToggleMarketingConsent = { viewModel.onIntent(TermsUiIntent.OnToggleMarketingConsent) }, + onToggleAll = { viewModel.onIntent(TermsUiIntent.ToggleAll) }, + onToggleTermsOfService = { viewModel.onIntent(TermsUiIntent.ToggleTermsOfService) }, + onTogglePrivacyPolicy = { viewModel.onIntent(TermsUiIntent.TogglePrivacyPolicy) }, + onToggleMarketingConsent = { viewModel.onIntent(TermsUiIntent.ToggleMarketingConsent) }, onClickTermsOfServiceDetail = { activeDetailUrl = BuildConfig.TERMS_OF_SERVICE_URL }, onClickPrivacyPolicyDetail = { activeDetailUrl = BuildConfig.PRIVACY_POLICY_URL }, - onContinue = { viewModel.onIntent(TermsUiIntent.OnClickContinue) }, + onContinue = { viewModel.onIntent(TermsUiIntent.ClickContinue) }, modifier = modifier, ) } diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt index 13d089ac..62e5d2e3 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt @@ -17,58 +17,58 @@ import javax.inject.Inject @HiltViewModel internal class TermsViewModel - @Inject - constructor() : ViewModel() { - private val _uiState = MutableStateFlow(TermsUiState()) - val uiState: StateFlow = _uiState - private val currentState: TermsUiState - get() = uiState.value +@Inject +constructor() : ViewModel() { + private val _uiState = MutableStateFlow(TermsUiState()) + val uiState: StateFlow = _uiState + private val currentState: TermsUiState + get() = uiState.value - private val _uiEffect = Channel() - val uiEffect: Flow = _uiEffect.receiveAsFlow() + private val _uiEffect = Channel() + val uiEffect: Flow = _uiEffect.receiveAsFlow() - fun onIntent(intent: TermsUiIntent) { - when (intent) { - TermsUiIntent.OnToggleAll -> toggleAll() - TermsUiIntent.OnToggleTermsOfService -> toggleTermsOfService() - TermsUiIntent.OnTogglePrivacyPolicy -> togglePrivacyPolicy() - TermsUiIntent.OnToggleMarketingConsent -> toggleMarketingConsent() - TermsUiIntent.OnClickContinue -> handleClickContinue() - } + fun onIntent(intent: TermsUiIntent) { + when (intent) { + TermsUiIntent.ToggleAll -> toggleAll() + TermsUiIntent.ToggleTermsOfService -> toggleTermsOfService() + TermsUiIntent.TogglePrivacyPolicy -> togglePrivacyPolicy() + TermsUiIntent.ToggleMarketingConsent -> toggleMarketingConsent() + TermsUiIntent.ClickContinue -> handleClickContinue() } + } - private fun update(reducer: TermsUiState.() -> TermsUiState) { - _uiState.update(reducer) - } + private fun update(reducer: TermsUiState.() -> TermsUiState) { + _uiState.update(reducer) + } - private fun toggleAll() { - val newChecked = !currentState.isAllChecked + private fun toggleAll() { + val newChecked = !currentState.isAllChecked - update { - copy( - isTermsOfServiceChecked = newChecked, - isPrivacyPolicyChecked = newChecked, - isMarketingConsentChecked = newChecked, - ) - } + update { + copy( + isTermsOfServiceChecked = newChecked, + isPrivacyPolicyChecked = newChecked, + isMarketingConsentChecked = newChecked, + ) } + } - private fun toggleTermsOfService() { - update { copy(isTermsOfServiceChecked = !isTermsOfServiceChecked) } - } + private fun toggleTermsOfService() { + update { copy(isTermsOfServiceChecked = !isTermsOfServiceChecked) } + } - private fun togglePrivacyPolicy() { - update { copy(isPrivacyPolicyChecked = !isPrivacyPolicyChecked) } - } + private fun togglePrivacyPolicy() { + update { copy(isPrivacyPolicyChecked = !isPrivacyPolicyChecked) } + } - private fun toggleMarketingConsent() { - update { copy(isMarketingConsentChecked = !isMarketingConsentChecked) } - } + private fun toggleMarketingConsent() { + update { copy(isMarketingConsentChecked = !isMarketingConsentChecked) } + } - private fun handleClickContinue() { - if (!currentState.isRequiredChecked) return - viewModelScope.launch { - _uiEffect.send(TermsUiEffect.NavigateToHome) - } + private fun handleClickContinue() { + if (!currentState.isRequiredChecked) return + viewModelScope.launch { + _uiEffect.send(TermsUiEffect.NavigateToHome) } } +} diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt index cc123669..d4c0264c 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiIntent.kt @@ -1,13 +1,13 @@ package com.team.prezel.feature.login.impl.terms.contract internal sealed interface TermsUiIntent { - data object OnToggleAll : TermsUiIntent + data object ToggleAll : TermsUiIntent - data object OnToggleTermsOfService : TermsUiIntent + data object ToggleTermsOfService : TermsUiIntent - data object OnTogglePrivacyPolicy : TermsUiIntent + data object TogglePrivacyPolicy : TermsUiIntent - data object OnToggleMarketingConsent : TermsUiIntent + data object ToggleMarketingConsent : TermsUiIntent - data object OnClickContinue : TermsUiIntent + data object ClickContinue : TermsUiIntent } From 9036b9855cd5081ee66796236239e68c46343dd2 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Fri, 3 Apr 2026 14:06:41 +0900 Subject: [PATCH 14/14] =?UTF-8?q?style:=20TermsScreen=20=EB=82=B4=20UI=20I?= =?UTF-8?q?ntent=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=ED=8F=AC=EB=A7=B7=ED=8C=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 * refactor: `TermsUiIntent` 네이밍 변경 `TermsUiIntent`의 각 항목에서 불필요한 `On` 접두사를 제거하여 네이밍을 간결하게 수정했습니다. - `OnToggleAll` -> `ToggleAll` - `OnToggleTermsOfService` -> `ToggleTermsOfService` - `OnTogglePrivacyPolicy` -> `TogglePrivacyPolicy` - `OnToggleMarketingConsent` -> `ToggleMarketingConsent` - `OnClickContinue` -> `ClickContinue` * refactor: TermsViewModel 코드 포맷팅 및 구조 정리 - `TermsViewModel` 생성자 및 내부 메서드의 들여쓰기(Indentation)를 수정하여 가독성을 개선했습니다. - 변경된 `TermsUiIntent` 명칭에 맞춰 `onIntent` 내 `when` 분기 로직을 업데이트했습니다. * refactor: TermsScreen 내 Intent 호출부 업데이트 - `TermsScreen` 컴포저블에서 `ViewModel`로 Intent를 전달하는 코드를 신규 네이밍 규칙에 맞게 수정했습니다. --- .../login/impl/terms/TermsViewModel.kt | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt index 62e5d2e3..0d838b3b 100644 --- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt +++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt @@ -17,58 +17,58 @@ import javax.inject.Inject @HiltViewModel internal class TermsViewModel -@Inject -constructor() : ViewModel() { - private val _uiState = MutableStateFlow(TermsUiState()) - val uiState: StateFlow = _uiState - private val currentState: TermsUiState - get() = uiState.value + @Inject + constructor() : ViewModel() { + private val _uiState = MutableStateFlow(TermsUiState()) + val uiState: StateFlow = _uiState + private val currentState: TermsUiState + get() = uiState.value - private val _uiEffect = Channel() - val uiEffect: Flow = _uiEffect.receiveAsFlow() + private val _uiEffect = Channel() + val uiEffect: Flow = _uiEffect.receiveAsFlow() - fun onIntent(intent: TermsUiIntent) { - when (intent) { - TermsUiIntent.ToggleAll -> toggleAll() - TermsUiIntent.ToggleTermsOfService -> toggleTermsOfService() - TermsUiIntent.TogglePrivacyPolicy -> togglePrivacyPolicy() - TermsUiIntent.ToggleMarketingConsent -> toggleMarketingConsent() - TermsUiIntent.ClickContinue -> handleClickContinue() + fun onIntent(intent: TermsUiIntent) { + when (intent) { + TermsUiIntent.ToggleAll -> toggleAll() + TermsUiIntent.ToggleTermsOfService -> toggleTermsOfService() + TermsUiIntent.TogglePrivacyPolicy -> togglePrivacyPolicy() + TermsUiIntent.ToggleMarketingConsent -> toggleMarketingConsent() + TermsUiIntent.ClickContinue -> handleClickContinue() + } } - } - private fun update(reducer: TermsUiState.() -> TermsUiState) { - _uiState.update(reducer) - } + private fun update(reducer: TermsUiState.() -> TermsUiState) { + _uiState.update(reducer) + } - private fun toggleAll() { - val newChecked = !currentState.isAllChecked + private fun toggleAll() { + val newChecked = !currentState.isAllChecked - update { - copy( - isTermsOfServiceChecked = newChecked, - isPrivacyPolicyChecked = newChecked, - isMarketingConsentChecked = newChecked, - ) + update { + copy( + isTermsOfServiceChecked = newChecked, + isPrivacyPolicyChecked = newChecked, + isMarketingConsentChecked = newChecked, + ) + } } - } - private fun toggleTermsOfService() { - update { copy(isTermsOfServiceChecked = !isTermsOfServiceChecked) } - } + private fun toggleTermsOfService() { + update { copy(isTermsOfServiceChecked = !isTermsOfServiceChecked) } + } - private fun togglePrivacyPolicy() { - update { copy(isPrivacyPolicyChecked = !isPrivacyPolicyChecked) } - } + private fun togglePrivacyPolicy() { + update { copy(isPrivacyPolicyChecked = !isPrivacyPolicyChecked) } + } - private fun toggleMarketingConsent() { - update { copy(isMarketingConsentChecked = !isMarketingConsentChecked) } - } + private fun toggleMarketingConsent() { + update { copy(isMarketingConsentChecked = !isMarketingConsentChecked) } + } - private fun handleClickContinue() { - if (!currentState.isRequiredChecked) return - viewModelScope.launch { - _uiEffect.send(TermsUiEffect.NavigateToHome) + private fun handleClickContinue() { + if (!currentState.isRequiredChecked) return + viewModelScope.launch { + _uiEffect.send(TermsUiEffect.NavigateToHome) + } } } -}