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/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/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, - ) - } -} 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) + } }