Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -176,6 +177,7 @@ private fun PrezelAccordionPreview() {
modifier = Modifier.drawDashBorder(),
size = CheckboxSize.REGULAR,
onCheckedChange = { state = !state },
extraTouchPadding = PaddingValues(),
)
},
trailingContent = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
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.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
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
Expand All @@ -34,48 +39,47 @@ 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.semantics {
role = Role.Checkbox
toggleableState = ToggleableState(checked)
},
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,6 +28,7 @@ fun PrezelTopAppBar(
leadingIcon: @Composable () -> Unit = {},
trailingIcons: @Composable RowScope.() -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
windowInsets: WindowInsets = WindowInsets(),
) {
TopAppBar(
title = {
Expand All @@ -38,6 +40,7 @@ fun PrezelTopAppBar(
actions = trailingIcons,
colors = prezelTopAppBarColors(),
scrollBehavior = scrollBehavior,
windowInsets = windowInsets,
modifier = modifier.testTag("PrezelTopAppBar"),
)
}
Expand Down
11 changes: 11 additions & 0 deletions Prezel/feature/login/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -40,58 +42,54 @@ 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
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) {
LoginUiMessage.LoginCancelled -> R.string.feature_login_impl_kakao_cancelled
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))
}
}
}
}

LoginScreen(
uiState = uiState,
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
onLogin = { viewModel.onIntent(LoginUiIntent.OnClickLogin(provider = AuthProvider.KAKAO)) },
modifier = modifier,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -182,7 +181,7 @@ private fun LoginFooter(
PrezelButtonArea {
CustomButton(
iconResId = PrezelIcons.Kakao,
label = "카카오로 시작하기",
label = startWithKakaoLabel,
enabled = enabled,
onClick = onLogin,
config = kakaoButtonConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
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.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
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
Expand All @@ -15,20 +20,20 @@ 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<LoginUiState> = _uiState
val currentState: LoginUiState
private val currentState: LoginUiState
get() = uiState.value

private val _uiEffect = Channel<LoginUiEffect>()
val uiEffect: Flow<LoginUiEffect> = _uiEffect.receiveAsFlow()

fun onIntent(intent: LoginUiIntent) {
when (intent) {
is LoginUiIntent.OnClickLogin -> handleClickLogin()
is LoginUiIntent.OnClickLogin -> handleClickLogin(provider = intent.provider)
is LoginUiIntent.OnLoginResult -> handleLoginResult(result = intent.result)
}
}
Expand All @@ -37,14 +42,19 @@ 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)
// todo: MVP 개발 완료 후 해당 조건 제거
if (BuildConfig.DEBUG) {
update { copy(isLoading = false) }
_uiEffect.send(LoginUiEffect.NavigateToTerms)
} else {
_uiEffect.send(LoginUiEffect.LaunchLogin(provider = provider))
}
}
}

Expand All @@ -53,22 +63,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading