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(