diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt index 48c2f2377e..796f44ebff 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/SettingImplementedCoverageAndroidTest.kt @@ -1,6 +1,6 @@ package com.afternote.afternote_fe -import androidx.activity.compose.setContent +import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsProperties @@ -11,23 +11,25 @@ import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.captureToImage import androidx.compose.ui.test.hasScrollAction import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextInput import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.testing.TestNavHostController -import androidx.navigation.toRoute +import androidx.navigation.NavHostController import androidx.test.ext.junit.runners.AndroidJUnit4 import com.afternote.afternote_fe.navigation.AppNavigation -import com.afternote.afternote_fe.navigation.AppState +import com.afternote.afternote_fe.navigation.rememberAfternoteAppState import com.afternote.afternote_fe.test.FailureArtifactRule +import com.afternote.afternote_fe.test.HiltTestActivity +import com.afternote.core.domain.repository.UserRepository +import com.afternote.core.domain.testing.FakeUserRepository import com.afternote.core.ui.Route import com.afternote.core.ui.theme.AfternoteTheme -import com.afternote.feature.setting.presentation.navigation.SettingRoute import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertEquals @@ -35,6 +37,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import javax.inject.Inject @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -43,7 +46,7 @@ class SettingImplementedCoverageAndroidTest { val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) - val composeRule = createAndroidComposeRule() + val composeRule = createAndroidComposeRule() @get:Rule(order = 2) val failureArtifactRule = @@ -51,30 +54,35 @@ class SettingImplementedCoverageAndroidTest { composeRule.onRoot().captureToImage().asAndroidBitmap() } - private lateinit var navController: TestNavHostController + @Inject + lateinit var userRepository: UserRepository + + private val fakeUserRepository get() = userRepository as FakeUserRepository + + private lateinit var navController: NavHostController + private lateinit var restorationTester: StateRestorationTester @Before fun setUp() { hiltRule.inject() - composeRule.activityRule.scenario.onActivity { activity -> - navController = - TestNavHostController(activity).apply { - navigatorProvider.addNavigator(ComposeNavigator()) - } - activity.setContent { - AfternoteTheme { - AppNavigation( - startDestination = Route.Setting, - appState = AppState(navController), - ) - } + fakeUserRepository.onGetReceiverDetail = null + fakeUserRepository.onGetReceiverDeliveryConditions = null + restorationTester = StateRestorationTester(composeRule) + restorationTester.setContent { + val appState = rememberAfternoteAppState() + SideEffect { navController = appState.navController } + AfternoteTheme { + AppNavigation( + startDestination = Route.Setting(), + appState = appState, + ) } } } @Test fun actualSettingNavHost_receiverManageListRowNavigatesToEditWithExactReceiverId() { - waitForRoute() + waitForRootHost() waitForSettingHomeContent() composeRule.onAllNodes(hasText("수신자 목록")).run { assertCountEquals(2) @@ -85,13 +93,13 @@ class SettingImplementedCoverageAndroidTest { composeRule.onNodeWithText("김수신").performClick() - val editRoute = waitForRoute() - assertEquals(RECEIVER_ID, editRoute.receiverId) + waitForText("수신자 수정") + assertEquals(RECEIVER_ID, fakeUserRepository.receiverDetailCalls.last()) } @Test fun actualSettingNavHost_deliveryConditionReceiverSelectionPreservesExactReceiverId() { - waitForRoute() + waitForRootHost() waitForSettingHomeContent() composeRule .onNodeWithText("사후 전달 조건") @@ -104,20 +112,20 @@ class SettingImplementedCoverageAndroidTest { } composeRule.onNodeWithText("수신자 선택 완료하기").performClick() - val deliveryRoute = waitForRoute() - assertEquals(RECEIVER_ID, deliveryRoute.receiverId) + waitForText("마지막 인사말 수정하기") + assertEquals(RECEIVER_ID, fakeUserRepository.deliveryLoadCalls.last()) composeRule .onNodeWithText("마지막 인사말 수정하기") .assertIsDisplayed() .performClick() - val editRoute = waitForRoute() - assertEquals(RECEIVER_ID, editRoute.receiverId) + waitForText("수신자 수정") + assertEquals(RECEIVER_ID, fakeUserRepository.receiverDetailCalls.last()) } @Test fun actualSettingNavHost_withdrawGuideCancelThenAgreementConfirmPreservesBoundary() { - waitForRoute() + waitForRootHost() openWithdrawGuide() composeRule .onNode(hasScrollAction()) @@ -129,7 +137,7 @@ class SettingImplementedCoverageAndroidTest { .onNodeWithText("취소하기") .performClick() - waitForRoute() + waitForRootHost() openWithdrawGuide() composeRule .onNode(hasScrollAction()) @@ -142,12 +150,30 @@ class SettingImplementedCoverageAndroidTest { .onNodeWithText("탈퇴하기") .performClick() - waitForRoute() + waitForText("안전한 탈퇴 진행을 위해 아래 문장을 입력해 주세요.") composeRule .onNodeWithText("안전한 탈퇴 진행을 위해 아래 문장을 입력해 주세요.") .assertIsDisplayed() } + @Test + fun actualSettingNavHost_savedRouteAndEditInputRestoreThenBackReturnsToReceiverList() { + waitForSettingHomeContent() + composeRule.onAllNodes(hasText("수신자 목록"))[1].performScrollTo().performClick() + composeRule.onNodeWithText("김수신").performClick() + waitForText("수신자 수정") + composeRule.onNodeWithText("김수신").performTextInput("복원") + + restorationTester.emulateSavedInstanceStateRestore() + + waitForText("김수신복원") + assertEquals(RECEIVER_ID, fakeUserRepository.receiverDetailCalls.last()) + composeRule.activityRule.scenario.onActivity { it.onBackPressedDispatcher.onBackPressed() } + waitForText("김수신") + composeRule.onAllNodes(checkboxMatcher).assertCountEquals(0) + waitForRootHost() + } + private fun openWithdrawGuide() { waitForSettingHomeContent() composeRule @@ -156,7 +182,7 @@ class SettingImplementedCoverageAndroidTest { composeRule .onNodeWithText("회원 탈퇴") .performClick() - waitForRoute() + waitForText("회원 탈퇴 안내") } private fun waitForSettingHomeContent() { @@ -165,13 +191,15 @@ class SettingImplementedCoverageAndroidTest { } } - private inline fun waitForRoute(): T { + private fun waitForRootHost() { composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { - navController.currentDestination?.hasRoute() == true + navController.currentDestination?.hasRoute() == true } - return composeRule.runOnIdle { - navController.currentBackStackEntry?.toRoute() - ?: error("Current back stack entry is missing") + } + + private fun waitForText(text: String) { + composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { + composeRule.onAllNodes(hasText(text)).fetchSemanticsNodes().isNotEmpty() } } diff --git a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigation.kt b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigation.kt index 8048096c65..be13f2cfb3 100644 --- a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigation.kt +++ b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigation.kt @@ -23,10 +23,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.toRoute import com.afternote.afternote_fe.notification.NotificationPermissionEffect import com.afternote.core.ui.Route import com.afternote.core.ui.bottombar.BottomBar -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.afternote.presentation.navigation.AfternoteNavHost import com.afternote.feature.afternote.presentation.receiver.navigation.ReceivedAfternoteNavHost @@ -36,7 +37,7 @@ import com.afternote.feature.home.presentation.receiver.ReceiverHomeEntry import com.afternote.feature.mindrecord.presentation.navigation.mindRecordNavGraph import com.afternote.feature.onboarding.presentation.navigation.OnboardingNavHost import com.afternote.feature.receiver.presentation.navigation.ReceiverNavHost -import com.afternote.feature.setting.presentation.navigation.settingNavGraph +import com.afternote.feature.setting.presentation.navigation.SettingNavHost import com.afternote.feature.timeletter.presentation.navigation.timeLetterNavGraph import kotlinx.coroutines.launch @@ -57,7 +58,7 @@ fun AppNavigation( val currentTab = appState.getCurrentNavTab(currentDestination) val mindRecordNavActions = rememberMindRecordNavActions(appState.navController) - val settingNavActions = rememberSettingNavActions(appState) + val settingExternalActions = rememberSettingExternalActions(appState) val timeLetterNavActions = rememberTimeLetterNavActions(appState.navController) val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -73,9 +74,9 @@ fun AppNavigation( // 로컬 스택 바닥에서의 back 은 루트 백스택 pop 으로 돌려준다. 루트가 NavDisplay 로 바뀌어도 // 계약은 그대로고 이 구현만 갈린다 (#1702). // 바텀바가 없는 그래프는 깊이를 셸에 올릴 일이 없다. - val popRootBoundary = rememberRootPopBoundary(appState, onAtRootChanged = {}) - val afternoteBoundary = - rememberRootPopBoundary(appState) { isAtRoot -> isAfternoteStackAtRoot = isAtRoot } + val rootNavigationCallbacks = rememberRootNavigationCallbacks(appState, onAtRootChanged = {}) + val afternoteNavigationCallbacks = + rememberRootNavigationCallbacks(appState) { isAtRoot -> isAfternoteStackAtRoot = isAtRoot } // 13+ 는 런타임 권한이 없으면 알림이 한 건도 게시되지 않는다 (#1454). NotificationPermissionEffect(snackbarHostState = snackbarHostState) @@ -105,25 +106,25 @@ fun AppNavigation( // ── Navigation 3 로컬 스택을 가진 그래프 (#1698) — 루트엔 host destination 하나씩만 둔다. composable { OnboardingNavHost( - boundary = popRootBoundary, + boundary = rootNavigationCallbacks, externalActions = onboardingExternalActions, ) } composable { AfternoteNavHost( - boundary = afternoteBoundary, + navigationCallbacks = afternoteNavigationCallbacks, externalActions = afternoteExternalActions, ) } // 수신 애프터노트 화면은 애프터노트 피처가 갖는다 (#1461). Route.Afternote 그래프는 // 발신자용 지문 관문을 시작점으로 삼으므로 그 안에 중첩하지 않고 루트에 직접 등록한다. composable { - ReceivedAfternoteNavHost(boundary = popRootBoundary) + ReceivedAfternoteNavHost(navigationCallbacks = rootNavigationCallbacks) } composable { ReceiverNavHost( homeContent = { ReceiverHomeEntry(actions = receiverHomeActions) }, - boundary = popRootBoundary, + navigationCallbacks = rootNavigationCallbacks, ) } @@ -154,12 +155,13 @@ fun AppNavigation( actions = homeTabActions, ) } - settingNavGraph( - graphScopedParentEntry = { - appState.navController.getBackStackEntry() - }, - actions = settingNavActions, - ) + composable { entry -> + SettingNavHost( + navigationCallbacks = rootNavigationCallbacks, + externalActions = settingExternalActions, + startWithRecipientRegistration = entry.toRoute().startWithRecipientRegistration, + ) + } mindRecordNavGraph(actions = mindRecordNavActions) timeLetterNavGraph( navController = appState.navController, @@ -175,12 +177,12 @@ fun AppNavigation( * @param onAtRootChanged 바텀바 판정에 깊이를 합성해야 하는 그래프만 넘긴다. */ @Composable -private fun rememberRootPopBoundary( +private fun rememberRootNavigationCallbacks( appState: AppState, onAtRootChanged: (Boolean) -> Unit, -): FeatureStackBoundary = +): FeatureNavigationCallbacks = remember(appState, onAtRootChanged) { - object : FeatureStackBoundary { + object : FeatureNavigationCallbacks { override fun exit() { appState.navController.popBackStack() } diff --git a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt index 963c44fed4..1128df8c41 100644 --- a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt +++ b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt @@ -15,8 +15,7 @@ import com.afternote.feature.mindrecord.presentation.navigation.MindRecordNavAct import com.afternote.feature.mindrecord.presentation.navigation.MindRecordRoute import com.afternote.feature.onboarding.presentation.navigation.OnboardingExternalActions import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRoute -import com.afternote.feature.setting.presentation.navigation.SettingNavActions -import com.afternote.feature.setting.presentation.navigation.SettingRoute +import com.afternote.feature.setting.presentation.navigation.SettingExternalActions import com.afternote.feature.timeletter.presentation.navigation.TimeLetterNavActions import com.afternote.feature.timeletter.presentation.navigation.TimeLetterRoute @@ -94,7 +93,7 @@ fun rememberTimeLetterNavActions(navController: NavController): TimeLetterNavAct remember(navController) { object : TimeLetterNavActions { override fun onSettingClick() { - navController.navigate(Route.Setting) + navController.navigate(Route.Setting()) } override fun onNavigateToWrite() { @@ -144,152 +143,18 @@ fun rememberTimeLetterNavActions(navController: NavController): TimeLetterNavAct } @Composable -fun rememberSettingNavActions(appState: AppState): SettingNavActions = +fun rememberSettingExternalActions(appState: AppState): SettingExternalActions = remember(appState) { - object : SettingNavActions { - override fun onSettingBack() { - appState.navController.popBackStack() - } - - override fun onLogoutSuccess() { - appState.navController.navigate(Route.Onboarding) { - // 로그아웃 — 인증 이후 모든 stack 비우고 Onboarding 진입. 뒤로가기로 로그인 상태 화면에 못 돌아가게. - popUpTo(0) { inclusive = true } - } - } - - override fun onNavigateToWithdrawGuide() { - appState.navController.navigate(SettingRoute.WithdrawGuideRoute) - } - - override fun onNavigateToWithdrawConfirm() { - appState.navController.navigate(SettingRoute.WithdrawConfirmRoute) - } + object : SettingExternalActions { + override fun onLogoutSuccess() = openOnboarding() - override fun onWithdrawGuideBack() { - appState.navController.popBackStack() - } + override fun onWithdrawSuccess() = openOnboarding() - override fun onWithdrawConfirmBack() { - appState.navController.popBackStack() - } - - override fun onWithdrawSuccess() { + private fun openOnboarding() { appState.navController.navigate(Route.Onboarding) { - // 탈퇴 — 계정 사라진 상태라 stack 전체 비우고 Onboarding 진입. 뒤로가기로 인증된 화면에 못 돌아가게. popUpTo(0) { inclusive = true } } } - - override fun onNavigateToProfileEdit() { - appState.navController.navigate(SettingRoute.ProfileEditRoute) - } - - override fun onProfileEditBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToLinkedAccount() { - appState.navController.navigate(SettingRoute.LinkedAccountRoute) - } - - override fun onLinkedAccountBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToNotification() { - appState.navController.navigate(SettingRoute.NotificationRoute) - } - - override fun onNotificationBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToPushNotification() { - appState.navController.navigate(SettingRoute.PushNotificationRoute) - } - - override fun onPushNotificationBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToRecipientList() { - appState.navController.navigate(SettingRoute.RecipientListRoute()) - } - - override fun onNavigateToRecipientListForDeliveryConditions() { - appState.navController.navigate( - SettingRoute.RecipientListRoute(selectForDeliveryConditions = true), - ) - } - - override fun onRecipientListBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToRecipientRegister() { - appState.navController.navigate(SettingRoute.RecipientRegisterRoute) - } - - override fun onRecipientRegisterBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToRecipientEdit(receiverId: Long) { - appState.navController.navigate(SettingRoute.RecipientEditRoute(receiverId)) - } - - override fun onRecipientEditBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToAfterDelivery(receiverId: Long) { - appState.navController.navigate(SettingRoute.AfterDeliveryRoute(receiverId)) - } - - override fun onAfterDeliveryBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToPasskey() { - appState.navController.navigate(SettingRoute.PasskeyRoute) - } - - override fun onPasskeyBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToPasskeyMaking() { - appState.navController.navigate(SettingRoute.PasskeyMakingRoute) - } - - override fun onPasskeyMakingBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToPasskeyPassword() { - appState.navController.navigate(SettingRoute.PasskeyPasswordRoute) - } - - override fun onPasskeyPasswordBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToAppLock() { - appState.navController.navigate(SettingRoute.AppLockSetupRoute) - } - - override fun onAppLockBack() { - appState.navController.popBackStack() - } - - override fun onNavigateToNotice() { - appState.navController.navigate(SettingRoute.NoticeRoute) - } - - override fun onNoticeBack() { - appState.navController.popBackStack() - } } } @@ -307,7 +172,7 @@ fun rememberHomeTabActions( // 나오고 사용자는 다시 등록 화면을 찾아야 한다. 상태를 해소하는 화면으로 보낸다. // // 목적지가 등록인지 목록인지는 기획 확정 전이다 (#506) — docs/qa/assumptions.md 참고. - appState.navController.navigate(SettingRoute.RecipientRegisterRoute) + appState.navController.navigate(Route.Setting(startWithRecipientRegistration = true)) } override fun onAnswerClick() { @@ -349,7 +214,7 @@ fun rememberHomeTabActions( } override fun onSettingClick() { - appState.navController.navigate(Route.Setting) + appState.navController.navigate(Route.Setting()) } override fun onRetryLoad() { @@ -400,7 +265,7 @@ fun rememberAfternoteExternalActions( } override fun navigateToSetting() { - appState.navController.navigate(Route.Setting) + appState.navController.navigate(Route.Setting()) } override fun onFingerprintAuthFailed(message: String) { diff --git a/app/src/test/java/com/afternote/afternote_fe/navigation/AuthBoundaryBackStackTest.kt b/app/src/test/java/com/afternote/afternote_fe/navigation/AuthBoundaryBackStackTest.kt index ed6c7ec1e6..917c8475e3 100644 --- a/app/src/test/java/com/afternote/afternote_fe/navigation/AuthBoundaryBackStackTest.kt +++ b/app/src/test/java/com/afternote/afternote_fe/navigation/AuthBoundaryBackStackTest.kt @@ -2,6 +2,7 @@ package com.afternote.afternote_fe.navigation import android.app.Application import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.navigation.toRoute import com.afternote.core.ui.Route import org.junit.Assert.assertEquals import org.junit.Rule @@ -68,7 +69,6 @@ class AuthBoundaryBackStackTest { start(Route.Home) composeRule.runOnIdle { harness.homeActions.onSettingClick() } - composeRule.runOnIdle { harness.settingActions.onNavigateToWithdrawGuide() } composeRule.runOnIdle { harness.settingActions.onLogoutSuccess() } @@ -81,8 +81,6 @@ class AuthBoundaryBackStackTest { start(Route.Home) composeRule.runOnIdle { harness.homeActions.onSettingClick() } - composeRule.runOnIdle { harness.settingActions.onNavigateToWithdrawGuide() } - composeRule.runOnIdle { harness.settingActions.onNavigateToWithdrawConfirm() } composeRule.runOnIdle { harness.settingActions.onWithdrawSuccess() } @@ -90,6 +88,23 @@ class AuthBoundaryBackStackTest { assertEquals(false, composeRule.runOnIdle { harness.navController.popBackStack() }) } + @Test + fun `home recipient shortcut opens registration host and returns directly to home`() { + start(Route.Home) + composeRule.runOnIdle { harness.homeActions.onRecipientChipClick() } + assertEquals(listOf("NavHostRoot", "Home", "Setting"), routes()) + assertEquals( + true, + composeRule.runOnIdle { + harness.navController.currentBackStackEntry!! + .toRoute() + .startWithRecipientRegistration + }, + ) + composeRule.runOnIdle { harness.navController.popBackStack() } + assertEquals(listOf("NavHostRoot", "Home"), routes()) + } + @Test fun `이관된 그래프는 루트에 host destination 한 칸만 쌓는다`() { start(Route.Home) diff --git a/app/src/test/java/com/afternote/afternote_fe/navigation/NavBackStackHarness.kt b/app/src/test/java/com/afternote/afternote_fe/navigation/NavBackStackHarness.kt index 95c4e85a37..e4207baaea 100644 --- a/app/src/test/java/com/afternote/afternote_fe/navigation/NavBackStackHarness.kt +++ b/app/src/test/java/com/afternote/afternote_fe/navigation/NavBackStackHarness.kt @@ -21,8 +21,7 @@ import com.afternote.core.ui.Route import com.afternote.feature.home.presentation.HomeTabActions import com.afternote.feature.mindrecord.presentation.navigation.MindRecordRoute import com.afternote.feature.onboarding.presentation.navigation.OnboardingExternalActions -import com.afternote.feature.setting.presentation.navigation.SettingNavActions -import com.afternote.feature.setting.presentation.navigation.SettingRoute +import com.afternote.feature.setting.presentation.navigation.SettingExternalActions import com.afternote.feature.timeletter.presentation.navigation.TimeLetterNavActions import com.afternote.feature.timeletter.presentation.navigation.TimeLetterRoute @@ -41,7 +40,7 @@ import com.afternote.feature.timeletter.presentation.navigation.TimeLetterRoute internal class NavBackStackHarness( val appState: AppState, val onboardingExternalActions: OnboardingExternalActions, - val settingActions: SettingNavActions, + val settingActions: SettingExternalActions, val timeLetterActions: TimeLetterNavActions, val homeActions: HomeTabActions, /** predictive back 제스처를 프로그램으로 흘려 넣기 위한 호스트 dispatcher. */ @@ -62,7 +61,7 @@ internal fun SkeletonAppNavigation( ) { val appState = rememberAfternoteAppState() val onboardingExternalActions = rememberOnboardingExternalActions(appState) - val settingActions = rememberSettingNavActions(appState) + val settingActions = rememberSettingExternalActions(appState) val timeLetterActions = rememberTimeLetterNavActions(appState.navController) val homeActions = rememberHomeTabActions(appState, onRetryLoad = { }) val backDispatcher = @@ -117,12 +116,8 @@ internal fun NavGraphBuilder.appRouteSkeleton() { // app — AppNavigation.kt 가 직접 등록하는 홈 탭 stubScreen(stateful = true) - // feature/setting — SettingNavGraph.kt. 로그아웃·탈퇴 경계가 지나는 화면만 옮긴다. - navigation(startDestination = SettingRoute.SettingHomeRoute) { - stubScreen() - stubScreen() - stubScreen() - } + // Settings owns a local stack; the shell only registers its host. + stubScreen() // feature/mindrecord — MindRecordNavGraph.kt. 중첩 없이 루트에 직접 붙는 top-level 3종과 // 임시저장 목록·기록 상세만 옮긴다(작성 화면은 어느 테스트도 지나지 않는다). diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/Route.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/Route.kt index 447c06101b..640818aa7f 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/Route.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/Route.kt @@ -53,7 +53,9 @@ sealed interface Route { @Serializable data object Receiver : Route - /** 임시 로그아웃 진입용 설정 화면. 정식 설정 IA 확정 전까지 단일 화면. */ + /** Settings local-stack boundary, including the home recipient-registration shortcut. */ @Serializable - data object Setting : Route + data class Setting( + val startWithRecipientRegistration: Boolean = false, + ) : Route } diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplay.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplay.kt index 4e44f25ca5..1484a4fd4a 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplay.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplay.kt @@ -38,22 +38,24 @@ import androidx.navigation3.ui.NavDisplay * `else -> boundary.exit()` 에 실제로 도달하는 것은 **화면 안 back 버튼**(`popOrExit`)뿐이다. * `FeatureNavDisplayTest` 의 「back 은 스택만 줄이고 바닥에서는 이 표시부를 지나쳐 위로 흐른다」가 * 이 갈림을 잠근다. + * + * #1996: 온보딩의 named argument 호환을 위해 `boundary` 이름을 소비처 전환까지 유지한다. */ @Composable public fun FeatureNavDisplay( backStack: NavBackStack, - boundary: FeatureStackBoundary, + boundary: FeatureNavigationCallbacks, modifier: Modifier = Modifier, entryProvider: (NavKey) -> NavEntry, ) { - val currentBoundary by rememberUpdatedState(boundary) + val currentNavigationCallbacks by rememberUpdatedState(boundary) val isAtRoot = backStack.size <= 1 - LaunchedEffect(isAtRoot) { currentBoundary.onAtRootChanged(isAtRoot) } + LaunchedEffect(isAtRoot) { currentNavigationCallbacks.onAtRootChanged(isAtRoot) } // 탭 이탈로 host 가 컴포지션에서 빠질 때 깊이 신호를 되돌린다. 안 되돌리면 다른 탭의 // 바텀바 판정이 이 피처의 마지막 깊이에 오염된다. DisposableEffect(Unit) { - onDispose { currentBoundary.onAtRootChanged(true) } + onDispose { currentNavigationCallbacks.onAtRootChanged(true) } } NavDisplay( @@ -63,7 +65,7 @@ public fun FeatureNavDisplay( if (backStack.size > 1) { backStack.removeAt(backStack.lastIndex) } else { - currentBoundary.exit() + currentNavigationCallbacks.exit() } }, entryDecorators = rememberStandardNavEntryDecorators(), diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavigationCallbacks.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavigationCallbacks.kt new file mode 100644 index 0000000000..89710cafcf --- /dev/null +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureNavigationCallbacks.kt @@ -0,0 +1,31 @@ +package com.afternote.core.ui.navigation + +import androidx.compose.runtime.Stable + +/** + * 피처의 로컬 Navigation 3 스택이 부모 앱 셸에 요청·알림을 전달하는 콜백. + * + * 로컬 스택은 제 화면 사이의 push/pop 을 스스로 처리하고, 스스로 답할 수 없는 두 가지만 셸에 + * 돌려준다 — 스택 바닥에서의 back([exit])과 바텀바 표시 판정에 필요한 깊이([onAtRootChanged]). + * 그 밖의 이동(다른 소관 그래프로 가기)은 피처마다 달라서 각 host 가 제 콜백으로 받는다. + * + * 루트가 Nav2 인 동안 [exit] 는 루트 `NavController` 의 pop 이고, 루트가 `NavDisplay` 로 바뀌면 + * 루트 백스택의 pop 이 된다 — 구현만 갈리고 계약은 그대로다 (#1702 가 소비한다). + */ +@Stable +public interface FeatureNavigationCallbacks { + /** 로컬 스택 바닥에서 back 이 눌렸다. 셸이 이 피처를 백스택에서 내린다. */ + public fun exit() + + /** + * 현재 화면이 로컬 백스택의 시작 화면인지 여부가 변경됐다. + * + * 셸의 바텀바 판정은 Nav2 destination 만 보므로 로컬 스택 깊이를 모른다. 깊이를 아는 쪽이 + * 셸에 올려 판정에 합성한다. 피처를 떠날 때는 `true` 로 복원해 다른 탭 판정을 오염시키지 + * 않는다 — [FeatureNavDisplay] 가 대신 해 준다. + */ + public fun onAtRootChanged(isAtRoot: Boolean) {} +} + +/** 바텀바가 없어 셸로 돌아갈 길만 필요한 그래프용 [FeatureNavigationCallbacks]. */ +public fun FeatureNavigationCallbacks(onExit: () -> Unit): FeatureNavigationCallbacks = FeatureStackBoundary(onExit) diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureStackBoundary.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureStackBoundary.kt index 1208847c9f..28933cc551 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureStackBoundary.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/FeatureStackBoundary.kt @@ -1,33 +1,14 @@ package com.afternote.core.ui.navigation -import androidx.compose.runtime.Stable - /** - * 피처의 로컬 Navigation 3 스택이 앱 셸과 만나는 **최소 경계**. - * - * 로컬 스택은 제 화면 사이의 push/pop 을 스스로 처리하고, 스스로 답할 수 없는 두 가지만 셸에 - * 돌려준다 — 스택 바닥에서의 back([exit])과 바텀바 표시 판정에 필요한 깊이([onAtRootChanged]). - * 그 밖의 이동(다른 소관 그래프로 가기)은 피처마다 달라서 각 host 가 제 콜백으로 받는다. + * [FeatureNavigationCallbacks]의 이전 이름. * - * 루트가 Nav2 인 동안 [exit] 는 루트 `NavController` 의 pop 이고, 루트가 `NavDisplay` 로 바뀌면 - * 루트 백스택의 pop 이 된다 — 구현만 갈리고 계약은 그대로다 (#1702 가 소비한다). + * 온보딩 담당자가 소비처를 전환할 때까지 소스 호환을 유지한다 (#1996). + * 기존 이름의 소스 사용처가 모두 사라진 뒤 이 별칭과 아래 팩토리를 제거한다. */ -@Stable -public interface FeatureStackBoundary { - /** 로컬 스택 바닥에서 back 이 눌렸다. 셸이 이 피처를 백스택에서 내린다. */ - public fun exit() - - /** - * 로컬 스택이 바닥(= 피처 시작 화면)인지 바뀌었다. - * - * 셸의 바텀바 판정은 Nav2 destination 만 보므로 로컬 스택 깊이를 모른다. 깊이를 아는 쪽이 - * 셸에 올려 판정에 합성한다. 피처를 떠날 때는 `true` 로 복원해 다른 탭 판정을 오염시키지 - * 않는다 — [FeatureNavDisplay] 가 대신 해 준다. - */ - public fun onAtRootChanged(isAtRoot: Boolean) {} -} +public typealias FeatureStackBoundary = FeatureNavigationCallbacks -/** 바텀바가 없어 셸로 돌아갈 길만 필요한 그래프용 [FeatureStackBoundary]. */ +/** 기존 팩토리 호출의 소스 호환을 유지한다. 새 코드는 [FeatureNavigationCallbacks]를 사용한다. */ public fun FeatureStackBoundary(onExit: () -> Unit): FeatureStackBoundary = object : FeatureStackBoundary { override fun exit() = onExit() diff --git a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/NavBackStackOps.kt b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/NavBackStackOps.kt index fa3587fe69..c317bdf219 100644 --- a/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/NavBackStackOps.kt +++ b/core/ui/src/main/kotlin/com/afternote/core/ui/navigation/NavBackStackOps.kt @@ -4,17 +4,17 @@ import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey /** - * 한 칸 내린다 — **바닥이면 스택을 비우지 않고** [boundary] 에 넘긴다. + * 한 칸 내린다 — **바닥이면 스택을 비우지 않고** [navigationCallbacks] 에 넘긴다. * * Nav3 는 빈 백스택을 그릴 수 없고, 바닥에서의 back 은 이 피처를 떠난다는 뜻이라 셸이 판단할 * 몫이다. 이 파일의 나머지 확장도 같은 자리다 — Nav2 가 `popUpTo` 옵션으로 적던 «결과 상태» 를 * 로컬 스택에선 직접 만들어야 해서, 그 모양들을 한 곳에 모았다 (#1698). */ -public fun NavBackStack.popOrExit(boundary: FeatureStackBoundary) { +public fun NavBackStack.popOrExit(navigationCallbacks: FeatureNavigationCallbacks) { if (size > 1) { removeAt(lastIndex) } else { - boundary.exit() + navigationCallbacks.exit() } } diff --git a/core/ui/src/test/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplayTest.kt b/core/ui/src/test/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplayTest.kt index f274be9fad..3718887ec4 100644 --- a/core/ui/src/test/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplayTest.kt +++ b/core/ui/src/test/kotlin/com/afternote/core/ui/navigation/FeatureNavDisplayTest.kt @@ -63,8 +63,8 @@ class FeatureNavDisplayTest { private lateinit var parentStack: NavBackStack private lateinit var flowStack: NavBackStack - private val boundary = - object : FeatureStackBoundary { + private val navigationCallbacks = + object : FeatureNavigationCallbacks { override fun exit() { exits += 1 } @@ -81,7 +81,7 @@ class FeatureNavDisplayTest { FeatureNavDisplay( backStack = stack, - boundary = boundary, + boundary = navigationCallbacks, entryProvider = entryProvider { entry { BasicText("root") } @@ -138,12 +138,12 @@ class FeatureNavDisplayTest { } /** - * **바닥에서의 back 은 `boundary.exit()` 로 가지 않는다.** + * **바닥에서의 back 은 `navigationCallbacks.exit()` 로 가지 않는다.** * * `NavDisplay` 는 `isBackEnabled = scene.previousEntries.isNotEmpty()` 로 핸들러를 켜고 * (`NavDisplay.kt:558`), `SinglePaneScene.previousEntries` 는 `entries.dropLast(1)` 이다 * (`SinglePaneScene.kt:65`). 스택 크기 1 이면 그 목록이 비어 **핸들러 자체가 꺼지고** back 은 - * 상위로 흘러간다. [FeatureNavDisplay] 의 `onBack` 에 있는 `else -> boundary.exit()` 갈래는 + * 상위로 흘러간다. [FeatureNavDisplay] 의 `onBack` 에 있는 `else -> navigationCallbacks.exit()` 갈래는 * 화면 안 back 버튼(`popOrExit`)으로만 도달한다. */ @Test @@ -162,7 +162,7 @@ class FeatureNavDisplayTest { composeRule.waitForIdle() assertEquals("바닥에서는 스택을 비우지 않는다", 1, backStack.size) assertEquals("핸들러가 꺼져 있어 위로 흘러간다", 1, owner.fallbacks) - assertEquals("boundary.exit() 은 back 경로로 도달하지 않는다", 0, exits) + assertEquals("navigationCallbacks.exit() 은 back 경로로 도달하지 않는다", 0, exits) } private fun push(key: NavKey) = composeRule.runOnIdle { backStack.add(key) } @@ -182,7 +182,7 @@ class FeatureNavDisplayTest { FeatureNavDisplay( backStack = outer, - boundary = boundary, + boundary = navigationCallbacks, entryProvider = entryProvider { entry { @@ -200,7 +200,7 @@ class FeatureNavDisplayTest { SideEffect { flowStack = inner } FeatureNavDisplay( backStack = inner, - boundary = boundary, + boundary = navigationCallbacks, entryProvider = entryProvider { entry { key -> BasicText("step${key.step}") } diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowHost.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowHost.kt index ed984ae2e3..4c6a15373a 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowHost.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowHost.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import com.afternote.core.ui.navigation.FeatureNavDisplay -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.afternote.presentation.editor.AfternoteEditorNavigation import com.afternote.feature.afternote.presentation.editor.AfternoteEditorViewModel import com.afternote.feature.afternote.presentation.editor.memorial.AddSongViewModel @@ -44,12 +44,12 @@ internal fun AfternoteEditorFlowHost( creationCallback = { factory -> factory.create(key) }, ) val flowStack = rememberNavBackStack(AfternoteRoute.EditorRoute) - val boundary = remember(onExitFlow) { FeatureStackBoundary(onExitFlow) } + val navigationCallbacks = remember(onExitFlow) { FeatureNavigationCallbacks(onExitFlow) } val actions = - remember(flowStack, boundary, editorViewModel, onSaveSuccessNavigateHome) { + remember(flowStack, navigationCallbacks, editorViewModel, onSaveSuccessNavigateHome) { AfternoteEditorFlowLocalNavActions( flowStack = flowStack, - boundary = boundary, + navigationCallbacks = navigationCallbacks, onReceiversSelected = editorViewModel::onReceiversSelected, onSaveSuccessNavigateHome = onSaveSuccessNavigateHome, ) @@ -57,7 +57,7 @@ internal fun AfternoteEditorFlowHost( FeatureNavDisplay( backStack = flowStack, - boundary = boundary, + boundary = navigationCallbacks, entryProvider = entryProvider { entry { diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActions.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActions.kt index fd7d03de8c..00155a9367 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActions.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActions.kt @@ -3,7 +3,7 @@ package com.afternote.feature.afternote.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.afternote.core.ui.bottombar.BottomNavTab -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.core.ui.navigation.popOrExit import com.afternote.core.ui.navigation.replaceAllWith import com.afternote.feature.afternote.domain.AfternoteType @@ -17,10 +17,10 @@ import com.afternote.feature.afternote.presentation.navigation.model.AfternoteRo */ internal class AfternoteLocalNavActions( private val backStack: NavBackStack, - private val boundary: FeatureStackBoundary, + private val navigationCallbacks: FeatureNavigationCallbacks, private val externalActions: AfternoteExternalActions, ) : AfternoteNavActions { - override fun popBack(): Unit = backStack.popOrExit(boundary) + override fun popBack(): Unit = backStack.popOrExit(navigationCallbacks) override fun navigateToAfternoteDetail(itemId: Long) { backStack.add(AfternoteRoute.DetailRoute(itemId = itemId)) @@ -58,11 +58,11 @@ internal class AfternoteLocalNavActions( */ internal class AfternoteEditorFlowLocalNavActions( private val flowStack: NavBackStack, - private val boundary: FeatureStackBoundary, + private val navigationCallbacks: FeatureNavigationCallbacks, private val onReceiversSelected: (List) -> Unit, private val onSaveSuccessNavigateHome: () -> Unit, ) : AfternoteEditorFlowNavActions { - override fun popBack(): Unit = flowStack.popOrExit(boundary) + override fun popBack(): Unit = flowStack.popOrExit(navigationCallbacks) override fun navigateToMemorialPlaylist() { flowStack.add(AfternoteRoute.MemorialPlaylistRoute) diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteNavHost.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteNavHost.kt index 85618dec5f..8e3aa08220 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteNavHost.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteNavHost.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import com.afternote.core.ui.navigation.FeatureNavDisplay -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.afternote.presentation.AfternoteHostViewModel import com.afternote.feature.afternote.presentation.detail.AfternoteDetailNavigation import com.afternote.feature.afternote.presentation.detail.AfternoteDetailViewModel @@ -33,14 +33,14 @@ import com.afternote.feature.afternote.presentation.shared.fingerprint.Afternote */ @Composable public fun AfternoteNavHost( - boundary: FeatureStackBoundary, + navigationCallbacks: FeatureNavigationCallbacks, externalActions: AfternoteExternalActions, modifier: Modifier = Modifier, ) { val backStack = rememberNavBackStack(AfternoteRoute.FingerprintLoginRoute) val actions = - remember(backStack, boundary, externalActions) { - AfternoteLocalNavActions(backStack, boundary, externalActions) + remember(backStack, navigationCallbacks, externalActions) { + AfternoteLocalNavActions(backStack, navigationCallbacks, externalActions) } // 그래프 전체가 공유하는 ViewModel — 상세는 KDoc 참고. entry 안에서 만들면 그 화면이 pop 될 때 @@ -49,7 +49,7 @@ public fun AfternoteNavHost( FeatureNavDisplay( backStack = backStack, - boundary = boundary, + boundary = navigationCallbacks, modifier = modifier, entryProvider = entryProvider { diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActions.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActions.kt index ee444ba57b..c24d9e0ec5 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActions.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActions.kt @@ -2,7 +2,7 @@ package com.afternote.feature.afternote.presentation.receiver.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.core.ui.navigation.popOrExit import com.afternote.core.ui.navigation.popUpTo @@ -13,9 +13,9 @@ import com.afternote.core.ui.navigation.popUpTo */ internal class ReceivedAfternoteLocalNavActions( private val backStack: NavBackStack, - private val boundary: FeatureStackBoundary, + private val navigationCallbacks: FeatureNavigationCallbacks, ) : ReceivedAfternoteNavActions { - override fun popBack(): Unit = backStack.popOrExit(boundary) + override fun popBack(): Unit = backStack.popOrExit(navigationCallbacks) /** * 상세 하단 "애프터노트 확인하기" — 목록에서 상세로 들어온 왕복이라 [목록 → 상세 → 목록] 을 diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavHost.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavHost.kt index 6dfb59a003..bd4d986688 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavHost.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavHost.kt @@ -12,7 +12,7 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import com.afternote.core.ui.loading.LoadingBody import com.afternote.core.ui.navigation.FeatureNavDisplay -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.afternote.presentation.navigation.AfternoteLightTheme import com.afternote.feature.afternote.presentation.receiver.afternotelist.ReceiverAfternoteHomeEntry import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailRoute @@ -32,15 +32,15 @@ import com.afternote.feature.afternote.presentation.shared.detail.DetailLoadErro */ @Composable public fun ReceivedAfternoteNavHost( - boundary: FeatureStackBoundary, + navigationCallbacks: FeatureNavigationCallbacks, modifier: Modifier = Modifier, ) { val backStack = rememberNavBackStack(ReceivedAfternoteRoute.ListRoute) - val actions = remember(backStack, boundary) { ReceivedAfternoteLocalNavActions(backStack, boundary) } + val actions = remember(backStack, navigationCallbacks) { ReceivedAfternoteLocalNavActions(backStack, navigationCallbacks) } FeatureNavDisplay( backStack = backStack, - boundary = boundary, + boundary = navigationCallbacks, modifier = modifier, entryProvider = entryProvider { diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowLocalNavActionsTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowLocalNavActionsTest.kt index 9c5854a2de..5305ac05fd 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowLocalNavActionsTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteEditorFlowLocalNavActionsTest.kt @@ -2,7 +2,7 @@ package com.afternote.feature.afternote.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.afternote.presentation.navigation.model.AfternoteRoute import org.junit.Assert.assertEquals import org.junit.Test @@ -21,7 +21,7 @@ class AfternoteEditorFlowLocalNavActionsTest { private val actions = AfternoteEditorFlowLocalNavActions( flowStack = flowStack, - boundary = FeatureStackBoundary { exits += 1 }, + navigationCallbacks = FeatureNavigationCallbacks { exits += 1 }, onReceiversSelected = { confirmedReceivers += it }, onSaveSuccessNavigateHome = { savedHome += 1 }, ) diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActionsTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActionsTest.kt index f0179ab538..586fa3beb4 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActionsTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/navigation/AfternoteLocalNavActionsTest.kt @@ -3,7 +3,7 @@ package com.afternote.feature.afternote.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import com.afternote.core.ui.bottombar.BottomNavTab -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.afternote.domain.AfternoteType import com.afternote.feature.afternote.presentation.navigation.model.AfternoteRoute import org.junit.Assert.assertEquals @@ -24,7 +24,7 @@ class AfternoteLocalNavActionsTest { private val actions = AfternoteLocalNavActions( backStack = backStack, - boundary = FeatureStackBoundary { exits += 1 }, + navigationCallbacks = FeatureNavigationCallbacks { exits += 1 }, externalActions = external, ) diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActionsTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActionsTest.kt index 7b22527c77..7326d02f27 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActionsTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteLocalNavActionsTest.kt @@ -2,7 +2,7 @@ package com.afternote.feature.afternote.presentation.receiver.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import org.junit.Assert.assertEquals import org.junit.Test @@ -13,7 +13,7 @@ class ReceivedAfternoteLocalNavActionsTest { private val actions = ReceivedAfternoteLocalNavActions( backStack = backStack, - boundary = FeatureStackBoundary { exits += 1 }, + navigationCallbacks = FeatureNavigationCallbacks { exits += 1 }, ) private fun stack(): List = backStack.map { it::class.simpleName!! } diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowHost.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowHost.kt index 09ee8c52d0..05cf58ee04 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowHost.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowHost.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import com.afternote.core.ui.navigation.FeatureNavDisplay -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.receiver.presentation.deliveryverification.DeliveryVerificationCompleteScreen import com.afternote.feature.receiver.presentation.deliveryverification.DeliveryVerificationFlowViewModel import com.afternote.feature.receiver.presentation.deliveryverification.DocumentUploadScreen @@ -45,15 +45,15 @@ internal fun DeliveryVerificationFlowHost( creationCallback = { factory -> factory.create(key) }, ) val stepStack = rememberNavBackStack(ReceiverRoute.IdentityVerificationIntroRoute) - val boundary = remember(onExitFlow) { FeatureStackBoundary(onExitFlow) } + val navigationCallbacks = remember(onExitFlow) { FeatureNavigationCallbacks(onExitFlow) } val actions = - remember(stepStack, boundary, onExitToReceivedRecords) { - DeliveryVerificationFlowLocalNavActions(stepStack, boundary, onExitToReceivedRecords) + remember(stepStack, navigationCallbacks, onExitToReceivedRecords) { + DeliveryVerificationFlowLocalNavActions(stepStack, navigationCallbacks, onExitToReceivedRecords) } FeatureNavDisplay( backStack = stepStack, - boundary = boundary, + boundary = navigationCallbacks, entryProvider = entryProvider { entry { diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActions.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActions.kt index 8c96a3ce9c..bc7576e3b8 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActions.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActions.kt @@ -2,7 +2,7 @@ package com.afternote.feature.receiver.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.core.ui.navigation.popOrExit import com.afternote.core.ui.navigation.popUpTo import com.afternote.core.ui.navigation.replaceAllWith @@ -15,9 +15,9 @@ import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRout */ internal class ReceiverLocalNavActions( private val backStack: NavBackStack, - private val boundary: FeatureStackBoundary, + private val navigationCallbacks: FeatureNavigationCallbacks, ) : ReceiverNavActions { - override fun popBack(): Unit = backStack.popOrExit(boundary) + override fun popBack(): Unit = backStack.popOrExit(navigationCallbacks) override fun navigateToSenderRegistration() { backStack.add(ReceiverRoute.SenderRegistrationRoute) @@ -47,10 +47,10 @@ internal class ReceiverLocalNavActions( */ internal class DeliveryVerificationFlowLocalNavActions( private val stepStack: NavBackStack, - private val boundary: FeatureStackBoundary, + private val navigationCallbacks: FeatureNavigationCallbacks, private val onExitToReceivedRecords: () -> Unit, ) : DeliveryVerificationFlowNavActions { - override fun popBack(): Unit = stepStack.popOrExit(boundary) + override fun popBack(): Unit = stepStack.popOrExit(navigationCallbacks) override fun navigateToIdentityVerificationEmail() { stepStack.add(ReceiverRoute.IdentityVerificationEmailRoute) diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverNavHost.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverNavHost.kt index 04fbb1c548..fe7887eb3c 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverNavHost.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverNavHost.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import com.afternote.core.ui.navigation.FeatureNavDisplay -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRoute import com.afternote.feature.receiver.presentation.recordsbox.ReceivedRecordsScreen import com.afternote.feature.receiver.presentation.recordsbox.SenderRegistrationScreen @@ -20,20 +20,20 @@ import com.afternote.feature.receiver.presentation.senderdetail.SenderDetailScre * 보존하기 위해서다. * * @param homeContent 앱 셸이 주는 수신자 홈 — 다른 피처의 수신 화면을 조합하므로 셸이 만든다. - * @param boundary 스택 바닥에서의 back 경계. + * @param navigationCallbacks 스택 바닥에서의 back 경계. */ @Composable public fun ReceiverNavHost( homeContent: @Composable () -> Unit, - boundary: FeatureStackBoundary, + navigationCallbacks: FeatureNavigationCallbacks, modifier: Modifier = Modifier, ) { val backStack = rememberNavBackStack(ReceiverRoute.ReceivedRecordsRoute) - val actions = remember(backStack, boundary) { ReceiverLocalNavActions(backStack, boundary) } + val actions = remember(backStack, navigationCallbacks) { ReceiverLocalNavActions(backStack, navigationCallbacks) } FeatureNavDisplay( backStack = backStack, - boundary = boundary, + boundary = navigationCallbacks, modifier = modifier, entryProvider = entryProvider { diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowLocalNavActionsTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowLocalNavActionsTest.kt index 3b73f83d86..41dde96695 100644 --- a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowLocalNavActionsTest.kt +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/DeliveryVerificationFlowLocalNavActionsTest.kt @@ -2,7 +2,7 @@ package com.afternote.feature.receiver.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRoute import org.junit.Assert.assertEquals import org.junit.Test @@ -20,7 +20,7 @@ class DeliveryVerificationFlowLocalNavActionsTest { private val actions = DeliveryVerificationFlowLocalNavActions( stepStack = stepStack, - boundary = FeatureStackBoundary { exits += 1 }, + navigationCallbacks = FeatureNavigationCallbacks { exits += 1 }, onExitToReceivedRecords = { exitsToRecords += 1 }, ) diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActionsTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActionsTest.kt index 1e5c1d822e..ed177b7578 100644 --- a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActionsTest.kt +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/navigation/ReceiverLocalNavActionsTest.kt @@ -2,7 +2,7 @@ package com.afternote.feature.receiver.presentation.navigation import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey -import com.afternote.core.ui.navigation.FeatureStackBoundary +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks import com.afternote.feature.receiver.presentation.navigation.model.ReceiverRoute import org.junit.Assert.assertEquals import org.junit.Test @@ -16,7 +16,7 @@ class ReceiverLocalNavActionsTest { private val actions = ReceiverLocalNavActions( backStack = backStack, - boundary = FeatureStackBoundary { exits += 1 }, + navigationCallbacks = FeatureNavigationCallbacks { exits += 1 }, ) private fun stack(): List = backStack.map { it::class.simpleName!! } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/DeviceAlarmOffSection.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/DeviceAlarmOffSection.kt index e5de47f6d7..f3aab4e617 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/DeviceAlarmOffSection.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/DeviceAlarmOffSection.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.presentation.R @@ -69,14 +68,3 @@ fun DeviceAlarmOffSection( } } } - -@Preview(showBackground = true) -@Composable -private fun DeviceAlarmOffSectionPrev() { - DeviceAlarmOffSection( - uiState = PushNotificationUiState(), - onSmsCheck = {}, - onEmailCheck = {}, - onPushCheck = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/InsertPasswordContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/InsertPasswordContent.kt index 27c6c71f0a..6fa049d78d 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/InsertPasswordContent.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/InsertPasswordContent.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.presentation.R @@ -51,15 +50,3 @@ fun InsertPasswordContent( Spacer(modifier = Modifier.height(40.dp)) } } - -@Preview(showBackground = true) -@Composable -private fun Prev() { - InsertPasswordContent( - titleText = "비밀번호 입력해주세요", - passwordLength = 4, - onDigitClick = {}, - onDeleteClick = {}, - onConfirmClick = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/LabeledSwitchRow.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/LabeledSwitchRow.kt index a1265367ae..5010d05869 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/LabeledSwitchRow.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/LabeledSwitchRow.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign @@ -50,9 +49,3 @@ fun LabeledSwitchRow( ) } } - -@Preview(showBackground = true) -@Composable -private fun LabeledSwitchRowPrev() { - LabeledSwitchRow(label = "애프터노트", true, {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/NoticeListItem.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/NoticeListItem.kt index 27dae07eb2..0f2a194fe1 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/NoticeListItem.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/NoticeListItem.kt @@ -15,12 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.domain.Notice import com.afternote.feature.setting.presentation.R -import java.time.LocalDate import java.time.format.DateTimeFormatter private val noticeDateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") @@ -58,16 +56,3 @@ fun NoticeListItem( ) } } - -@Preview(showBackground = true) -@Composable -private fun NoticeListItemPrev() { - NoticeListItem( - notice = - Notice( - date = LocalDate.of(2022, 11, 20), - title = "서비스 점검 안내", - content = "서버 점검으로 인해 서비스가 일시 중단됩니다.", - ), - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/PasskeyListItem.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/PasskeyListItem.kt index bf6469498b..1d9c71c058 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/PasskeyListItem.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/PasskeyListItem.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.feature.setting.presentation.R @@ -38,9 +37,3 @@ fun PasskeyListItem(modifier: Modifier = Modifier) { } } } - -@Preview(showBackground = true) -@Composable -private fun PasskeyListItemPrev() { - PasskeyListItem() -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingMenuItem.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingMenuItem.kt index f9ef93e5f5..d08c6e7b78 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingMenuItem.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingMenuItem.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.presentation.R @@ -42,9 +41,3 @@ fun SettingMenuItem( ) } } - -@Preview(showBackground = true) -@Composable -private fun SettingMenuItemPrev() { - SettingMenuItem(label = "프로필 수정", onClick = {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingProfile.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingProfile.kt index df4cf8c436..75e929ca44 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingProfile.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingProfile.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.presentation.R @@ -111,14 +110,3 @@ fun SettingProfile( } } } - -@Preview(showBackground = true) -@Composable -private fun SettingProfilePrev() { - SettingProfile( - name = "박서연", - email = "afternote@email.com", - onNoticeClick = {}, - onRecipientListClick = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingSourceItem.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingSourceItem.kt index 103a90c79a..2257eb96ab 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingSourceItem.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SettingSourceItem.kt @@ -22,14 +22,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.icon.RightArrowIcon import com.afternote.core.ui.theme.AfternoteDesign -import com.afternote.core.ui.theme.AfternoteTheme -import com.afternote.feature.setting.presentation.R data class SettingSourceItemData( val name: String, @@ -100,25 +96,3 @@ fun SettingSourceItem( } } } - -@Preview(showBackground = true) -@Composable -private fun SettingSourceItemPreview() { - AfternoteTheme { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - SettingSourceItem( - item = - SettingSourceItemData( - name = "갤러리", - date = "최종 작성일 2025.11.26.", - iconBgColor = Color(0xFFF5F5F5), - ), - icon = painterResource(R.drawable.ic_default_profile), - onClick = {}, - ) - } - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SocialAccountRow.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SocialAccountRow.kt index a48b87e643..3d544bbfaa 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SocialAccountRow.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/component/SocialAccountRow.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.feature.setting.presentation.R @@ -82,19 +81,3 @@ fun SocialAccountRow( ) } } - -@Preview(showBackground = true) -@Composable -private fun RowPrev() { - SocialAccountRow( - account = - SocialAccountState( - provider = "google", - iconRes = R.drawable.ic_google_logo, - labelRes = R.string.login_with_google, - isConnected = true, - email = "example@gmail.com", - ), - onToggle = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingExternalActions.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingExternalActions.kt new file mode 100644 index 0000000000..10f3195984 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingExternalActions.kt @@ -0,0 +1,8 @@ +package com.afternote.feature.setting.presentation.navigation + +/** Auth changes belong to the app shell; each clears the authenticated root stack. */ +public interface SettingExternalActions { + public fun onLogoutSuccess() + + public fun onWithdrawSuccess() +} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActions.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActions.kt new file mode 100644 index 0000000000..6dfe9335d8 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActions.kt @@ -0,0 +1,84 @@ +package com.afternote.feature.setting.presentation.navigation + +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks +import com.afternote.core.ui.navigation.popOrExit + +internal class SettingLocalNavActions( + private val backStack: NavBackStack, + private val navigationCallbacks: FeatureNavigationCallbacks, + private val externalActions: SettingExternalActions, +) : SettingNavActions { + override fun popBack(): Unit = backStack.popOrExit(navigationCallbacks) + + override fun onLogoutSuccess(): Unit = externalActions.onLogoutSuccess() + + override fun onWithdrawSuccess(): Unit = externalActions.onWithdrawSuccess() + + override fun onWithdrawGuideClick() { + backStack.add(SettingRoute.WithdrawGuideRoute) + } + + override fun onWithdrawConfirmClick() { + backStack.add(SettingRoute.WithdrawConfirmRoute) + } + + override fun onProfileEditClick() { + backStack.add(SettingRoute.ProfileEditRoute) + } + + override fun onLinkedAccountClick() { + backStack.add(SettingRoute.LinkedAccountRoute) + } + + override fun onNotificationClick() { + backStack.add(SettingRoute.NotificationRoute) + } + + override fun onPushNotificationClick() { + backStack.add(SettingRoute.PushNotificationRoute) + } + + override fun onRecipientListClick() { + backStack.add(SettingRoute.RecipientListRoute()) + } + + override fun onDeliveryConditionsClick() { + backStack.add( + SettingRoute.RecipientListRoute(selectForDeliveryConditions = true), + ) + } + + override fun onRecipientRegisterClick() { + backStack.add(SettingRoute.RecipientRegisterRoute) + } + + override fun onRecipientEditClick(receiverId: Long) { + backStack.add(SettingRoute.RecipientEditRoute(receiverId)) + } + + override fun onDeliveryConditionsRecipientSelected(receiverId: Long) { + backStack.add(SettingRoute.DeliveryConditionsRoute(receiverId)) + } + + override fun onPasskeyClick() { + backStack.add(SettingRoute.PasskeyRoute) + } + + override fun onPasskeyRegisterClick() { + backStack.add(SettingRoute.PasskeyMakingRoute) + } + + override fun onPasswordAuthClick() { + backStack.add(SettingRoute.PasskeyPasswordRoute) + } + + override fun onAppLockClick() { + backStack.add(SettingRoute.AppLockSetupRoute) + } + + override fun onNoticeClick() { + backStack.add(SettingRoute.NoticeRoute) + } +} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt index d781055ce9..6e3bca8b3b 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt @@ -1,71 +1,41 @@ package com.afternote.feature.setting.presentation.navigation -interface SettingNavActions { - fun onSettingBack() +internal interface SettingNavActions { + fun popBack() fun onLogoutSuccess() - fun onNavigateToWithdrawGuide() + fun onWithdrawGuideClick() - fun onNavigateToWithdrawConfirm() - - fun onWithdrawGuideBack() - - fun onWithdrawConfirmBack() + fun onWithdrawConfirmClick() fun onWithdrawSuccess() - fun onNavigateToProfileEdit() - - fun onProfileEditBack() - - fun onNavigateToLinkedAccount() - - fun onLinkedAccountBack() - - fun onNavigateToNotification() - - fun onNotificationBack() - - fun onNavigateToPushNotification() - - fun onPushNotificationBack() - - fun onNavigateToRecipientList() - - fun onNavigateToRecipientListForDeliveryConditions() - - fun onRecipientListBack() - - fun onNavigateToRecipientRegister() - - fun onRecipientRegisterBack() - - fun onNavigateToRecipientEdit(receiverId: Long) + fun onProfileEditClick() - fun onRecipientEditBack() + fun onLinkedAccountClick() - fun onNavigateToAfterDelivery(receiverId: Long) + fun onNotificationClick() - fun onAfterDeliveryBack() + fun onPushNotificationClick() - fun onNavigateToPasskey() + fun onRecipientListClick() - fun onPasskeyBack() + fun onDeliveryConditionsClick() - fun onNavigateToPasskeyMaking() + fun onRecipientRegisterClick() - fun onPasskeyMakingBack() + fun onRecipientEditClick(receiverId: Long) - fun onNavigateToPasskeyPassword() + fun onDeliveryConditionsRecipientSelected(receiverId: Long) - fun onPasskeyPasswordBack() + fun onPasskeyClick() - fun onNavigateToAppLock() + fun onPasskeyRegisterClick() - fun onAppLockBack() + fun onPasswordAuthClick() - fun onNavigateToNotice() + fun onAppLockClick() - fun onNoticeBack() + fun onNoticeClick() } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt deleted file mode 100644 index fa5667506f..0000000000 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.afternote.feature.setting.presentation.navigation - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navigation -import androidx.navigation.toRoute -import com.afternote.core.ui.Route -import com.afternote.feature.setting.presentation.component.PinSetupStep -import com.afternote.feature.setting.presentation.screen.AppLockSetupScreen -import com.afternote.feature.setting.presentation.screen.ConnectedAccountsScreen -import com.afternote.feature.setting.presentation.screen.DeliveryConditionScreen -import com.afternote.feature.setting.presentation.screen.NoticeListScreen -import com.afternote.feature.setting.presentation.screen.NotificationSettingScreen -import com.afternote.feature.setting.presentation.screen.PassKeyListScreen -import com.afternote.feature.setting.presentation.screen.PassKeyMakingScreen -import com.afternote.feature.setting.presentation.screen.PassKeyPasswordScreen -import com.afternote.feature.setting.presentation.screen.PassKeyScreen -import com.afternote.feature.setting.presentation.screen.ProfileEditScreen -import com.afternote.feature.setting.presentation.screen.PushNotificationScreen -import com.afternote.feature.setting.presentation.screen.ReceiverEditScreen -import com.afternote.feature.setting.presentation.screen.ReceiverListScreen -import com.afternote.feature.setting.presentation.screen.ReceiverManageScreen -import com.afternote.feature.setting.presentation.screen.ReceiverRegisterScreen -import com.afternote.feature.setting.presentation.screen.SettingScreen -import com.afternote.feature.setting.presentation.screen.WithdrawConfirmScreen -import com.afternote.feature.setting.presentation.screen.WithdrawGuideScreen -import com.afternote.feature.setting.presentation.viewmodel.PassKeyViewModel -import com.afternote.feature.setting.presentation.viewmodel.ReceiverListViewModel -import com.afternote.feature.setting.presentation.viewmodel.SettingViewModel - -fun NavGraphBuilder.settingNavGraph( - graphScopedParentEntry: () -> NavBackStackEntry, - actions: SettingNavActions, -) { - navigation(startDestination = SettingRoute.SettingHomeRoute) { - composable { - SettingScreen( - onBackClick = actions::onSettingBack, - onLogoutSuccess = actions::onLogoutSuccess, - onProfileEditClick = actions::onNavigateToProfileEdit, - onPasswordChangeClick = {}, - onLinkedAccountClick = actions::onNavigateToLinkedAccount, - onNotificationClick = actions::onNavigateToNotification, - onRecipientListClick = actions::onNavigateToRecipientList, - onRecipientRegisterClick = actions::onNavigateToRecipientRegister, - onAfterDeliveryClick = { - actions.onNavigateToRecipientListForDeliveryConditions() - }, - onPasskeyClick = actions::onNavigateToPasskey, - onAppLockClick = actions::onNavigateToAppLock, - onFaqClick = {}, - onInquiryClick = {}, - onNoticeClick = actions::onNavigateToNotice, - onTermsClick = {}, - onPrivacyClick = {}, - onServiceInfoClick = {}, - onWithdrawGuideClick = actions::onNavigateToWithdrawGuide, - ) - } - - composable { - val parentEntry = remember(it) { graphScopedParentEntry() } - val viewModel: SettingViewModel = hiltViewModel(parentEntry) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - WithdrawGuideScreen( - uiState = uiState, - onBackClick = actions::onWithdrawGuideBack, - onCancelClick = actions::onWithdrawGuideBack, - onConfirmClick = actions::onNavigateToWithdrawConfirm, - ) - } - - composable { - val parentEntry = remember(it) { graphScopedParentEntry() } - val viewModel: SettingViewModel = hiltViewModel(parentEntry) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - WithdrawConfirmScreen( - uiState = uiState, - onBackClick = actions::onWithdrawConfirmBack, - onWithdrawSuccess = actions::onWithdrawSuccess, - viewModel = viewModel, - ) - } - - composable { - ProfileEditScreen( - onBackClick = actions::onProfileEditBack, - onWithdrawGuideClick = actions::onNavigateToWithdrawGuide, - ) - } - - composable { - ConnectedAccountsScreen( - onBack = actions::onLinkedAccountBack, - ) - } - - composable { - NotificationSettingScreen( - onBack = actions::onNotificationBack, - onPushNotificationClick = actions::onNavigateToPushNotification, - ) - } - - composable { - PushNotificationScreen( - onBack = actions::onPushNotificationBack, - ) - } - - composable { - val route = it.toRoute() - val viewModel: ReceiverListViewModel = hiltViewModel() - val receivers by viewModel.receivers.collectAsStateWithLifecycle() - if (route.selectForDeliveryConditions) { - ReceiverListScreen( - receivers = receivers, - onBackClick = actions::onRecipientListBack, - onConfirmClick = { receiver -> - actions.onNavigateToAfterDelivery(receiver.receiverId) - }, - ) - } else { - ReceiverManageScreen( - receivers = receivers, - onBackClick = actions::onRecipientListBack, - onReceiverClick = actions::onNavigateToRecipientEdit, - ) - } - } - - composable { - ReceiverRegisterScreen( - onBackClick = actions::onRecipientRegisterBack, - onRegisterSuccess = actions::onRecipientRegisterBack, - ) - } - - composable { - ReceiverEditScreen( - onBackClick = actions::onRecipientEditBack, - onEditSuccess = actions::onRecipientEditBack, - ) - } - - composable { - val route = it.toRoute() - DeliveryConditionScreen( - onBack = actions::onAfterDeliveryBack, - onSaveSuccess = actions::onAfterDeliveryBack, - onLastGreetingEditClick = { - actions.onNavigateToRecipientEdit(route.receiverId) - }, - ) - } - - composable { - val viewModel: PassKeyViewModel = hiltViewModel() - val isPasskeyRegistered by viewModel.isPasskeyRegistered.collectAsStateWithLifecycle() - if (isPasskeyRegistered == true) { - PassKeyListScreen(onBackClick = actions::onPasskeyBack) - } else if (isPasskeyRegistered == false) { - PassKeyScreen( - onBackClick = actions::onPasskeyBack, - onRegisterClick = actions::onNavigateToPasskeyMaking, - ) - } - } - - composable { - PassKeyMakingScreen( - onBackClick = actions::onPasskeyMakingBack, - onPasswordAuthClick = actions::onNavigateToPasskeyPassword, - ) - } - - composable { - PassKeyPasswordScreen( - onPinComplete = { actions.onPasskeyPasswordBack() }, - onBack = actions::onPasskeyPasswordBack, - ) - } - - composable { - AppLockSetupScreen( - step = PinSetupStep.ENTER_NEW, - onPinComplete = { actions.onAppLockBack() }, - onBack = actions::onAppLockBack, - ) - } - - composable { - NoticeListScreen( - notices = emptyList(), - onBackClick = actions::onNoticeBack, - ) - } - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavHost.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavHost.kt new file mode 100644 index 0000000000..f6f5fa4eb5 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavHost.kt @@ -0,0 +1,230 @@ +package com.afternote.feature.setting.presentation.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import com.afternote.core.ui.navigation.FeatureNavDisplay +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks +import com.afternote.feature.setting.presentation.component.PinSetupStep +import com.afternote.feature.setting.presentation.screen.AppLockSetupScreen +import com.afternote.feature.setting.presentation.screen.ConnectedAccountsScreen +import com.afternote.feature.setting.presentation.screen.DeliveryConditionScreen +import com.afternote.feature.setting.presentation.screen.NoticeListScreen +import com.afternote.feature.setting.presentation.screen.NotificationSettingScreen +import com.afternote.feature.setting.presentation.screen.PassKeyListScreen +import com.afternote.feature.setting.presentation.screen.PassKeyMakingScreen +import com.afternote.feature.setting.presentation.screen.PassKeyPasswordScreen +import com.afternote.feature.setting.presentation.screen.PassKeyScreen +import com.afternote.feature.setting.presentation.screen.ProfileEditScreen +import com.afternote.feature.setting.presentation.screen.PushNotificationScreen +import com.afternote.feature.setting.presentation.screen.ReceiverEditScreen +import com.afternote.feature.setting.presentation.screen.ReceiverListScreen +import com.afternote.feature.setting.presentation.screen.ReceiverManageScreen +import com.afternote.feature.setting.presentation.screen.ReceiverRegisterScreen +import com.afternote.feature.setting.presentation.screen.SettingScreen +import com.afternote.feature.setting.presentation.screen.WithdrawConfirmScreen +import com.afternote.feature.setting.presentation.screen.WithdrawGuideScreen +import com.afternote.feature.setting.presentation.viewmodel.DeliveryConditionViewModel +import com.afternote.feature.setting.presentation.viewmodel.PassKeyViewModel +import com.afternote.feature.setting.presentation.viewmodel.ReceiverEditViewModel +import com.afternote.feature.setting.presentation.viewmodel.ReceiverListViewModel +import com.afternote.feature.setting.presentation.viewmodel.SettingViewModel + +/** + * Settings owns its saved local stack. The parent entry owns the withdrawal ViewModel, + * so guide/confirmation share it until this entire host is removed (#1702 host lifetime). + * Home keeps its existing entry-scoped ViewModel. Direct recipient registration starts + * at registration, so completing it returns to the caller without inserting Settings home. + */ +@Composable +public fun SettingNavHost( + navigationCallbacks: FeatureNavigationCallbacks, + externalActions: SettingExternalActions, + modifier: Modifier = Modifier, + startWithRecipientRegistration: Boolean = false, +) { + val initialRoute = + if (startWithRecipientRegistration) { + SettingRoute.RecipientRegisterRoute + } else { + SettingRoute.SettingHomeRoute + } + val backStack = rememberNavBackStack(initialRoute) + val actions = + remember(backStack, navigationCallbacks, externalActions) { + SettingLocalNavActions(backStack, navigationCallbacks, externalActions) + } + val hostOwner = checkNotNull(LocalViewModelStoreOwner.current) + FeatureNavDisplay( + backStack = backStack, + boundary = navigationCallbacks, + modifier = modifier, + entryProvider = + entryProvider { + entry { + SettingScreen( + onBackClick = actions::popBack, + onLogoutSuccess = actions::onLogoutSuccess, + onProfileEditClick = actions::onProfileEditClick, + onLinkedAccountClick = actions::onLinkedAccountClick, + onNotificationClick = actions::onNotificationClick, + onRecipientListClick = actions::onRecipientListClick, + onRecipientRegisterClick = actions::onRecipientRegisterClick, + onDeliveryConditionsClick = actions::onDeliveryConditionsClick, + onPasskeyClick = actions::onPasskeyClick, + onAppLockClick = actions::onAppLockClick, + onNoticeClick = actions::onNoticeClick, + onWithdrawGuideClick = actions::onWithdrawGuideClick, + ) + } + + entry { + val hostViewModel: SettingViewModel = hiltViewModel(hostOwner) + val uiState by hostViewModel.uiState.collectAsStateWithLifecycle() + WithdrawGuideScreen( + uiState = uiState, + onBackClick = actions::popBack, + onCancelClick = actions::popBack, + onConfirmClick = actions::onWithdrawConfirmClick, + ) + } + + entry { + val hostViewModel: SettingViewModel = hiltViewModel(hostOwner) + val uiState by hostViewModel.uiState.collectAsStateWithLifecycle() + WithdrawConfirmScreen( + uiState = uiState, + onBackClick = actions::popBack, + onWithdrawSuccess = actions::onWithdrawSuccess, + viewModel = hostViewModel, + ) + } + + entry { + ProfileEditScreen( + onBackClick = actions::popBack, + onWithdrawGuideClick = actions::onWithdrawGuideClick, + ) + } + + entry { + ConnectedAccountsScreen( + onBack = actions::popBack, + ) + } + + entry { + NotificationSettingScreen( + onBack = actions::popBack, + onPushNotificationClick = actions::onPushNotificationClick, + ) + } + + entry { + PushNotificationScreen( + onBack = actions::popBack, + ) + } + + entry { route -> + val viewModel: ReceiverListViewModel = hiltViewModel() + val receivers by viewModel.receivers.collectAsStateWithLifecycle() + if (route.selectForDeliveryConditions) { + ReceiverListScreen( + receivers = receivers, + onBackClick = actions::popBack, + onConfirmClick = { receiver -> + actions.onDeliveryConditionsRecipientSelected(receiver.receiverId) + }, + ) + } else { + ReceiverManageScreen( + receivers = receivers, + onBackClick = actions::popBack, + onReceiverClick = actions::onRecipientEditClick, + ) + } + } + + entry { + ReceiverRegisterScreen( + onBackClick = actions::popBack, + onRegisterSuccess = actions::popBack, + ) + } + + entry { route -> + ReceiverEditScreen( + viewModel = + hiltViewModel( + creationCallback = { it.create(route) }, + ), + onBackClick = actions::popBack, + onEditSuccess = actions::popBack, + ) + } + + entry { route -> + DeliveryConditionScreen( + viewModel = + hiltViewModel( + creationCallback = { it.create(route) }, + ), + onBack = actions::popBack, + onSaveSuccess = actions::popBack, + onLastGreetingEditClick = { + actions.onRecipientEditClick(route.receiverId) + }, + ) + } + + entry { + val viewModel: PassKeyViewModel = hiltViewModel() + val isPasskeyRegistered by viewModel.isPasskeyRegistered.collectAsStateWithLifecycle() + if (isPasskeyRegistered == true) { + PassKeyListScreen(onBackClick = actions::popBack) + } else if (isPasskeyRegistered == false) { + PassKeyScreen( + onBackClick = actions::popBack, + onRegisterClick = actions::onPasskeyRegisterClick, + ) + } + } + + entry { + PassKeyMakingScreen( + onBackClick = actions::popBack, + onPasswordAuthClick = actions::onPasswordAuthClick, + ) + } + + entry { + PassKeyPasswordScreen( + onPinComplete = { actions.popBack() }, + onBack = actions::popBack, + ) + } + + entry { + AppLockSetupScreen( + step = PinSetupStep.ENTER_NEW, + onPinComplete = { actions.popBack() }, + onBack = actions::popBack, + ) + } + + entry { + NoticeListScreen( + notices = emptyList(), + onBackClick = actions::popBack, + ) + } + }, + ) +} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt index 2d7fa57681..fb25bd7d56 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt @@ -1,8 +1,9 @@ package com.afternote.feature.setting.presentation.navigation +import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable -sealed interface SettingRoute { +internal sealed interface SettingRoute : NavKey { @Serializable data object SettingHomeRoute : SettingRoute @@ -38,7 +39,7 @@ sealed interface SettingRoute { ) : SettingRoute @Serializable - data class AfterDeliveryRoute( + data class DeliveryConditionsRoute( val receiverId: Long, ) : SettingRoute diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/AppLockSetupContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/AppLockSetupContent.kt index 7980f9b4ba..5cd417646a 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/AppLockSetupContent.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/AppLockSetupContent.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.component.InsertPasswordContent @@ -67,42 +66,3 @@ internal fun AppLockSetupContent( } } } - -@Preview(name = "비밀번호 입력", showBackground = true) -@Composable -private fun PreviewEnterNew() { - AppLockSetupContent( - step = PinSetupStep.ENTER_NEW, - passwordLength = 0, - onDigitClick = {}, - onDeleteClick = {}, - onConfirmClick = {}, - onBack = {}, - ) -} - -@Preview(name = "비밀번호 재입력", showBackground = true) -@Composable -private fun PreviewConfirmNew() { - AppLockSetupContent( - step = PinSetupStep.CONFIRM_NEW, - passwordLength = 3, - onDigitClick = {}, - onDeleteClick = {}, - onConfirmClick = {}, - onBack = {}, - ) -} - -@Preview(name = "변경할 비밀번호 입력", showBackground = true) -@Composable -private fun PreviewEnterCurrent() { - AppLockSetupContent( - step = PinSetupStep.ENTER_CURRENT, - passwordLength = 0, - onDigitClick = {}, - onDeleteClick = {}, - onConfirmClick = {}, - onBack = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ConnectedAccountsScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ConnectedAccountsScreen.kt index a0a4c79cb2..9cf6376a5d 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ConnectedAccountsScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ConnectedAccountsScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.credentials.CredentialManager import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -95,9 +94,3 @@ fun ConnectedAccountsScreen( modifier = modifier, ) } - -@Preview(showBackground = true) -@Composable -private fun ConnectedAccountScreenPrev() { - ConnectedAccountsScreen(onBack = {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/DeliveryConditionScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/DeliveryConditionScreen.kt index 7c636c2ec9..9c0bd09684 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/DeliveryConditionScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/DeliveryConditionScreen.kt @@ -25,13 +25,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.core.model.delivery.DeliveryConditionType import com.afternote.core.ui.theme.AfternoteDesign -import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.component.RadioGroup @@ -41,11 +38,11 @@ import com.afternote.feature.setting.presentation.viewmodel.DeliveryConditionUiS import com.afternote.feature.setting.presentation.viewmodel.DeliveryConditionViewModel @Composable -fun DeliveryConditionScreen( +internal fun DeliveryConditionScreen( onBack: () -> Unit, onSaveSuccess: () -> Unit, onLastGreetingEditClick: () -> Unit, - viewModel: DeliveryConditionViewModel = hiltViewModel(), + viewModel: DeliveryConditionViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val currentOnSaveSuccess by rememberUpdatedState(onSaveSuccess) @@ -189,17 +186,3 @@ private fun SectionLabel( color = AfternoteDesign.colors.gray9, ) } - -@Preview(showBackground = true) -@Composable -private fun DeliveryConditionContentPreview() { - AfternoteTheme { - DeliveryConditionContent( - uiState = DeliveryConditionUiState(), - onBack = {}, - onConditionTypeSelect = {}, - onLastGreetingEditClick = {}, - onSave = {}, - ) - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/NoticeListScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/NoticeListScreen.kt index b9ca353ffe..55f213ba15 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/NoticeListScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/NoticeListScreen.kt @@ -11,14 +11,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.domain.Notice import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.component.NoticeListItem -import java.time.LocalDate // 설정 - 공지사항 @Composable @@ -73,33 +71,3 @@ private fun NoticeEmptyState(modifier: Modifier = Modifier) { ) } } - -@Preview(showBackground = true) -@Composable -private fun NoticeListScreenPrev() { - NoticeListScreen( - onBackClick = {}, - notices = - listOf( - Notice( - date = LocalDate.of(2022, 11, 20), - title = "서비스 점검 안내", - content = "서버 점검으로 인해 서비스가 일시 중단됩니다.", - ), - Notice( - date = LocalDate.of(2023, 3, 5), - title = "개인정보 처리방침 변경 안내", - content = "개인정보 처리방침이 변경되었습니다.", - ), - ), - ) -} - -@Preview(showBackground = true) -@Composable -private fun NoticeListScreenEmptyPrev() { - NoticeListScreen( - onBackClick = {}, - notices = emptyList(), - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyListScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyListScreen.kt index 2d5c51dae9..1919b90461 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyListScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyListScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.topbar.DetailTopBar @@ -64,9 +63,3 @@ fun PassKeyListScreen( } } } - -@Preview(showBackground = true) -@Composable -private fun PassKeyListScreenPrev() { - PassKeyListScreen(onBackClick = {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyMakingScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyMakingScreen.kt index 9145ac69ed..76443b0aba 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyMakingScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyMakingScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -151,9 +150,3 @@ private suspend fun authenticate(activity: FragmentActivity): BiometricResult = continuation.invokeOnCancellation { biometricPrompt.cancelAuthentication() } biometricPrompt.authenticate(promptInfo) } - -@Preview(showBackground = true) -@Composable -private fun PassKeyMakingScreenPrev() { - PassKeyMakingScreen(onBackClick = {}, onPasswordAuthClick = {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyPasswordScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyPasswordScreen.kt index 289b6aae7e..368cd751ee 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyPasswordScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyPasswordScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.feature.setting.presentation.viewmodel.AppLockSetupViewModel @@ -39,12 +38,3 @@ fun PassKeyPasswordScreen( modifier = modifier, ) } - -@Preview(showBackground = true) -@Composable -private fun PassKeyPasswordScreenPreview() { - PassKeyPasswordScreen( - onPinComplete = {}, - onBack = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyScreen.kt index f7df5cab09..4e497599cf 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PassKeyScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.button.AfternoteButton import com.afternote.core.ui.button.AfternoteButtonType @@ -92,9 +91,3 @@ fun PassKeyScreen( } } } - -@Preview(showBackground = true) -@Composable -private fun PassKeyScreenPrev() { - PassKeyScreen(onBackClick = {}, onRegisterClick = {}) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ProfileEditScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ProfileEditScreen.kt index 778d02c4c5..e31833d030 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ProfileEditScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ProfileEditScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -239,23 +238,3 @@ private fun ProfileEditForm( } } } - -@Preview(showBackground = true) -@Composable -private fun ProfileEditScreenPrev() { - Scaffold( - topBar = { DetailTopBar(title = "프로필 설정") }, - ) { innerPadding -> - ProfileEditForm( - state = - ProfileEditUiState.Success( - name = "박서연", - phone = "01012345678", - email = "afternote@email.com", - ), - onUpdateClick = { _, _ -> }, - onWithdrawGuideClick = {}, - modifier = Modifier.padding(innerPadding), - ) - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PushNotificationContent.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PushNotificationContent.kt index 34f480ab9e..8652647017 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PushNotificationContent.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/PushNotificationContent.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.topbar.DetailTopBar @@ -66,15 +65,3 @@ internal fun PushNotificationContent( } } } - -@Preview(showBackground = true) -@Composable -private fun PushNotificationContentPreview() { - PushNotificationContent( - uiState = PushNotificationUiState(isAfternoteOn = true), - onBack = {}, - onNewsletterToggle = {}, - onMindRecordToggle = {}, - onAfternoteToggle = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverEditScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverEditScreen.kt index 8854638062..846ddd2be2 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverEditScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverEditScreen.kt @@ -5,17 +5,16 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.feature.setting.presentation.viewmodel.ReceiverEditEvent import com.afternote.feature.setting.presentation.viewmodel.ReceiverEditViewModel @Composable -fun ReceiverEditScreen( +internal fun ReceiverEditScreen( onBackClick: () -> Unit, onEditSuccess: () -> Unit, modifier: Modifier = Modifier, - viewModel: ReceiverEditViewModel = hiltViewModel(), + viewModel: ReceiverEditViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val currentOnEditSuccess by rememberUpdatedState(onEditSuccess) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverListScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverListScreen.kt index 4a980dd1fe..390421ebce 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverListScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverListScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import com.afternote.core.model.setting.ReceiverListItem import com.afternote.core.ui.receiver.ReceiverSelectItem import com.afternote.core.ui.receiver.ReceiverSelectScreen @@ -49,18 +48,3 @@ fun ReceiverListScreen( modifier = modifier, ) } - -@Preview(showBackground = true) -@Composable -private fun ReceiverListScreenPrev() { - ReceiverListScreen( - receivers = - listOf( - ReceiverListItem(receiverId = 1L, name = "박경민", relation = "친구"), - ReceiverListItem(receiverId = 2L, name = "김철수", relation = "가족"), - ReceiverListItem(receiverId = 3L, name = "이영희", relation = "연인"), - ), - onBackClick = {}, - onConfirmClick = { _ -> }, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverManageScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverManageScreen.kt index 8af25db0e5..adc1a5137e 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverManageScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverManageScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.model.setting.ReceiverListItem import com.afternote.core.ui.receiver.ReceiverProfileRow @@ -80,18 +79,3 @@ private fun ReceiverManageRow( }, ) } - -@Preview(showBackground = true) -@Composable -private fun ReceiverManageScreenPrev() { - ReceiverManageScreen( - receivers = - listOf( - ReceiverListItem(receiverId = 1L, name = "박경민", relation = "친구"), - ReceiverListItem(receiverId = 2L, name = "김철수", relation = "가족"), - ReceiverListItem(receiverId = 3L, name = "이영희", relation = "연인"), - ), - onBackClick = {}, - onReceiverClick = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverRegisterScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverRegisterScreen.kt index 5b1b5b0b8f..9464b7a1ed 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverRegisterScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/ReceiverRegisterScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -47,7 +46,6 @@ import com.afternote.core.ui.PhoneNumberVisualTransformation import com.afternote.core.ui.UiText import com.afternote.core.ui.asString import com.afternote.core.ui.theme.AfternoteDesign -import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.viewmodel.ReceiverPhoneValidation @@ -358,19 +356,3 @@ internal fun ReceiverRegisterContent( } } } - -@Preview(showBackground = true) -@Composable -private fun ReceiverRegisterContentPreview() { - AfternoteTheme { - ReceiverRegisterContent( - title = "수신자 등록", - actionText = "등록", - isPhoneRequired = true, - isLoading = false, - errorMessage = null, - onBackClick = {}, - onRegister = { _, _, _, _, _ -> }, - ) - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/RecipientSetScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/RecipientSetScreen.kt index a4c4f38fc5..1049d89377 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/RecipientSetScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/RecipientSetScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.AfternoteTextField import com.afternote.core.ui.button.AfternoteButton @@ -153,9 +152,3 @@ private fun ReadOnlyField( ) } } - -@Preview(showBackground = true) -@Composable -private fun RecipientSetScreenPrev() { - RecipientSetScreen() -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/SettingScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/SettingScreen.kt index 54a29eed20..471c031e48 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/SettingScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/SettingScreen.kt @@ -7,18 +7,20 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle @@ -33,31 +35,30 @@ import com.afternote.feature.setting.presentation.component.SettingProfile import com.afternote.feature.setting.presentation.component.SettingSection import com.afternote.feature.setting.presentation.viewmodel.SettingUiState import com.afternote.feature.setting.presentation.viewmodel.SettingViewModel +import kotlinx.coroutines.launch // 설정-메인 @Composable -fun SettingScreen( +internal fun SettingScreen( onBackClick: () -> Unit, onLogoutSuccess: () -> Unit, onProfileEditClick: () -> Unit, - onPasswordChangeClick: () -> Unit, onLinkedAccountClick: () -> Unit, onNotificationClick: () -> Unit, onRecipientListClick: () -> Unit, onRecipientRegisterClick: () -> Unit, - onAfterDeliveryClick: () -> Unit, + onDeliveryConditionsClick: () -> Unit, onPasskeyClick: () -> Unit, onAppLockClick: () -> Unit, - onFaqClick: () -> Unit, - onInquiryClick: () -> Unit, onNoticeClick: () -> Unit, - onTermsClick: () -> Unit, - onPrivacyClick: () -> Unit, - onServiceInfoClick: () -> Unit, onWithdrawGuideClick: () -> Unit, modifier: Modifier = Modifier, viewModel: SettingViewModel = hiltViewModel(), ) { + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val unavailableMessage = stringResource(R.string.settings_menu_unavailable) + val acknowledgeLabel = stringResource(R.string.settings_menu_acknowledge) val uiState by viewModel.uiState.collectAsStateWithLifecycle() val logoutCompleted by viewModel.logoutCompleted.collectAsStateWithLifecycle() val currentOnLogoutSuccess by rememberUpdatedState(onLogoutSuccess) @@ -77,6 +78,7 @@ fun SettingScreen( } Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { DetailTopBar( title = stringResource(R.string.settings_title), @@ -89,21 +91,21 @@ fun SettingScreen( SettingScreenContent( uiState = uiState, onLogoutClick = viewModel::logout, + onUnavailableClick = { + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar(unavailableMessage, actionLabel = acknowledgeLabel) + } + }, onProfileEditClick = onProfileEditClick, - onPasswordChangeClick = onPasswordChangeClick, onLinkedAccountClick = onLinkedAccountClick, onNotificationClick = onNotificationClick, onRecipientListClick = onRecipientListClick, onRecipientRegisterClick = onRecipientRegisterClick, - onAfterDeliveryClick = onAfterDeliveryClick, + onDeliveryConditionsClick = onDeliveryConditionsClick, onPasskeyClick = onPasskeyClick, onAppLockClick = onAppLockClick, - onFaqClick = onFaqClick, - onInquiryClick = onInquiryClick, onNoticeClick = onNoticeClick, - onTermsClick = onTermsClick, - onPrivacyClick = onPrivacyClick, - onServiceInfoClick = onServiceInfoClick, onWithdrawGuideClick = onWithdrawGuideClick, modifier = Modifier.padding(innerPadding), ) @@ -114,21 +116,16 @@ fun SettingScreen( private fun SettingScreenContent( uiState: SettingUiState, onLogoutClick: () -> Unit, + onUnavailableClick: () -> Unit, onProfileEditClick: () -> Unit, - onPasswordChangeClick: () -> Unit, onLinkedAccountClick: () -> Unit, onNotificationClick: () -> Unit, onRecipientListClick: () -> Unit, onRecipientRegisterClick: () -> Unit, - onAfterDeliveryClick: () -> Unit, + onDeliveryConditionsClick: () -> Unit, onPasskeyClick: () -> Unit, onAppLockClick: () -> Unit, - onFaqClick: () -> Unit, - onInquiryClick: () -> Unit, onNoticeClick: () -> Unit, - onTermsClick: () -> Unit, - onPrivacyClick: () -> Unit, - onServiceInfoClick: () -> Unit, onWithdrawGuideClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -179,7 +176,7 @@ private fun SettingScreenContent( ) SettingMenuItem( label = stringResource(R.string.settings_account_password_change), - onClick = onPasswordChangeClick, + onClick = onUnavailableClick, ) SettingMenuItem( label = stringResource(R.string.settings_account_linked_account), @@ -202,7 +199,7 @@ private fun SettingScreenContent( ) SettingMenuItem( label = stringResource(R.string.settings_recipient_after_delivery), - onClick = onAfterDeliveryClick, + onClick = onDeliveryConditionsClick, ) } @@ -220,11 +217,11 @@ private fun SettingScreenContent( SettingSection(title = stringResource(R.string.settings_section_support)) { SettingMenuItem( label = stringResource(R.string.settings_support_faq), - onClick = onFaqClick, + onClick = onUnavailableClick, ) SettingMenuItem( label = stringResource(R.string.settings_support_inquiry), - onClick = onInquiryClick, + onClick = onUnavailableClick, ) SettingMenuItem( label = stringResource(R.string.settings_support_notice), @@ -232,15 +229,15 @@ private fun SettingScreenContent( ) SettingMenuItem( label = stringResource(R.string.settings_support_terms), - onClick = onTermsClick, + onClick = onUnavailableClick, ) SettingMenuItem( label = stringResource(R.string.settings_support_privacy), - onClick = onPrivacyClick, + onClick = onUnavailableClick, ) SettingMenuItem( label = stringResource(R.string.settings_support_service_info), - onClick = onServiceInfoClick, + onClick = onUnavailableClick, ) } @@ -269,33 +266,3 @@ private fun SettingScreenContent( } } } - -@Preview(showBackground = true) -@Composable -private fun SettingScreenPrev() { - Scaffold( - topBar = { DetailTopBar(title = "설정") }, - ) { innerPadding -> - SettingScreenContent( - uiState = SettingUiState.Success(name = "박서연", email = "afternote@email.com"), - onLogoutClick = {}, - onProfileEditClick = {}, - onPasswordChangeClick = {}, - onLinkedAccountClick = {}, - onNotificationClick = {}, - onRecipientListClick = {}, - onRecipientRegisterClick = {}, - onAfterDeliveryClick = {}, - onPasskeyClick = {}, - onAppLockClick = {}, - onFaqClick = {}, - onInquiryClick = {}, - onNoticeClick = {}, - onTermsClick = {}, - onPrivacyClick = {}, - onServiceInfoClick = {}, - onWithdrawGuideClick = {}, - modifier = Modifier.padding(innerPadding), - ) - } -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/WithdrawGuideScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/WithdrawGuideScreen.kt index 6b5eb4b122..14895b35b7 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/WithdrawGuideScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/WithdrawGuideScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.afternote.core.ui.button.AfternoteButton import com.afternote.core.ui.button.AfternoteButtonType @@ -280,14 +279,3 @@ private fun BulletItem(text: String) { ) } } - -@Preview(showBackground = true) -@Composable -private fun WithdrawGuideScreenPrev() { - WithdrawGuideScreen( - uiState = SettingUiState.Success(name = "홍길동", email = "hong@example.com"), - onBackClick = {}, - onCancelClick = {}, - onConfirmClick = {}, - ) -} diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/DeliveryConditionViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/DeliveryConditionViewModel.kt index 13ec93769f..b7d0644513 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/DeliveryConditionViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/DeliveryConditionViewModel.kt @@ -1,9 +1,7 @@ package com.afternote.feature.setting.presentation.viewmodel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.domain.repository.UserRepository import com.afternote.core.model.delivery.ConditionState @@ -12,6 +10,9 @@ import com.afternote.core.model.delivery.DeliveryConditionType import com.afternote.core.model.delivery.DeliveryContentType import com.afternote.core.model.delivery.InactivityPeriod import com.afternote.feature.setting.presentation.navigation.SettingRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -20,16 +21,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DeliveryConditionViewModel - @Inject +@HiltViewModel(assistedFactory = DeliveryConditionViewModel.Factory::class) +internal class DeliveryConditionViewModel + @AssistedInject constructor( - savedStateHandle: SavedStateHandle, + @Assisted route: SettingRoute.DeliveryConditionsRoute, private val userRepository: UserRepository, ) : ViewModel() { - private val receiverId = savedStateHandle.toRoute().receiverId + private val receiverId = route.receiverId private val _uiState = MutableStateFlow(DeliveryConditionUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -129,4 +129,9 @@ class DeliveryConditionViewModel gracePeriodStartedAt = null, fulfilledAt = null, ) + + @AssistedFactory + interface Factory { + fun create(route: SettingRoute.DeliveryConditionsRoute): DeliveryConditionViewModel + } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverEditViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverEditViewModel.kt index 2d1b0dbf41..d6fa313535 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverEditViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/viewmodel/ReceiverEditViewModel.kt @@ -1,14 +1,15 @@ package com.afternote.feature.setting.presentation.viewmodel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.domain.repository.UserRepository import com.afternote.core.ui.UiText import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.navigation.SettingRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -16,16 +17,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ReceiverEditViewModel - @Inject +@HiltViewModel(assistedFactory = ReceiverEditViewModel.Factory::class) +internal class ReceiverEditViewModel + @AssistedInject constructor( - savedStateHandle: SavedStateHandle, + @Assisted route: SettingRoute.RecipientEditRoute, private val userRepository: UserRepository, ) : ViewModel() { - private val receiverId = savedStateHandle.toRoute().receiverId + private val receiverId = route.receiverId private val _uiState = MutableStateFlow(ReceiverEditUiState()) val uiState = _uiState.asStateFlow() @@ -114,4 +114,9 @@ class ReceiverEditViewModel } } } + + @AssistedFactory + interface Factory { + fun create(route: SettingRoute.RecipientEditRoute): ReceiverEditViewModel + } } diff --git a/feature/setting/presentation/src/main/res/values/strings.xml b/feature/setting/presentation/src/main/res/values/strings.xml index 36cb614a82..edbe58f905 100644 --- a/feature/setting/presentation/src/main/res/values/strings.xml +++ b/feature/setting/presentation/src/main/res/values/strings.xml @@ -165,4 +165,6 @@ 입력하신 내용을 확인해 주세요. 기본 정보는 수정됐지만 마지막 인사말 수정에 실패했습니다. 수신자 정보를 불러오지 못했습니다. + 현재 이 메뉴는 이용할 수 없습니다. + 확인 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 ce03d0030b..6e9d0bda26 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,7 +6,6 @@ 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.SavedStateHandle import com.afternote.core.domain.testing.FakeAuthRepository import com.afternote.core.domain.testing.FakeUserRepository import com.afternote.core.domain.testing.FakeUserRepository.ConnectedAccountLinkCall @@ -24,6 +23,7 @@ import com.afternote.core.model.user.UserConnectedAccount import com.afternote.core.ui.UiText import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.setting.presentation.component.PinSetupStep +import com.afternote.feature.setting.presentation.navigation.SettingRoute import com.afternote.feature.setting.presentation.screen.AppLockSetupScreen import com.afternote.feature.setting.presentation.screen.PassKeyListScreen import com.afternote.feature.setting.presentation.screen.PassKeyScreen @@ -202,7 +202,7 @@ class SettingAccountSecurityTest { } val viewModel = DeliveryConditionViewModel( - savedStateHandle = SavedStateHandle(mapOf("receiverId" to RECEIVER_ID)), + route = SettingRoute.DeliveryConditionsRoute(RECEIVER_ID), userRepository = repository, ) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { 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 6406e615b3..59c49c865c 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 @@ -14,7 +14,6 @@ import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput -import androidx.lifecycle.SavedStateHandle import androidx.test.core.app.ApplicationProvider import com.afternote.core.common.reporting.ErrorReporter import com.afternote.core.domain.testing.FakeAuthRepository @@ -34,6 +33,7 @@ import com.afternote.core.model.user.UserMarketingConsent import com.afternote.core.model.user.UserPushSetting import com.afternote.core.ui.UiText import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.setting.presentation.navigation.SettingRoute import com.afternote.feature.setting.presentation.screen.ReceiverEditScreen import com.afternote.feature.setting.presentation.screen.ReceiverRegisterScreen import com.afternote.feature.setting.presentation.viewmodel.ConnectedAccountsViewModel @@ -375,7 +375,7 @@ class SettingCompletionTest { val finalMessageGate = scenario.enqueueReceiverMessageUpdate() val viewModel = ReceiverEditViewModel( - savedStateHandle = SavedStateHandle(mapOf("receiverId" to RECEIVER_ID)), + route = SettingRoute.RecipientEditRoute(RECEIVER_ID), userRepository = repository, ) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { @@ -468,7 +468,7 @@ class SettingCompletionTest { val repository = scenario.repository val viewModel = ReceiverEditViewModel( - savedStateHandle = SavedStateHandle(mapOf("receiverId" to RECEIVER_ID)), + route = SettingRoute.RecipientEditRoute(RECEIVER_ID), userRepository = repository, ) @@ -526,7 +526,7 @@ class SettingCompletionTest { val retryGate = scenario.enqueueDeliveryUpdate() val viewModel = DeliveryConditionViewModel( - savedStateHandle = SavedStateHandle(mapOf("receiverId" to RECEIVER_ID)), + route = SettingRoute.DeliveryConditionsRoute(RECEIVER_ID), userRepository = repository, ) composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) { 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 9d4eaaf7a9..cea0bb42c9 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 @@ -1,6 +1,8 @@ package com.afternote.feature.setting.presentation import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -112,6 +114,30 @@ class SettingFlowTest { assertEquals(listOf(Triple(false, null, null)), user.pushSettingUpdates) } + @Test + fun unavailableMenus_showFeedbackAndKeepSettingsUsable() { + val viewModel = SettingViewModel(settingFlowAuthRepository(loggedIn = true), settingFlowUserRepository()) + var profileOpened = false + setSettingContent(viewModel = viewModel, onProfileEdit = { profileOpened = true }) + val resources = ApplicationProvider.getApplicationContext().resources + val menuIds = + listOf( + R.string.settings_account_password_change, + R.string.settings_support_faq, + R.string.settings_support_inquiry, + R.string.settings_support_terms, + R.string.settings_support_privacy, + R.string.settings_support_service_info, + ) + menuIds.forEach { menuId -> + composeRule.onNode(hasText(resources.getString(menuId)) and hasClickAction()).performScrollTo().performClick() + composeRule.onNodeWithText("현재 이 메뉴는 이용할 수 없습니다.").assertIsDisplayed() + composeRule.onNodeWithText("확인").performClick() + } + composeRule.onNodeWithText("프로필 수정").performScrollTo().performClick() + assertEquals(true, profileOpened) + } + private fun setSettingContent( viewModel: SettingViewModel, onLogoutSuccess: () -> Unit = {}, @@ -124,20 +150,14 @@ class SettingFlowTest { onBackClick = {}, onLogoutSuccess = onLogoutSuccess, onProfileEditClick = onProfileEdit, - onPasswordChangeClick = {}, onLinkedAccountClick = {}, onNotificationClick = {}, onRecipientListClick = {}, onRecipientRegisterClick = {}, - onAfterDeliveryClick = {}, + onDeliveryConditionsClick = {}, onPasskeyClick = {}, onAppLockClick = onAppLock, - onFaqClick = {}, - onInquiryClick = {}, onNoticeClick = {}, - onTermsClick = {}, - onPrivacyClick = {}, - onServiceInfoClick = {}, onWithdrawGuideClick = {}, viewModel = viewModel, ) diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActionsTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActionsTest.kt new file mode 100644 index 0000000000..01600b1465 --- /dev/null +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/navigation/SettingLocalNavActionsTest.kt @@ -0,0 +1,126 @@ +package com.afternote.feature.setting.presentation.navigation + +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.serialization.NavBackStackSerializer +import androidx.navigation3.runtime.serialization.NavKeySerializer +import androidx.savedstate.serialization.decodeFromSavedState +import androidx.savedstate.serialization.encodeToSavedState +import com.afternote.core.ui.navigation.FeatureNavigationCallbacks +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class SettingLocalNavActionsTest { + private val stack = NavBackStack(SettingRoute.SettingHomeRoute) + private var exits = 0 + private var logouts = 0 + private var withdrawals = 0 + private val actions = + SettingLocalNavActions( + stack, + FeatureNavigationCallbacks { exits++ }, + object : SettingExternalActions { + override fun onLogoutSuccess() { + logouts++ + } + + override fun onWithdrawSuccess() { + withdrawals++ + } + }, + ) + + @Test + fun `delivery selection and edit preserve the receiver and back chain`() { + actions.onDeliveryConditionsClick() + actions.onDeliveryConditionsRecipientSelected(37L) + actions.onRecipientEditClick(37L) + assertEquals( + listOf( + SettingRoute.SettingHomeRoute, + SettingRoute.RecipientListRoute(true), + SettingRoute.DeliveryConditionsRoute(37L), + SettingRoute.RecipientEditRoute(37L), + ), + stack.toList(), + ) + repeat(3) { actions.popBack() } + assertEquals(listOf(SettingRoute.SettingHomeRoute), stack.toList()) + assertEquals(0, exits) + actions.popBack() + assertEquals(1, exits) + assertEquals(1, stack.size) + } + + @Test + fun `recipient management and registration return to their origin`() { + actions.onRecipientListClick() + assertEquals(SettingRoute.RecipientListRoute(false), stack.last()) + actions.popBack() + actions.onRecipientRegisterClick() + actions.popBack() + assertEquals(listOf(SettingRoute.SettingHomeRoute), stack.toList()) + stack[0] = SettingRoute.RecipientRegisterRoute + actions.popBack() + assertEquals(1, exits) + assertEquals(listOf(SettingRoute.RecipientRegisterRoute), stack.toList()) + } + + @Test + fun `withdrawal and logout only notify the root auth navigationCallbacks`() { + actions.onWithdrawGuideClick() + actions.onWithdrawConfirmClick() + val before = stack.toList() + actions.onWithdrawSuccess() + actions.onLogoutSuccess() + assertEquals(1, withdrawals) + assertEquals(1, logouts) + assertEquals(before, stack.toList()) + } + + @Test + fun `passkey and notification nested routes pop one screen at a time`() { + actions.onPasskeyClick() + actions.onPasskeyRegisterClick() + actions.onPasswordAuthClick() + actions.popBack() + assertEquals(SettingRoute.PasskeyMakingRoute, stack.last()) + repeat(2) { actions.popBack() } + actions.onNotificationClick() + actions.onPushNotificationClick() + actions.popBack() + assertEquals(SettingRoute.NotificationRoute, stack.last()) + } + + @Test + fun `all destination keys and route arguments survive serialization`() { + val routes = + listOf( + SettingRoute.SettingHomeRoute, + SettingRoute.WithdrawGuideRoute, + SettingRoute.WithdrawConfirmRoute, + SettingRoute.ProfileEditRoute, + SettingRoute.LinkedAccountRoute, + SettingRoute.NotificationRoute, + SettingRoute.PushNotificationRoute, + SettingRoute.RecipientListRoute(true), + SettingRoute.RecipientRegisterRoute, + SettingRoute.RecipientEditRoute(37L), + SettingRoute.DeliveryConditionsRoute(91L), + SettingRoute.PasskeyRoute, + SettingRoute.PasskeyMakingRoute, + SettingRoute.PasskeyPasswordRoute, + SettingRoute.AppLockSetupRoute, + SettingRoute.NoticeRoute, + ) + val backStack = NavBackStack(*routes.toTypedArray()) + val serializer = NavBackStackSerializer(NavKeySerializer()) + val restored = decodeFromSavedState(serializer, encodeToSavedState(serializer, backStack)) + assertEquals(routes, restored.toList()) + } +}