From 44c2ccaf883ee9f09941cecda3f5db573f7089bf Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 15:43:15 -0400 Subject: [PATCH] refactor(money): consolidate fiat appreciation formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the appreciation sign convention into one shared helper, `Fiat.formattedAppreciation()` (core/money), used by both the balance header (CurrencyAppreciationLabel) and the per-token cards: - Always shows "+$0.00" for a zero change, normalizing a rounds-to-zero amount to a positive zero first — fixes the "+-$0.00" double sign that a negative-zero value produced (formatter renders -0.0 as "-$0.00" while `>= 0` is true). - Gains get "+", losses keep the formatter's own "-". Also drop the stray space `extraPrefix` inserted at the source in `Fiat.formatted` ("+ $" -> "+$"); the four intentional callers that wanted the space (fee "- ", approx "~ ", market-cap "+ ") now bake it into their prefix string. Tests updated accordingly. --- .../flipcash/app/core/money/FormatUtils.kt | 19 +++++++++++++++++++ .../app/core/ui/CurrencyAppreciationLabel.kt | 10 ++-------- .../tokens/internal/TokenBuyReceiptScreen.kt | 2 +- .../tokens/internal/TokenSellReceiptScreen.kt | 2 +- .../components/info/MarketCapSection.kt | 2 +- .../internal/components/TransactionReceipt.kt | 2 +- ...WithdrawalConfirmationScreenContentTest.kt | 2 +- .../getcode/opencode/model/financial/Fiat.kt | 2 +- .../opencode/model/financial/FiatTests.kt | 2 +- 9 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/money/FormatUtils.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/money/FormatUtils.kt index c71be70a0..245e81b66 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/money/FormatUtils.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/money/FormatUtils.kt @@ -43,3 +43,22 @@ fun Int.withCommas(): String { fun LocalFiat.formatted(formatting: Fiat.FormattingRule = Fiat.FormattingRule.None): String { return nativeAmount.formatted(rule = formatting) } + +/** + * Formats a fiat appreciation/depreciation with the shared sign convention used across the balance + * header ([com.flipcash.app.core.ui.CurrencyAppreciationLabel]) and the per-token cards: a leading + * "+" for a gain or a zero change (always "+$0.00"), and the formatter's own "-" for a loss. + * + * A change that rounds to zero is normalized to a positive zero before formatting — otherwise a + * tiny-negative amount would render as "-$0.00" (the formatter uses the raw value while + * [Fiat.valueNonZero]/[Fiat.toDouble] round to the currency's precision). + */ +fun Fiat.formattedAppreciation(): String { + val isZero = !valueNonZero() + val hasAppreciation = toDouble() >= 0 + return when { + isZero -> copy(quarks = 0).formatted(extraPrefix = "+") + hasAppreciation -> formatted(extraPrefix = "+") + else -> formatted() + } +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt index 86a21e095..00d7a06d1 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.flipcash.app.core.money.formattedAppreciation import com.flipcash.core.R import com.getcode.opencode.model.financial.Fiat import com.getcode.theme.CodeTheme @@ -22,7 +23,6 @@ fun CurrencyAppreciationLabel( appreciation: Fiat, modifier: Modifier = Modifier, ) { - val isZero = !appreciation.valueNonZero() val hasAppreciation = appreciation.toDouble() >= 0 val changeColor = if (hasAppreciation) { CodeTheme.colors.successText @@ -45,13 +45,7 @@ fun CurrencyAppreciationLabel( vertical = 2.dp, horizontal = CodeTheme.dimens.grid.x1 ), - value = appreciation.formatted( - extraPrefix = when { - isZero -> null - hasAppreciation -> "+" - else -> null - }, - ), + value = appreciation.formattedAppreciation(), style = CodeTheme.typography.textSmall, color = changeColor, ) 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 87653f50e..2e170fcf6 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 @@ -196,7 +196,7 @@ private fun BuyReceipt( modifier = Modifier.fillMaxWidth(), label = stringResource(R.string.label_exchangeFee), amount = feeAmount.formatted( - extraPrefix = if (feeAmount.decimalValue < 0.01) "~" else null, + extraPrefix = if (feeAmount.decimalValue < 0.01) "~ " else null, ) ) } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt index 503638d3e..c226471a1 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenSellReceiptScreen.kt @@ -145,7 +145,7 @@ private fun SellReceipt( modifier = Modifier.fillMaxWidth(), label = stringResource(R.string.label_percentFee, feePercentage.roundToInt()), amount = feeAmount.formatted( - extraPrefix = if (feeAmount.decimalValue < 0.01) "~" else null, + extraPrefix = if (feeAmount.decimalValue < 0.01) "~ " else null, ), ) } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt index ed8a22f91..6e731493f 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/MarketCapSection.kt @@ -284,7 +284,7 @@ private fun MarketCapChangeLabel( horizontal = CodeTheme.dimens.grid.x1 ), text = change.formatted( - extraPrefix = if (change.decimalValue >= 0) "+" else null, + extraPrefix = if (change.decimalValue >= 0) "+ " else null, suffix = when (period) { Period.All -> stringResource(R.string.label_marketCapAllTime) Period.Day -> stringResource(R.string.label_marketCapDay) diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt index 5bb70c14e..a9a499100 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt @@ -133,7 +133,7 @@ private fun LineItems( ReceiptLineItem( modifier = Modifier.fillMaxWidth(), label = AnnotatedString(stringResource(R.string.label_lessFee)), - amount = fee.formatted(extraPrefix = "-"), + amount = fee.formatted(extraPrefix = "- "), ) val netAmount = remember(transferAmount) { diff --git a/apps/flipcash/features/withdrawal/src/test/kotlin/com/flipcash/app/withdrawal/internal/confirmation/WithdrawalConfirmationScreenContentTest.kt b/apps/flipcash/features/withdrawal/src/test/kotlin/com/flipcash/app/withdrawal/internal/confirmation/WithdrawalConfirmationScreenContentTest.kt index 38e944f67..66f54c04b 100644 --- a/apps/flipcash/features/withdrawal/src/test/kotlin/com/flipcash/app/withdrawal/internal/confirmation/WithdrawalConfirmationScreenContentTest.kt +++ b/apps/flipcash/features/withdrawal/src/test/kotlin/com/flipcash/app/withdrawal/internal/confirmation/WithdrawalConfirmationScreenContentTest.kt @@ -224,7 +224,7 @@ class WithdrawalConfirmationScreenContentTest { @Test fun `receipt shows formatted USD fee with negative prefix`() { setScreen(testState(amount = 5.0, fee = Fiat(0.50, CurrencyCode.USD))) - // fee.formatted(extraPrefix = "-") → "- $0.50" + // fee.formatted(extraPrefix = "- ") → "- $0.50" composeTestRule.onNodeWithText("- $0.50", substring = true).assertIsDisplayed() } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/Fiat.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/Fiat.kt index 994e14081..d4c58c9cb 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/Fiat.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/Fiat.kt @@ -108,7 +108,7 @@ data class Fiat( .orEmpty() val fullPrefix = if (extraPrefix != null) { - "$extraPrefix $prefix" + "$extraPrefix$prefix" } else { prefix } diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/FiatTests.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/FiatTests.kt index de2c7ba17..ac61d85e3 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/FiatTests.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/model/financial/FiatTests.kt @@ -566,7 +566,7 @@ class FiatTests { fun `formatted with extraPrefix and suffix`() { val fiat = Fiat(fiat = 10.0) val result = fiat.formatted(extraPrefix = "~", suffix = "USD") - assertEquals("~ $10.00 USD", result) + assertEquals("~$10.00 USD", result) } @Test