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 50c5abffe2..d4fb9ff928 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(),
) {
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 9870386420..1cc58f7cf7 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 0000000000..591ace3733
--- /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 86417e48b7..5e0820f27f 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 11ee5a121a..8e2c733c0f 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 641a127076..a0f79be472 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 e31419906b..69458a6775 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 b9253d4a11..0ceb1c435c 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()