diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt index 6220c494dd..aae396e094 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt @@ -69,8 +69,6 @@ import com.getcode.navigation.results.rememberNavResultStateRegistry import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy import com.getcode.navigation.scrim.LocalScrimController import com.getcode.navigation.scrim.ScrimController -import com.flipcash.app.bills.BillOverlay -import com.getcode.navigation.scrim.ScrimOverlay import com.getcode.theme.CodeTheme import com.getcode.ui.biometrics.LocalBiometricsState import com.getcode.ui.biometrics.rememberBiometricsState @@ -223,12 +221,12 @@ internal fun App( ) } - ScrimOverlay(scrimController) - - // Bills render at the app root (not inside the scanner) so a - // presented bill appears over any screen. Reads the app-scoped - // billState via LocalSessionController. - BillOverlay() + // The scrim + bill overlay are hosted per-entry by + // NavBillOverlayEntryDecorator (added to the AppNavHost + // decorators) rather than as app-root siblings — that keeps + // the bill above the current screen while letting a bottom + // sheet (hosted in the same NavDisplay) render ABOVE the + // bill. See NavBillOverlayEntryDecorator. } val emailCodeChannel = LocalEmailCodeChannel.current diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt index f13283eb79..01b7b9468d 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt @@ -31,6 +31,7 @@ import com.flipcash.app.core.navigation.DeeplinkAction import com.flipcash.app.core.navigation.asNavBarTab import com.flipcash.app.core.navigation.LocalTabBarPadding import com.flipcash.app.internal.ui.AppNavigationBar +import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBillOverlayEntryDecorator import com.flipcash.app.internal.ui.navigation.decorators.rememberNavBlockingOverlayEntryDecorator import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator import com.getcode.navigation.AppNavHost @@ -54,6 +55,10 @@ internal fun AppContent( navigator = codeNavigator, resultStateRegistry = resultStateRegistry, decorators = listOf( + // First = outermost decorator overlay: the bill draws above the screen content (as it + // did at the app root). It's skipped for sheet entries, and NavDisplay paints the sheet + // scene above the base entry — so sheets open over the bill. See the decorator's docs. + rememberNavBillOverlayEntryDecorator(), rememberNavMessagingEntryDecorator( codeNavigator.backStack, barManager @@ -163,6 +168,10 @@ internal fun NewAppContent( navigator = codeNavigator, resultStateRegistry = resultStateRegistry, decorators = listOf( + // First = outermost: bill draws above screen content but is skipped for sheet + // entries, so NavDisplay paints the sheet scene above the bill-bearing base + // entry — sheets open OVER the bill. See NavBillOverlayEntryDecorator. + rememberNavBillOverlayEntryDecorator(), rememberNavMessagingEntryDecorator( codeNavigator.backStack, barManager diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavBillOverlayEntryDecorator.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavBillOverlayEntryDecorator.kt new file mode 100644 index 0000000000..2a87c54f09 --- /dev/null +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavBillOverlayEntryDecorator.kt @@ -0,0 +1,49 @@ +package com.flipcash.app.internal.ui.navigation.decorators + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation3.runtime.NavEntryDecorator +import androidx.navigation3.runtime.NavKey +import com.flipcash.app.bills.BillOverlay +import com.getcode.navigation.NavMetadataKeys +import com.getcode.navigation.scrim.LocalScrimController +import com.getcode.navigation.scrim.ScrimOverlay + +/** + * Draws the app's bill overlay (tip card / payable) — and the scrim beneath it — on top of each + * NON-sheet navigation entry. + * + * Previously the bill was a sibling at the app root, painted *after* the [NavDisplay]; because a + * bottom sheet is hosted inside that same NavDisplay, the bill always covered it. Hosting the bill + * as a per-entry decorator instead means: + * - it still floats above the current screen (the decorator draws it over `entry.Content()`), but + * - it is skipped for the sheet entry, and [NavDisplay] paints the sheet's overlay scene *above* + * the base scene — so a sheet opened over a presented bill (the tip token / amount pickers) + * renders above the bill as a normal sheet, gestures intact, without a separate window. + * + * Opening a sheet doesn't change the base entry, so the bill's animation state stays stable through + * the interaction. The scrim ([LocalScrimController]) is drawn just beneath the bill so it keeps + * dimming the screen content below the bill rather than the bill itself. + */ +@Suppress("FunctionName") +fun NavBillOverlayEntryDecorator(): NavEntryDecorator { + return NavEntryDecorator { entry -> + Box(modifier = Modifier.fillMaxSize()) { + entry.Content() + + // Sheets are painted above the base scene by NavDisplay, so they must NOT carry the + // bill themselves — otherwise the bill would cover the sheet again. + val isSheet = entry.metadata[NavMetadataKeys.IsSheet.key] == true + if (!isSheet) { + ScrimOverlay(LocalScrimController.current) + BillOverlay() + } + } + } +} + +@Composable +fun rememberNavBillOverlayEntryDecorator() = remember { NavBillOverlayEntryDecorator() } diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenSelectionPill.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenSelectionPill.kt index 5726bdc8de..ec82200dd9 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenSelectionPill.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenSelectionPill.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape @@ -62,7 +63,7 @@ fun TokenSelectionPill( Image( modifier = Modifier - .width(CodeTheme.dimens.grid.x4), + .size(CodeTheme.dimens.grid.x4), painter = painterResource(R.drawable.ic_dropdown), contentDescription = "" ) diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillOverlay.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillOverlay.kt index 8542ea0d3c..5f00cce55a 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillOverlay.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/BillOverlay.kt @@ -73,8 +73,13 @@ fun BillOverlay(modifier: Modifier = Modifier) { // shared root [ScrimController] (its ScrimOverlay sits just below this overlay), so it uses // the theme scrim colour. Skipped over the scanner tab, where the live camera stays visible. val scrimController = LocalScrimController.current - val overCamera = - (LocalCodeNavigator.current.currentRouteKey as? AppRoute)?.asNavBarTab() == NavBarButton.Scanner + // Resolve the tab UNDERNEATH any presented sheet, not the sheet route itself: a sheet opened + // over the bill (the tip token / amount pickers) pushes a route whose tab is null, which + // would flip this decision and fade the bill's scrim out/in around the sheet. Keying off the + // base tab keeps the scrim decision — and thus the scrim — stable across the sheet. + val baseRoute = LocalCodeNavigator.current.backStack + .lastOrNull { it !is AppRoute.Main.Sheet } as? AppRoute + val overCamera = baseRoute?.asNavBarTab() == NavBarButton.Scanner val showScrim = billState.bill != null && !overCamera LaunchedEffect(showScrim) { if (showScrim) { diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/modals/TipUserModal.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/modals/TipUserModal.kt index 4dee837454..0e63a600d2 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/modals/TipUserModal.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/modals/TipUserModal.kt @@ -61,12 +61,31 @@ internal fun TipUserModal( Modal( verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset), + contentPadding = PaddingValues( + start = CodeTheme.dimens.inset, + end = CodeTheme.dimens.inset, + top = CodeTheme.dimens.inset, + bottom = CodeTheme.dimens.grid.x2 + ), ) { - Text( - text = stringResource(id = R.string.title_sendTip), - style = CodeTheme.typography.displaySmall, - color = CodeTheme.colors.textMain, - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = R.string.title_sendTip), + style = CodeTheme.typography.displaySmall, + color = CodeTheme.colors.textMain, + ) + + TipTokenRow( + token = selection.token, + onSelectToken = { + navigator.openAsSheet(AppRoute.Sheets.TokenSelection(TokenPurpose.Tip(selection.minimum))) + }, + ) + } PresetOptions( // Presets follow the selected region reactively (see TippingCoordinator.selection). @@ -77,14 +96,6 @@ internal fun TipUserModal( onCustomClicked = { navigator.openAsSheet(AppRoute.Sheets.TipAmountEntry) }, ) - TipTokenRow( - token = selection.token, - modifier = Modifier.fillMaxWidth(), - onSelectToken = { - navigator.openAsSheet(AppRoute.Sheets.TokenSelection(TokenPurpose.Tip( selection.minimum))) - }, - ) - SlideToConfirm( onConfirm = { tip.confirmTip() }, modifier = Modifier.fillMaxWidth(), @@ -96,38 +107,24 @@ internal fun TipUserModal( } } -/** "of [token]" row — the token the tip will be sent in; tapping the pill opens the token picker. */ @Composable private fun TipTokenRow( token: Token?, onSelectToken: () -> Unit, modifier: Modifier = Modifier, ) { - Row( + TokenSelectionPill( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x2, - Alignment.CenterHorizontally + token = token, + background = White10, + textStyle = CodeTheme.typography.textMedium, + imageSize = CodeTheme.dimens.staticGrid.x3, + contentPadding = PaddingValues( + horizontal = CodeTheme.dimens.grid.x1 + 1.dp, + vertical = CodeTheme.dimens.grid.x1 - 1.dp, ), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.label_of), - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textSecondary, - ) - TokenSelectionPill( - token = token, - background = White10, - textStyle = CodeTheme.typography.textMedium, - imageSize = CodeTheme.dimens.staticGrid.x3, - contentPadding = PaddingValues( - horizontal = CodeTheme.dimens.grid.x1 + 1.dp, - vertical = CodeTheme.dimens.grid.x1 - 1.dp, - ), - onClick = onSelectToken, - ) - } + onClick = onSelectToken, + ) } @Composable diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt index 0a2888638a..6ee996386c 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt @@ -200,7 +200,11 @@ internal class ModalBottomSheetScene constructor( val progress = sheetState .progress(SheetDetent.Hidden, Expanded) .coerceIn(0f, 1f) - drawRect(scrimBaseColor.copy(alpha = scrimBaseColor.alpha * progress)) + // A bill beneath the sheet already dims the screen with its own + // (constant) scrim, so don't stack a second dim here — otherwise + // opening the sheet would visibly darken the backdrop. + val alpha = if (scrim.visible) 0f else scrimBaseColor.alpha * progress + drawRect(scrimBaseColor.copy(alpha = alpha)) } .then( if (effectiveProperties.dismissOnClickOutside) { @@ -212,7 +216,10 @@ internal class ModalBottomSheetScene constructor( UnstyledBottomSheet( state = sheetState, modifier = Modifier.fillMaxSize(), - enabled = !navigator.sheetDragDisabled && !scrim.visible, + // Only block dragging for a scrim that puts CONTENT over the sheet; a plain + // bill dim beneath the sheet must not disable the sheet's own gestures. + enabled = !navigator.sheetDragDisabled && + !(scrim.visible && scrim.overlayContent != null), ) { Sheet( modifier = Modifier @@ -237,7 +244,11 @@ internal class ModalBottomSheetScene constructor( ) { entry.Content() } - if (!isWrapContent) { + // Only surface the shared scrim inside the sheet when it has + // CONTENT to show over it. A plain bill dim must not be redrawn + // here — it would double-dim the sheet and its tap-to-dismiss + // would close the bill from on top of the sheet. + if (!isWrapContent && scrim.overlayContent != null) { ScrimOverlay(scrim) } }