fix(nav): recovery back-stack loop + the Today tab's inconsistent back arrow

Two navigation fixes, one found the expensive way.

1. Post-recovery BACK bounced users to the Recovery screen, forever. The Home
   entry that routed to Recovery stayed on the back stack holding
   needsRecovery=true, and its LaunchedEffect re-fires whenever the entry is
   returned to — so after a successful recovery, BACK landed on stale-Home,
   which instantly redirected to Recovery again. It looks EXACTLY like the
   couple key was lost again (it wasn't; verified: the keyset survives process
   kill and cold-starts straight to Home). Cost an hour of key-loss ghost
   hunting live before the process-alive evidence gave it away. onRecovered and
   the partner-restore onDone now popUpTo(HOME) INCLUSIVE + launchSingleTop —
   the stale entry is replaced, not buried; BACK from Home exits the app like
   every other root tab. C-NAV-002 class; the restore variant was worse (it
   left Home AND Recovery underneath).

2. C-TODAY-BACK-001 (open P3 from the report): the Today tab drew a back arrow
   the other four tabs don't. DAILY_QUESTION is a bottom-nav tab — the bar IS
   the navigation. onBack is now nullable-defaulting-null and the nav wiring
   passes none; LocalQuestionContent already renders the arrow only when
   non-null. Verified live: no arrow, other tabs unchanged.

Suite green, verified on-device (BACK exits; Today clean).
This commit is contained in:
null 2026-07-16 01:06:40 -05:00
parent 4856638097
commit c636749cf3
2 changed files with 18 additions and 4 deletions

View File

@ -268,7 +268,9 @@ fun AppNavigation(
route = AppRoute.DAILY_QUESTION,
deepLinks = listOf(navDeepLink { uriPattern = "closer://closer.app/daily_question" })
) {
DailyQuestionScreen(onNavigate = navigateRoute, onBack = navigateBackOrHome)
// No onBack: it's a bottom-nav tab (C-TODAY-BACK-001 — the arrow made Today
// inconsistent with the other four tabs, whose bar IS the navigation).
DailyQuestionScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.QUESTION_PACKS) {
QuestionPackLibraryScreen(onNavigate = navigateRoute)
@ -409,8 +411,14 @@ fun AppNavigation(
composable(route = AppRoute.RECOVERY) {
RecoveryScreen(
onRecovered = {
// Pop up to and INCLUDING the old HOME, not just Recovery: the Home entry that
// routed here still holds needsRecovery=true, and its LaunchedEffect re-fires
// whenever it's returned to — so BACK from the new Home bounced users straight
// back to Recovery, forever, looking exactly like the key had been lost again
// (C-NAV-002 class; cost an hour of key-loss ghost-hunting live).
navController.navigate(AppRoute.HOME) {
popUpTo(AppRoute.RECOVERY) { inclusive = true }
popUpTo(AppRoute.HOME) { inclusive = true }
launchSingleTop = true
}
},
onPartnerRestore = { navController.navigate(AppRoute.RESTORE_REQUEST) }
@ -421,8 +429,11 @@ fun AppNavigation(
composable(route = AppRoute.RESTORE_REQUEST) {
RestoreRequestScreen(
onDone = {
// Same stale-Home disease as onRecovered above — and worse: popping only this
// screen left BOTH the stale Home and the Recovery entry underneath.
navController.navigate(AppRoute.HOME) {
popUpTo(AppRoute.RESTORE_REQUEST) { inclusive = true }
popUpTo(AppRoute.HOME) { inclusive = true }
launchSingleTop = true
}
},
onBack = navigateBackOrHome

View File

@ -11,7 +11,10 @@ import app.closer.domain.model.Question
@Composable
fun DailyQuestionScreen(
onNavigate: (String) -> Unit = {},
onBack: () -> Unit = {},
// Nullable, defaulting to none: DAILY_QUESTION is a bottom-nav TAB, and the bar is the navigation
// there — the other four tabs draw no back arrow, and LocalQuestionContent only renders one when
// onBack is non-null (C-TODAY-BACK-001). Pass a handler only from non-tab embeddings, if any ever exist.
onBack: (() -> Unit)? = null,
viewModel: DailyQuestionViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()