Commit 2694379c authored by Jack Short's avatar Jack Short Committed by GitHub

chore: currency percentages (#7358)

* formatPercent

* hook deps

* price chart

* price chart formatting

* bug bash findings

* Update src/utils/formatNumbers.ts
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>

* fixing merge errors

* unit tests

* special cases

---------
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
parent 82aaf078
...@@ -9,7 +9,7 @@ import { Power } from 'components/Icons/Power' ...@@ -9,7 +9,7 @@ import { Power } from 'components/Icons/Power'
import { Settings } from 'components/Icons/Settings' import { Settings } from 'components/Icons/Settings'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading' 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 Tooltip from 'components/Tooltip'
import { getConnection } from 'connection' import { getConnection } from 'connection'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
...@@ -161,7 +161,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account ...@@ -161,7 +161,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable) const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const shouldShowBuyFiatButton = useIsNotOriginCountry('GB') const shouldShowBuyFiatButton = useIsNotOriginCountry('GB')
const { formatNumber } = useFormatter() const { formatNumber, formatPercent } = useFormatter()
const shouldDisableNFTRoutes = useDisableNFTRoutes() const shouldDisableNFTRoutes = useDisableNFTRoutes()
...@@ -284,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account ...@@ -284,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
{`${formatNumber({ {`${formatNumber({
input: Math.abs(absoluteChange as number), input: Math.abs(absoluteChange as number),
type: NumberType.PortfolioBalance, type: NumberType.PortfolioBalance,
})} (${formatDelta(percentChange)})`} })} (${formatPercent(percentChange)})`}
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
</> </>
)} )}
......
...@@ -2,7 +2,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an ...@@ -2,7 +2,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import { TraceEvent } from 'analytics' import { TraceEvent } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import Row from 'components/Row' 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 { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util' import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
...@@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)` ...@@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)`
type PortfolioToken = NonNullable<TokenBalance['token']> type PortfolioToken = NonNullable<TokenBalance['token']>
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) { function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
const { formatPercent } = useFormatter()
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0 const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
const navigate = useNavigate() const navigate = useNavigate()
...@@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok ...@@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
</ThemedText.SubHeader> </ThemedText.SubHeader>
<Row justify="flex-end"> <Row justify="flex-end">
<DeltaArrow delta={percentChange} /> <DeltaArrow delta={percentChange} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary> <ThemedText.BodySecondary>{formatPercent(percentChange)}</ThemedText.BodySecondary>
</Row> </Row>
</> </>
) )
......
...@@ -20,7 +20,7 @@ import { ThemedText } from 'theme/components' ...@@ -20,7 +20,7 @@ import { ThemedText } from 'theme/components'
import { textFadeIn } from 'theme/styles' import { textFadeIn } from 'theme/styles'
import { useFormatter } from 'utils/formatNumbers' 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 } const CHART_MARGIN = { top: 100, bottom: 48, crosshair: 72 }
...@@ -52,9 +52,11 @@ interface ChartDeltaProps { ...@@ -52,9 +52,11 @@ interface ChartDeltaProps {
function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) { function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) {
const delta = calculateDelta(startingPrice.value, endingPrice.value) const delta = calculateDelta(startingPrice.value, endingPrice.value)
const { formatPercent } = useFormatter()
return ( return (
<DeltaContainer> <DeltaContainer>
{formatDelta(delta)} {formatPercent(delta)}
<DeltaArrow delta={delta} noColor={noColor} /> <DeltaArrow delta={delta} noColor={noColor} />
</DeltaContainer> </DeltaContainer>
) )
......
...@@ -128,7 +128,7 @@ interface TokenRowProps { ...@@ -128,7 +128,7 @@ interface TokenRowProps {
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => { export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addRecentlySearchedAsset = useAddRecentlySearchedAsset() const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate() const navigate = useNavigate()
const { formatFiatPrice } = useFormatter() const { formatFiatPrice, formatPercent } = useFormatter()
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
...@@ -191,7 +191,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, ...@@ -191,7 +191,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
<DeltaArrow delta={token.market?.pricePercentChange?.value} /> <DeltaArrow delta={token.market?.pricePercentChange?.value} />
<ThemedText.BodySmall> <ThemedText.BodySmall>
<DeltaText delta={token.market?.pricePercentChange?.value}> <DeltaText delta={token.market?.pricePercentChange?.value}>
{Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}% {formatPercent(Math.abs(token.market?.pricePercentChange?.value ?? 0))}
</DeltaText> </DeltaText>
</ThemedText.BodySmall> </ThemedText.BodySmall>
</PriceChangeContainer> </PriceChangeContainer>
......
...@@ -24,13 +24,6 @@ interface DeltaArrowProps { ...@@ -24,13 +24,6 @@ interface DeltaArrowProps {
size?: number 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) { export function DeltaArrow({ delta, noColor = false, size = 16 }: DeltaArrowProps) {
if (!isValidDelta(delta)) return null if (!isValidDelta(delta)) return null
......
...@@ -34,7 +34,7 @@ import { ...@@ -34,7 +34,7 @@ import {
TokenSortMethod, TokenSortMethod,
useSetSortMethod, useSetSortMethod,
} from '../state' } from '../state'
import { DeltaArrow, DeltaText, formatDelta } from '../TokenDetails/Delta' import { DeltaArrow, DeltaText } from '../TokenDetails/Delta'
const Cell = styled.div` const Cell = styled.div`
display: flex; display: flex;
...@@ -441,7 +441,7 @@ interface LoadedRowProps { ...@@ -441,7 +441,7 @@ interface LoadedRowProps {
/* Loaded State: row component with token information */ /* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => { export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { formatFiatPrice, formatNumber } = useFormatter() const { formatFiatPrice, formatNumber, formatPercent } = useFormatter()
const { tokenListIndex, tokenListLength, token, sortRank } = props const { tokenListIndex, tokenListLength, token, sortRank } = props
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
...@@ -450,7 +450,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -450,7 +450,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const chainId = supportedChainIdFromGQLChain(filterNetwork) const chainId = supportedChainIdFromGQLChain(filterNetwork)
const timePeriod = useAtomValue(filterTimeAtom) const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value const delta = token.market?.pricePercentChange?.value
const formattedDelta = formatDelta(delta) const formattedDelta = formatPercent(delta)
const exploreTokenSelectedEventProperties = { const exploreTokenSelectedEventProperties = {
chain_id: chainId, chain_id: chainId,
......
...@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro' ...@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import Column from 'components/Column' import Column from 'components/Column'
import CurrencyLogo from 'components/Logo/CurrencyLogo' import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row from 'components/Row' import Row from 'components/Row'
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta' import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { PoolData } from 'graphql/thegraph/PoolData' import { PoolData } from 'graphql/thegraph/PoolData'
import { useCurrency } from 'hooks/Tokens' import { useCurrency } from 'hooks/Tokens'
import { useColor } from 'hooks/useColor' import { useColor } from 'hooks/useColor'
...@@ -199,7 +199,7 @@ const StatItemText = styled(Text)` ...@@ -199,7 +199,7 @@ const StatItemText = styled(Text)`
` `
function StatItem({ title, value, delta }: { title: ReactNode; value: number; delta?: number }) { function StatItem({ title, value, delta }: { title: ReactNode; value: number; delta?: number }) {
const { formatNumber } = useFormatter() const { formatNumber, formatPercent } = useFormatter()
return ( return (
<StatItemColumn> <StatItemColumn>
...@@ -214,7 +214,7 @@ function StatItem({ title, value, delta }: { title: ReactNode; value: number; de ...@@ -214,7 +214,7 @@ function StatItem({ title, value, delta }: { title: ReactNode; value: number; de
{!!delta && ( {!!delta && (
<Row width="max-content" padding="4px 0px"> <Row width="max-content" padding="4px 0px">
<DeltaArrow delta={delta} /> <DeltaArrow delta={delta} />
<ThemedText.BodySecondary>{formatDelta(delta)}</ThemedText.BodySecondary> <ThemedText.BodySecondary>{formatPercent(delta)}</ThemedText.BodySecondary>
</Row> </Row>
)} )}
</StatsTextContainer> </StatsTextContainer>
......
...@@ -460,3 +460,37 @@ describe('formatReviewSwapCurrencyAmount', () => { ...@@ -460,3 +460,37 @@ describe('formatReviewSwapCurrencyAmount', () => {
expect(formatReviewSwapCurrencyAmount(currencyAmount)).toBe('2000000') 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%')
})
})
...@@ -478,6 +478,18 @@ function formatSlippage(slippage: Percent | undefined, locale: SupportedLocale = ...@@ -478,6 +478,18 @@ function formatSlippage(slippage: Percent | undefined, locale: SupportedLocale =
})}%` })}%`
} }
function formatPercent(percent: Nullish<number>, 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 { interface FormatPriceOptions {
price: Nullish<Price<Currency, Currency>> price: Nullish<Price<Currency, Currency>>
type: FormatterType type: FormatterType
...@@ -723,12 +735,18 @@ export function useFormatter() { ...@@ -723,12 +735,18 @@ export function useFormatter() {
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith] [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
) )
const formatPercentWithLocales = useCallback(
(percent: Nullish<number>) => formatPercent(percent, formatterLocale),
[formatterLocale]
)
return useMemo( return useMemo(
() => ({ () => ({
formatCurrencyAmount: formatCurrencyAmountWithLocales, formatCurrencyAmount: formatCurrencyAmountWithLocales,
formatFiatPrice: formatFiatPriceWithLocales, formatFiatPrice: formatFiatPriceWithLocales,
formatNumber: formatNumberWithLocales, formatNumber: formatNumberWithLocales,
formatNumberOrString: formatNumberOrStringWithLocales, formatNumberOrString: formatNumberOrStringWithLocales,
formatPercent: formatPercentWithLocales,
formatPrice: formatPriceWithLocales, formatPrice: formatPriceWithLocales,
formatPriceImpact: formatPriceImpactWithLocales, formatPriceImpact: formatPriceImpactWithLocales,
formatReviewSwapCurrencyAmount: formatReviewSwapCurrencyAmountWithLocales, formatReviewSwapCurrencyAmount: formatReviewSwapCurrencyAmountWithLocales,
...@@ -740,6 +758,7 @@ export function useFormatter() { ...@@ -740,6 +758,7 @@ export function useFormatter() {
formatFiatPriceWithLocales, formatFiatPriceWithLocales,
formatNumberOrStringWithLocales, formatNumberOrStringWithLocales,
formatNumberWithLocales, formatNumberWithLocales,
formatPercentWithLocales,
formatPriceImpactWithLocales, formatPriceImpactWithLocales,
formatPriceWithLocales, formatPriceWithLocales,
formatReviewSwapCurrencyAmountWithLocales, formatReviewSwapCurrencyAmountWithLocales,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment