From 5ea27e7b37ff5fd10a08693f6126d659c62ff492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 1 Oct 2026 20:46:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=84=A4=EC=A0=95=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=98=A4=EB=A5=98=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=EC=99=80=20=EC=A0=80=EC=9E=A5=20=EC=8B=A4=ED=8C=A8=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EC=97=B0=EA=B2=B0=20(#708)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/ConnectedAccountsContent.kt | 64 +++-- .../account/ConnectedAccountsContract.kt | 14 ++ .../account/ConnectedAccountsScreen.kt | 7 + .../account/ConnectedAccountsUiState.kt | 2 + .../account/ConnectedAccountsViewModel.kt | 58 ++++- .../presentation/home/SettingContract.kt | 68 ++++++ .../presentation/home/SettingScreen.kt | 219 +++++++++--------- .../presentation/home/SettingViewModel.kt | 156 +++++++------ .../home/WithdrawConfirmScreen.kt | 14 +- .../presentation/home/WithdrawGuideScreen.kt | 6 +- .../notification/PushNotificationContent.kt | 34 ++- .../notification/PushNotificationContract.kt | 2 + .../notification/PushNotificationScreen.kt | 1 + .../notification/PushNotificationUiState.kt | 2 + .../notification/PushNotificationViewModel.kt | 48 ++-- .../profile/ProfileEditContract.kt | 4 + .../presentation/profile/ProfileEditScreen.kt | 34 ++- .../profile/ProfileEditViewModel.kt | 41 ++-- .../component/SettingLoadErrorContent.kt | 42 ++++ .../src/main/res/values/strings.xml | 4 + .../ConnectedAccountsScreenScreenshotTest.kt | 21 +- .../PushNotificationScreenScreenshotTest.kt | 23 +- .../SettingAccountSecurityTest.kt | 10 +- .../presentation/SettingCompletionTest.kt | 14 +- .../SettingCoroutineCancellationTest.kt | 5 +- .../setting/presentation/SettingFlowTest.kt | 27 ++- .../presentation/SettingLoadFeedbackTest.kt | 111 +++++++++ .../presentation/SettingLoadRecoveryTest.kt | 184 +++++++++++++++ .../account/ConnectedAccountsMviTest.kt | 2 +- .../presentation/home/SettingViewModelTest.kt | 18 +- .../konsist/MviContractKonsistTest.kt | 1 - 31 files changed, 926 insertions(+), 310 deletions(-) create mode 100644 feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingContract.kt create mode 100644 feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/shared/component/SettingLoadErrorContent.kt create mode 100644 feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadFeedbackTest.kt create mode 100644 feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadRecoveryTest.kt diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContent.kt index 1a51323a4..5ae70a7d4 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContent.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContent.kt @@ -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 @@ -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 /** * 연결된 계정 화면의 상태 없는 본문. @@ -30,6 +33,7 @@ internal fun ConnectedAccountsContent( snackbarHostState: SnackbarHostState, onBack: () -> Unit, onToggle: (provider: String, enabled: Boolean) -> Unit, + onRetry: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -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)) + } + } } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContract.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContract.kt index 54ccab240..cb5c3ae9a 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContract.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsContract.kt @@ -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, @@ -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, ) : ConnectedAccountsReducerEvent diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt index 784e1d093..9decf5f17 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt @@ -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, ) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsUiState.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsUiState.kt index e17aaa182..56754d98a 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsUiState.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsUiState.kt @@ -6,6 +6,8 @@ data class ConnectedAccountsUiState( val isLoading: Boolean = false, val accounts: List = emptyList(), val errorMessage: String? = null, + val isUpdating: Boolean = false, + val pendingError: String? = null, /** * 화면이 플랫폼 인증(카카오 SDK·Credential Manager)을 시작해야 하는 제공자 신호다 (#1502). * diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt index 92a53c2e3..045b26bde 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt @@ -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 @@ -18,12 +19,22 @@ internal class ConnectedAccountsViewModel ) : MviViewModel( 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) } @@ -45,8 +56,26 @@ 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 -> { @@ -54,11 +83,11 @@ internal class ConnectedAccountsViewModel } 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 -> { @@ -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 { @@ -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())) } @@ -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())) } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingContract.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingContract.kt new file mode 100644 index 000000000..99198174b --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingContract.kt @@ -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 +} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingScreen.kt index 0474ef8b6..9178b05da 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingScreen.kt @@ -1,7 +1,10 @@ package com.afternote.feature.setting.presentation.home +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -26,12 +29,14 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.afternote.core.ui.mvi.ObserveSignal import com.afternote.core.ui.popup.Popup import com.afternote.core.ui.popup.PopupType import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.home.component.SettingProfile import com.afternote.feature.setting.presentation.home.component.SettingSection +import com.afternote.feature.setting.presentation.shared.component.SettingLoadErrorContent import com.afternote.feature.setting.presentation.shared.component.SettingMenuItem import kotlinx.coroutines.launch @@ -59,19 +64,18 @@ internal fun SettingScreen( val unavailableMessage = stringResource(R.string.setting_menu_unavailable) val acknowledgeLabel = stringResource(R.string.setting_menu_acknowledge) val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val logoutCompleted by viewModel.logoutCompleted.collectAsStateWithLifecycle() val currentOnLogoutSuccess by rememberUpdatedState(onLogoutSuccess) val lifecycle = LocalLifecycleOwner.current.lifecycle val lifecycleState by lifecycle.currentStateFlow.collectAsStateWithLifecycle() LaunchedEffect(lifecycleState) { if (lifecycleState == Lifecycle.State.RESUMED) { - viewModel.refresh() + viewModel.onIntent(SettingIntent.Refresh) } } - LaunchedEffect(logoutCompleted) { - if (logoutCompleted) { + uiState.logoutCompleted?.let { signal -> + ObserveSignal(signal, SettingIntent.ConsumeLogoutSuccess, viewModel::onIntent) { currentOnLogoutSuccess() } } @@ -88,8 +92,9 @@ internal fun SettingScreen( containerColor = Color.Transparent, ) { innerPadding -> SettingScreenContent( - uiState = uiState, - onLogoutClick = viewModel::logout, + uiState = uiState.profile, + onRetry = { viewModel.onIntent(SettingIntent.Refresh) }, + onLogoutClick = { viewModel.onIntent(SettingIntent.Logout) }, onUnavailableClick = { scope.launch { snackbarHostState.currentSnackbarData?.dismiss() @@ -114,7 +119,8 @@ internal fun SettingScreen( @Composable private fun SettingScreenContent( - uiState: SettingUiState, + uiState: SettingProfileState, + onRetry: () -> Unit, onLogoutClick: () -> Unit, onUnavailableClick: () -> Unit, onProfileEditClick: () -> Unit, @@ -146,23 +152,17 @@ private fun SettingScreenContent( ) } - when (val state = uiState) { - is SettingUiState.Loading -> { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() + Column( + modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()), + ) { + when (val state = uiState) { + SettingProfileState.Loading -> { + Box(Modifier.fillMaxWidth().height(160.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } } - } - is SettingUiState.Success -> { - Column( - modifier = - modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { + is SettingProfileState.Success -> { SettingProfile( name = state.name, email = state.email, @@ -170,101 +170,96 @@ private fun SettingScreenContent( onNoticeClick = onNoticeClick, onRecipientListClick = onRecipientListClick, ) + } - SettingSection(title = stringResource(R.string.setting_section_account)) { - SettingMenuItem( - label = stringResource(R.string.setting_account_profile_edit), - onClick = onProfileEditClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_account_password_change), - onClick = onPasswordChangeClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_account_linked_account), - onClick = onLinkedAccountClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_account_notification), - onClick = onNotificationClick, - ) - } - - SettingSection(title = stringResource(R.string.setting_section_recipient)) { - SettingMenuItem( - label = stringResource(R.string.setting_recipient_list), - onClick = onRecipientListClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_recipient_register), - onClick = onRecipientRegisterClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_recipient_after_delivery), - onClick = onDeliveryConditionsClick, - ) - } - - SettingSection(title = stringResource(R.string.setting_section_security)) { - SettingMenuItem( - label = stringResource(R.string.setting_security_passkey), - onClick = onPasskeyClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_security_app_lock), - onClick = onAppLockClick, - ) - } - - SettingSection(title = stringResource(R.string.setting_section_support)) { - SettingMenuItem( - label = stringResource(R.string.setting_support_faq), - onClick = onUnavailableClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_support_inquiry), - onClick = onUnavailableClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_support_notice), - onClick = onNoticeClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_support_terms), - onClick = onUnavailableClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_support_privacy), - onClick = onUnavailableClick, - ) - SettingMenuItem( - label = stringResource(R.string.setting_support_service_info), - onClick = onUnavailableClick, - ) - } - - SettingMenuItem( - label = stringResource(R.string.setting_logout), - onClick = { showLogoutDialog = true }, - modifier = Modifier.padding(top = 8.dp), - ) - SettingMenuItem( - label = stringResource(R.string.setting_account_withdraw), - onClick = onWithdrawGuideClick, + SettingProfileState.Error -> { + SettingLoadErrorContent( + message = stringResource(R.string.setting_profile_load_error), + onRetry = onRetry, + modifier = Modifier.height(160.dp), ) } } + SettingSection(title = stringResource(R.string.setting_section_account)) { + SettingMenuItem( + label = stringResource(R.string.setting_account_profile_edit), + onClick = onProfileEditClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_account_password_change), + onClick = onPasswordChangeClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_account_linked_account), + onClick = onLinkedAccountClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_account_notification), + onClick = onNotificationClick, + ) + } - is SettingUiState.Error -> { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SettingMenuItem( - label = stringResource(R.string.setting_logout), - onClick = { showLogoutDialog = true }, - ) - } + SettingSection(title = stringResource(R.string.setting_section_recipient)) { + SettingMenuItem( + label = stringResource(R.string.setting_recipient_list), + onClick = onRecipientListClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_recipient_register), + onClick = onRecipientRegisterClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_recipient_after_delivery), + onClick = onDeliveryConditionsClick, + ) + } + + SettingSection(title = stringResource(R.string.setting_section_security)) { + SettingMenuItem( + label = stringResource(R.string.setting_security_passkey), + onClick = onPasskeyClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_security_app_lock), + onClick = onAppLockClick, + ) + } + + SettingSection(title = stringResource(R.string.setting_section_support)) { + SettingMenuItem( + label = stringResource(R.string.setting_support_faq), + onClick = onUnavailableClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_support_inquiry), + onClick = onUnavailableClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_support_notice), + onClick = onNoticeClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_support_terms), + onClick = onUnavailableClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_support_privacy), + onClick = onUnavailableClick, + ) + SettingMenuItem( + label = stringResource(R.string.setting_support_service_info), + onClick = onUnavailableClick, + ) } + + SettingMenuItem( + label = stringResource(R.string.setting_logout), + onClick = { showLogoutDialog = true }, + modifier = Modifier.padding(top = 8.dp), + ) + SettingMenuItem( + label = stringResource(R.string.setting_account_withdraw), + onClick = onWithdrawGuideClick, + ) } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt index e28c05820..6e5f405d8 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt @@ -1,47 +1,16 @@ package com.afternote.feature.setting.presentation.home -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.domain.repository.MyProfileRepository import com.afternote.core.domain.repository.auth.AuthRepository +import com.afternote.core.ui.mvi.MviViewModel import com.afternote.feature.setting.domain.SettingAccountRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import javax.inject.Inject -sealed interface SettingUiState { - data object Loading : SettingUiState - - data class Success( - val name: String, - val email: String, - ) : SettingUiState - - data class Error( - val message: String, - ) : SettingUiState -} - -sealed interface WithdrawUiState { - data object Idle : WithdrawUiState - - data object Loading : WithdrawUiState - - data object Success : WithdrawUiState - - data object Error : WithdrawUiState -} - -/** - * 설정 화면용 ViewModel. - * - * 로그아웃 시 [AuthRepository.logout] 한 번 호출로 끝낸다. 내부적으로 서버 호출은 best-effort, - * 로컬 토큰은 결과 무관하게 정리하도록 위임돼 있다. 호출처는 토큰 정리 여부를 신경 쓸 필요 없음. - */ @HiltViewModel internal class SettingViewModel @Inject @@ -49,64 +18,99 @@ internal class SettingViewModel private val authRepository: AuthRepository, private val myProfileRepository: MyProfileRepository, private val accountRepository: SettingAccountRepository, - ) : ViewModel() { - private val _uiState = MutableStateFlow(SettingUiState.Loading) - val uiState = _uiState.asStateFlow() - - private val _logoutCompleted = MutableStateFlow(false) - val logoutCompleted = _logoutCompleted.asStateFlow() + ) : MviViewModel(SettingUiState()) { + private var loadJob: Job? = null + private var logoutJob: Job? = null init { loadProfile() } - fun refresh() = loadProfile() - - private fun loadProfile() { - viewModelScope.launch { - runCatchingCancellable { myProfileRepository.getMyProfile() } - .onSuccess { profile -> - _uiState.value = - SettingUiState.Success( - name = profile.name, - email = profile.email, - ) - }.onFailure { - _uiState.value = SettingUiState.Error("프로필을 불러올 수 없습니다.") - } + override fun onIntent(intent: SettingIntent) { + when (intent) { + SettingIntent.Refresh -> loadProfile() + SettingIntent.Logout -> logout() + SettingIntent.ConsumeLogoutSuccess -> dispatch(SettingReducerEvent.LogoutConsumed) + SettingIntent.DeleteAccount -> deleteAccount() + SettingIntent.DismissWithdrawError -> dispatch(SettingReducerEvent.WithdrawErrorDismissed) } } - fun logout() { - viewModelScope.launch { - // logout() 내부에서 서버 호출(best-effort) + 로컬 토큰 정리까지 처리한다. - authRepository.logout() - _logoutCompleted.value = true - } - } + override fun reduce( + state: SettingUiState, + event: SettingReducerEvent, + ): SettingUiState = + when (event) { + SettingReducerEvent.ProfileLoading -> { + if (state.profile is SettingProfileState.Success) state else state.copy(profile = SettingProfileState.Loading) + } - private val _withdrawUiState = MutableStateFlow(WithdrawUiState.Idle) - val withdrawUiState = _withdrawUiState.asStateFlow() + is SettingReducerEvent.ProfileLoaded -> { + state.copy(profile = SettingProfileState.Success(event.name, event.email)) + } - fun deleteAccount() { - if (_withdrawUiState.value == WithdrawUiState.Loading) return - _withdrawUiState.value = WithdrawUiState.Loading + SettingReducerEvent.ProfileFailed -> { + state.copy(profile = SettingProfileState.Error) + } - viewModelScope.launch { - try { - accountRepository.deleteAccount() - _withdrawUiState.value = WithdrawUiState.Success - } catch (exception: CancellationException) { - throw exception - } catch (_: Exception) { - _withdrawUiState.value = WithdrawUiState.Error + SettingReducerEvent.LoggedOut -> { + state.copy(logoutCompleted = Unit) + } + + SettingReducerEvent.LogoutConsumed -> { + state.copy(logoutCompleted = null) + } + + SettingReducerEvent.Withdrawing -> { + state.copy(withdraw = WithdrawUiState.Loading) + } + + SettingReducerEvent.Withdrawn -> { + state.copy(withdraw = WithdrawUiState.Success) + } + + SettingReducerEvent.WithdrawFailed -> { + state.copy(withdraw = WithdrawUiState.Error) + } + + SettingReducerEvent.WithdrawErrorDismissed -> { + if (state.withdraw == + WithdrawUiState.Error + ) { + state.copy(withdraw = WithdrawUiState.Idle) + } else { + state + } } } + + private fun loadProfile() { + if (loadJob?.isActive == true) return + loadJob = + viewModelScope.launch { + dispatch(SettingReducerEvent.ProfileLoading) + runCatchingCancellable { myProfileRepository.getMyProfile() } + .onSuccess { dispatch(SettingReducerEvent.ProfileLoaded(it.name, it.email)) } + .onFailure { dispatch(SettingReducerEvent.ProfileFailed) } + } } - fun dismissWithdrawError() { - if (_withdrawUiState.value == WithdrawUiState.Error) { - _withdrawUiState.value = WithdrawUiState.Idle + private fun logout() { + if (logoutJob?.isActive == true || currentState.logoutCompleted != null) return + logoutJob = + viewModelScope.launch { + authRepository.logout() + dispatch(SettingReducerEvent.LoggedOut) + } + } + + private fun deleteAccount() { + if (currentState.withdraw == WithdrawUiState.Loading || currentState.withdraw == WithdrawUiState.Success) return + dispatch(SettingReducerEvent.Withdrawing) + viewModelScope.launch { + runCatchingCancellable { accountRepository.deleteAccount() } + .onSuccess { dispatch(SettingReducerEvent.Withdrawn) } + .onFailure { dispatch(SettingReducerEvent.WithdrawFailed) } } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt index ea165e883..67f490d96 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt @@ -1,10 +1,8 @@ package com.afternote.feature.setting.presentation.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.core.ui.popup.Popup import com.afternote.core.ui.popup.PopupType import com.afternote.feature.setting.presentation.R @@ -17,9 +15,9 @@ internal fun WithdrawConfirmScreen( viewModel: SettingViewModel, modifier: Modifier = Modifier, ) { - val userName = (uiState as? SettingUiState.Success)?.name.orEmpty() - val userEmail = (uiState as? SettingUiState.Success)?.email.orEmpty() - val withdrawUiState by viewModel.withdrawUiState.collectAsStateWithLifecycle() + val userName = (uiState.profile as? SettingProfileState.Success)?.name.orEmpty() + val userEmail = (uiState.profile as? SettingProfileState.Success)?.email.orEmpty() + val withdrawUiState = uiState.withdraw when (withdrawUiState) { WithdrawUiState.Success -> { @@ -38,8 +36,8 @@ internal fun WithdrawConfirmScreen( message = stringResource(R.string.setting_withdraw_failed_message), confirmText = stringResource(R.string.setting_withdraw_retry_button), dismissText = stringResource(R.string.setting_withdraw_close_button), - onConfirm = viewModel::deleteAccount, - onDismiss = viewModel::dismissWithdrawError, + onConfirm = { viewModel.onIntent(SettingIntent.DeleteAccount) }, + onDismiss = { viewModel.onIntent(SettingIntent.DismissWithdrawError) }, ) } @@ -52,7 +50,7 @@ internal fun WithdrawConfirmScreen( userName = userName, userEmail = userEmail, onBackClick = onBackClick, - onWithdrawClick = viewModel::deleteAccount, + onWithdrawClick = { viewModel.onIntent(SettingIntent.DeleteAccount) }, isLoading = withdrawUiState == WithdrawUiState.Loading, modifier = modifier, ) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawGuideScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawGuideScreen.kt index 391638503..c5204986d 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawGuideScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawGuideScreen.kt @@ -35,15 +35,15 @@ import com.afternote.feature.setting.presentation.R // 설정 - 회원 탈퇴 안내 @Composable -fun WithdrawGuideScreen( +internal fun WithdrawGuideScreen( uiState: SettingUiState, onBackClick: () -> Unit, onCancelClick: () -> Unit, onConfirmClick: () -> Unit, modifier: Modifier = Modifier, ) { - val userName = (uiState as? SettingUiState.Success)?.name.orEmpty() - val userEmail = (uiState as? SettingUiState.Success)?.email.orEmpty() + val userName = (uiState.profile as? SettingProfileState.Success)?.name.orEmpty() + val userEmail = (uiState.profile as? SettingProfileState.Success)?.email.orEmpty() var agreed by remember { mutableStateOf(false) } Scaffold( topBar = { diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContent.kt index 231273c3c..d2fdd7c93 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContent.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContent.kt @@ -1,8 +1,10 @@ package com.afternote.feature.setting.presentation.notification +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.Scaffold @@ -12,10 +14,13 @@ 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.asString +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.notification.component.PushToggleSection +import com.afternote.feature.setting.presentation.shared.component.SettingLoadErrorContent /** * 푸시 알림 설정 화면의 상태 없는 본문. @@ -31,6 +36,7 @@ internal fun PushNotificationContent( onNewsletterToggle: (Boolean) -> Unit, onMindRecordToggle: (Boolean) -> Unit, onAfternoteToggle: (Boolean) -> Unit, + onRetry: () -> Unit, ) { Scaffold( topBar = { @@ -48,12 +54,28 @@ internal fun PushNotificationContent( .padding(padding) .padding(horizontal = 20.dp, vertical = 16.dp), ) { - PushToggleSection( - uiState = uiState, - onNewsletterToggle = onNewsletterToggle, - onMindRecordToggle = onMindRecordToggle, - onAfternoteToggle = onAfternoteToggle, - ) + when { + uiState.isLoading -> { + LoadingBody(modifier = Modifier.weight(1f)) + } + + uiState.errorMessage != null -> { + SettingLoadErrorContent( + message = uiState.errorMessage.asString(), + onRetry = onRetry, + modifier = Modifier.weight(1f), + ) + } + + else -> { + PushToggleSection( + uiState = uiState, + onNewsletterToggle = onNewsletterToggle, + onMindRecordToggle = onMindRecordToggle, + onAfternoteToggle = onAfternoteToggle, + ) + } + } Spacer(Modifier.height(24.dp)) Text( diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContract.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContract.kt index 0ac593a7a..3ad1bf42a 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContract.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationContract.kt @@ -6,6 +6,8 @@ import com.afternote.core.ui.mvi.MviIntent import com.afternote.core.ui.mvi.ReducerEvent internal sealed interface PushNotificationIntent : MviIntent { + data object RetryLoad : PushNotificationIntent + /** 기기 알림 허용 여부를 다시 읽는다. 화면 재개와 권한 요청 결과 때 보낸다. */ data object RefreshDeviceAlarmStatus : PushNotificationIntent diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt index ae9c1ec4e..a0e68dfe4 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt @@ -17,6 +17,7 @@ internal fun PushNotificationScreen( PushNotificationContent( uiState = uiState, onBack = onBack, + onRetry = { viewModel.onIntent(PushNotificationIntent.RetryLoad) }, onNewsletterToggle = { viewModel.onIntent(PushNotificationIntent.TogglePushSetting(PushSetting.NEWSLETTER, it)) }, onMindRecordToggle = { viewModel.onIntent(PushNotificationIntent.TogglePushSetting(PushSetting.MIND_RECORD, it)) }, onAfternoteToggle = { viewModel.onIntent(PushNotificationIntent.TogglePushSetting(PushSetting.AFTERNOTE, it)) }, diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationUiState.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationUiState.kt index adcf6fd7a..71c9a1f27 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationUiState.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationUiState.kt @@ -1,9 +1,11 @@ package com.afternote.feature.setting.presentation.notification +import com.afternote.core.ui.UiText import com.afternote.core.ui.mvi.UiState internal data class PushNotificationUiState( val isLoading: Boolean = false, + val errorMessage: UiText? = null, val isDeviceAlarmOn: Boolean = false, // 마케팅 알림 (기기 알림 꺼졌을 때) val isSmsChecked: Boolean = true, diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt index da43aa7fc..de78ce49b 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt @@ -7,11 +7,14 @@ import androidx.lifecycle.viewModelScope import com.afternote.core.common.reporting.ErrorReporter import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.domain.error.PushSettingFailure +import com.afternote.core.ui.UiText import com.afternote.core.ui.mvi.MviViewModel import com.afternote.feature.setting.domain.SettingNotificationRepository +import com.afternote.feature.setting.presentation.R import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import jakarta.inject.Inject +import kotlinx.coroutines.Job import kotlinx.coroutines.launch @HiltViewModel @@ -21,7 +24,11 @@ internal class PushNotificationViewModel @ApplicationContext private val context: Context, private val notificationRepository: SettingNotificationRepository, private val errorReporter: ErrorReporter, - ) : MviViewModel(PushNotificationUiState()) { + ) : MviViewModel( + PushNotificationUiState(isLoading = true), + ) { + private var loadJob: Job? = null + init { refreshDeviceAlarmStatus() loadPushSettings() @@ -30,6 +37,10 @@ internal class PushNotificationViewModel override fun onIntent(intent: PushNotificationIntent) { when (intent) { + PushNotificationIntent.RetryLoad -> { + if (currentState.errorMessage != null) loadPushSettings() + } + PushNotificationIntent.RefreshDeviceAlarmStatus -> { refreshDeviceAlarmStatus() } @@ -74,12 +85,13 @@ internal class PushNotificationViewModel } PushNotificationReducerEvent.PushSettingsLoading -> { - state.copy(isLoading = true) + state.copy(isLoading = true, errorMessage = null) } is PushNotificationReducerEvent.PushSettingsLoaded -> { state.copy( isLoading = false, + errorMessage = null, isNewsletterOn = event.setting.timeLetter, isMindRecordOn = event.setting.mindRecord, isAfternoteOn = event.setting.afterNote, @@ -87,7 +99,7 @@ internal class PushNotificationViewModel } PushNotificationReducerEvent.PushSettingsLoadFailed -> { - state.copy(isLoading = false) + state.copy(isLoading = false, errorMessage = UiText.Resource(R.string.setting_push_load_error)) } is PushNotificationReducerEvent.MarketingConsentsLoaded -> { @@ -148,19 +160,21 @@ internal class PushNotificationViewModel } private fun loadPushSettings() { - viewModelScope.launch { - Log.d(TAG, "loadPushSettings: start") - dispatch(PushNotificationReducerEvent.PushSettingsLoading) - runCatchingCancellable { notificationRepository.getMyPushSettings() } - .onSuccess { setting -> - Log.d(TAG, "loadPushSettings: success=$setting") - dispatch(PushNotificationReducerEvent.PushSettingsLoaded(setting)) - }.onFailure { e -> - // 조회 실패는 Logcat 에만 남긴다. 반복 조회 잡음이 저장 실패 진단의 보관 한도를 밀어내지 않게 한다 (#963). - Log.e(TAG, "loadPushSettings: failed", e) - dispatch(PushNotificationReducerEvent.PushSettingsLoadFailed) - } - } + if (loadJob?.isActive == true) return + dispatch(PushNotificationReducerEvent.PushSettingsLoading) + loadJob = + viewModelScope.launch { + Log.d(TAG, "loadPushSettings: start") + runCatchingCancellable { notificationRepository.getMyPushSettings() } + .onSuccess { setting -> + Log.d(TAG, "loadPushSettings: success=$setting") + dispatch(PushNotificationReducerEvent.PushSettingsLoaded(setting)) + }.onFailure { e -> + // 조회 실패는 Logcat 에만 남긴다. 반복 조회 잡음이 저장 실패 진단의 보관 한도를 밀어내지 않게 한다 (#963). + Log.e(TAG, "loadPushSettings: failed", e) + dispatch(PushNotificationReducerEvent.PushSettingsLoadFailed) + } + } } private fun loadMarketingConsents() { @@ -204,7 +218,7 @@ internal class PushNotificationViewModel } private fun updatePushSetting(update: PushSettingUpdate) { - if (currentState.isUpdating(update.setting)) return + if (currentState.isLoading || currentState.errorMessage != null || currentState.isUpdating(update.setting)) return val previousValue = currentState.valueOf(update.setting) dispatch(PushNotificationReducerEvent.PushSettingSaving(update)) viewModelScope.launch { diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditContract.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditContract.kt index a6001854a..bdb8ee77c 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditContract.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditContract.kt @@ -4,6 +4,8 @@ import com.afternote.core.ui.mvi.MviIntent import com.afternote.core.ui.mvi.ReducerEvent internal sealed interface ProfileEditIntent : MviIntent { + data object RetryLoad : ProfileEditIntent + data class SelectPhoto( val uri: String, ) : ProfileEditIntent @@ -20,6 +22,8 @@ internal sealed interface ProfileEditIntent : MviIntent { } internal sealed interface ProfileEditReducerEvent : ReducerEvent { + data object Loading : ProfileEditReducerEvent + data class Loaded( val name: String, val phone: String, diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditScreen.kt index 7100002ce..9907ed70a 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditScreen.kt @@ -19,12 +19,15 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState 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.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -45,6 +48,8 @@ import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.receiver.ReceiverPhoneValidation import com.afternote.feature.setting.presentation.receiver.validateReceiverPhone +import com.afternote.feature.setting.presentation.shared.component.SettingLoadErrorContent +import kotlinx.coroutines.launch @Composable internal fun ProfileEditScreen( @@ -54,6 +59,9 @@ internal fun ProfileEditScreen( viewModel: ProfileEditViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + val feedbackScope = rememberCoroutineScope() + val updateFailedMessage = stringResource(R.string.setting_profile_update_error) val onPickProfileImage = rememberProfileImagePicker( canAcceptPhoto = (uiState as? ProfileEditUiState.Success)?.isUpdating == false, @@ -68,13 +76,19 @@ internal fun ProfileEditScreen( onIntent = viewModel::onIntent, ) { event -> when (event) { - ProfileEditEvent.UpdateSuccess -> onBackClick() - ProfileEditEvent.UpdateFailure -> Unit + ProfileEditEvent.UpdateSuccess -> { + onBackClick() + } + + ProfileEditEvent.UpdateFailure -> { + feedbackScope.launch { snackbarHostState.showSnackbar(updateFailedMessage) } + } } } } ProfileEditContent( + snackbarHostState = snackbarHostState, uiState = uiState, onPickImageClick = onPickProfileImage, onIntent = viewModel::onIntent, @@ -86,6 +100,7 @@ internal fun ProfileEditScreen( @Composable private fun ProfileEditContent( + snackbarHostState: SnackbarHostState, uiState: ProfileEditUiState, onPickImageClick: () -> Unit, onIntent: (ProfileEditIntent) -> Unit, @@ -102,6 +117,7 @@ private fun ProfileEditContent( }, modifier = modifier, containerColor = Color.Transparent, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> when (val state = uiState) { is ProfileEditUiState.Loading -> { @@ -127,15 +143,11 @@ private fun ProfileEditContent( } is ProfileEditUiState.Error -> { - Box( - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding), - contentAlignment = Alignment.Center, - ) { - Text(text = "프로필을 불러올 수 없습니다.") - } + SettingLoadErrorContent( + message = stringResource(R.string.setting_profile_load_error), + onRetry = { onIntent(ProfileEditIntent.RetryLoad) }, + modifier = Modifier.padding(innerPadding), + ) } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditViewModel.kt index bc71680f7..782c1d5ff 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/profile/ProfileEditViewModel.kt @@ -8,6 +8,7 @@ import com.afternote.core.ui.mvi.MviViewModel import com.afternote.feature.setting.presentation.receiver.ReceiverPhoneValidation import com.afternote.feature.setting.presentation.receiver.validateReceiverPhone import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import javax.inject.Inject @@ -20,12 +21,15 @@ internal class ProfileEditViewModel private val myProfileRepository: MyProfileRepository, private val photoUploadRepository: PhotoUploadRepository, ) : MviViewModel(ProfileEditUiState.Loading) { + private var loadJob: Job? = null + init { loadProfile() } override fun onIntent(intent: ProfileEditIntent) { when (intent) { + ProfileEditIntent.RetryLoad -> if (currentState == ProfileEditUiState.Error) loadProfile() is ProfileEditIntent.SelectPhoto -> dispatch(ProfileEditReducerEvent.PhotoSelected(intent.uri)) is ProfileEditIntent.UpdateProfile -> updateProfile(intent.name, intent.phone) is ProfileEditIntent.ConsumeEvent -> dispatch(ProfileEditReducerEvent.EventConsumed(intent.event)) @@ -37,6 +41,10 @@ internal class ProfileEditViewModel event: ProfileEditReducerEvent, ): ProfileEditUiState = when (event) { + ProfileEditReducerEvent.Loading -> { + ProfileEditUiState.Loading + } + is ProfileEditReducerEvent.Loaded -> { ProfileEditUiState.Success( name = event.name, @@ -73,21 +81,24 @@ internal class ProfileEditViewModel } private fun loadProfile() { - viewModelScope.launch { - runCatchingCancellable { myProfileRepository.getMyProfile() } - .onSuccess { user -> - dispatch( - ProfileEditReducerEvent.Loaded( - name = user.name, - phone = user.phone.orEmpty(), - email = user.email, - profileImageUrl = user.profileImageUrl, - ), - ) - }.onFailure { - dispatch(ProfileEditReducerEvent.LoadFailed) - } - } + if (loadJob?.isActive == true) return + dispatch(ProfileEditReducerEvent.Loading) + loadJob = + viewModelScope.launch { + runCatchingCancellable { myProfileRepository.getMyProfile() } + .onSuccess { user -> + dispatch( + ProfileEditReducerEvent.Loaded( + name = user.name, + phone = user.phone.orEmpty(), + email = user.email, + profileImageUrl = user.profileImageUrl, + ), + ) + }.onFailure { + dispatch(ProfileEditReducerEvent.LoadFailed) + } + } } private fun updateProfile( diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/shared/component/SettingLoadErrorContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/shared/component/SettingLoadErrorContent.kt new file mode 100644 index 000000000..a742b5525 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/shared/component/SettingLoadErrorContent.kt @@ -0,0 +1,42 @@ +package com.afternote.feature.setting.presentation.shared.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.afternote.core.ui.theme.AfternoteDesign +import com.afternote.feature.setting.presentation.R + +@Composable +internal fun SettingLoadErrorContent( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize().padding(horizontal = 20.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = message, + style = AfternoteDesign.typography.bodySmallR, + color = AfternoteDesign.colors.gray6, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = onRetry) { + Text(text = stringResource(R.string.setting_load_retry)) + } + } +} diff --git a/feature/setting/presentation/src/main/res/values/strings.xml b/feature/setting/presentation/src/main/res/values/strings.xml index 1bcff3c58..5733f63bb 100644 --- a/feature/setting/presentation/src/main/res/values/strings.xml +++ b/feature/setting/presentation/src/main/res/values/strings.xml @@ -181,4 +181,8 @@ 소셜 로그인으로 가입한 계정은 비밀번호를 변경할 수 없습니다. 네트워크 연결을 확인해주세요. 비밀번호를 변경하지 못했습니다. 입력하신 내용을 확인해 주세요. + 프로필을 불러올 수 없습니다. + 프로필 수정에 실패했습니다. 다시 시도해 주세요. + 알림 설정을 불러올 수 없습니다. + 다시 시도 diff --git a/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTest.kt b/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTest.kt index 680fa0fba..cca855c10 100644 --- a/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTest.kt +++ b/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTest.kt @@ -24,10 +24,11 @@ internal fun connectedAccountsScreenCompactScreenshot() { } @Composable -private fun ConnectedAccountsScreenScreenshotContent() { +private fun ConnectedAccountsScreenScreenshotContent(uiState: ConnectedAccountsUiState = connectedAccountsPreviewState) { AfternoteTheme { ConnectedAccountsContent( - uiState = connectedAccountsPreviewState, + onRetry = {}, + uiState = uiState, snackbarHostState = remember { SnackbarHostState() }, onBack = {}, onToggle = { _, _ -> }, @@ -68,3 +69,19 @@ private val connectedAccountsPreviewState = ), ), ) + +@PreviewTest +@Preview(showBackground = true) +@Preview(showBackground = true, device = COMPACT_DEVICE_SPEC) +@Composable +internal fun connectedAccountsLoadingScreenshot() { + ConnectedAccountsScreenScreenshotContent(ConnectedAccountsUiState(isLoading = true)) +} + +@PreviewTest +@Preview(showBackground = true) +@Preview(showBackground = true, device = COMPACT_DEVICE_SPEC) +@Composable +internal fun connectedAccountsLoadErrorScreenshot() { + ConnectedAccountsScreenScreenshotContent(ConnectedAccountsUiState(errorMessage = "계정 정보를 불러올 수 없습니다.")) +} diff --git a/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTest.kt b/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTest.kt index d3168b6c7..07e49c4ea 100644 --- a/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTest.kt +++ b/feature/setting/presentation/src/screenshotTest/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTest.kt @@ -2,8 +2,10 @@ package com.afternote.feature.setting.presentation.notification import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import com.afternote.core.ui.UiText import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.setting.presentation.COMPACT_DEVICE_SPEC +import com.afternote.feature.setting.presentation.R import com.android.tools.screenshot.PreviewTest @PreviewTest @@ -21,10 +23,11 @@ internal fun pushNotificationCompactScreenshot() { } @Composable -private fun PushNotificationScreenshotContent() { +private fun PushNotificationScreenshotContent(uiState: PushNotificationUiState = PushNotificationUiState(isAfternoteOn = true)) { AfternoteTheme { PushNotificationContent( - uiState = PushNotificationUiState(isAfternoteOn = true), + onRetry = {}, + uiState = uiState, onBack = {}, onNewsletterToggle = {}, onMindRecordToggle = {}, @@ -32,3 +35,19 @@ private fun PushNotificationScreenshotContent() { ) } } + +@PreviewTest +@Preview(showBackground = true) +@Preview(showBackground = true, device = COMPACT_DEVICE_SPEC) +@Composable +internal fun pushNotificationLoadingScreenshot() { + PushNotificationScreenshotContent(PushNotificationUiState(isLoading = true)) +} + +@PreviewTest +@Preview(showBackground = true) +@Preview(showBackground = true, device = COMPACT_DEVICE_SPEC) +@Composable +internal fun pushNotificationLoadErrorScreenshot() { + PushNotificationScreenshotContent(PushNotificationUiState(errorMessage = UiText.Resource(R.string.setting_push_load_error))) +} diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingAccountSecurityTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingAccountSecurityTest.kt index bc2d64031..6de740f10 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingAccountSecurityTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingAccountSecurityTest.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.core.domain.testing.FakeAuthRepository import com.afternote.core.domain.testing.FakeMyProfileRepository import com.afternote.core.domain.testing.FakeMyProfileRepository.ProfileUpdateCall @@ -34,7 +35,6 @@ import com.afternote.feature.setting.presentation.applock.PinSetupStep import com.afternote.feature.setting.presentation.delivery.DeliveryConditionError import com.afternote.feature.setting.presentation.delivery.DeliveryConditionIntent import com.afternote.feature.setting.presentation.delivery.DeliveryConditionViewModel -import com.afternote.feature.setting.presentation.home.SettingUiState import com.afternote.feature.setting.presentation.home.SettingViewModel import com.afternote.feature.setting.presentation.home.WithdrawConfirmScreen import com.afternote.feature.setting.presentation.home.WithdrawUiState @@ -131,7 +131,7 @@ class SettingAccountSecurityTest { linkRepository.onLinkConnectedAccount = { _, _ -> throw IllegalStateException("oauth rejected") } composeRule.runOnIdle { linkViewModel.onIntent(ConnectedAccountsIntent.Link(provider = "google", accessToken = "google-token")) } composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - linkViewModel.uiState.value.errorMessage == "계정 연결에 실패했습니다." + linkViewModel.uiState.value.pendingError == "계정 연결에 실패했습니다." } assertEquals( @@ -156,7 +156,7 @@ class SettingAccountSecurityTest { } assertEquals(listOf("google"), unlinkRepository.connectedUnlinkCalls) - assertEquals("계정 연결 해제에 실패했습니다.", unlinkViewModel.uiState.value.errorMessage) + assertEquals("계정 연결 해제에 실패했습니다.", unlinkViewModel.uiState.value.pendingError) } @Test @@ -313,7 +313,7 @@ class SettingAccountSecurityTest { composeRule.setContent { AfternoteTheme { WithdrawConfirmScreen( - uiState = SettingUiState.Success(name = DEFAULT_USER.name, email = DEFAULT_USER.email), + uiState = viewModel.uiState.collectAsStateWithLifecycle().value, onBackClick = {}, onWithdrawSuccess = { successCalls += 1 }, viewModel = viewModel, @@ -332,7 +332,7 @@ class SettingAccountSecurityTest { } composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - viewModel.withdrawUiState.value == WithdrawUiState.Error + viewModel.uiState.value.withdraw == WithdrawUiState.Error } composeRule .onNodeWithText("회원 탈퇴에 실패했습니다. 잠시 후 다시 시도해 주세요.") diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCompletionTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCompletionTest.kt index a88953554..e4cb44880 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCompletionTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCompletionTest.kt @@ -44,6 +44,8 @@ import com.afternote.feature.setting.presentation.account.ConnectedAccountsViewM import com.afternote.feature.setting.presentation.delivery.DeliveryConditionError import com.afternote.feature.setting.presentation.delivery.DeliveryConditionIntent import com.afternote.feature.setting.presentation.delivery.DeliveryConditionViewModel +import com.afternote.feature.setting.presentation.home.SettingIntent +import com.afternote.feature.setting.presentation.home.SettingProfileState import com.afternote.feature.setting.presentation.home.SettingUiState import com.afternote.feature.setting.presentation.home.SettingViewModel import com.afternote.feature.setting.presentation.home.WithdrawUiState @@ -643,28 +645,28 @@ class SettingCompletionTest { } val viewModel = SettingViewModel(authRepository, profileRepository, repository) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - viewModel.uiState.value is SettingUiState.Success + viewModel.uiState.value.profile is SettingProfileState.Success } - composeRule.runOnIdle { viewModel.deleteAccount() } + composeRule.runOnIdle { viewModel.onIntent(SettingIntent.DeleteAccount) } composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { repository.deleteAccountCalls == 1 } - assertEquals(WithdrawUiState.Loading, viewModel.withdrawUiState.value) + assertEquals(WithdrawUiState.Loading, viewModel.uiState.value.withdraw) firstGate.completeExceptionally(IllegalStateException("서버가 503 으로 거절했다")) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - viewModel.withdrawUiState.value == WithdrawUiState.Error + viewModel.uiState.value.withdraw == WithdrawUiState.Error } - composeRule.runOnIdle { viewModel.deleteAccount() } + composeRule.runOnIdle { viewModel.onIntent(SettingIntent.DeleteAccount) } composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { repository.deleteAccountCalls == 2 } retryGate.complete(Unit) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - viewModel.withdrawUiState.value == WithdrawUiState.Success + viewModel.uiState.value.withdraw == WithdrawUiState.Success } assertEquals(2, repository.deleteAccountCalls) } diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt index 4f78f1ac5..a2a78bae5 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt @@ -15,6 +15,7 @@ import com.afternote.feature.setting.domain.testing.FakeSettingAccountRepository import com.afternote.feature.setting.domain.testing.FakeSettingNotificationRepository import com.afternote.feature.setting.presentation.account.ConnectedAccountsIntent import com.afternote.feature.setting.presentation.account.ConnectedAccountsViewModel +import com.afternote.feature.setting.presentation.home.SettingProfileState import com.afternote.feature.setting.presentation.home.SettingUiState import com.afternote.feature.setting.presentation.home.SettingViewModel import com.afternote.feature.setting.presentation.notification.PushNotificationIntent @@ -238,7 +239,7 @@ class SettingCoroutineCancellationTest { runCurrent() assertTrue(pending.isCancelled) - assertEquals(SettingUiState.Loading, viewModel.uiState.value) + assertEquals(SettingProfileState.Loading, viewModel.uiState.value.profile) } @Test @@ -326,7 +327,7 @@ class SettingCoroutineCancellationTest { ) runCurrent() - assertEquals(SettingUiState.Error("프로필을 불러올 수 없습니다."), viewModel.uiState.value) + assertEquals(SettingProfileState.Error, viewModel.uiState.value.profile) } private fun pushViewModel( diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingFlowTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingFlowTest.kt index 3e40f3677..adac4e7fb 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingFlowTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingFlowTest.kt @@ -14,6 +14,7 @@ import com.afternote.core.domain.testing.FakeMyProfileRepository import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.setting.domain.testing.FakeSettingAccountRepository import com.afternote.feature.setting.domain.testing.FakeSettingNotificationRepository +import com.afternote.feature.setting.presentation.home.SettingIntent import com.afternote.feature.setting.presentation.home.SettingScreen import com.afternote.feature.setting.presentation.home.SettingViewModel import com.afternote.feature.setting.presentation.notification.PushNotificationIntent @@ -34,6 +35,30 @@ class SettingFlowTest { @get:Rule val composeRule = createComposeRule() + @Test + fun profileLoadFailureKeepsSettingsEntriesAndRetryRecoversProfile() { + var reads = 0 + var offline = true + val profile = + com.afternote.core.domain.testing.FakeMyProfileRepository.strict().apply { + onGetMyProfile = { + reads++ + if (offline) throw java.io.IOException("offline") + com.afternote.core.model.user + .User("복구 사용자", "test@example.com", null, null) + } + } + val viewModel = SettingViewModel(settingFlowAuthRepository(loggedIn = true), profile, settingFlowAccountRepository()) + setSettingContent(viewModel) + composeRule.onNodeWithText("프로필을 불러올 수 없습니다.").assertIsDisplayed() + composeRule.onNodeWithText("프로필 수정").assertIsDisplayed() + val failedReads = reads + composeRule.runOnIdle { offline = false } + composeRule.onNodeWithText("다시 시도").performClick() + composeRule.onNodeWithText("복구 사용자").assertIsDisplayed() + assertEquals(failedReads + 1, reads) + } + @Test fun profileAndSecurityEntries_emitExpectedNavigation() { val auth = settingFlowAuthRepository(loggedIn = true) @@ -85,7 +110,7 @@ class SettingFlowTest { composeRule.setContent { AfternoteTheme {} } assertEquals(0, account.deleteAccountCalls) - composeRule.runOnIdle { viewModel.deleteAccount() } + composeRule.runOnIdle { viewModel.onIntent(SettingIntent.DeleteAccount) } composeRule.waitUntil(timeoutMillis = 5_000) { account.deleteAccountCalls == 1 } assertEquals(1, account.deleteAccountCalls) diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadFeedbackTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadFeedbackTest.kt new file mode 100644 index 000000000..80e9b3200 --- /dev/null +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadFeedbackTest.kt @@ -0,0 +1,111 @@ +package com.afternote.feature.setting.presentation + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasScrollToIndexAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performScrollToNode +import com.afternote.core.domain.testing.FakeMyProfileRepository +import com.afternote.core.domain.testing.FakePhotoUploadRepository +import com.afternote.core.model.user.User +import com.afternote.core.ui.UiText +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.setting.domain.testing.FakeSettingAccountRepository +import com.afternote.feature.setting.presentation.account.ConnectedAccountsContent +import com.afternote.feature.setting.presentation.account.ConnectedAccountsIntent +import com.afternote.feature.setting.presentation.account.ConnectedAccountsScreen +import com.afternote.feature.setting.presentation.account.ConnectedAccountsUiState +import com.afternote.feature.setting.presentation.account.ConnectedAccountsViewModel +import com.afternote.feature.setting.presentation.notification.PushNotificationContent +import com.afternote.feature.setting.presentation.notification.PushNotificationUiState +import com.afternote.feature.setting.presentation.profile.ProfileEditIntent +import com.afternote.feature.setting.presentation.profile.ProfileEditScreen +import com.afternote.feature.setting.presentation.profile.ProfileEditUiState +import com.afternote.feature.setting.presentation.profile.ProfileEditViewModel +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode +import java.io.IOException + +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [35]) +class SettingLoadFeedbackTest { + @get:Rule val composeRule = createComposeRule() + + @Test fun pushLoadErrorHidesSwitchesAndExposesRetry() { + var retries = 0 + composeRule.setContent { + AfternoteTheme { + PushNotificationContent( + uiState = PushNotificationUiState(errorMessage = UiText.Resource(R.string.setting_push_load_error)), + onBack = {}, + onNewsletterToggle = {}, + onMindRecordToggle = {}, + onAfternoteToggle = {}, + onRetry = { retries++ }, + ) + } + } + composeRule.onNodeWithText("알림 설정을 불러올 수 없습니다.").assertIsDisplayed() + composeRule.onNodeWithText("뉴스레터").assertDoesNotExist() + composeRule.onNodeWithText("다시 시도").performClick() + assertEquals(1, retries) + } + + @Test fun accountLoadErrorExposesRetryInsteadOfAnEmptyAccountList() { + var retries = 0 + composeRule.setContent { + AfternoteTheme { + ConnectedAccountsContent( + uiState = ConnectedAccountsUiState(errorMessage = "계정 정보를 불러올 수 없습니다."), + snackbarHostState = SnackbarHostState(), + onBack = {}, + onToggle = { _, _ -> }, + onRetry = { retries++ }, + ) + } + } + composeRule.onNodeWithText("계정 정보를 불러올 수 없습니다.").assertIsDisplayed() + composeRule.onNodeWithText("다시 시도").performClick() + assertEquals(1, retries) + } + + @Test fun profileLoadRetryRecoversAndSaveFailureIsVisible() { + var reads = 0 + val repository = + FakeMyProfileRepository.strict().apply { + onGetMyProfile = { if (++reads == 1) throw IOException() else User("테스트", "test@example.com", "01012345678", null) } + onUpdateMyProfile = { _, _, _ -> throw IOException() } + } + val vm = ProfileEditViewModel(repository, FakePhotoUploadRepository.strict()) + composeRule.setContent { AfternoteTheme { ProfileEditScreen({}, {}, viewModel = vm) } } + composeRule.onNodeWithText("프로필을 불러올 수 없습니다.").assertIsDisplayed() + composeRule.onNodeWithText("다시 시도").performClick() + composeRule.waitUntil(5_000) { vm.uiState.value is ProfileEditUiState.Success } + composeRule.onNode(hasScrollToIndexAction()).performScrollToNode(hasText("수정하기")) + composeRule.onNodeWithText("수정하기").performClick() + composeRule.onNodeWithText("프로필 수정에 실패했습니다. 다시 시도해 주세요.").assertIsDisplayed() + assertEquals(2, reads) + } + + @Test fun unlinkFailureShowsFeedbackAndKeepsAccountRows() { + val repository = FakeSettingAccountRepository().apply { onUnlinkConnectedAccount = { throw IOException() } } + val vm = ConnectedAccountsViewModel(repository) + composeRule.setContent { AfternoteTheme { ConnectedAccountsScreen({}, viewModel = vm) } } + composeRule.waitUntil(5_000) { !vm.uiState.value.isLoading } + composeRule.runOnIdle { vm.onIntent(ConnectedAccountsIntent.Toggle("kakao", false)) } + composeRule.onNodeWithText("계정 연결 해제에 실패했습니다.").assertIsDisplayed() + assertEquals(4, vm.uiState.value.accounts.size) + assertEquals(null, vm.uiState.value.errorMessage) + } +} diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadRecoveryTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadRecoveryTest.kt new file mode 100644 index 000000000..43ee6a447 --- /dev/null +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingLoadRecoveryTest.kt @@ -0,0 +1,184 @@ +package com.afternote.feature.setting.presentation + +import androidx.test.core.app.ApplicationProvider +import com.afternote.core.domain.testing.FakeAuthRepository +import com.afternote.core.domain.testing.FakeMyProfileRepository +import com.afternote.core.domain.testing.FakePhotoUploadRepository +import com.afternote.core.model.user.User +import com.afternote.core.model.user.UserPushSetting +import com.afternote.feature.setting.domain.testing.FakeSettingAccountRepository +import com.afternote.feature.setting.domain.testing.FakeSettingNotificationRepository +import com.afternote.feature.setting.presentation.account.ConnectedAccountsIntent +import com.afternote.feature.setting.presentation.account.ConnectedAccountsViewModel +import com.afternote.feature.setting.presentation.home.SettingIntent +import com.afternote.feature.setting.presentation.home.SettingProfileState +import com.afternote.feature.setting.presentation.home.SettingViewModel +import com.afternote.feature.setting.presentation.home.WithdrawUiState +import com.afternote.feature.setting.presentation.notification.PushNotificationIntent +import com.afternote.feature.setting.presentation.notification.PushNotificationViewModel +import com.afternote.feature.setting.presentation.notification.PushSetting +import com.afternote.feature.setting.presentation.profile.ProfileEditIntent +import com.afternote.feature.setting.presentation.profile.ProfileEditUiState +import com.afternote.feature.setting.presentation.profile.ProfileEditViewModel +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.io.IOException + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class SettingLoadRecoveryTest { + private val dispatcher = StandardTestDispatcher() + private val profile = User("테스트", "test@example.com", "01012345678", null) + + @Before fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @After fun tearDown() { + Dispatchers.resetMain() + } + + @Test fun `home retry recovers profile without resetting account operation`() = + runTest(dispatcher) { + var reads = 0 + val gate = CompletableDeferred() + val repository = + FakeMyProfileRepository.strict().apply { + onGetMyProfile = { if (++reads == 1) throw IOException() else gate.await() } + } + val vm = SettingViewModel(FakeAuthRepository.strict(), repository, FakeSettingAccountRepository()) + runCurrent() + assertEquals(SettingProfileState.Error, vm.uiState.value.profile) + vm.onIntent(SettingIntent.DeleteAccount) + runCurrent() + vm.onIntent(SettingIntent.Refresh) + vm.onIntent(SettingIntent.Refresh) + runCurrent() + assertEquals(2, reads) + assertEquals(WithdrawUiState.Success, vm.uiState.value.withdraw) + gate.complete(profile) + runCurrent() + assertEquals(SettingProfileState.Success(profile.name, profile.email), vm.uiState.value.profile) + assertEquals(WithdrawUiState.Success, vm.uiState.value.withdraw) + } + + @Test fun `profile retry is single flight and failed load cannot submit a patch`() = + runTest(dispatcher) { + var reads = 0 + val gate = CompletableDeferred() + val repository = + FakeMyProfileRepository.strict().apply { + onGetMyProfile = { if (++reads == 1) throw IOException() else gate.await() } + } + val vm = ProfileEditViewModel(repository, FakePhotoUploadRepository.strict()) + runCurrent() + assertEquals(ProfileEditUiState.Error, vm.uiState.value) + vm.onIntent(ProfileEditIntent.UpdateProfile("new", "01012345678")) + assertTrue(repository.profileUpdateCalls.isEmpty()) + vm.onIntent(ProfileEditIntent.RetryLoad) + vm.onIntent(ProfileEditIntent.RetryLoad) + assertEquals(ProfileEditUiState.Loading, vm.uiState.value) + runCurrent() + assertEquals(2, reads) + gate.complete(profile) + runCurrent() + assertEquals(profile.phone, (vm.uiState.value as ProfileEditUiState.Success).phone) + vm.onIntent(ProfileEditIntent.RetryLoad) + runCurrent() + assertEquals(2, reads) + } + + @Test fun `account load failure blocks writes and retry restores rows`() = + runTest(dispatcher) { + var fail = true + val repository = + FakeSettingAccountRepository().apply { + onGetConnectedAccounts = { if (fail) throw IOException() else connectedAccounts } + } + val vm = ConnectedAccountsViewModel(repository) + runCurrent() + assertNotNull(vm.uiState.value.errorMessage) + vm.onIntent(ConnectedAccountsIntent.Toggle("kakao", false)) + vm.onIntent(ConnectedAccountsIntent.Link("kakao", "unused")) + runCurrent() + assertTrue(repository.connectedUnlinkCalls.isEmpty()) + assertTrue(repository.connectedLinkCalls.isEmpty()) + fail = false + vm.onIntent(ConnectedAccountsIntent.RetryLoad) + vm.onIntent(ConnectedAccountsIntent.RetryLoad) + runCurrent() + assertEquals(2, repository.getConnectedAccountsCalls) + assertNull(vm.uiState.value.errorMessage) + assertEquals(4, vm.uiState.value.accounts.size) + } + + @Test fun `account write failure preserves rows and consumes feedback independently of load state`() = + runTest(dispatcher) { + val repository = FakeSettingAccountRepository().apply { onUnlinkConnectedAccount = { throw IOException() } } + val vm = ConnectedAccountsViewModel(repository) + runCurrent() + val rows = vm.uiState.value.accounts + vm.onIntent(ConnectedAccountsIntent.Toggle("kakao", false)) + vm.onIntent(ConnectedAccountsIntent.Toggle("kakao", false)) + runCurrent() + assertEquals(listOf("kakao"), repository.connectedUnlinkCalls) + assertEquals(rows, vm.uiState.value.accounts) + assertNull(vm.uiState.value.errorMessage) + val error = requireNotNull(vm.uiState.value.pendingError) + vm.onIntent(ConnectedAccountsIntent.ConsumeError("another message")) + assertEquals(error, vm.uiState.value.pendingError) + vm.onIntent(ConnectedAccountsIntent.ConsumeError(error)) + assertNull(vm.uiState.value.pendingError) + } + + @Test fun `push load error cannot save default values and retry reads real settings`() = + runTest(dispatcher) { + var reads = 0 + var writes = 0 + var fail = true + val repository = + FakeSettingNotificationRepository().apply { + onGetMyPushSettings = { + reads++ + if (fail) throw IOException() else UserPushSetting(timeLetter = true, mindRecord = true, afterNote = false) + } + onUpdateMyPushSettings = { _, _, _ -> + writes++ + UserPushSetting(false, false, false) + } + } + val vm = PushNotificationViewModel(ApplicationProvider.getApplicationContext(), repository, NoOpErrorReporter) + assertTrue(vm.uiState.value.isLoading) + vm.onIntent(PushNotificationIntent.TogglePushSetting(PushSetting.NEWSLETTER, true)) + runCurrent() + assertNotNull(vm.uiState.value.errorMessage) + vm.onIntent(PushNotificationIntent.TogglePushSetting(PushSetting.NEWSLETTER, true)) + runCurrent() + assertEquals(0, writes) + fail = false + vm.onIntent(PushNotificationIntent.RetryLoad) + vm.onIntent(PushNotificationIntent.RetryLoad) + runCurrent() + assertEquals(2, reads) + assertNull(vm.uiState.value.errorMessage) + assertTrue(vm.uiState.value.isNewsletterOn) + assertTrue(vm.uiState.value.isMindRecordOn) + } +} diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsMviTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsMviTest.kt index 6baf72799..c2eb7d27a 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsMviTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsMviTest.kt @@ -100,7 +100,7 @@ class ConnectedAccountsMviTest { repeat(2) { viewModel.onIntent(ConnectedAccountsIntent.Link(provider = "google", accessToken = "token-$it")) - assertEquals("계정 연결에 실패했습니다.", viewModel.uiState.value.errorMessage) + assertEquals("계정 연결에 실패했습니다.", viewModel.uiState.value.pendingError) assertFalse( viewModel.uiState.value.accounts .first { it.provider == "google" } diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModelTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModelTest.kt index f1582f606..eee0ea026 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModelTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModelTest.kt @@ -35,11 +35,11 @@ class SettingViewModelTest { runTest(dispatcher) { val viewModel = viewModel(onDeleteAccount = {}) - viewModel.deleteAccount() + viewModel.onIntent(SettingIntent.DeleteAccount) - assertEquals(WithdrawUiState.Loading, viewModel.withdrawUiState.value) + assertEquals(WithdrawUiState.Loading, viewModel.uiState.value.withdraw) runCurrent() - assertEquals(WithdrawUiState.Success, viewModel.withdrawUiState.value) + assertEquals(WithdrawUiState.Success, viewModel.uiState.value.withdraw) } @Test @@ -47,12 +47,12 @@ class SettingViewModelTest { runTest(dispatcher) { val viewModel = viewModel(onDeleteAccount = { throw IllegalStateException("failure") }) - viewModel.deleteAccount() + viewModel.onIntent(SettingIntent.DeleteAccount) runCurrent() - assertEquals(WithdrawUiState.Error, viewModel.withdrawUiState.value) - viewModel.dismissWithdrawError() - assertEquals(WithdrawUiState.Idle, viewModel.withdrawUiState.value) + assertEquals(WithdrawUiState.Error, viewModel.uiState.value.withdraw) + viewModel.onIntent(SettingIntent.DismissWithdrawError) + assertEquals(WithdrawUiState.Idle, viewModel.uiState.value.withdraw) } @Test @@ -61,8 +61,8 @@ class SettingViewModelTest { var requestCount = 0 val viewModel = viewModel(onDeleteAccount = { requestCount++ }) - viewModel.deleteAccount() - viewModel.deleteAccount() + viewModel.onIntent(SettingIntent.DeleteAccount) + viewModel.onIntent(SettingIntent.DeleteAccount) runCurrent() assertEquals(1, requestCount) diff --git a/konsist/src/test/kotlin/com/afternote/konsist/MviContractKonsistTest.kt b/konsist/src/test/kotlin/com/afternote/konsist/MviContractKonsistTest.kt index 75710a336..68d4d7a0f 100644 --- a/konsist/src/test/kotlin/com/afternote/konsist/MviContractKonsistTest.kt +++ b/konsist/src/test/kotlin/com/afternote/konsist/MviContractKonsistTest.kt @@ -557,7 +557,6 @@ class MviContractKonsistTest { private val ISSUE_1805_SETTING = setOf( "com.afternote.feature.setting.presentation.applock.AppLockSetupViewModel", - "com.afternote.feature.setting.presentation.home.SettingViewModel", "com.afternote.feature.setting.presentation.passkey.PassKeyViewModel", "com.afternote.feature.setting.presentation.receiver.ReceiverRegisterViewModel", ) From 23bb6579df8f385c780e77c3513f3e56488d13ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:57:46 +0000 Subject: [PATCH 2/2] test: update screenshot baselines in CI --- ...edAccountsLoadErrorScreenshot_748aa731_0.png | Bin 0 -> 25034 bytes ...edAccountsLoadErrorScreenshot_c2abd7e6_0.png | Bin 0 -> 14760 bytes ...ctedAccountsLoadingScreenshot_748aa731_0.png | Bin 0 -> 20275 bytes ...ctedAccountsLoadingScreenshot_c2abd7e6_0.png | Bin 0 -> 11199 bytes ...tificationLoadErrorScreenshot_748aa731_0.png | Bin 0 -> 32053 bytes ...tificationLoadErrorScreenshot_c2abd7e6_0.png | Bin 0 -> 19910 bytes ...NotificationLoadingScreenshot_748aa731_0.png | Bin 0 -> 26864 bytes ...NotificationLoadingScreenshot_c2abd7e6_0.png | Bin 0 -> 16020 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_748aa731_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_c2abd7e6_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadingScreenshot_748aa731_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadingScreenshot_c2abd7e6_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_748aa731_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_c2abd7e6_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadingScreenshot_748aa731_0.png create mode 100644 feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadingScreenshot_c2abd7e6_0.png diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_748aa731_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_748aa731_0.png new file mode 100644 index 0000000000000000000000000000000000000000..6852feed9e83dbdb9ef325d0785302f2ffb08d54 GIT binary patch literal 25034 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 nc_jd0ohqzX75;}XCHP(v_KzG%HyohMy~ literal 0 HcmV?d00001 diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_c2abd7e6_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadErrorScreenshot_c2abd7e6_0.png new file mode 100644 index 0000000000000000000000000000000000000000..74ab59ac8d6e29da32a982bd9541154bac943525 GIT binary patch literal 14760 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 literal 0 HcmV?d00001 diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadingScreenshot_c2abd7e6_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreenScreenshotTestKt/connectedAccountsLoadingScreenshot_c2abd7e6_0.png new file mode 100644 index 0000000000000000000000000000000000000000..04fd32d7af59a3d13db5140c4f69983a0c341780 GIT binary patch literal 11199 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 literal 0 HcmV?d00001 diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_748aa731_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_748aa731_0.png new file mode 100644 index 0000000000000000000000000000000000000000..236b3751984a0e51d7718dba67a533d83b3ba9d6 GIT binary patch literal 32053 zcmeIbd05ih*EdXBdYZK1lxe8VcCs`x73WT6R;FgA=165~Ih)~xKsI?wh0`%LM=Hx% z$qAeQtCN&CWQwRLP^74cqzI@8ykF1nci;DOU++Kf`&`c-&-1?iBNqtYz4qFBt@T-J zueJ8$i%#|%*J-T-fj}G2ojrXC1X>LNf!1FAMFsdIOe>iN0wpe;JN^6RIKQb8X_ikw zlHl8dZI?wzm$`BDqryF}BCj58KY4n?{SB`+-1B*`*&Ymevg@A4jTY9$#z&i`52~EL z=P`Y%)npHS->0}$`?l;l@Z!up>S^R)5VQUGWj>*-xxBfonfznno9Fr_7BPfggfSylir21Ajr^*Mfn+C#;f`ftNq@B9}j# zS*-^A-Br-K{OiBBuKehK2_ufy>GfOp}RabVB(@HHjE_A7ntlc z3$MWEwtpZeHY^Afd{hhjh*bAV+O-y>j0cr0nGHxcMwGQ3Ui}+o?FpMC<-665lRH^c zjc>Ep+iOSNfq~b5^zj?Fp8dJE^RX_Z)WY`uVd7QK+IZnuuJ_XXxQ$)IrHKjV3HW}P;=g2e3@r-Aj)w{5*t6B$s0Tah2PYP67ifgqTD%34M zw{(f~PB0GkhofY_fQ@UpJm!~Zuy8YEyFesw%zcCZtH@ zVP#^LX)kB>-D_*XwRgDrw!wa)c8^%(H9>Rt_rQ@Ry)q+E%Xm`+ZrsyvPR#Id_g9G@ zn+Qt4%Rb%GEpJP`)EGHUIw~#pM_0CO@XymopyTG9U^C|V4MyKmofB(@kTvJ*85%q# zN?&_-g2d>Vx$}_!BO|(?RAzi>dP6W~BB%Dmi=!A}3iq_!U6C7b6_5=FS zqt%!r-uTP@de8hrvDDA0LhB6MVm@6z_HsQ%WX$5~+~p^?8YvSLjP}W_r3c#3nX+Ij zkYOIbv?Awi{UY zOFFufOv;Veq+h@v8WkjIx$u%t{6~6u_15j@+4C|1iZwNxGm_xPTM}ko=BQ)~qSaf) zr`S8b-iRtZ7=62ru!?QR&p4NP*wZUb^T%|vU}8}GS{+#eFrYYbysv<+um7};vcca+ zBLQQxSUxFnfp^>)zAdb@mOekctMfqQ(xeB*%|Fopr7HTiqz#TnM8Jma;t!8kQ~KQ* zzpmdOnTP0aw8qu)44TczGQJ?84J=`zO3^`oMrrGTegU6q@DH%&wmp~?&C0~4pgFJV zSFzu_qPuJeTE?HW^#xOdc*HlW;L**`r|I7H!7!7+9h|05%!Vj`5&Txo+LNlcSnr~S zN_8vSE^LAn;T}|rjbio7pVi&}y?T?9z2{d6N?-5@%*Me$%OoD|d1%pCEY zBoJjc+GGX&I4EnXTKcMh4iSCOEp6Kd7}51ftwtNChe12?y(1bcxzTkU#b0!cKgCul z!n^V5j`z&H$xEY#_DC1guJvDQ9)w9q)HOlyjvMXO^&X!O{$6Swey`Q2o+X=(?L?-q zz6zU0Sq`5L#)dv~yW2!I8_d&_E$==zn3}~;zO~fwWf3d?z(YXKp4=6A;a&btP$7(~ zZ9BPeSUWJ_FY%IgmR|y(3onyko%Pl@(UUA+5a(&z;sXB`J(|< zWq45!-VyLDsc?8dKyfg~N+NrP&G^8IzX5a7jk`))@84c_g8wF->Z2_rT6})Aac7RQ zTu2U*4x|KV_+3uJwl|=+f`(S3&ox9VU!7~8eKUeB<}YX-h+YWrm(pY6}sqM*5 z@<`Dj)Q@Z(zJt^9ML%{;o#2-%b3@j}CJ|@jFOjo#2r7`H8}4&%f*k3P*oQka#0lf^OVnv^J|6Shhe(07&AsinDhSa&Qodh}I* zh6lws6_DHGmZT*&8q(qDj+lZ6;T+NzePMVVrtx5P(S;43202t;%~7S62Sr;#nR9I) zY4fu;-(MEEvd3{1bFNBu(mdp3I<~aU=^!SoUV$xKf)5sfW8q5=T*$?`iutNwX7=~L z)$=YR!k*ju7uq<1cARF9pBe@|C;n*dL8caYleg_F3NbL48^ZK&C_kMC*n1Z~A#Aq_ zdP_uqMf}?ZaXlUO{lr?6Q3zEs_I(%&?~11_9G0`z>Xx+OGq8;#F(ZXT_=C~WRZHQ>s}l(r!~yxjCLo$$s=c?00Om@>@{M; zL8(r`?1LJx5$Pd)d!O4gZ>*us$d9pQ{33q?!#SP!`KKhz_6?T~VDdsQ_26Ap&{0~k z?1QM89%Sd*fJ?*T`H6#D+epYBKf|e@FXvCy#yfZ0NJFD(W~9MoRawTdnfo(VZG*3e zZ+(jvG*N7f4M7TXS(iB}g8;JHaEd)|2*~S;n})IR@BUh!Yb&3@kYyUpt_& zcQ%J(A@L@J^YbXv2wh=SY;2}xI7a)-W5<9?U)2lZZgV&PC6=UQw-h_(nwn6vpd$;K zfc^g-jg>7u6XKpx+*QJ$?xw-4>SO7ErO)03DTw8&fEaaL=0S%zC!bwel-*5a4;_C^ zu}enQ*N1+d?R5{**V27s5hj6uMy*C4^PJSg&floL{!xVByoYon7R(PmE%cc~Dd4s{ z7P;4p9>qJ8=RX?e7$Uz>^C5Jb*sYJUp+!p)ThX)LGQ_XkJVbc+z%@Xi1O1!T!RaGj zUKN0ut=|YTv>r7`lWixz;l{RRk?#a{KfldGN+ijM)$blMO+TohYYu&GMq3q`11fRi z@GYrGpWHJX`5~3id z3yMS0rT5TA@4)L&3K$V;5@^t64f>UVVDC7-mcd%0sdct9MjBVWB|@YX(Qu^tR1cFLOvXsH18Vv!lTvFZNDM%AlTp^_v{ zTEqZh5nH!52n%yE(YY%h!oFPJ`J#2E&y{ECa6JZtU`0_pI0r%pYu;@g(32^yf`z2Jff<<>%6dwK75}kFK0s2xd8*Kbir)>XtXTbM%al^80ONfv? zqg)zdY`?D)_S_5p1RS#IxXPRq!`)$@GAMuN$%drREGs>(zN8*lU5h^np;?ERCakZRtwU;x;bDp zV~gPs&e&IViQE16M-AWE_m`-kqv1;SS-%N>K%9Sob`YXe(KQw0Nvhz>h&HV4hwk~? z6ni-I(3otA#&h3PjtAZ?|m{{S>S*VLAt$S)7_;D+D>7rE;}k9J4aHKZ$FMWz`? z4#!h47KA>6Eu$MEgG!lEM!o^tGV@~+Vcj;=utk2966MEg^tV)t$Y*O~)y?O>O!}mq zkKflZ0tTg~xAst;#oqn{4DvVbYK@|H5=;#l*C++DtNe8ekF+uhSH;JV+vWshMI zX{4L9_m-Mkl0T$S4J^}P@QPU|8T3x39KG=hULj{O#sRgE@ zj-pncCs{@r-6WPAf2u4Bbab5(<<}xoI%g?(Cnzx7K#7u=jY$bj^3Me%%>Sl{BhXQACCYI}gkCZrKftIr0jXG?>QEBrc_F56p3h53G~e>SF#__4BJ<>xZF7>^cw*EGeAcX|Lp-i5!*6qhI!pcu`qb*PNk>tYS76Z zF|u2taT1jJ81OK7I#7>JQ)Z>L&fSTvkTd1lkhY1Ez33`G)DD>vrJ>{FGVT7u@)>sQ zWy(?QQ3Lb`W>j1~`TL*u)x!+DVxokt%H=tF*u=7-n3IjYA^<#uW#*xN7xts#yz8;o z1w^sum!})=h5~L?hNZc9Kn9~3PQS9={?Bj@?C}VLF2vIl6G zf(rLVc3kqL$ljh(kC1n>NUzLgoS@4r)vFMFcOTY2agmF1qV<7{RwMHye{tYW$bry- zw=Wy7ety`Aj9`)z(k3jfDwHiTcq`dlFHOGcDtI|grIPUDoBe<4p?~5f_F&}YT7gZ9 z(RQg_iv~UBXo%vOS4E78A|Fz8A=cqPWY66{|& z^;kLnSvdz=IS^bq8C^LdUODz&IsXSZumTBIAi)YGSb+rp2SS2*#ni}P`o2*j>l>=) znmxn`0^V(z`0Y=`_bV}Hcb%wE{=;o5g`K*I`Zo2E-O*j9)lb}h*<|_0s-(NuRI8)I z+vORBUW3_gwE7#dbb_=oZqMI0-21&9vJTm1EF4&oi4~dnKTsxC@*7tYJpoX+lJUBd z`n!??{Xd`AJ@Bh8Vy=f>PxsrIz|D`fpspGUCoPP<+n?og><*BX0?SlxJ+7qMs&oe0 zN(GJ|zj}6&H%^lk@^{Y2+4PrC*`!DQXl^A&YI|q=3!5aJ-U)_8UTsK=?zWDN{CZAr zBO!kFvz~gFtzzjN4khEgb2J+{9yk6nSp_9!uq+r?g>i_O0#|2i7+(;hkjwH%K}XYC zS4|zNzaK5-6VweF*!SL5Q*{zmNq{Ch_QP^n~0uZIDoA|{u==FE| z=#hBYs3B@1H2O70dp>PRghTmSuHyhH6w`;Xf%N}+sg%BOo(oB8f@k6WK4pt2G`rs~DhD9*x5qnLb=cRJquLS|<@0%c?7~Lf0fu0;_ z4v)&N+iN^zc@b8S0MD-hQX7(Wkqy&P{e~5MSkYkWjETB{JonYXlA1e4RO}3IxE3bs zb+W9iO?2lM#sqKa5L=`S@RR&$P;6SFLMF-2p9e&>rq+)&*{C%Fb!40Kg=OeFx6p!i zecV)q@i76w*nES&7FN-^OCrxcQs?2-YIjHM*`Sz;#bb=68VGgsFR(Wc$HLltcLxww zqb=G4>SPwL-Vj^*eiU|cum5G!=25Hzj{osAgpoK|T5Q2P-YH$`+m})(3emYIo{jHY zT2&ajj^3x7f=DQh3_W7*gkJjb{siEV; z0@-oXja<`FQFg$|&Hx(stk5QR@_4Etd8sBk*XFn$zh$b1YC)xxx(bKmEW#9yE_szWdo=Hf0>v-_LY2WBqsjV6go8?!%>4`^&aQ71FM-?ez?0`%=+jB0>!e`O@gf8rquJj}HG>7kJiMI#o;2 zIfAbU>}VAAdjm`>-HXrhXMU={K|hi#&R4y1g74(qb6fLdaVi|=UA)*g8eZ&p%&G|? z1k_h+7m&tnnb}(1uvky9&b-v^-?{&bA1aRoY>lGh+|4D8W;y=j0RMO<;mN_9V@F^f zK~m}fzqsVtA|s^Rzq+Jp5A}@wsw0Kp&r5GUwfHOa*JG2vJlyI8xfbThuV5mjd2;^RoGBtAx9y$=jLc)4x8rup>G8KkfZ~n+&gYp|P|U-T`;(NfN;U&K zA*9Fk^5h}qLPqE9C+&6SkS)sgzG`^$M;6ao7E-3u%$Ng>oe)GC?q1tXOSpJy;zA2O zh`l3zuE*XoYhON%Ea#Ec3XqEvU8Y(4BbQG3E|kRVU|<5ao3yQ4TS)yJ{;28vIb*+P z!Y<2*pA*H{DsP;VYU7W;94!2Y$taXzv8jo(OSZ_os`V?F`nsFo2_;*!7s=+&`{HAV zUCery75G*TufaU?KgCYGuMnn&g6HE&dYWOX=rm+$9_29fEth7q?R8AuBrAmSYC&K& za>td652K=S1r@(|3J9buj*k7ml=ZLe-Q`YT;UH&wF zv)STaA<+paO#7E=8_If|kFWhgx-j*+*DEHNU7~;ZICo7+{=(R{UnlEw$NWp71;w&; zq>lMu%Im74FD>M!IqPo>McdSL$b<^VTyXT>!d91_Vq!%lj$Vw5KOg9Z(iPd{-nsmH zr->0Y!{I!_^g=>6-6bI124niwidc?5A8Z&AZ2#?6aeaqS;)Zj;ViAraPnRKujaPr7 z)qm_-FWFbR+>L--2$_CPchM5@!lUyo_WOU`{T)zO!CjAY=7rfo1)5-sJ2&SeY7^fT zxP43+6J92t#N@7jvG9<%w8XW=&X>=>;*=Mj4Q}p~mO@)|g-U%I(wvfN1f$4KfJJSB znD$#e41SHi;d$rYV2;E0;plFDXzc-7D9$ov$S>MbMDQ)nH^B^-c4v+~-TB5wxmf$@ zv!9asUS-O@^fu+UJAVPJnTN{@*_z0`y)gyS4Pepw$TgW3cUsU#O4n^C9;c()irar!#Je!4b`4 z-X#GhMHWY1H&*yD#HN{z3O5&FUL7{M^0F|e53(AaMpVe9AreoNstL*go^N+KuTl~z zL-hAt;*Sb%LO%!WH9>ua7%@wd$fJtFUn^kU!k#YG(C1F}t-VnFQpv zJ-?Xz4z)0vl@DV~9!wc~DX2fyhtwI@LLbqdt4AExHi&L@iT3w_(cC`lwVc%842c1WDlQ%GZNYfzqFjBCe6OhO(rV@$ zrOx}IWTJ2T^7#~RvX?XCbJJ+1w*{ee?YUgs$2uuAL;LMH_kcttO4p_0v5|Og{s;2l z7^kwgR}28QH~^+v60-Mw6EP-|K7hd>-(|Qw-NdUhNk7n_zjFAJa?SQ$0>>mLd7GlWWt3q$(|R=iZvu6BjBXk&MZef2Mk$>2H7E5(&2S(WWPKrt^{ zmlGwPq^r1^rqGCJF6YtC@aGAAZ4eXkDM$2`q!NT()*Lrzv*rHC6T1Jw#te(JmkSRG z>b*`bgBJj*{QmaGSog=LQge=zax`9y{hDnS+Ht+M0PA)jGBhn`XOT>Ybsl!(3jBii z8y)HRr!=5wrL;CNV@?xt=tcj&FNNkgf$<6en0W(vSuq*;zd|P8{_MGR1g1$7&)S6r zum>&(ZEQQWGi~bi&inSoq()>YQDUs*=PoJR3ukN7#~3#Y#56zmU^Pt7EF-)b06c$< zMdPQ?@zFYOZWV9s+ya@LBR|oA9lH84#o|cho&DvD!zGeHqQst2BDppao+*idCKa}s zS)6oagPuG6n+Q|tAz*Kf&avO!|M1v7q$(|k@S~&Hq zqExuwQccL??|=eNirK>uwsh3ohuo6cIKr3Gr^WzC^B^sgt_U~8-q@yOue-Pb413}$ z^SxYGQh06h4}8uq%{COVfMq)zjv&$a_F2B$|3NFD?O&GOTSl-;&Y3ZwIawwX>%}p_ z$VTXmL>MhsI^7%*3AjzMI)XF7s^fr#nGTK4aT`j~39@W6b7n6C)Z{E)Wux0PwmwrP(U@FbD~vki!Gir6Y*$F+GuUg4dm^O>|6b*a8x82^s_maz#ga#>b@#q z5<>)8o=kFQcWVHUHdqVjY=ENgzeT13aKA)*)YZxSjS#|hD=x0n(;ZpYnLkj;Oe*+T zu`HGWM|MIMkMlUJ4HHR)@t$0Zp>hDeJZO#jd$aupCrf_ax2H(hVkh9g90N-ly8i@* zL99lvOY!iFA02vn3lNVx%Oa8kJ+THovwb5Nn>hQK-M~S6OXeq25`7b9X&z8*WDgD& zJOUuA+dsc0>#6}lQ~9p8+s3lt`xAYO?#_=I(BVc*CqCcuOYL)5BJg~RjBx-(f}PF} z4vP<4{*fEdPa$i%?~m+wg^*s#x~+4BJeCt;LDd_q=uIeEgYG;|mBs?r#r|O)0P=YT zoBfJqR`Pky2kOs$t~WgQUsS#=uXqmzceX|W2sSpeJ2=yDAZ1|M95UJWXe=y*G7sq9 z$qKV`L>(BaGptYT6r6~6vt4r?SfwM8eWiMO`FFlXqVGOATYo#U=g zkywegSsbqxJ_b$g12As+ab^ZJY5(Fl zwmYW}M@($4T$~>>5lCZ;t>k^JJvOk4TX!B}ZmmWmJGMe51+QgZMEN2wUj&NT0{BjD zfSn2pVvh@d&9K-9uglN*QA2DAqs6}PKqLxtB(i2#9n^Jp3Hd`0IYZcOi+T%q+4YL5 z#w#|8C1Dpn=UP8x6eznzS|wp48uM-F4$LC-k?mJjkUIjd=UN^{c#mDqeKyk> z!qom77|jFNz}Eq3%}2q3a78~}^5v9zrMEi@m45y8xieBDK(P*_tya70u^D2D2mIMS zA28t8oDbS5EQ2NlRa6l9eyyu>#7$(C zj#b>-+7@8T{p&%XEAfUcKi{)kla%ipcU#rg-Z#G8W2dn(wu%Xu&My8nw`E+l!~9>7 z2vTrx3l_O}i_2n!H%%11t0XXo7GgmI=U4g~{Qsy|!zOmAJrg`UlgoX?L~=M?vz4<8 zLD0%Hj)Xw$^CX@Bt8E?VFtFF}g0k`#*le<*Ipk46-1xYM!Z^}O`h#&_zYBgNQb==F_lut#%=0 z#Vb0$4w_E7@Wcaj!WJlK7zaQwwo)g=IC5!Tl{^7{SiQkM64W}j1p;~z)Y&?d3|D+L z-x!c{jy$^_>}}j1$Zq&irwT-2@6z^1f=+bjBo&TvZW=-#v{sw=Z-an#0bmY|O0wABJa4wMH>S-7yA+QaU}T85=>8hyVPbcpDMsJt}l21NQOro}Q~N3?p}- zLgO|7Honw$s)0Fq$RGLnj8tIA$U%D*1)jUw>-j(K=89{z($2UPB!>B2QKzS?qKS37jb5OYZxl<{9-U)Wl${B_YInpH@eR zC1tTaS^z`0?BG&tA#d?~Yg>x}PaE`J2 z2^Ne_5s;*$TED@Ln|~f(UM2T`wtOytAjF?N?w`UX9!=eDl08P#ur{6lSS{HQ`w5Di zCf19_HiB7+!jY>AT!EK{a`5*qal&3y1GgT=D}Z&3+{f6?zp7;hp1%LO8vP3x^fqCc zc2DET!<=t$KX1mE6;Rqtu+`36LWM&Km0G=Zz2@yBgqF9{rV5_z$k7I28<##(B>;%( zKk%Pag>-E-!tIsw(mw3nQq{uld6$x=|FA;~!h}nE-`OfCy#BD-d++pgVVKAr#ZK!> z^|MmLrDTOr4zstP)7&wzy z4|)p**rW6dSeP47k)rdi`Y@nmPAC9w|FSEx!|+xGsCD-L`5Vwmv!In^!IkuimGp|0 z^a_9jE9n(0=@tL;=@p=r27UkEX*wv6fJ+wkIl4)r=50-1TtBh??~1D@ez&`DaNR+# z9h)}%aTR3z$E)jSemC+*);r6@rN(*E3zKUtn^_~gk8a~5#=VTK(nO1uB5lBrUokB! zrUl@@ifLIfEi2yUinqDqZLWBmE0ACX60AUil_u>g-Rf7`_5-IGE0ACX68!HC37(HH zmjm@+P)i(tpd?8!R`elWxcqLs7ATOZUQWM{?oAq*PmR(7YM0Q3Tq~}*zO|&WJHHt! z?8VCOh)FUaN#E8?8|0MUYSaTXrKSw3E9`A%pEgc3&jaO4*501;{0Nk#RFdPI%Y`Zy z>5Wpy83|b-8wbh=W_zRn0flt3Tm+;Q#tin91JziCxj+`p_oKjmyK>YIuvJ!|F!{qb6! z88B(hWQK^C;9}fthjBL;0o+1i>lcOUhBKp={7^F~(8Re{8pQGOdK`k^)65iA6*3~o zvT?aoV7n;9Dy(7^y)on51gKCbo2?MaHNA^7n~zYtk>c7xGV14j-8`8TPxg1!UR>}Q zJEgn$WlHdT7U4>`MZgJgQHvzL7Q_Z1ushsLL?o4;BH?pMNKoDpCCX`6l+SYATI05d zewAM^l7%)il2NXlC_qGr1X9aYp7F_#NmZZSVoAZg)tb_Qh-fTsEs@7)xN;YO|H{V8kwti$zq|6RArvq(Lw|xN8f)l5s+WU0r zwOb&;&VPDkHbrba8GKjvBi&g?H`rA_McsPtOApmgT*H~Dr`1Du^BsMkp4)LZb-Ctf zF!9ix)ZSRpW9P){;(3-_OyCw7-o8ZjYbFyYl}DyIb9O~bM;M81lh!6SU40889r#qW z!}~UG=4>WM5>Lzu2}$6~6(<;5RtTXgL>)e zcWdp~J6eSKf;@QMu~7=l8+aff4rH{SsLVr~g>%1WHg z@5{3DKVGWtDzpfF*~#tqj>yj?l`m&Ris!evwoRnu_0^yz9H2SY(|!Ij$pT?czktfb zWAL5%*yynjppy7-6UGH1|@w*t}wu;2G_xzd1Y;jCsk_wB>dCz`N> z9y0HIPLY~<844F&kt$)tvP-P(hh&G!(t0(rYZ9dw;mG=h!xnvEy{l?ktC`Q}_%&!Z zu6p+Me->}_y((I(Pt3h`1%KV*-^(Yzz{0J|f?+NGzLkgXcs9yNUM_U4T{!&2FjwtQXN$-wMqhU& zaBst{hM!4=?x%|@u2e{)Ds=^>i2UUS|5?MatfWGgg|(CSYi)g~{xeP;(k^gr%#0jA zE#~F*gI~hxE z#;KpU!1Mrtgk5rk%b9~QB_v7bG!(uswO3MVYxdz)^v`FGnTY)K0_?I3P8j#UKk2I( zl;?hbVS6%#2mw^jP~vuCMCLz>#;*px5plf<|t^ zU?%@I^$4M(h+Zd;C>KT_wnQWHEqtz9A&KI#=eIRG5QcCdn_EW!l*d}?JnK_bRo24( z>Y5P~eHEErOuPepC>1lRNbY_w_S$Z|@t-oUj*y^+_H z?Khqb$(j)F?Z<71*b3>NkZjq33w|qTjnblg!qQfq)xLLGe^Pd}p%dPsb>d+$Qa_|} zA5C-CY@@a;#8+R&e~?Ry;FXEFDEkFC`uYaA7eW@ZTui8^XWz7O*uVCWb z8^in&vU{GC3(tK=FHymm{4vFKx!<`g_gNj~HawAJN_yy!>*XsU9y{Ylcz$MP#KFn0#!!1h%!4_i~DGDTIP z$=uW?$v}B<$8Z>4^KONOO5zh=EbkK4NB@=oKnWK`JGY-_?pfCy&(B0jMq+hkKlE&~ zo1Jlc&NX*u015vs+bZ0}3^HxHzub?CI1Hb_Wat|9dJ4y}Szg)Rb^DPF@*BL8M~5c% z!%z)z@^lpg(7jQNJafflInGb&HGK+I^0nsO$-%%u4fDU363I(s;k;(~2R+7I0JpF# zJq^#8OJ?$oBj>UPs;tidv##(a`O=~puPz}}GV_P$E7bl(&41ACdKHN{z!Q3KUzbS& zKQ<2SEiwJ2?!&Y5L?(xGK{gE3sN_9buL)_>zxiy)F}mmT`y7BJ)V^tam_C%h#lndN z+*~1h(d7OqSSM^@k}LwxabJ~0Yo%gbimE>4y;*gZEW(wZIvwo5e)W6*p87pkJlr*P zxi6D-a+%gU>~EOs1fJ`9ikwBY+gkR@Ewt&3U0fre&P@B-ez6^lx8$eIOhA{*EB8aXp9Ldb5~Q;S)3mOt%tztFdUSA@UOf8Iw%aIbLJc5b;)+2=N1qVLidPAHF&RdS11 zzAgAbG+}~JMZdN>B_@IyGsugmq&Krf4el3~v17*wzBFSXq^dD6M*7597Jt%o!Mn}+ zx5;5sna&oVh>;TNMKq}YZdr9a;uv|gtX)?2?&2V&MmGVe!IzHiNo-r(VdNCDxOK^( z)`49@JlRIsNrpQE#WFf-VYuuku~emt6RMw5Du$ll{%gpb;;v2VpW_Z`(o+Bybf02h z`BCR*3o2?nnpAiwF>El*_G0}j&-?VDJu#d#vo1LiFE$t&O=!pP_Q0%Syi0HDIh4nv zZJ&4A0M)qL($^i1NaXA^%yvE!q{L`#?dV)`L)~k0dytE>(ft(#Cljk~JB1wWqJYUw*n+R4|V5XT=PLAYqMtF{}h9 z4I;)~rgIl**U;B4d?n0GO@8=~KLBs6;|8}cUFy49m+w=H|Blnz zkl~vp`yvc6_$r~=&8Lc#n*&(a5bW`+AZT()B3oJa`3RtP0^qAe{YO{^i^P~ogr$?9 za_26IN0h}akH($#xiE*gfRL_q_+}w6Cn0lSO zA;rnhqD=Dp&PdveI+S0muh{El1@!@1K}vH2z`3q8>8uby`gca#p|UVpMoRF*x)CI8 z>94*QWL7(Qe22KlfQlSn;Jbd8P~+vPBKVoZDK})h56K>%1*(RF)hRKIp$iF>s+g_fy2gek;V zM6nox=M7p7?l!=HTtU{TnoLt4C|(hSEA0(6V?63a6l#=z1nRPbHY9+vpE zvb_S*{CvwFz*5z#WWPbb(JeNt9iN2gy}{o>3t7pItRP-0j&KR`=^8$dO^b}(9%L_ZW@!{)tmlR@Pr$$f~Nx8E_fYEvuy%Tai|8yOBAIqR< zsll|~jy!_TkE|izYl}PQmE>=`8XaG3K3}$#Mw?lL-Rc;kk*Y3TR-UoQD;B>k^PSe0 z(=r)a7y9^&Z|kTAh4AgPYujb#OnI|U%JIPlHJ^Eayi`WJkf5375!mX4)J1q3@!)w` zI1p|Kp!D+oDwWlR0k!oeZ>$47W889@E{BrM?6hr+3iNJ31-!*06NU@E%FNQJ!YPXr zRih>PB-$}?7O`p+XYCL|e5HNQWb`y4r7ynCYn-tUohiRqTZA6nq#72bA}2!L6^pT`LNy>Y726rH>!c9$44pkF`BrH&oRwW5oz)V z&+5Mq7Ucsj_D8_0aPc*HF%qFcE#M(axa7`a-*01ej`3LS^F%Iw!8}BOP@o&nHy(U7N&(cA+@{qrX;&Iz%<*VCFAj5$V{F85A#v3K}jZ72<8`bt_{YiA(!s4gCdsWaeg7q{Yd`j1Lwz@|=*W?2$1OTzuyY z4Y>1bbh|ZOdizomXCEslxvrC4jz8cR^>JIRlxD;f>69g%W1MtX7I}4fl~z@D!yq2y zH3a-=492odif7O>blWEMD)DE~dPl29uf(p&59lXOCl|0@=5YmCzjb{Ns*o31TzDPs zv73}nf8&S6dz|{1F@wxiL2F$Vb5U3B)T8Z!|MY?%p&=#(r3zM2n3Pn-x~DZI*4tWH zUP@bghQe^F?gTZetBGR!{5HVGo$^&*`w16!hN$P6Uc}Ms)!y)-^hE`SV-USv*`ANM z^I@YISn%|6)~;#E@toNqd~5ZQBxQEuCddIV*l>*9V=v8MjIa0A%r%d_&Xs1gHTjMd zyZd#)*(C~VCul(XlMb7_kFC{{h?tv<4Wyx&g@E)`abXYn&KrF6EWr`Imi*QpfdN#x zBTV&VXOm>*X-!!6Z@5#oSIUVS-T5lyvm4!qWOSlVu{Pv9LTB=8Yl;dyH)pm0 z2wW63jk657B4cq1&_ZP#=-*?b7QBr;-EKUoOf4e1CIHl0USFgsjgxr~Um>(nOuvSd!iR0~pr0n5a+mm`R@5^yuYe z^)%rA%%7{##%usUSQ!nUGj~w_Gr+(LaGPs5bBIZT0ew(F=YKw)FjY+nl;B3XDHGO7 z{{Q2kYr)+)7AL|NyT6!vj8D>cPcG;b8}5lbY&I$?w?;0OFa8Q_v(QbDE>IHZQnJ_6 z%@8rp*0vSsCIVTAd@`go7_rei)(-T4f9SCj)3nFO8QY>J4;jIn(7+y;<8n*4qWgJP z%)re_{*9$L)_rvwh^5tn)sot(JPi#~z*kM*_9qP3wF4i|Reb{Q0Y0Eh24AW|sZ~EO zDi_QQ$cBd8xT^<#tW$T?g~R|o4m>)oi%ais4JwOAa@Z9C-+9~$H*DB?1=5A$*fvM&%1#eb;I;L?1 z7<7_#N&kV#T`d9GDePgz*7`JkA%NzZ2f0fIivWs}rG}kn$Mxftf7k=@*!fGP93I|j z4GuYSZJnN`Z#ZN5+22mCK_CB|wD;Y)HR#$O$5o(?c7Lb>jh@DVkc<^j@|laSB8rDN z+h&H=G*^LKvL?#6eQpN&C&W=X27;M8$v*Lh|JoWen%<7=fzZ1?NlQRUxD6?%J&Z6C zv&6W}^=@36rDD|mORZxjS>Z%ce<2*`(=(6f6j-|c0;ck37gt3tu2wUsu=pJs20NUq qLs18TS_k*6oV5MFJ86^Yo+)1Y`reB+U*Kp4bk5G{bk)hL_x>*y%(FZI literal 0 HcmV?d00001 diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_c2abd7e6_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadErrorScreenshot_c2abd7e6_0.png new file mode 100644 index 0000000000000000000000000000000000000000..42ec702aad1f4fcf14a50f8e57e700b2b4ca1214 GIT binary patch literal 19910 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 E55aoH2LJ#7 literal 0 HcmV?d00001 diff --git a/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadingScreenshot_748aa731_0.png b/feature/setting/presentation/src/screenshotTestDebug/reference/com/afternote/feature/setting/presentation/notification/PushNotificationScreenScreenshotTestKt/pushNotificationLoadingScreenshot_748aa731_0.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4783be9e1b614ecd4ea7a4b59c48d65fe406d6 GIT binary patch literal 26864 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 N;mqaJ6(?`r{V)IQym