From 9cd285ec40730fe2d8bf45ab4a6218b06288ed76 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 08:02:55 +0000 Subject: [PATCH 1/2] fix(ui): stop iOS clipping the vault and rewards balance figures The vault balance card set its 26px value on a 24px line box and the rewards summary set its 26px value on a 26px one; iOS clips glyphs that overflow the line box, cutting the tops of the digits and the `$`. Both values now get a 32px line, with the vault card's margins trimmed so its layout height is unchanged. The vault card also relied on Intl compact notation, which Hermes on iOS ignores, so balances showed in full ("6,759.16 USD") and wrapped inside the fixed-height card. The compact figure is now formatted by hand, the value stays on one line and shrinks to fit on native, and the card uses a min height so wrapped text on web is no longer cut off. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013FB6mLN2hZfRTDENZCHKhw --- .../Rewards/NewRewards/RewardsSummaryCard.tsx | 5 +- .../Savings/NewSavings/VaultBalanceCard.tsx | 36 +++++++------- .../__tests__/vaultBalanceFormat.test.ts | 47 +++++++++++++++++++ .../Savings/NewSavings/vaultBalanceFormat.ts | 37 +++++++++++++++ 4 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 components/Savings/NewSavings/__tests__/vaultBalanceFormat.test.ts create mode 100644 components/Savings/NewSavings/vaultBalanceFormat.ts diff --git a/components/Rewards/NewRewards/RewardsSummaryCard.tsx b/components/Rewards/NewRewards/RewardsSummaryCard.tsx index 1488efcf1..d3f3ae791 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)} diff --git a/components/Savings/NewSavings/VaultBalanceCard.tsx b/components/Savings/NewSavings/VaultBalanceCard.tsx index 8c406ab3e..29e7d399c 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,15 @@ const VaultStat = ({ ) : ( <> - + {/* A line box shorter than the 26px glyphs clips their tops on iOS, so + the value gets 32px and the margins give the difference back. */} + {formatCompactVaultUsd(value)} - + {formatExactVaultUsd(value)} @@ -132,7 +134,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)}`; From 88472fee1523ad400e3d93cf62412e720d6efe83 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 08:21:41 +0000 Subject: [PATCH 2/2] fix(ui): give the balance cards room on Android as well as iOS Mona Sans has a 1.41em natural line (1.09em ascent, 0.32em descent). When a lineHeight is shorter, iOS keeps the full descent and trims the top of the first line, while Android trims both edges evenly, cutting the bottom of the last line; both then clip the glyphs to the view. Web does neither, which is why only the native apps showed it. The vault card's exact-USD line gets the same ~1.15em headroom as the balance figure (its `$` sat within a pixel of the edge), and the rewards card and its value row use min heights so larger system font sizes, common on Android, grow the card instead of clipping the text. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013FB6mLN2hZfRTDENZCHKhw --- components/Rewards/NewRewards/RewardsSummaryCard.tsx | 6 +++--- components/Savings/NewSavings/VaultBalanceCard.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/Rewards/NewRewards/RewardsSummaryCard.tsx b/components/Rewards/NewRewards/RewardsSummaryCard.tsx index d3f3ae791..9cdc6974b 100644 --- a/components/Rewards/NewRewards/RewardsSummaryCard.tsx +++ b/components/Rewards/NewRewards/RewardsSummaryCard.tsx @@ -120,7 +120,7 @@ const SummaryStat = ({ label, value, onPress }: SummaryStatProps) => ( {formatBalanceUSD(value)} @@ -142,7 +142,7 @@ const RewardsSummaryCard = ({ onReferralsPress, }: RewardsSummaryCardProps) => { return ( - + @@ -155,7 +155,7 @@ const RewardsSummaryCard = ({ - + } {...cashbackDetails} diff --git a/components/Savings/NewSavings/VaultBalanceCard.tsx b/components/Savings/NewSavings/VaultBalanceCard.tsx index 29e7d399c..32720499f 100644 --- a/components/Savings/NewSavings/VaultBalanceCard.tsx +++ b/components/Savings/NewSavings/VaultBalanceCard.tsx @@ -55,15 +55,16 @@ const VaultStat = ({ ) : ( <> - {/* A line box shorter than the 26px glyphs clips their tops on iOS, so - the value gets 32px and the margins give the difference back. */} + {/* 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)}