Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
@@ -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,
)
}
}

This file was deleted.

5 changes: 3 additions & 2 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -808,8 +808,9 @@

<string name="action_discover">Discover</string>

<string name="title_depositUsdcAsUsdf">Deposit USDC</string>
<string name="description_depositUsdcAsUsdf">Your Solana USDC will be converted 1:1 to USD on Flipcash (USDF)</string>
<string name="title_depositUsdcAsDollars">Deposit USDC</string>
<!-- The reserve is branded "Dollars"; the 1:1 conversion is the only thing the screen explains. -->
<string name="description_depositUsdcAsDollars">Your USDC will be converted 1:1 to Dollars on Flipcash</string>

<string name="title_buyWithPhantom">Buy With Phantom</string>
<string name="description_buyWithPhantom">Purchase using Solana USDC in Phantom. Simply connect your wallet and confirm the transaction</string>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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
Expand All@@ -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
Expand DownExpand Up@@ -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,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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
Expand DownExpand Up@@ -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),
Expand DownExpand Up@@ -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,
)
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
}
}
Loading