diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedDialog.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedDialog.kt new file mode 100644 index 00000000..afbde5e0 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedDialog.kt @@ -0,0 +1,126 @@ +package com.ninecraft.booket.core.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme + +@Composable +fun ReedDialog( + title: String, + confirmButtonText: String, + onConfirmRequest: () -> Unit, + modifier: Modifier = Modifier, + description: String? = null, + dismissButtonText: String? = null, + onDismissRequest: () -> Unit = {}, +) { + Dialog( + onDismissRequest = { + onDismissRequest() + }, + ) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = ReedTheme.colors.basePrimary, + shape = RoundedCornerShape( + ReedTheme.radius.lg, + ), + ) + .padding( + start = ReedTheme.spacing.spacing5, + top = ReedTheme.spacing.spacing8, + end = ReedTheme.spacing.spacing5, + bottom = ReedTheme.spacing.spacing5, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = title, + color = ReedTheme.colors.contentPrimary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.headline1SemiBold, + ) + description?.let { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + Text( + text = description, + color = ReedTheme.colors.contentSecondary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.body2Medium, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + Row( + modifier = Modifier.fillMaxWidth(), + ) { + dismissButtonText?.let { + ReedButton( + onClick = { + onDismissRequest() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.SECONDARY, + modifier = Modifier.weight(1f), + text = dismissButtonText, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + } + ReedButton( + onClick = { + onConfirmRequest() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY, + modifier = Modifier.weight(1f), + text = confirmButtonText, + ) + } + } + } +} + +@Preview +@Composable +private fun ReedConfirmDialogPreview() { + ReedTheme { + ReedDialog( + title = "Title", + confirmButtonText = "확인", + onConfirmRequest = {}, + description = "subtext", + ) + } +} + +@Preview +@Composable +private fun ReedChoiceDialogPreview() { + ReedTheme { + ReedDialog( + title = "Title", + confirmButtonText = "확인", + onConfirmRequest = {}, + description = "subtext", + dismissButtonText = "취소", + ) + } +} diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt index 8bf4d852..d23c2ec7 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.utils.handleException -import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.screens.LibraryScreen import com.ninecraft.booket.screens.LoginScreen @@ -25,7 +24,6 @@ import kotlinx.coroutines.launch class LibraryPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, - private val authRepository: AuthRepository, private val userRepository: UserRepository, ) : Presenter { @@ -80,35 +78,6 @@ class LibraryPresenter @AssistedInject constructor( is LibraryUiEvent.OnSettingsClick -> { navigator.goTo(SettingsScreen) } - - is LibraryUiEvent.OnLogoutButtonClick -> { - scope.launch { - try { - isLoading = true - authRepository.logout() - .onSuccess { - navigator.resetRoot(LoginScreen) - } - .onFailure { exception -> - val handleErrorMessage = { message: String -> - Logger.e(message) - sideEffect = LibrarySideEffect.ShowToast(message) - } - - handleException( - exception = exception, - onServerError = handleErrorMessage, - onNetworkError = handleErrorMessage, - onLoginRequired = { - navigator.resetRoot(LoginScreen) - }, - ) - } - } finally { - isLoading = false - } - } - } } } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt index b3644586..b10b812b 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt @@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -17,13 +15,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.DevicePreview -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.screens.LibraryScreen import com.slack.circuit.codegen.annotations.CircuitInject @@ -86,19 +80,6 @@ internal fun LibraryContent( Spacer(modifier = Modifier.height(16.dp)) Text(text = state.email) } - ReedButton( - onClick = { - state.eventSink(LibraryUiEvent.OnLogoutButtonClick) - }, - modifier = Modifier - .fillMaxWidth() - .padding(start = 32.dp, end = 32.dp, bottom = 32.dp) - .height(56.dp) - .align(Alignment.BottomCenter), - colorStyle = ReedButtonColorStyle.PRIMARY, - sizeStyle = largeButtonStyle, - text = stringResource(id = R.string.logout), - ) if (state.isLoading) { CircularProgressIndicator( diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt index d90106b7..51285063 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt @@ -18,5 +18,4 @@ sealed interface LibrarySideEffect { sealed interface LibraryUiEvent : CircuitUiEvent { data object InitSideEffect : LibraryUiEvent data object OnSettingsClick : LibraryUiEvent - data object OnLogoutButtonClick : LibraryUiEvent } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt index 8d3bed49..ce38951a 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt @@ -3,9 +3,14 @@ package com.ninecraft.booket.feature.settings import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import com.ninecraft.booket.core.common.utils.handleException +import com.ninecraft.booket.core.data.api.repository.AuthRepository +import com.ninecraft.booket.screens.LoginScreen import com.ninecraft.booket.screens.OssLicensesScreen import com.ninecraft.booket.screens.SettingsScreen +import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator @@ -14,19 +19,28 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.components.ActivityRetainedComponent +import kotlinx.coroutines.launch class SettingsPresenter @AssistedInject constructor( @Assisted val navigator: Navigator, + private val authRepository: AuthRepository, ) : Presenter { @Composable override fun present(): SettingsUiState { - var isLogoutBottomSheetVisible by rememberRetained { mutableStateOf(false) } + val scope = rememberCoroutineScope() + var isLoading by rememberRetained { mutableStateOf(false) } + var sideEffect by rememberRetained { mutableStateOf(null) } + var isLogoutDialogVisible by rememberRetained { mutableStateOf(false) } var isWithdrawBottomSheetVisible by rememberRetained { mutableStateOf(false) } var isWithdrawConfirmed by rememberRetained { mutableStateOf(false) } fun handleEvent(event: SettingsUiEvent) { when (event) { + is SettingsUiEvent.InitSideEffect -> { + sideEffect = null + } + is SettingsUiEvent.OnBackClick -> { navigator.pop() } @@ -40,7 +54,7 @@ class SettingsPresenter @AssistedInject constructor( } is SettingsUiEvent.OnLogoutClick -> { - isLogoutBottomSheetVisible = true + isLogoutDialogVisible = true } is SettingsUiEvent.OnWithdrawClick -> { @@ -48,7 +62,7 @@ class SettingsPresenter @AssistedInject constructor( } is SettingsUiEvent.OnBottomSheetDismissed -> { - isLogoutBottomSheetVisible = false + isLogoutDialogVisible = false isWithdrawBottomSheetVisible = false isWithdrawConfirmed = false } @@ -58,7 +72,33 @@ class SettingsPresenter @AssistedInject constructor( } is SettingsUiEvent.Logout -> { - // TODO: 로그아웃 처리 -> 성공 시 로그인 화면으로 이동 + scope.launch { + try { + isLoading = true + authRepository.logout() + .onSuccess { + navigator.resetRoot(LoginScreen) + } + .onFailure { exception -> + val handleErrorMessage = { message: String -> + Logger.e(message) + sideEffect = SettingsSideEffect.ShowToast(message) + } + + handleException( + exception = exception, + onServerError = handleErrorMessage, + onNetworkError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen) + }, + ) + } + } finally { + isLoading = false + } + } + isLogoutDialogVisible = false } is SettingsUiEvent.Withdraw -> { @@ -67,9 +107,11 @@ class SettingsPresenter @AssistedInject constructor( } } return SettingsUiState( - isLogoutBottomSheetVisible = isLogoutBottomSheetVisible, + isLoading = isLoading, + isLogoutDialogVisible = isLogoutDialogVisible, isWithdrawBottomSheetVisible = isWithdrawBottomSheetVisible, isWithdrawConfirmed = isWithdrawConfirmed, + sideEffect = sideEffect, eventSink = ::handleEvent, ) } diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsScreen.kt index 10c6e9aa..250103f5 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsScreen.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.feature.settings import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -8,6 +9,7 @@ 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.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -24,11 +26,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import com.ninecraft.booket.core.common.extensions.clickableSingle import com.ninecraft.booket.core.designsystem.DevicePreview +import com.ninecraft.booket.core.designsystem.component.ReedDialog import com.ninecraft.booket.core.designsystem.component.appbar.ReedBackTopAppBar import com.ninecraft.booket.core.designsystem.component.divider.ReedDivider import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.feature.settings.component.LogoutConfirmationBottomSheet import com.ninecraft.booket.feature.settings.component.WithdrawConfirmationBottomSheet import com.ninecraft.booket.screens.SettingsScreen import com.slack.circuit.codegen.annotations.CircuitInject @@ -42,7 +44,11 @@ internal fun Settings( state: SettingsUiState, modifier: Modifier = Modifier, ) { - val logoutSheetState = rememberModalBottomSheetState() + HandleSettingsSideEffects( + state = state, + eventSink = state.eventSink, + ) + val withDrawSheetState = rememberModalBottomSheetState() val coroutineScope = rememberCoroutineScope() @@ -131,21 +137,28 @@ internal fun Settings( ) } - if (state.isLogoutBottomSheetVisible) { - LogoutConfirmationBottomSheet( + if (state.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = ReedTheme.colors.contentBrand, + ) + } + } + + if (state.isLogoutDialogVisible) { + ReedDialog( + title = stringResource(R.string.settings_logout_title), + confirmButtonText = stringResource(R.string.settings_logout), + dismissButtonText = stringResource(R.string.settings_cancel), + onConfirmRequest = { + state.eventSink(SettingsUiEvent.Logout) + }, onDismissRequest = { state.eventSink(SettingsUiEvent.OnBottomSheetDismissed) }, - sheetState = logoutSheetState, - onCancelButtonClick = { - coroutineScope.launch { - logoutSheetState.hide() - state.eventSink(SettingsUiEvent.OnBottomSheetDismissed) - } - }, - onLogoutButtonClick = { - state.eventSink(SettingsUiEvent.Logout) - }, ) } @@ -212,9 +225,6 @@ private fun SettingsScreenPreview() { ReedTheme { Settings( state = SettingsUiState( - isLogoutBottomSheetVisible = false, - isWithdrawBottomSheetVisible = false, - isWithdrawConfirmed = false, eventSink = {}, ), ) diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsSideEffect.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsSideEffect.kt new file mode 100644 index 00000000..8f5cc8c8 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsSideEffect.kt @@ -0,0 +1,27 @@ +package com.ninecraft.booket.feature.settings + +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.skydoves.compose.effects.RememberedEffect + +@Composable +internal fun HandleSettingsSideEffects( + state: SettingsUiState, + eventSink: (SettingsUiEvent) -> Unit, +) { + val context = LocalContext.current + + RememberedEffect(state.sideEffect) { + when (state.sideEffect) { + is SettingsSideEffect.ShowToast -> { + Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() + } + null -> {} + } + + if (state.sideEffect != null) { + eventSink(SettingsUiEvent.InitSideEffect) + } + } +} diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt index 0b47776d..3d6bb3a9 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsUiState.kt @@ -4,13 +4,20 @@ import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState data class SettingsUiState( - val isLogoutBottomSheetVisible: Boolean, - val isWithdrawBottomSheetVisible: Boolean, - val isWithdrawConfirmed: Boolean, + val isLoading: Boolean = false, + val isLogoutDialogVisible: Boolean = false, + val isWithdrawBottomSheetVisible: Boolean = false, + val isWithdrawConfirmed: Boolean = false, + val sideEffect: SettingsSideEffect? = null, val eventSink: (SettingsUiEvent) -> Unit, ) : CircuitUiState +sealed interface SettingsSideEffect { + data class ShowToast(val message: String) : SettingsSideEffect +} + sealed interface SettingsUiEvent : CircuitUiEvent { + data object InitSideEffect : SettingsUiEvent data object OnBackClick : SettingsUiEvent data class OnTermDetailClick(val title: String) : SettingsUiEvent data object OnOssLicensesClick : SettingsUiEvent diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/LogoutConfirmationBottomSheet.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/LogoutConfirmationBottomSheet.kt deleted file mode 100644 index 1f3897b2..00000000 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/component/LogoutConfirmationBottomSheet.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.ninecraft.booket.feature.settings.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.component.bottomsheet.ReedBottomSheet -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.feature.settings.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LogoutConfirmationBottomSheet( - onDismissRequest: () -> Unit, - sheetState: SheetState, - onCancelButtonClick: () -> Unit, - onLogoutButtonClick: () -> Unit, -) { - ReedBottomSheet( - onDismissRequest = { - onDismissRequest() - }, - sheetState = sheetState, - ) { - Column( - modifier = Modifier - .padding( - start = ReedTheme.spacing.spacing5, - top = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - ) { - Text( - text = stringResource(R.string.settings_logout_title), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = ReedTheme.spacing.spacing3), - color = ReedTheme.colors.contentPrimary, - textAlign = TextAlign.Center, - style = ReedTheme.typography.heading2SemiBold, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - ReedButton( - onClick = { - onCancelButtonClick() - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.SECONDARY, - modifier = Modifier.weight(1f), - text = stringResource(R.string.settings_cancel), - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - ReedButton( - onClick = { - onLogoutButtonClick() - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.PRIMARY, - modifier = Modifier.weight(1f), - text = stringResource(R.string.settings_logout), - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview(showBackground = true) -@Composable -private fun LogoutConfirmationBottomSheetPreview() { - val sheetState = SheetState( - skipPartiallyExpanded = true, - initialValue = SheetValue.Expanded, - positionalThreshold = { 0f }, - velocityThreshold = { 0f }, - ) - ReedTheme { - LogoutConfirmationBottomSheet( - onDismissRequest = {}, - sheetState = sheetState, - onCancelButtonClick = {}, - onLogoutButtonClick = {}, - ) - } -}