From 7973c1bcb4f35a51af43c86fbe030bbe3b83323d Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 20 Jul 2026 13:42:26 -0400 Subject: [PATCH] chore: polish and fixes for buy/create with another currency * lock selected currency to the balance (TVL) to ensure math works for curve * copy changes * remove balance filtering for buy with currency Signed-off-by: Brandon McAnsh --- .../core/src/main/res/values/strings.xml | 5 +- .../app/tokens/TokenBuyReceiptScreen.kt | 5 +- .../app/tokens/TokenSellReceiptScreen.kt | 3 +- .../tokens/internal/TokenBuyReceiptScreen.kt | 130 ++++++++++++------ .../app/tokens/internal/TokenSelectScreen.kt | 3 - .../flipcash/app/tokens/ui/SwapViewModel.kt | 1 + 6 files changed, 98 insertions(+), 49 deletions(-) diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index 9ed1b33358..60e432d6e7 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -538,12 +538,15 @@ Sell Amount to Withdraw Solana USDC with - Buy + Buy %1$s Sell %1$s + Confirm Purchase + Confirm Sale Purchasing %1$s Adding Money Selling %1$s Review the above before confirming.\nOnce made, your transaction is irreversible. + Review the above before confirming.\nOnce made, your transaction is irreversible. Yes No diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenBuyReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenBuyReceiptScreen.kt index 966f54546c..193a8de8cc 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenBuyReceiptScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenBuyReceiptScreen.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.tokens.SwapResult import com.flipcash.app.core.tokens.SwapStep import com.flipcash.app.tokens.internal.TokenBuyReceiptScreen @@ -23,13 +25,14 @@ import kotlinx.coroutines.flow.onEach internal fun BuyReceiptScreen() { val flowNavigator = rememberFlowNavigator() val viewModel = flowSharedViewModel() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarWithTitle( - title = stringResource(R.string.title_buyToken), + title = stringResource(R.string.title_confirmPurchase), titleAlignment = Alignment.CenterHorizontally, backButton = true, onBackIconClicked = { flowNavigator.back() } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt index a9293236fd..04303129b3 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenSellReceiptScreen.kt @@ -19,7 +19,6 @@ import com.getcode.navigation.flow.rememberFlowNavigator import com.getcode.ui.components.AppBarWithTitle import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @Composable @@ -33,7 +32,7 @@ internal fun SellReceiptScreen() { horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarWithTitle( - title = stringResource(R.string.title_sellToken, state.tokenName), + title = stringResource(R.string.title_confirmSale), titleAlignment = Alignment.CenterHorizontally, backButton = true, onBackIconClicked = { diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenBuyReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenBuyReceiptScreen.kt index 38e668abac..87653f50e9 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenBuyReceiptScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenBuyReceiptScreen.kt @@ -3,13 +3,19 @@ package com.flipcash.app.tokens.internal import androidx.compose.foundation.background import androidx.compose.foundation.border 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.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -25,6 +31,7 @@ import com.flipcash.app.core.ui.ReceiptLineItem import com.flipcash.app.core.ui.TokenBalanceRow import com.flipcash.app.core.ui.TokenBalanceStyle import com.flipcash.app.core.ui.rememberTokenBalanceRowStyling +import com.flipcash.app.core.ui.shimmer import com.flipcash.app.tokens.ui.SwapViewModel import com.flipcash.features.tokens.R import com.getcode.opencode.model.financial.Fiat @@ -62,7 +69,7 @@ private fun TokenBuyReceiptScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = CodeTheme.dimens.grid.x5), - text = stringResource(R.string.label_sellWarning), + text = stringResource(R.string.label_buyWarning), style = CodeTheme.typography.textSmall, color = CodeTheme.colors.textSecondary, textAlign = TextAlign.Center, @@ -97,9 +104,9 @@ private fun TokenBuyReceiptScreen( ) ) { BuyReceipt( - fundingToken = state.fundingTokenWithBalance!!.token, - desiredToken = state.tokenWithBalance!!.token, - purchaseAmount = state.confirmedEnteredAmount!!, + fundingToken = state.fundingTokenWithBalance?.token, + desiredToken = state.tokenWithBalance?.token, + purchaseAmount = state.confirmedEnteredAmount, feeAmount = state.feeAmount, ) } @@ -108,16 +115,20 @@ private fun TokenBuyReceiptScreen( @Composable private fun BuyReceipt( - fundingToken: Token, - desiredToken: Token, - purchaseAmount: Fiat, + fundingToken: Token?, + desiredToken: Token?, + purchaseAmount: Fiat?, feeAmount: Fiat, modifier: Modifier = Modifier, ) { + // The funding token and confirmed amount resolve asynchronously after landing here, so + // either can briefly be null on first composition. Render a shimmer in place of each token + // row until its data arrives rather than crashing or hiding the row entirely. val feeAdjustedPurchaseAmount by remember(purchaseAmount, feeAmount) { derivedStateOf { - if (!feeAmount.hasDisplayableValue) return@derivedStateOf purchaseAmount - purchaseAmount + feeAmount + purchaseAmount?.let { + if (!feeAmount.hasDisplayableValue) it else it + feeAmount + } } } @@ -144,29 +155,34 @@ private fun BuyReceipt( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.subtitle_youPay), + text = stringResource(R.string.subtitle_youReceive), style = CodeTheme.typography.textSmall, color = CodeTheme.colors.textSecondary, ) - TokenBalanceRow( - tokenWithBalance = TokenWithBalance( - token = fundingToken, - balance = feeAdjustedPurchaseAmount - ), - showName = false, - showLogo = true, - showFlag = false, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - styling = rememberTokenBalanceRowStyling( - balanceDisplayStyle = TokenBalanceStyle.Large( - textStyle = CodeTheme.typography.displaySmall.bolded() + + if (desiredToken != null && purchaseAmount != null) { + TokenBalanceRow( + tokenWithBalance = TokenWithBalance( + token = desiredToken, + balance = purchaseAmount ), - ), - contentPadding = PaddingValues(0.dp), - ) + showName = false, + showLogo = true, + showFlag = false, + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), + styling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Large( + textStyle = CodeTheme.typography.displaySmall.bolded() + ), + ), + contentPadding = PaddingValues(0.dp), + ) + } else { + TokenBalanceRowPlaceholder() + } } - if (feeAmount.isPositive) { + if (feeAmount.isPositive && purchaseAmount != null) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), @@ -192,26 +208,56 @@ private fun BuyReceipt( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(R.string.subtitle_youReceive), + text = stringResource(R.string.subtitle_youPay), style = CodeTheme.typography.textSmall, color = CodeTheme.colors.textSecondary, ) - TokenBalanceRow( - tokenWithBalance = TokenWithBalance( - token = desiredToken, - balance = purchaseAmount - ), - showName = false, - showLogo = true, - showFlag = false, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - styling = rememberTokenBalanceRowStyling( - balanceDisplayStyle = TokenBalanceStyle.Large( - textStyle = CodeTheme.typography.displaySmall.bolded() + if (fundingToken != null && feeAdjustedPurchaseAmount != null) { + TokenBalanceRow( + tokenWithBalance = TokenWithBalance( + token = fundingToken, + balance = feeAdjustedPurchaseAmount!! ), - ), - contentPadding = PaddingValues(0.dp), - ) + showName = false, + showLogo = true, + showFlag = false, + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), + styling = rememberTokenBalanceRowStyling( + balanceDisplayStyle = TokenBalanceStyle.Large( + textStyle = CodeTheme.typography.displaySmall.bolded() + ), + ), + contentPadding = PaddingValues(0.dp), + ) + } else { + TokenBalanceRowPlaceholder() + } } } +} + +/** + * Shimmer stand-in for a [TokenBalanceRow] shown with a logo + large balance, used while the + * token or amount is still resolving. Mirrors the row's layout: a circular logo followed by the + * balance text. + */ +@Composable +private fun TokenBalanceRowPlaceholder(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .size(CodeTheme.dimens.staticGrid.x6) + .shimmer(CircleShape) + ) + Box( + Modifier + .width(CodeTheme.dimens.grid.x20) + .height(CodeTheme.dimens.grid.x6) + .shimmer() + ) + } } \ No newline at end of file diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt index 6f97bab265..080c0993da 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSelectScreen.kt @@ -67,9 +67,6 @@ private fun SelectTokenScreenContent( }, enableGreaterThanAmount = { _, amount -> when (val purpose = state.purpose) { - is TokenPurpose.Swap -> { - amount.nativeAmount.valueGreaterThanOrEqualTo(purpose.amount) - } is TokenPurpose.LaunchFunding -> { amount.nativeAmount.valueGreaterThanOrEqualTo(purpose.amount) } 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 9b46d83a15..101afe5cb5 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 @@ -860,6 +860,7 @@ class SwapViewModel @Inject constructor( val amountFiat = verifiedFiatCalculator.compute( amount = Fiat(delegateState.enteredAmount, rate.currency), token = token, + balance = tokenCoordinator.balanceForToken(token), rate = rate, ).getOrElse { BottomBarManager.showAlert(