From 91c6a66c40f3887e0993c6dd59c14b55bf944ec4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 13 Aug 2026 13:20:21 -0400 Subject: [PATCH 1/2] feat(tip): move the token pill into the Send-a-Tip header Relocate the tip token selector from a separate "of " row beneath the presets up into the modal header next to the title, and square the pill's dropdown icon (width -> size) so the chevron isn't stretched. --- .../app/core/ui/TokenSelectionPill.kt | 3 +- .../flipcash/app/bills/modals/TipUserModal.kt | 71 +++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) 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/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 From 0f7a3e6d36573bd4c82ab9f87751d7bd05f807a0 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 13 Aug 2026 13:20:43 -0400 Subject: [PATCH 2/2] fix(tip): render the token/amount sheets over the tip card, not behind it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sheets opened from the tip modal (token selection, custom amount) are hosted by the root NavDisplay, which BillOverlay is painted on top of as an app-root sibling — so they appeared behind the tip card and modal. Host the bill (and its scrim) as a NavDisplay entry decorator on non-sheet entries instead of an app-root sibling: the bill still floats above the current screen, but a sheet's overlay scene is painted above the base entry, so tip sheets now render over the card as normal, gesture-driven sheets. This avoids a separate window (a Popup broke the nested sheet NavDisplay's back dispatcher). Keep the bill's scrim decision keyed off the tab beneath any sheet so it stays constant across the sheet (no fade), and when a plain bill dim is already present the sheet no longer stacks its own dim, disables its drag, or redraws the bill scrim on top (which had been dismissing the bill). --- .../com/flipcash/app/internal/ui/App.kt | 14 +++--- .../app/internal/ui/navigation/AppContent.kt | 9 ++++ .../NavBillOverlayEntryDecorator.kt | 49 +++++++++++++++++++ .../com/flipcash/app/bills/BillOverlay.kt | 9 +++- .../scenes/ModalBottomSheetSceneStrategy.kt | 17 +++++-- 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavBillOverlayEntryDecorator.kt 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/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/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) } }