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)}`;