Commit f9a94695 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: [info] Add token description to TDP (#7504)

* update token description for TDP

* add tooltip to fee

* show buy/sell fees

* remove token description from PDP

* remove unused styles

* update snapshots

* undo fee component testing

* isInfoTDPEnabled

* update explorer fn
parent b995f4d6
......@@ -41,10 +41,10 @@ export const LeftPanel = styled.div`
max-width: 780px;
overflow: hidden;
`
export const RightPanel = styled.div`
export const RightPanel = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: none;
flex-direction: column;
gap: 20px;
gap: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? 40 : 20)}px;
width: ${SWAP_COMPONENT_WIDTH}px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
......
......@@ -34,11 +34,10 @@ describe('TokenDescription', () => {
})
it('renders token information correctly with defaults', () => {
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} />)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('USDC')).toBeVisible()
expect(screen.getByText('USDCoin')).toBeVisible()
expect(screen.getByText('Info')).toBeVisible()
expect(screen.getByText('Website')).toBeVisible()
expect(screen.getByText('Twitter')).toBeVisible()
expect(screen.getByText('Etherscan')).toBeVisible()
......@@ -46,7 +45,7 @@ describe('TokenDescription', () => {
})
it('truncates description and shows more', async () => {
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} />)
expect(asFragment()).toMatchSnapshot()
const truncatedDescription = screen.getByTestId('token-description-truncated')
......@@ -61,19 +60,12 @@ describe('TokenDescription', () => {
expect(screen.getByText('Hide')).toBeVisible()
})
it('copy address button hidden when flagged', async () => {
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} />)
expect(asFragment()).toMatchSnapshot()
expect(screen.queryByText('0xA0b8...eB48')).toBeNull()
})
it('no description or social buttons shown when not available', async () => {
mocked(useTokenProjectQuery).mockReturnValue({ data: undefined } as unknown as QueryResult<
TokenProjectQuery,
Exact<{ chain: Chain; address?: string }>
>)
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} />)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('No token information available')).toBeVisible()
......
......@@ -4,14 +4,14 @@ import Column from 'components/Column'
import { EtherscanLogo } from 'components/Icons/Etherscan'
import { Globe } from 'components/Icons/Globe'
import { TwitterXLogo } from 'components/Icons/TwitterX'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row from 'components/Row'
import { FOTTooltipContent } from 'components/swap/SwapLineItem'
import { NoInfoAvailable, truncateDescription, TruncateDescriptionButton } from 'components/Tokens/TokenDetails/shared'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { useTokenProjectQuery } from 'graphql/data/__generated__/types-and-hooks'
import { chainIdToBackendName } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens'
import { useColor } from 'hooks/useColor'
import useCopyClipboard from 'hooks/useCopyClipboard'
import { useSwapTaxes } from 'hooks/useSwapTaxes'
import { useCallback, useReducer } from 'react'
import { Copy } from 'react-feather'
import styled, { useTheme } from 'styled-components'
......@@ -19,10 +19,12 @@ import { BREAKPOINTS } from 'theme'
import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components'
import { opacify } from 'theme/utils'
import { shortenAddress } from 'utils'
import { useFormatter } from 'utils/formatNumbers'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
const TokenInfoSection = styled(Column)`
gap: 12px;
gap: 16px;
width: 100%;
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.sm}px) {
......@@ -35,10 +37,6 @@ const TokenNameRow = styled(Row)`
width: 100%;
`
const TokenName = styled(ThemedText.BodyPrimary)`
${EllipsisStyle}
`
const TokenButtonRow = styled(TokenNameRow)`
flex-wrap: wrap;
`
......@@ -73,25 +71,30 @@ const TRUNCATE_CHARACTER_COUNT = 75
export function TokenDescription({
tokenAddress,
chainId = ChainId.MAINNET,
showCopy = false,
isNative = false,
characterCount = TRUNCATE_CHARACTER_COUNT,
}: {
tokenAddress: string
chainId?: number
showCopy?: boolean
isNative?: boolean
characterCount?: number
}) {
const currency = useCurrency(tokenAddress, chainId)
const theme = useTheme()
const color = useColor(currency?.wrapped, theme.surface1, theme.darkMode)
const color = useTheme().neutral1
const chainName = chainIdToBackendName(chainId)
const { data: tokenQuery } = useTokenProjectQuery({
variables: {
address: tokenAddress,
chain: chainIdToBackendName(chainId),
address: isNative ? getNativeTokenDBAddress(chainName) : tokenAddress,
chain: chainName,
},
errorPolicy: 'all',
})
const tokenProject = tokenQuery?.token?.project
const description = tokenProject?.description
const explorerUrl = getExplorerLink(chainId, tokenAddress, ExplorerDataType.TOKEN)
const explorerUrl = getExplorerLink(
chainId,
tokenAddress,
isNative ? ExplorerDataType.NATIVE : ExplorerDataType.TOKEN
)
const [, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
......@@ -99,19 +102,25 @@ export function TokenDescription({
}, [tokenAddress, setCopied])
const [isDescriptionTruncated, toggleIsDescriptionTruncated] = useReducer((x) => !x, true)
const truncatedDescription = truncateDescription(description ?? '', TRUNCATE_CHARACTER_COUNT)
const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT
const truncatedDescription = truncateDescription(description ?? '', characterCount)
const shouldTruncate = !!description && description.length > characterCount
const showTruncatedDescription = shouldTruncate && isDescriptionTruncated
const { inputTax: sellFee, outputTax: buyFee } = useSwapTaxes(tokenAddress, tokenAddress)
const { formatPercent } = useFormatter()
const { sellFeeString, buyFeeString } = {
sellFeeString: formatPercent(sellFee),
buyFeeString: formatPercent(buyFee),
}
const hasFee = Boolean(parseFloat(sellFeeString)) || Boolean(parseFloat(buyFee.toFixed(2)))
const sameFee = sellFeeString === buyFeeString
return (
<TokenInfoSection>
<TokenNameRow>
<CurrencyLogo currency={currency} size="20px" />
<TokenName>{currency?.name}</TokenName>
<ThemedText.BodySecondary>{currency?.symbol}</ThemedText.BodySecondary>
</TokenNameRow>
<ThemedText.HeadlineSmall>
<Trans>Info</Trans>
</ThemedText.HeadlineSmall>
<TokenButtonRow>
{showCopy && (
{!isNative && (
<TokenInfoButton tokenColor={color} onClick={copy}>
<Copy width="18px" height="18px" color={color} />
{shortenAddress(tokenAddress)}
......@@ -165,6 +174,37 @@ export function TokenDescription({
</TruncateDescriptionButton>
)}
</TokenDescriptionContainer>
{hasFee && (
<MouseoverTooltip
placement="left"
size={TooltipSize.Small}
text={
<ThemedText.Caption color="neutral2">
<FOTTooltipContent />
</ThemedText.Caption>
}
>
<Column gap="sm">
{sameFee ? (
<ThemedText.BodyPrimary>
{tokenQuery?.token?.symbol}&nbsp;
<Trans>fee:</Trans>&nbsp;{sellFeeString}
</ThemedText.BodyPrimary>
) : (
<>
<ThemedText.BodyPrimary>
{tokenQuery?.token?.symbol}&nbsp;
<Trans>buy fee:</Trans>&nbsp;{buyFeeString}
</ThemedText.BodyPrimary>{' '}
<ThemedText.BodyPrimary>
{tokenQuery?.token?.symbol}&nbsp;
<Trans>sell fee:</Trans>&nbsp;{sellFeeString}
</ThemedText.BodyPrimary>{' '}
</>
)}
</Column>
</MouseoverTooltip>
)}
</TokenInfoSection>
)
}
......@@ -43,6 +43,7 @@ import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import { OnChangeTimePeriod } from './ChartSection'
import InvalidTokenDetails from './InvalidTokenDetails'
import { TokenDescription } from './TokenDescription'
const TokenSymbol = styled.span`
text-transform: uppercase;
......@@ -258,7 +259,7 @@ export default function TokenDetails({
<TokenDetailsSkeleton />
)}
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
<RightPanel isInfoTDPEnabled={isInfoTDPEnabled} onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
<Swap
chainId={pageChainId}
......@@ -270,6 +271,14 @@ export default function TokenDetails({
</div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{!isInfoTDPEnabled && detailedToken && <BalanceSummary token={detailedToken} />}
{isInfoTDPEnabled && (
<TokenDescription
tokenAddress={address}
chainId={pageChainId}
isNative={detailedToken?.isNative}
characterCount={200}
/>
)}
</RightPanel>
{!isInfoTDPEnabled && detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
......
......@@ -63,7 +63,7 @@ const AutoBadge = styled(ThemedText.LabelMicro).attrs({ fontWeight: 535 })`
}
`
function FOTTooltipContent() {
export function FOTTooltipContent() {
return (
<>
<Trans>
......
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
import { TokenDescription } from 'components/Tokens/TokenDetails/TokenDescription'
import { getValidUrlChainName, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { usePoolData } from 'graphql/thegraph/PoolData'
import NotFound from 'pages/NotFound'
import { useReducer } from 'react'
import { useParams } from 'react-router-dom'
import { Text } from 'rebass'
import styled from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { isAddress } from 'utils'
......@@ -76,28 +73,6 @@ const RightColumn = styled(Column)`
}
`
const TokenDetailsWrapper = styled(Column)`
gap: 24px;
padding: 20px;
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.sm}px) {
flex-direction: row;
flex-wrap: wrap;
padding: unset;
}
@media (max-width: ${BREAKPOINTS.sm - 1}px) {
padding: unset;
}
`
const TokenDetailsHeader = styled(Text)`
width: 100%;
font-size: 24px;
font-weight: 485;
line-height: 32px;
`
export default function PoolDetailsPage() {
const { poolAddress, chainName } = useParams<{
poolAddress: string
......@@ -152,15 +127,8 @@ export default function PoolDetailsPage() {
</Row>
))}
</LinkColumn>
) : (
<TokenDetailsWrapper>
<TokenDetailsHeader>
<Trans>Info</Trans>
</TokenDetailsHeader>
{token0 && <TokenDescription tokenAddress={token0.id} chainId={chainId} />}
{token1 && <TokenDescription tokenAddress={token1.id} chainId={chainId} />}
</TokenDetailsWrapper>
))}
) : null)}
{/* TODO(WEB-2985) replace with new Token Links component */}
</RightColumn>
</PageWrapper>
)
......
......@@ -20,6 +20,7 @@ export enum ExplorerDataType {
TOKEN = 'token',
ADDRESS = 'address',
BLOCK = 'block',
NATIVE = 'native',
}
/**
......
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