chore: currency percentages (#7358)

* formatPercent

* hook deps

* price chart

* price chart formatting

* bug bash findings

* Update src/utils/formatNumbers.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* fixing merge errors

* unit tests

* special cases

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
This commit is contained in:
Jack Short 2023-10-03 13:29:36 -07:00 committed by GitHub
parent 82aaf0784a
commit 2694379c97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 71 additions and 22 deletions

@ -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)})`}
</ThemedText.BodySecondary>
</>
)}

@ -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<TokenBalance['token']>
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
</ThemedText.SubHeader>
<Row justify="flex-end">
<DeltaArrow delta={percentChange} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
<ThemedText.BodySecondary>{formatPercent(percentChange)}</ThemedText.BodySecondary>
</Row>
</>
)

@ -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 (
<DeltaContainer>
{formatDelta(delta)}
{formatPercent(delta)}
<DeltaArrow delta={delta} noColor={noColor} />
</DeltaContainer>
)

@ -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,
<DeltaArrow delta={token.market?.pricePercentChange?.value} />
<ThemedText.BodySmall>
<DeltaText delta={token.market?.pricePercentChange?.value}>
{Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}%
{formatPercent(Math.abs(token.market?.pricePercentChange?.value ?? 0))}
</DeltaText>
</ThemedText.BodySmall>
</PriceChangeContainer>

@ -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

@ -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<HTMLDivElement>) => {
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<HT
const chainId = supportedChainIdFromGQLChain(filterNetwork)
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value
const formattedDelta = formatDelta(delta)
const formattedDelta = formatPercent(delta)
const exploreTokenSelectedEventProperties = {
chain_id: chainId,

@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
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 { useCurrency } from 'hooks/Tokens'
import { useColor } from 'hooks/useColor'
@ -199,7 +199,7 @@ const StatItemText = styled(Text)`
`
function StatItem({ title, value, delta }: { title: ReactNode; value: number; delta?: number }) {
const { formatNumber } = useFormatter()
const { formatNumber, formatPercent } = useFormatter()
return (
<StatItemColumn>
@ -214,7 +214,7 @@ function StatItem({ title, value, delta }: { title: ReactNode; value: number; de
{!!delta && (
<Row width="max-content" padding="4px 0px">
<DeltaArrow delta={delta} />
<ThemedText.BodySecondary>{formatDelta(delta)}</ThemedText.BodySecondary>
<ThemedText.BodySecondary>{formatPercent(delta)}</ThemedText.BodySecondary>
</Row>
)}
</StatsTextContainer>

@ -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%')
})
})

@ -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 {
price: Nullish<Price<Currency, Currency>>
type: FormatterType
@ -723,12 +735,18 @@ export function useFormatter() {
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
)
const formatPercentWithLocales = useCallback(
(percent: Nullish<number>) => 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,