diff --git a/src/components/AccountDrawer/AuthenticatedHeader.tsx b/src/components/AccountDrawer/AuthenticatedHeader.tsx index 53dd6f0881..db9c25f308 100644 --- a/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -9,7 +9,7 @@ import { Power } from 'components/Icons/Power' import { Settings } from 'components/Icons/Settings' import { AutoRow } from 'components/Row' import { LoadingBubble } from 'components/Tokens/loading' -import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta' +import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' import Tooltip from 'components/Tooltip' import { getConnection } from 'connection' import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' @@ -161,7 +161,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable) const shouldShowBuyFiatButton = useIsNotOriginCountry('GB') - const { formatNumber } = useFormatter() + const { formatNumber, formatPercent } = useFormatter() const shouldDisableNFTRoutes = useDisableNFTRoutes() @@ -284,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account {`${formatNumber({ input: Math.abs(absoluteChange as number), type: NumberType.PortfolioBalance, - })} (${formatDelta(percentChange)})`} + })} (${formatPercent(percentChange)})`} )} diff --git a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx index fab6e69a08..3f09e73329 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx @@ -2,7 +2,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an import { TraceEvent } from 'analytics' import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' import Row from 'components/Row' -import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta' +import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks' import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util' import { useAtomValue } from 'jotai/utils' @@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)` type PortfolioToken = NonNullable function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) { + const { formatPercent } = useFormatter() const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0 const navigate = useNavigate() @@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok - {formatDelta(percentChange)} + {formatPercent(percentChange)} ) diff --git a/src/components/Charts/PriceChart/index.tsx b/src/components/Charts/PriceChart/index.tsx index 62134f7cbb..a8a72fd3c6 100644 --- a/src/components/Charts/PriceChart/index.tsx +++ b/src/components/Charts/PriceChart/index.tsx @@ -20,7 +20,7 @@ import { ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' import { useFormatter } from 'utils/formatNumbers' -import { calculateDelta, DeltaArrow, formatDelta } from '../../Tokens/TokenDetails/Delta' +import { calculateDelta, DeltaArrow } from '../../Tokens/TokenDetails/Delta' const CHART_MARGIN = { top: 100, bottom: 48, crosshair: 72 } @@ -52,9 +52,11 @@ interface ChartDeltaProps { function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) { const delta = calculateDelta(startingPrice.value, endingPrice.value) + const { formatPercent } = useFormatter() + return ( - {formatDelta(delta)} + {formatPercent(delta)} ) diff --git a/src/components/NavBar/SuggestionRow.tsx b/src/components/NavBar/SuggestionRow.tsx index 2a74561006..c8fdc95aa1 100644 --- a/src/components/NavBar/SuggestionRow.tsx +++ b/src/components/NavBar/SuggestionRow.tsx @@ -128,7 +128,7 @@ interface TokenRowProps { export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => { const addRecentlySearchedAsset = useAddRecentlySearchedAsset() const navigate = useNavigate() - const { formatFiatPrice } = useFormatter() + const { formatFiatPrice, formatPercent } = useFormatter() const handleClick = useCallback(() => { const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address @@ -191,7 +191,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, - {Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}% + {formatPercent(Math.abs(token.market?.pricePercentChange?.value ?? 0))} diff --git a/src/components/Tokens/TokenDetails/Delta.tsx b/src/components/Tokens/TokenDetails/Delta.tsx index c2d251bbab..c59b605365 100644 --- a/src/components/Tokens/TokenDetails/Delta.tsx +++ b/src/components/Tokens/TokenDetails/Delta.tsx @@ -24,13 +24,6 @@ interface DeltaArrowProps { size?: number } -export function formatDelta(delta: number | null | undefined) { - if (!isValidDelta(delta)) return '-' - - const formattedDelta = Math.abs(delta).toFixed(2) + '%' - return formattedDelta -} - export function DeltaArrow({ delta, noColor = false, size = 16 }: DeltaArrowProps) { if (!isValidDelta(delta)) return null diff --git a/src/components/Tokens/TokenTable/TokenRow.tsx b/src/components/Tokens/TokenTable/TokenRow.tsx index d060a72100..88706f1dec 100644 --- a/src/components/Tokens/TokenTable/TokenRow.tsx +++ b/src/components/Tokens/TokenTable/TokenRow.tsx @@ -34,7 +34,7 @@ import { TokenSortMethod, useSetSortMethod, } from '../state' -import { DeltaArrow, DeltaText, formatDelta } from '../TokenDetails/Delta' +import { DeltaArrow, DeltaText } from '../TokenDetails/Delta' const Cell = styled.div` display: flex; @@ -441,7 +441,7 @@ interface LoadedRowProps { /* Loaded State: row component with token information */ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef) => { - const { formatFiatPrice, formatNumber } = useFormatter() + const { formatFiatPrice, formatNumber, formatPercent } = useFormatter() const { tokenListIndex, tokenListLength, token, sortRank } = props const filterString = useAtomValue(filterStringAtom) @@ -450,7 +450,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef @@ -214,7 +214,7 @@ function StatItem({ title, value, delta }: { title: ReactNode; value: number; de {!!delta && ( - {formatDelta(delta)} + {formatPercent(delta)} )} diff --git a/src/utils/formatNumbers.test.ts b/src/utils/formatNumbers.test.ts index 1da13d0795..b0cd018f56 100644 --- a/src/utils/formatNumbers.test.ts +++ b/src/utils/formatNumbers.test.ts @@ -460,3 +460,37 @@ describe('formatReviewSwapCurrencyAmount', () => { expect(formatReviewSwapCurrencyAmount(currencyAmount)).toBe('2000000') }) }) + +describe('formatPercent', () => { + beforeEach(() => { + mocked(useLocalCurrencyConversionRate).mockReturnValue({ data: 1.0, isLoading: false }) + mocked(useCurrencyConversionFlagEnabled).mockReturnValue(true) + }) + + it.each([[null], [undefined], [Infinity], [NaN]])('should correctly format %p', (value) => { + const { formatPercent } = renderHook(() => useFormatter()).result.current + + expect(formatPercent(value)).toBe('-') + }) + + it('correctly formats a percent with 2 decimal places', () => { + const { formatPercent } = renderHook(() => useFormatter()).result.current + + expect(formatPercent(0)).toBe('0.00%') + expect(formatPercent(0.1)).toBe('0.10%') + expect(formatPercent(1)).toBe('1.00%') + expect(formatPercent(10)).toBe('10.00%') + expect(formatPercent(100)).toBe('100.00%') + }) + + it('correctly formats a percent with 2 decimal places in french locale', () => { + mocked(useActiveLocale).mockReturnValue('fr-FR') + const { formatPercent } = renderHook(() => useFormatter()).result.current + + expect(formatPercent(0)).toBe('0,00%') + expect(formatPercent(0.1)).toBe('0,10%') + expect(formatPercent(1)).toBe('1,00%') + expect(formatPercent(10)).toBe('10,00%') + expect(formatPercent(100)).toBe('100,00%') + }) +}) diff --git a/src/utils/formatNumbers.ts b/src/utils/formatNumbers.ts index 1b49e394cb..bf98667940 100644 --- a/src/utils/formatNumbers.ts +++ b/src/utils/formatNumbers.ts @@ -478,6 +478,18 @@ function formatSlippage(slippage: Percent | undefined, locale: SupportedLocale = })}%` } +function formatPercent(percent: Nullish, locale: SupportedLocale = DEFAULT_LOCALE) { + if (percent === null || percent === undefined || percent === Infinity || isNaN(percent)) { + return '-' + } + + return `${Number(Math.abs(percent).toFixed(2)).toLocaleString(locale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: false, + })}%` +} + interface FormatPriceOptions { price: Nullish> type: FormatterType @@ -723,12 +735,18 @@ export function useFormatter() { [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith] ) + const formatPercentWithLocales = useCallback( + (percent: Nullish) => formatPercent(percent, formatterLocale), + [formatterLocale] + ) + return useMemo( () => ({ formatCurrencyAmount: formatCurrencyAmountWithLocales, formatFiatPrice: formatFiatPriceWithLocales, formatNumber: formatNumberWithLocales, formatNumberOrString: formatNumberOrStringWithLocales, + formatPercent: formatPercentWithLocales, formatPrice: formatPriceWithLocales, formatPriceImpact: formatPriceImpactWithLocales, formatReviewSwapCurrencyAmount: formatReviewSwapCurrencyAmountWithLocales, @@ -740,6 +758,7 @@ export function useFormatter() { formatFiatPriceWithLocales, formatNumberOrStringWithLocales, formatNumberWithLocales, + formatPercentWithLocales, formatPriceImpactWithLocales, formatPriceWithLocales, formatReviewSwapCurrencyAmountWithLocales,