Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ fun CodeNavigator.navigateTo(route: NavKey, options: NavOptions = NavOptions())
} else {
route
}
navigate(destination, options)
val needsSheet = destination is AppRoute.Main.Sheet
val hasSheet = backStack.any { it is AppRoute.Main.Sheet }

if (hasSheet && needsSheet) {
pendingSheetDismiss = {
Snapshot.withMutableSnapshot {
sheetGeneration++
navigate(destination, options)
}
}
} else {
navigate(destination, options)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,68 @@ class NavigateToTest {
}

// endregion

// region Single-route navigateTo dismiss-then-replace

@Test
fun `single-route navigateTo with existing sheet sets pendingSheetDismiss`() {
val navigator = createNavigator(
AppRoute.Main.Scanner,
AppRoute.Main.Sheet(AppRoute.Sheets.Wallet),
)

navigator.navigateTo(AppRoute.Sheets.TokenDiscovery, options = quietOptions)

assertNotNull(navigator.pendingSheetDismiss)
// Backstack unchanged until the callback fires
assertEquals(2, navigator.backStack.size)
}

@Test
fun `single-route navigateTo callback navigates to new sheet after dismiss`() {
val navigator = createNavigator(
AppRoute.Main.Scanner,
AppRoute.Main.Sheet(AppRoute.Sheets.Wallet),
)

navigator.navigateTo(AppRoute.Sheets.TokenDiscovery, options = quietOptions)

// Simulate dismiss: remove old sheet entry, then callback fires
navigator.backStack.removeAt(navigator.backStack.lastIndex)
navigator.pendingSheetDismiss!!.invoke()

val last = navigator.backStack.last()
assertIs<AppRoute.Main.Sheet>(last)
assertEquals(AppRoute.Sheets.TokenDiscovery, last.initialRoute)
}

@Test
fun `single-route navigateTo increments sheetGeneration on dismiss-replace`() {
val navigator = createNavigator(
AppRoute.Main.Scanner,
AppRoute.Main.Sheet(AppRoute.Sheets.Wallet),
)
val initialGeneration = navigator.sheetGeneration

navigator.navigateTo(AppRoute.Sheets.TokenDiscovery, options = quietOptions)

// Simulate dismiss
navigator.backStack.removeAt(navigator.backStack.lastIndex)
navigator.pendingSheetDismiss!!.invoke()

assertEquals(initialGeneration + 1, navigator.sheetGeneration)
}

@Test
fun `single-route navigateTo without existing sheet navigates directly`() {
val navigator = createNavigator(AppRoute.Main.Scanner)

navigator.navigateTo(AppRoute.Sheets.TokenDiscovery, options = quietOptions)

assertNull(navigator.pendingSheetDismiss)
assertEquals(2, navigator.backStack.size)
assertIs<AppRoute.Main.Sheet>(navigator.backStack.last())
}

// endregion
}
26 changes: 26 additions & 0 deletions ui/navigation/src/main/kotlin/com/getcode/navigation/AppNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.navigation3.scene.OverlayScene
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
Expand All @@ -35,6 +37,7 @@ import com.getcode.navigation.decorators.rememberNavResultScopeEntryDecorator
import com.getcode.navigation.results.NavResultStateRegistry
import com.getcode.navigation.results.rememberNavResultStateRegistry
import com.getcode.theme.CodeTheme
import timber.log.Timber

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand All @@ -60,6 +63,29 @@ fun AppNavHost(
) {
ChangeSystemBarsTheme(CodeTheme.colors.background.luminance() < 0.5f)

// Safety net: async duplicate Sheet sanitization.
// Cannot prevent a same-frame crash, but cleans up residual duplicates
// from unforeseen race conditions before the next frame renders.
LaunchedEffect(navigator.backStack) {
snapshotFlow { navigator.backStack.toList() }
.collect { stack ->
val seen = mutableSetOf<String>()
val toRemove = mutableListOf<Int>()
for (i in stack.lastIndex downTo 0) {
val entry = stack[i]
if (entry is Sheet && !seen.add(entry.toString())) {
toRemove.add(i)
}
}
if (toRemove.isNotEmpty()) {
Timber.w("Duplicate Sheet keys detected, sanitizing backstack")
Snapshot.withMutableSnapshot {
toRemove.forEach { navigator.backStack.removeAt(it) }
}
}
}
}

NavDisplay(
backStack = navigator.backStack,
onBack = onBack ?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ data class CodeNavigator(
NavOptions.PopUpTo.ClearAll -> {
Snapshot.withMutableSnapshot {
if (currentRouteKey != route) {
if (route is Sheet) {
backStack.removeAll { it is Sheet && it.toString() == route.toString() }
}
backStack.add(route)
// Remove all entries before the new one
while (backStack.size > 1) {
Expand Down
Loading