diff --git a/components/Rewards/NewRewards/RewardsSummaryCard.tsx b/components/Rewards/NewRewards/RewardsSummaryCard.tsx index 1488efcf1..9cdc6974b 100644 --- a/components/Rewards/NewRewards/RewardsSummaryCard.tsx +++ b/components/Rewards/NewRewards/RewardsSummaryCard.tsx @@ -114,13 +114,14 @@ const SummaryStat = ({ label, value, onPress }: SummaryStatProps) => ( > {label} {formatBalanceUSD(value)} @@ -141,7 +142,7 @@ const RewardsSummaryCard = ({ onReferralsPress, }: RewardsSummaryCardProps) => { return ( - + @@ -154,7 +155,7 @@ const RewardsSummaryCard = ({ - + } {...cashbackDetails} diff --git a/components/Savings/NewSavings/VaultBalanceCard.tsx b/components/Savings/NewSavings/VaultBalanceCard.tsx index 8c406ab3e..32720499f 100644 --- a/components/Savings/NewSavings/VaultBalanceCard.tsx +++ b/components/Savings/NewSavings/VaultBalanceCard.tsx @@ -1,4 +1,4 @@ -import { View } from 'react-native'; +import { Platform, View } from 'react-native'; import { Address } from 'viem'; import { fuse, mainnet } from 'viem/chains'; @@ -16,21 +16,18 @@ import { useVaultBalance } from '@/hooks/useVault'; import { useVaultExchangeRate } from '@/hooks/useVaultExchangeRate'; import { ADDRESSES } from '@/lib/config'; import { SavingMode, VaultType } from '@/lib/types'; -import { formatNumber } from '@/lib/utils'; -export const formatCompactVaultUsd = (value: number) => { - const safeValue = Math.max(Number(value) || 0, 0); - const compact = new Intl.NumberFormat('en-US', { - notation: 'compact', - maximumFractionDigits: 2, - }) - .format(safeValue) - .toUpperCase(); - return `${compact} USD`; -}; +import { formatCompactVaultUsd, formatExactVaultUsd } from './vaultBalanceFormat'; -export const formatExactVaultUsd = (value: number) => - `$${formatNumber(Math.max(Number(value) || 0, 0), 1, 1)}`; +/** + * Native keeps the value on one line and shrinks it to fit the half-width + * column; web has no `adjustsFontSizeToFit`, so there it may wrap and the card + * grows instead. + */ +const singleLineFit = + Platform.OS === 'web' + ? {} + : ({ numberOfLines: 1, adjustsFontSizeToFit: true, minimumFontScale: 0.6 } as const); const VaultStat = ({ label, @@ -58,10 +55,16 @@ const VaultStat = ({ ) : ( <> - + {/* Native clips glyphs to the line box (iOS cuts the top, Android the + bottom) and Mona Sans needs ~1.15em to fit, so both lines get that + and the margins give the difference back. */} + {formatCompactVaultUsd(value)} - + {formatExactVaultUsd(value)} @@ -132,7 +135,7 @@ const VaultBalanceCard = ({ vaultType, balanceUsd }: VaultBalanceCardProps) => { const rewardsUsd = Math.max(rewardsInVaultCurrency * priceUsd, 0); return ( - + ({ + formatNumber: (value: number, max: number, min: number) => + new Intl.NumberFormat('en-us', { + maximumFractionDigits: max, + minimumFractionDigits: value >= 1 ? min : 0, + }).format(value), +})); + +describe('formatCompactVaultUsd', () => { + it('keeps values under a thousand as they are, to two decimals', () => { + expect(formatCompactVaultUsd(0)).toBe('0 USD'); + expect(formatCompactVaultUsd(1.764)).toBe('1.76 USD'); + expect(formatCompactVaultUsd(363.86)).toBe('363.86 USD'); + expect(formatCompactVaultUsd(12.5)).toBe('12.5 USD'); + }); + + it('compacts thousands and up without relying on Intl compact notation', () => { + expect(formatCompactVaultUsd(6759.16)).toBe('6.76K USD'); + expect(formatCompactVaultUsd(1000)).toBe('1K USD'); + expect(formatCompactVaultUsd(2_500_000)).toBe('2.5M USD'); + expect(formatCompactVaultUsd(3_210_000_000)).toBe('3.21B USD'); + expect(formatCompactVaultUsd(4e12)).toBe('4T USD'); + }); + + it('promotes a value that rounds up into the next unit', () => { + expect(formatCompactVaultUsd(999.999)).toBe('1K USD'); + expect(formatCompactVaultUsd(999_999)).toBe('1M USD'); + }); + + it('treats negative and non-numeric input as zero', () => { + expect(formatCompactVaultUsd(-5)).toBe('0 USD'); + expect(formatCompactVaultUsd(Number.NaN)).toBe('0 USD'); + }); +}); + +describe('formatExactVaultUsd', () => { + it('shows one decimal', () => { + expect(formatExactVaultUsd(363.86)).toBe('$363.9'); + expect(formatExactVaultUsd(-1)).toBe('$0'); + }); +}); diff --git a/components/Savings/NewSavings/vaultBalanceFormat.ts b/components/Savings/NewSavings/vaultBalanceFormat.ts new file mode 100644 index 000000000..905869dfb --- /dev/null +++ b/components/Savings/NewSavings/vaultBalanceFormat.ts @@ -0,0 +1,37 @@ +import { formatNumber } from '@/lib/utils'; + +/** Ascending, so a value that rounds up into the next unit is promoted. */ +const COMPACT_UNITS = [ + { value: 1, suffix: '' }, + { value: 1e3, suffix: 'K' }, + { value: 1e6, suffix: 'M' }, + { value: 1e9, suffix: 'B' }, + { value: 1e12, suffix: 'T' }, +] as const; + +const roundToHundredths = (value: number) => Math.round(value * 100) / 100; + +/** + * Compact USD figure for the vault balance card, e.g. `6.76K USD`. + * + * Built by hand rather than with `Intl.NumberFormat({ notation: 'compact' })`: + * Hermes on iOS ignores `notation`, so the card showed the full `6,759.16 USD` + * there, which wrapped and was clipped by the fixed-height card. + */ +export const formatCompactVaultUsd = (value: number) => { + const safeValue = Math.max(Number(value) || 0, 0); + + let unit: (typeof COMPACT_UNITS)[number] = COMPACT_UNITS[0]; + let scaled = roundToHundredths(safeValue); + for (const next of COMPACT_UNITS.slice(1)) { + // Compare the rounded figure so 999,999 reads 1M rather than 1000K. + if (scaled < 1000) break; + unit = next; + scaled = roundToHundredths(safeValue / next.value); + } + + return `${scaled}${unit.suffix} USD`; +}; + +export const formatExactVaultUsd = (value: number) => + `$${formatNumber(Math.max(Number(value) || 0, 0), 1, 1)}`;