From 668802c88afed6a993fd2bb616a74f461408c8e6 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 12 Aug 2026 16:59:27 -0400 Subject: [PATCH 1/2] fix(v2): keep the tab bar in place under BottomBar modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nav bar hid whenever a BottomBar message showed, which collapsed the scaffold's bottom slot and re-laid-out the tab content beneath — visibly re-fanning the wallet's collapsing card stack when the Add Money modal opened. The modal's scrim already covers the bar via z-order, so the bar no longer needs to hide for modals; it now only hides for a presented bill/tip card. --- .../com/flipcash/app/internal/ui/AppNavigationBar.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt index 50c5abffe..d4fb9ff92 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt @@ -27,7 +27,6 @@ import com.flipcash.app.core.ui.rememberNavigationBarState import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.session.LocalSessionController -import com.getcode.manager.BottomBarManager import com.getcode.navigation.core.CodeNavigator import com.getcode.theme.CodeTheme import kotlinx.coroutines.flow.flowOf @@ -55,10 +54,6 @@ internal fun AppNavigationBar( val selectedTab = navigator.backStack.firstNotNullOfOrNull { (it as? AppRoute)?.asNavBarTab() } val topTab = (navigator.currentRouteKey as? AppRoute)?.asNavBarTab() - // A BottomBar message (e.g. the deposit-options modal) renders above the nav host; hide the bar - // while one is showing so it sits below the modal instead of floating over it. - val bottomBarMessages by BottomBarManager.messages.collectAsStateWithLifecycle() - // A bill/tip card renders at the app root above everything; hide the bar so it doesn't show // beneath the presented bill. val session = LocalSessionController.current @@ -71,8 +66,11 @@ internal fun AppNavigationBar( .then(modifier), contentAlignment = Alignment.BottomCenter, ) { + // Keep the bar in place for BottomBar modals (e.g. Add Money): the modal's scrim renders over + // it via z-order, and hiding it would collapse the scaffold's bottom slot and re-lay-out the + // tab content beneath (visibly re-fanning the wallet's collapsing card stack). AnimatedVisibility( - visible = topTab != null && bottomBarMessages.isEmpty() && !billUp, + visible = topTab != null && !billUp, enter = slideInVertically { it } + fadeIn(), exit = slideOutVertically { it } + fadeOut(), ) { From f6e53ba52ecd82038f672863ff8a2c259900e3b1 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 12 Aug 2026 16:59:41 -0400 Subject: [PATCH 2/2] feat(v2): card-style add-money method rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign the 'Add Money With' purchase-method sheet for v2 as card rows — title + subtitle + trailing icon — instead of the v1 filled buttons. - BottomBarAction gains an optional `content` slot; when set, BottomBarView renders it as a full-width card row (own surface, ripple, click) rather than a centered text button. No impact on existing actions. - Buttons.kt builds the cards (Debit Card / Cash Reserves / Phantom / Other Wallet) when NewUi is on, gated via a FeatureFlagController injected into the controller, which also titles the sheet 'Add Money With'. v1 buttons unchanged. - Icons: Debit Card -> Google Pay mark (with the 'Pay' wordmark recoloured white), Phantom -> ghost, Other Wallet -> the exact Figma QR export (ic_qr_code); all sized 32dp, monochrome glyphs tinted white. --- .../res/drawable/ic_google_pay_brand_mark.xml | 6 +- .../core/src/main/res/drawable/ic_qr_code.xml | 26 ++++ .../core/src/main/res/values/strings.xml | 8 ++ .../flipcash/app/funding/internal/Buttons.kt | 121 ++++++++++++++++++ .../InternalPurchaseMethodController.kt | 10 +- .../com/getcode/manager/BottomBarManager.kt | 6 + .../ui/components/bars/BottomBarContainer.kt | 27 ++++ 7 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 apps/flipcash/core/src/main/res/drawable/ic_qr_code.xml diff --git a/apps/flipcash/core/src/main/res/drawable/ic_google_pay_brand_mark.xml b/apps/flipcash/core/src/main/res/drawable/ic_google_pay_brand_mark.xml index 987038642..1cc58f7cf 100644 --- a/apps/flipcash/core/src/main/res/drawable/ic_google_pay_brand_mark.xml +++ b/apps/flipcash/core/src/main/res/drawable/ic_google_pay_brand_mark.xml @@ -20,13 +20,13 @@ android:viewportHeight="24"> + android:fillColor="#FFFFFF"/> + android:fillColor="#FFFFFF"/> + android:fillColor="#FFFFFF"/> diff --git a/apps/flipcash/core/src/main/res/drawable/ic_qr_code.xml b/apps/flipcash/core/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 000000000..591ace373 --- /dev/null +++ b/apps/flipcash/core/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 86417e48b..5e0820f27 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -412,6 +412,14 @@ Solflare Wallet Other Wallet + + Add Money With + Debit Card + Cash Reserves + Deposit funds from your debit card + Deposit USDC from your Phantom wallet + Deposit USDC from a crypto wallet + Add Cash with Google Pay Add Cash with Debit Card Add Cash with Credit Card diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt index 11ee5a121..8e2c733c0 100644 --- a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/Buttons.kt @@ -1,17 +1,25 @@ package com.flipcash.app.funding.internal import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text 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.graphics.ColorFilter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString @@ -35,8 +43,10 @@ internal fun purchaseOptions( state: PurchaseMethodState, metadata: PurchaseMethodMetadata, resources: ResourceHelper, + isNewUi: Boolean, onClick: (PurchaseMethod) -> Unit ): List { + if (isNewUi) return purchaseMethodCards(state, metadata, resources, onClick) return buildList { if (state.coinbaseOnRampAvailable) { add( @@ -101,6 +111,117 @@ internal fun purchaseOptions( } } +/** + * v2 "Add Money With" method rows — each a card (title + subtitle + trailing icon) rendered via the + * [BottomBarAction.content] slot. Same methods and conditions as the v1 buttons above. + */ +private fun purchaseMethodCards( + state: PurchaseMethodState, + metadata: PurchaseMethodMetadata, + resources: ResourceHelper, + onClick: (PurchaseMethod) -> Unit, +): List = buildList { + if (state.coinbaseOnRampAvailable) { + add( + cardAction( + title = resources.getString(R.string.label_debitCard), + subtitle = resources.getString(R.string.subtitle_depositDebitCard), + iconRes = R.drawable.ic_google_pay_brand_mark, + tintIcon = false, + testTag = "purchase_method_coinbase", + onClick = { onClick(PurchaseMethod.CoinbaseOnRamp) }, + ) + ) + } + if (state.hasReserves && metadata.showReserves) { + val minimumAmountNeeded = metadata.purchaseAmount ?: Fiat.MIN_VALUE + if (state.reservesBalance.nativeAmount >= minimumAmountNeeded) { + add( + cardAction( + title = resources.getString(R.string.label_cashReserves), + subtitle = state.reservesBalance.formatted(), + iconRes = R.drawable.ic_wallet, + testTag = "purchase_method_reserves", + onClick = { onClick(PurchaseMethod.CashReserves(state.reservesBalance)) }, + ) + ) + } + } + add( + cardAction( + title = resources.getString(R.string.label_phantom), + subtitle = resources.getString(R.string.subtitle_depositPhantom), + iconRes = R.drawable.ic_phantom_wallet, + testTag = "purchase_method_phantom", + onClick = { onClick(PurchaseMethod.PhantomWallet) }, + ) + ) + if (state.canUseOtherWallets) { + add( + cardAction( + title = resources.getString(R.string.title_onrampProviderOtherWallet), + subtitle = resources.getString(R.string.subtitle_depositOtherWallet), + iconRes = R.drawable.ic_qr_code, + testTag = "purchase_method_other_wallet", + onClick = { onClick(PurchaseMethod.OtherWallet) }, + ) + ) + } + add( + BottomBarAction( + text = resources.getString(R.string.action_dismiss), + style = BottomBarManager.BottomBarButtonStyle.Text, + ) + ) +} + +private fun cardAction( + title: String, + subtitle: String, + iconRes: Int, + tintIcon: Boolean = true, + testTag: String? = null, + onClick: () -> Unit, +): BottomBarAction = BottomBarAction( + text = AnnotatedString(title), + inlineContentMap = emptyMap(), + testTag = testTag, + onClick = onClick, + content = { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = CodeTheme.typography.textMedium, + color = Color.White, + ) + Text( + text = subtitle, + style = CodeTheme.typography.textSmall, + color = CodeTheme.colors.textSecondary, + ) + } + if (tintIcon) { + // Monochrome glyph (phantom, QR, reserves) — tint white and size square. + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(iconRes), + tint = Color.White, + contentDescription = null, + ) + } else { + // Brand mark (Google Pay) — keep its own colours; constrain height, let width wrap. + Image( + modifier = Modifier.height(28.dp), + painter = painterResource(iconRes), + contentDescription = null, + ) + } + }, +) + private fun buildButtonAction( prefix: String?, suffix: String?, diff --git a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt index 641a12707..a0f79be47 100644 --- a/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt +++ b/apps/flipcash/shared/funding/src/main/kotlin/com/flipcash/app/funding/internal/InternalPurchaseMethodController.kt @@ -5,6 +5,8 @@ import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.core.tokens.FundingSource import com.flipcash.app.core.tokens.SwapPurpose +import com.flipcash.app.featureflags.FeatureFlag +import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.funding.PaymentAction import com.flipcash.app.funding.PurchaseMethod import com.flipcash.app.funding.PurchaseMethodController @@ -54,6 +56,7 @@ class InternalPurchaseMethodController @Inject constructor( private val resources: ResourceHelper, private val userManager: UserManager, private val analytics: FlipcashAnalyticsService, + private val featureFlags: FeatureFlagController, ) : PurchaseMethodController { private val scope = CoroutineScope(SupervisorJob()) @@ -104,14 +107,17 @@ class InternalPurchaseMethodController @Inject constructor( var selected = false - val title = when (metadata.purpose) { + val isNewUi = featureFlags.observe(FeatureFlag.NewUi).value + val title = if (isNewUi) { + resources.getString(R.string.prompt_title_addMoneyWith) + } else when (metadata.purpose) { PurchasePurpose.Buy -> resources.getString(R.string.prompt_title_selectPurchaseMethod) PurchasePurpose.Deposit -> resources.getString(R.string.prompt_title_selectMethod) } BottomBarManager.showMessage( title = title, - actions = purchaseOptions(_state.value, metadata, resources) { method -> + actions = purchaseOptions(_state.value, metadata, resources, isNewUi) { method -> selected = true scope.launch { val selection = PurchaseMethodSelection(method, metadata) diff --git a/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt b/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt index e31419906..69458a677 100644 --- a/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt +++ b/libs/messaging/src/main/kotlin/com/getcode/manager/BottomBarManager.kt @@ -1,6 +1,8 @@ package com.getcode.manager +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import kotlinx.coroutines.flow.* import java.util.* @@ -14,6 +16,10 @@ data class BottomBarAction( val enabled: Boolean = true, // Optional UI-test anchor; surfaced as a resource-id when testTagsAsResourceId is on. val testTag: String? = null, + // Optional custom body. When set, the renderer draws this inside a content-slot button (its own + // background/ripple/click) instead of the default centered text button — e.g. the v2 add-money + // method cards (title + subtitle + trailing icon). + val content: (@Composable RowScope.() -> Unit)? = null, val onClick: () -> Unit = { } ) { constructor( diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt index b9253d4a1..0ceb1c435 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt @@ -11,6 +11,8 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.ui.draw.clip +import com.getcode.theme.White10 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -304,6 +306,31 @@ fun BottomBarView( } actions.fastForEachIndexed { index, action -> + val actionContent = action.content + if (actionContent != null) { + // v2 card row: title + subtitle + trailing icon on its own surface, spread + // full-width. (A content-slot CodeButton centres and shrinks its body, so it + // can't lay a left title against a right-edge icon.) + Row( + modifier = Modifier + .fillMaxWidth() + .addIf(action.testTag != null) { + Modifier.testTag(action.testTag!!) + } + .clip(CodeTheme.shapes.medium) + .background(White10) + .clickable(enabled = action.enabled) { + action.onClick() + onClose(SelectedBottomBarAction(index.takeIf { action.isUser } ?: -1)) + } + .padding(CodeTheme.dimens.grid.x3), + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), + verticalAlignment = Alignment.CenterVertically, + ) { + actionContent() + } + return@fastForEachIndexed + } CodeButton( modifier = Modifier .fillMaxWidth()