From 51b496d0ec695370b3ba181ace617e1df2c8dd8e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 21 Aug 2026 15:55:26 -0400 Subject: [PATCH 1/2] feat(deposit): show a USDC to Dollars conversion graphic The Add Money -> Other Wallet education screen was still on the flattened v1 asset, which spelled the reserve as "USDF" and pointed the arrow the wrong way for a deposit. Hoist the withdrawal screen's composed v2 art (node 9216:19798) into core-ui as ConversionGraphic, parameterised by which coin sits on each side, and point both screens at it. The convention plugin already wires core-ui into every feature module, so neither build file changes. Center-aligning the two coins was subtly wrong: the badged USDC art is 111x112 (a 100dp face plus the Solana badge overhanging its corner) against a flat 100dp coin, so the two faces sat 6dp out of line. The shared component top-aligns and nudges the arrow onto the face centre line instead. The deposit copy drops the USDF branding, and its measure widens to match the withdraw screen now that the line is longer. --- .../flipcash/app/core/ui/ConversionGraphic.kt | 75 +++++++++++++++++++ .../res/drawable/ic_deposit_usdc_as_usdf.xml | 60 --------------- .../core/src/main/res/values/strings.xml | 5 +- .../internal/UsdcDepositInformationScreen.kt | 16 ++-- .../UsdcWithdrawalInformationScreen.kt | 40 ++-------- 5 files changed, 92 insertions(+), 104 deletions(-) create mode 100644 apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/ConversionGraphic.kt delete mode 100644 apps/flipcash/core/src/main/res/drawable/ic_deposit_usdc_as_usdf.xml diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/ConversionGraphic.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/ConversionGraphic.kt new file mode 100644 index 0000000000..a4c2713e51 --- /dev/null +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/ConversionGraphic.kt @@ -0,0 +1,75 @@ +package com.flipcash.app.core.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.flipcash.core.R + +/** + * A coin face the [ConversionGraphic] can show on either side of the arrow. + */ +enum class ConversionCoin { + /** The gold "Dollars" coin — Flipcash's own reserve currency. */ + Dollars, + + /** The USDC coin with the Solana badge on its bottom-right corner. */ + UsdcOnSolana, +} + +/** + * The "converted 1:1" education art (Figma node 9216:19798): two coins separated by an arrow. + * + * Composed here rather than shipped as one flattened asset so the same art serves both directions — + * [ConversionCoin.Dollars] to [ConversionCoin.UsdcOnSolana] when withdrawing, and the inverse when + * adding money from another wallet. + */ +@Composable +fun ConversionGraphic( + from: ConversionCoin, + to: ConversionCoin, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + // The badged coin's art is 111x112 — a 100 dp coin face plus the Solana badge overhanging + // its bottom-right corner. Top-aligning keeps both 100 dp faces on the same line; centering + // the boxes instead would push the badged one 6 dp out of line with its partner. + verticalAlignment = Alignment.Top, + // 14 dp between each pair, matching the 278x124 Figma frame's coin/arrow gaps. + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Coin(from) + Image( + // Nudged onto the coin faces' centre line, which the top alignment above leaves at + // 50 dp: (100 - 28) / 2. + modifier = Modifier.padding(top = 36.dp).size(28.dp), + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + ) + Coin(to) + } +} + +@Composable +private fun Coin(coin: ConversionCoin) { + when (coin) { + ConversionCoin.Dollars -> Image( + modifier = Modifier.size(100.dp), + painter = painterResource(R.drawable.ic_coin_dollars), + contentDescription = null, + ) + + // Natural size (111x112): the 100 dp coin plus the badge overhanging its corner. + ConversionCoin.UsdcOnSolana -> Image( + painter = painterResource(R.drawable.ic_usdc_on_solana), + contentDescription = null, + ) + } +} diff --git a/apps/flipcash/core/src/main/res/drawable/ic_deposit_usdc_as_usdf.xml b/apps/flipcash/core/src/main/res/drawable/ic_deposit_usdc_as_usdf.xml deleted file mode 100644 index 466d874318..0000000000 --- a/apps/flipcash/core/src/main/res/drawable/ic_deposit_usdc_as_usdf.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index da36deba76..37a603b5d1 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -808,8 +808,9 @@ Discover - Deposit USDC - Your Solana USDC will be converted 1:1 to USD on Flipcash (USDF) + Deposit USDC + + Your USDC will be converted 1:1 to Dollars on Flipcash Buy With Phantom Purchase using Solana USDC in Phantom. Simply connect your wallet and confirm the transaction diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt index ce692fef68..571e6dbf25 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/UsdcDepositInformationScreen.kt @@ -1,6 +1,5 @@ package com.flipcash.app.deposit.internal -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,11 +11,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.flipcash.app.core.deposit.DepositResult import com.flipcash.app.core.deposit.DepositStep +import com.flipcash.app.core.ui.ConversionCoin +import com.flipcash.app.core.ui.ConversionGraphic import com.flipcash.core.R import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.solana.keys.Mint @@ -82,24 +82,24 @@ internal fun UsdcDepositInformationScreen(showOtherOptions: Boolean) { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - Image( - painter = painterResource(R.drawable.ic_deposit_usdc_as_usdf), - contentDescription = null, + ConversionGraphic( + from = ConversionCoin.UsdcOnSolana, + to = ConversionCoin.Dollars, ) } Column( - modifier = Modifier.fillMaxWidth(0.60f), + modifier = Modifier.fillMaxWidth(0.80f), verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.title_depositUsdcAsUsdf), + text = stringResource(R.string.title_depositUsdcAsDollars), style = CodeTheme.typography.textLarge, color = CodeTheme.colors.textMain, ) Text( - text = stringResource(R.string.description_depositUsdcAsUsdf), + text = stringResource(R.string.description_depositUsdcAsDollars), style = CodeTheme.typography.textSmall, color = CodeTheme.colors.textSecondary, textAlign = TextAlign.Center, diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt index 4d1dd4b1a6..08bc2cc2f0 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/screens/UsdcWithdrawalInformationScreen.kt @@ -4,12 +4,10 @@ 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.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -18,8 +16,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.flipcash.app.core.ui.ConversionCoin +import com.flipcash.app.core.ui.ConversionGraphic import com.flipcash.app.core.withdrawal.WithdrawalResult import com.flipcash.app.core.withdrawal.WithdrawalStep import com.flipcash.app.featureflags.FeatureFlag @@ -96,7 +95,10 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { contentAlignment = Alignment.Center, ) { if (isNewUi) { - ConversionGraphic() + ConversionGraphic( + from = ConversionCoin.Dollars, + to = ConversionCoin.UsdcOnSolana, + ) } else { Image( painter = painterResource(R.drawable.ic_withdraw_usdf_as_usdc), @@ -132,33 +134,3 @@ internal fun UsdcWithdrawalInformationScreen(showOtherOptions: Boolean) { } } } - -/** - * v2 "Dollars → USDC" graphic (Figma node 9216:19798). The v1 art carried the Flipcash "F" mark on - * the left; v2 swaps in the gold Dollars coin, so the two halves are composed here rather than - * shipped as one flattened asset. - */ -@Composable -private fun ConversionGraphic() { - Row( - verticalAlignment = Alignment.CenterVertically, - // 14 dp between each pair, matching the 278x124 Figma frame's coin/arrow gaps. - horizontalArrangement = Arrangement.spacedBy(14.dp), - ) { - Image( - modifier = Modifier.size(100.dp), - painter = painterResource(R.drawable.ic_coin_dollars), - contentDescription = null, - ) - Image( - modifier = Modifier.size(28.dp), - painter = painterResource(R.drawable.ic_arrow_right), - contentDescription = null, - ) - // Natural size (111x112): the 100 dp coin plus the Solana badge overhanging its corner. - Image( - painter = painterResource(R.drawable.ic_usdc_on_solana), - contentDescription = null, - ) - } -} From 602ac8960f1dc42bbe7cd698def31317c3fbd0ac Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 21 Aug 2026 15:55:45 -0400 Subject: [PATCH 2/2] fix(tokens): drop the "of X" suffix when a swap lands in Dollars The swap success title reads " of ", which names what the user received. That only helps when the destination is a community currency: adding money and converting into the reserve both rendered "$1.00 of Dollars", where the amount alone already said it. Suppress the suffix for those two, keeping it everywhere it still reads correctly. Buy is matched on a non-Flexible funding source, mirroring the condition the top bar already uses to show "Adding Money"; Convert gets an isConvertingToDollars flag on the state, alongside the existing isConvertingFromDollars. Both convert-direction flags were untested, so cover them here. --- .../internal/TokenTxProcessingScreen.kt | 14 +++++++--- .../flipcash/app/tokens/ui/SwapViewModel.kt | 4 +++ .../app/tokens/ui/SwapViewModelStateTest.kt | 27 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenTxProcessingScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenTxProcessingScreen.kt index a68130b5b7..7cc9260a0d 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenTxProcessingScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenTxProcessingScreen.kt @@ -127,12 +127,20 @@ private fun TokenTxProcessingScreen( LoadingSuccessState.State.Idle -> "" LoadingSuccessState.State.Loading -> stringResource(R.string.title_processingYourTransaction) LoadingSuccessState.State.Success -> { - val name = when (state.purpose) { - is SwapPurpose.Convert -> state.destinationTokenName + val name = when (val purpose = state.purpose) { + // Adding money always lands in the reserve, so naming it reads as + // "$1.00 of Dollars" — the amount alone already says it. + is SwapPurpose.Buy if purpose.fundingSource != FundingSource.Flexible -> null + // Converting into the reserve reads the same way, so it goes unnamed + // too; every other destination still names what the user received. + is SwapPurpose.Convert -> + state.destinationTokenName.takeUnless { state.isConvertingToDollars } is SwapPurpose.BalanceIncrease -> state.tokenName else -> stringResource(R.string.title_cashReserves) } - state.netTransferAmount.formatted(suffix = stringResource(R.string.label_ofToken, name)) + state.netTransferAmount.formatted( + suffix = name?.let { stringResource(R.string.label_ofToken, it) }, + ) } }, style = CodeTheme.typography.textLarge, diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt index 63a177f07f..7c3e2a5aa7 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt @@ -291,6 +291,10 @@ class SwapViewModel @Inject constructor( val isConvertingFromDollars: Boolean get() = (purpose as? SwapPurpose.Convert)?.mint == Mint.usdf + /** Whether a conversion lands in Dollars, which the reserve's own name never has to spell out. */ + val isConvertingToDollars: Boolean + get() = (purpose as? SwapPurpose.Convert)?.destinationMint == Mint.usdf + val canTransact: Boolean get() = buyProgress.isIdle && sellProgress.isIdle && processingProgress.isIdle diff --git a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelStateTest.kt b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelStateTest.kt index e5dcabb3c0..750fc3a87a 100644 --- a/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelStateTest.kt +++ b/apps/flipcash/shared/tokens/src/test/kotlin/com/flipcash/app/tokens/ui/SwapViewModelStateTest.kt @@ -277,4 +277,31 @@ class SwapViewModelStateTest { fun `netTransferAmount is Zero when confirmedNetTransferAmount is null`() { assertEquals(Fiat.Zero, SwapViewModel.State().netTransferAmount) } + + // --- Computed: convert direction --- + + @Test + fun `isConvertingToDollars is true when a conversion lands in the reserve`() { + val state = SwapViewModel.State( + purpose = SwapPurpose.Convert(mint = mint(), destinationMint = Mint.usdf), + ) + assertTrue(state.isConvertingToDollars) + assertFalse(state.isConvertingFromDollars) + } + + @Test + fun `isConvertingToDollars is false when a conversion lands in another token`() { + val state = SwapViewModel.State( + purpose = SwapPurpose.Convert(mint = Mint.usdf, destinationMint = mint()), + ) + assertFalse(state.isConvertingToDollars) + assertTrue(state.isConvertingFromDollars) + } + + @Test + fun `convert direction flags are false for non-convert purposes`() { + val state = SwapViewModel.State(purpose = SwapPurpose.Sell(Mint.usdf)) + assertFalse(state.isConvertingToDollars) + assertFalse(state.isConvertingFromDollars) + } }