Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions components/Rewards/NewRewards/RewardsSummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,14 @@ const SummaryStat = ({ label, value, onPress }: SummaryStatProps) => (
>
<Text
className="text-base text-white/70"
style={{ fontFamily: 'MonaSans_400Regular', lineHeight: 16 }}
style={{ fontFamily: 'MonaSans_400Regular', lineHeight: 20 }}
>
{label}
</Text>
<Text
className="text-[26px] text-white"
style={{ fontFamily: 'MonaSans_600SemiBold', lineHeight: 26 }}
// Native clips glyphs to the line box, and Mona Sans needs ~1.15em to fit.
style={{ fontFamily: 'MonaSans_600SemiBold', lineHeight: 32 }}
>
{formatBalanceUSD(value)}
</Text>
Expand All @@ -141,7 +142,7 @@ const RewardsSummaryCard = ({
onReferralsPress,
}: RewardsSummaryCardProps) => {
return (
<View className="relative mx-4 h-40 overflow-hidden rounded-twice bg-card">
<View className="relative mx-4 min-h-40 overflow-hidden rounded-twice bg-card">
<RewardsGradient />

<View className="h-16 flex-row items-center px-3">
Expand All @@ -154,7 +155,7 @@ const RewardsSummaryCard = ({
</Text>
</View>

<View className="h-24 flex-row">
<View className="min-h-24 flex-row">
<CashbackDetailsSheet
trigger={<SummaryStat label="Cashback" value={cashback} />}
{...cashbackDetails}
Expand Down
37 changes: 20 additions & 17 deletions components/Savings/NewSavings/VaultBalanceCard.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand Down Expand Up @@ -58,10 +55,16 @@ const VaultStat = ({
</>
) : (
<>
<Text className="mt-[7px] text-[26px] font-semibold leading-6 text-white">
{/* 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. */}
<Text
className="mt-[3px] text-[26px] font-semibold leading-8 text-white"
{...singleLineFit}
>
{formatCompactVaultUsd(value)}
</Text>
<Text className="mt-[9px] text-[16px] leading-[17px] text-white/70">
<Text className="mt-[3px] text-[16px] leading-5 text-white/70">
{formatExactVaultUsd(value)}
</Text>
</>
Expand Down Expand Up @@ -132,7 +135,7 @@ const VaultBalanceCard = ({ vaultType, balanceUsd }: VaultBalanceCardProps) => {
const rewardsUsd = Math.max(rewardsInVaultCurrency * priceUsd, 0);

return (
<View className="mx-4 h-[118px] flex-row overflow-hidden rounded-[20px] bg-[#1C1C1C]">
<View className="mx-4 min-h-[118px] flex-row overflow-hidden rounded-[20px] bg-[#1C1C1C]">
<VaultStat
label="Your Balance"
tooltip="The amount currently available in this vault."
Expand Down
47 changes: 47 additions & 0 deletions components/Savings/NewSavings/__tests__/vaultBalanceFormat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
formatCompactVaultUsd,
formatExactVaultUsd,
} from '@/components/Savings/NewSavings/vaultBalanceFormat';

// The real barrel pulls in wagmi; only `formatNumber`'s one-decimal case matters here.
jest.mock('@/lib/utils', () => ({
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');
});
});
37 changes: 37 additions & 0 deletions components/Savings/NewSavings/vaultBalanceFormat.ts
Original file line number Diff line number Diff line change
@@ -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)}`;
Loading