Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.afternote.feature.setting.presentation.account

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -14,10 +15,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.afternote.core.ui.loading.LoadingBody
import com.afternote.core.ui.theme.AfternoteDesign
import com.afternote.core.ui.topbar.DetailTopBar
import com.afternote.feature.setting.presentation.R
import com.afternote.feature.setting.presentation.account.component.SocialAccountRow
import com.afternote.feature.setting.presentation.shared.component.SettingLoadErrorContent

/**
* 연결된 계정 화면의 상태 없는 본문.
Expand All @@ -30,6 +33,7 @@ internal fun ConnectedAccountsContent(
snackbarHostState: SnackbarHostState,
onBack: () -> Unit,
onToggle: (provider: String, enabled: Boolean) -> Unit,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
Expand All @@ -43,30 +47,46 @@ internal fun ConnectedAccountsContent(
)
},
) { paddingValues ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 20.dp, vertical = 16.dp),
) {
Text(
text = stringResource(R.string.setting_sns_login_section_title),
style = AfternoteDesign.typography.bodyLargeR,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.setting_sns_login_section_desc),
style = AfternoteDesign.typography.bodySmallR,
)
Spacer(modifier = Modifier.height(24.dp))
when {
uiState.isLoading -> {
LoadingBody(modifier = Modifier.padding(paddingValues))
}

uiState.accounts.forEach { account ->
SocialAccountRow(
account = account,
onToggle = { enabled -> onToggle(account.provider, enabled) },
uiState.errorMessage != null -> {
SettingLoadErrorContent(
message = uiState.errorMessage,
onRetry = onRetry,
modifier = Modifier.padding(paddingValues),
)
Spacer(modifier = Modifier.height(8.dp))
}

else -> {
Column(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 20.dp, vertical = 16.dp),
) {
Text(
text = stringResource(R.string.setting_sns_login_section_title),
style = AfternoteDesign.typography.bodyLargeR,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.setting_sns_login_section_desc),
style = AfternoteDesign.typography.bodySmallR,
)
Spacer(modifier = Modifier.height(24.dp))

uiState.accounts.forEach { account ->
SocialAccountRow(
account = account,
onToggle = { enabled -> if (!uiState.isUpdating) onToggle(account.provider, enabled) },
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import com.afternote.core.ui.mvi.MviIntent
import com.afternote.core.ui.mvi.ReducerEvent

internal sealed interface ConnectedAccountsIntent : MviIntent {
data object RetryLoad : ConnectedAccountsIntent

data class ConsumeError(
val message: String,
) : ConnectedAccountsIntent

data class Toggle(
val provider: String,
val enabled: Boolean,
Expand All @@ -22,6 +28,14 @@ internal sealed interface ConnectedAccountsIntent : MviIntent {
}

internal sealed interface ConnectedAccountsReducerEvent : ReducerEvent {
data object Loading : ConnectedAccountsReducerEvent

data object Changing : ConnectedAccountsReducerEvent

data class ErrorConsumed(
val message: String,
) : ConnectedAccountsReducerEvent

data class Loaded(
val accounts: List<SocialAccountState>,
) : ConnectedAccountsReducerEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,17 @@ internal fun ConnectedAccountsScreen(
}
}

uiState.pendingError?.let { message ->
ObserveSignal(message, ConnectedAccountsIntent.ConsumeError(message), viewModel::onIntent) {
linkScope.launch { snackbarHostState.showSnackbar(it) }
}
}

ConnectedAccountsContent(
uiState = uiState,
snackbarHostState = snackbarHostState,
onBack = onBack,
onRetry = { viewModel.onIntent(ConnectedAccountsIntent.RetryLoad) },
onToggle = { provider, enabled -> viewModel.onIntent(ConnectedAccountsIntent.Toggle(provider, enabled)) },
modifier = modifier,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ data class ConnectedAccountsUiState(
val isLoading: Boolean = false,
val accounts: List<SocialAccountState> = emptyList(),
val errorMessage: String? = null,
val isUpdating: Boolean = false,
val pendingError: String? = null,
/**
* 화면이 플랫폼 인증(카카오 SDK·Credential Manager)을 시작해야 하는 제공자 신호다 (#1502).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.afternote.core.ui.mvi.MviViewModel
import com.afternote.feature.setting.domain.SettingAccountRepository
import com.afternote.feature.setting.presentation.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -18,12 +19,22 @@ internal class ConnectedAccountsViewModel
) : MviViewModel<ConnectedAccountsIntent, ConnectedAccountsUiState, ConnectedAccountsReducerEvent>(
ConnectedAccountsUiState(isLoading = true),
) {
private var loadJob: Job? = null

init {
loadConnectedAccounts()
}

override fun onIntent(intent: ConnectedAccountsIntent) {
when (intent) {
ConnectedAccountsIntent.RetryLoad -> {
if (currentState.errorMessage != null) loadConnectedAccounts()
}

is ConnectedAccountsIntent.ConsumeError -> {
dispatch(ConnectedAccountsReducerEvent.ErrorConsumed(intent.message))
}

is ConnectedAccountsIntent.Toggle -> {
toggle(intent.provider, intent.enabled)
}
Expand All @@ -45,20 +56,38 @@ internal class ConnectedAccountsViewModel
event: ConnectedAccountsReducerEvent,
): ConnectedAccountsUiState =
when (event) {
ConnectedAccountsReducerEvent.Loading -> {
state.copy(isLoading = true, errorMessage = null)
}

ConnectedAccountsReducerEvent.Changing -> {
state.copy(isUpdating = true, pendingError = null)
}

is ConnectedAccountsReducerEvent.ErrorConsumed -> {
if (state.pendingError ==
event.message
) {
state.copy(pendingError = null)
} else {
state
}
}

is ConnectedAccountsReducerEvent.Loaded -> {
state.copy(isLoading = false, accounts = event.accounts)
state.copy(isLoading = false, errorMessage = null, accounts = event.accounts)
}

is ConnectedAccountsReducerEvent.LoadFailed -> {
state.copy(isLoading = false, errorMessage = event.message)
}

is ConnectedAccountsReducerEvent.AccountsChanged -> {
state.copy(accounts = event.accounts)
state.copy(accounts = event.accounts, isUpdating = false)
}

is ConnectedAccountsReducerEvent.ChangeFailed -> {
state.copy(errorMessage = event.message)
state.copy(pendingError = event.message, isUpdating = false)
}

is ConnectedAccountsReducerEvent.LinkRequested -> {
Expand All @@ -72,20 +101,24 @@ internal class ConnectedAccountsViewModel
}

private fun loadConnectedAccounts() {
viewModelScope.launch {
runCatchingCancellable { accountRepository.getConnectedAccounts() }
.onSuccess { accounts ->
dispatch(ConnectedAccountsReducerEvent.Loaded(accounts.toStateList()))
}.onFailure {
dispatch(ConnectedAccountsReducerEvent.LoadFailed("계정 정보를 불러올 수 없습니다."))
}
}
if (loadJob?.isActive == true) return
dispatch(ConnectedAccountsReducerEvent.Loading)
loadJob =
viewModelScope.launch {
runCatchingCancellable { accountRepository.getConnectedAccounts() }
.onSuccess { accounts ->
dispatch(ConnectedAccountsReducerEvent.Loaded(accounts.toStateList()))
}.onFailure {
dispatch(ConnectedAccountsReducerEvent.LoadFailed("계정 정보를 불러올 수 없습니다."))
}
}
}

private fun toggle(
provider: String,
enabled: Boolean,
) {
if (currentState.isLoading || currentState.errorMessage != null || currentState.isUpdating) return
if (enabled) {
dispatch(ConnectedAccountsReducerEvent.LinkRequested(provider))
} else {
Expand All @@ -97,6 +130,8 @@ internal class ConnectedAccountsViewModel
provider: String,
accessToken: String,
) {
if (currentState.isLoading || currentState.errorMessage != null || currentState.isUpdating) return
dispatch(ConnectedAccountsReducerEvent.Changing)
viewModelScope.launch {
runCatchingCancellable { accountRepository.linkConnectedAccount(provider, accessToken) }
.onSuccess { accounts -> dispatch(ConnectedAccountsReducerEvent.AccountsChanged(accounts.toStateList())) }
Expand All @@ -105,6 +140,7 @@ internal class ConnectedAccountsViewModel
}

private fun unlink(provider: String) {
dispatch(ConnectedAccountsReducerEvent.Changing)
viewModelScope.launch {
runCatchingCancellable { accountRepository.unlinkConnectedAccount(provider) }
.onSuccess { accounts -> dispatch(ConnectedAccountsReducerEvent.AccountsChanged(accounts.toStateList())) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.afternote.feature.setting.presentation.home

import com.afternote.core.ui.mvi.MviIntent
import com.afternote.core.ui.mvi.ReducerEvent
import com.afternote.core.ui.mvi.UiState

internal data class SettingUiState(
val profile: SettingProfileState = SettingProfileState.Loading,
val logoutCompleted: Unit? = null,
val withdraw: WithdrawUiState = WithdrawUiState.Idle,
) : UiState

internal sealed interface SettingProfileState {
data object Loading : SettingProfileState

data class Success(
val name: String,
val email: String,
) : SettingProfileState

/** 문구는 화면이 `setting_profile_load_error` 리소스로 그린다. 프로필 수정 화면과 같은 문장이다. */
data object Error : SettingProfileState
}

internal sealed interface WithdrawUiState {
data object Idle : WithdrawUiState

data object Loading : WithdrawUiState

data object Success : WithdrawUiState

data object Error : WithdrawUiState
}

internal sealed interface SettingIntent : MviIntent {
data object Refresh : SettingIntent

data object Logout : SettingIntent

data object ConsumeLogoutSuccess : SettingIntent

data object DeleteAccount : SettingIntent

data object DismissWithdrawError : SettingIntent
}

internal sealed interface SettingReducerEvent : ReducerEvent {
data object ProfileLoading : SettingReducerEvent

data class ProfileLoaded(
val name: String,
val email: String,
) : SettingReducerEvent

data object ProfileFailed : SettingReducerEvent

data object LoggedOut : SettingReducerEvent

data object LogoutConsumed : SettingReducerEvent

data object Withdrawing : SettingReducerEvent

data object Withdrawn : SettingReducerEvent

data object WithdrawFailed : SettingReducerEvent

data object WithdrawErrorDismissed : SettingReducerEvent
}
Loading
Loading