feat: preserve input currency on TDP when navigating (#6209)
* fix: NPE when connector is undefined * feat: retain input token when switching Token Detail Page * fix: remove logic from string template
This commit is contained in:
parent
09e6d38f25
commit
46724cd8f6
@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { Currency, Field } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import { AboutSection } from 'components/Tokens/TokenDetails/About'
|
||||
@ -23,6 +23,7 @@ import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget from 'components/Widget'
|
||||
import { SwapTokens } from 'components/Widget/inputs'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
@ -33,6 +34,7 @@ import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
||||
import { getTokenAddress } from 'lib/utils/analytics'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@ -88,6 +90,7 @@ function useRelevantToken(
|
||||
|
||||
type TokenDetailsProps = {
|
||||
urlAddress: string | undefined
|
||||
inputTokenAddress?: string
|
||||
chain: Chain
|
||||
tokenQuery: TokenQuery
|
||||
tokenPriceQuery: TokenPriceQuery | undefined
|
||||
@ -95,6 +98,7 @@ type TokenDetailsProps = {
|
||||
}
|
||||
export default function TokenDetails({
|
||||
urlAddress,
|
||||
inputTokenAddress,
|
||||
chain,
|
||||
tokenQuery,
|
||||
tokenPriceQuery,
|
||||
@ -120,7 +124,8 @@ export default function TokenDetails({
|
||||
[tokenQueryData]
|
||||
)
|
||||
|
||||
const { token, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
|
||||
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
|
||||
const { token: inputToken } = useRelevantToken(inputTokenAddress, pageChainId, undefined)
|
||||
|
||||
const tokenWarning = address ? checkWarning(address) : null
|
||||
const isBlockedToken = tokenWarning?.canProceed === false
|
||||
@ -134,18 +139,27 @@ export default function TokenDetails({
|
||||
const bridgedAddress = crossChainMap[update]
|
||||
if (bridgedAddress) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
|
||||
} else if (didFetchFromChain || token?.isNative) {
|
||||
} else if (didFetchFromChain || detailedToken?.isNative) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
}
|
||||
},
|
||||
[address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
|
||||
[address, chain, crossChainMap, didFetchFromChain, navigate, detailedToken?.isNative]
|
||||
)
|
||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||
|
||||
const navigateToWidgetSelectedToken = useCallback(
|
||||
(token: Currency) => {
|
||||
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
(tokens: SwapTokens) => {
|
||||
const newDefaultToken = tokens[Field.OUTPUT] ?? tokens.default
|
||||
const address = newDefaultToken?.isNative ? NATIVE_CHAIN_ID : newDefaultToken?.address
|
||||
startTokenTransition(() =>
|
||||
navigate(
|
||||
getTokenDetailsURL({
|
||||
address,
|
||||
chain,
|
||||
inputAddress: tokens[Field.INPUT] ? getTokenAddress(tokens[Field.INPUT] as Currency) : null,
|
||||
})
|
||||
)
|
||||
)
|
||||
},
|
||||
[chain, navigate]
|
||||
)
|
||||
@ -170,30 +184,30 @@ export default function TokenDetails({
|
||||
)
|
||||
|
||||
// address will never be undefined if token is defined; address is checked here to appease typechecker
|
||||
if (token === undefined || !address) {
|
||||
if (detailedToken === undefined || !address) {
|
||||
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
|
||||
}
|
||||
return (
|
||||
<Trace
|
||||
page={InterfacePageName.TOKEN_DETAILS_PAGE}
|
||||
properties={{ tokenAddress: address, tokenName: token?.name }}
|
||||
properties={{ tokenAddress: address, tokenName: detailedToken?.name }}
|
||||
shouldLogImpression
|
||||
>
|
||||
<TokenDetailsLayout>
|
||||
{token && !isPending ? (
|
||||
{detailedToken && !isPending ? (
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
|
||||
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer data-testid="token-info-container">
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
|
||||
<CurrencyLogo currency={detailedToken} size="32px" hideL2Icon={false} />
|
||||
|
||||
{token.name ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
{detailedToken.name ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{detailedToken.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
<ShareButton currency={token} />
|
||||
<ShareButton currency={detailedToken} />
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
|
||||
@ -211,7 +225,7 @@ export default function TokenDetails({
|
||||
homepageUrl={tokenQueryData?.project?.homepageUrl}
|
||||
twitterName={tokenQueryData?.project?.twitterName}
|
||||
/>
|
||||
{!token.isNative && <AddressSection address={address} />}
|
||||
{!detailedToken.isNative && <AddressSection address={address} />}
|
||||
</LeftPanel>
|
||||
) : (
|
||||
<TokenDetailsSkeleton />
|
||||
@ -221,16 +235,17 @@ export default function TokenDetails({
|
||||
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
|
||||
<Widget
|
||||
defaultTokens={{
|
||||
default: token ?? undefined,
|
||||
[Field.INPUT]: inputToken ?? undefined,
|
||||
default: detailedToken ?? undefined,
|
||||
}}
|
||||
onDefaultTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
</div>
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
||||
{token && <BalanceSummary token={token} />}
|
||||
{detailedToken && <BalanceSummary token={detailedToken} />}
|
||||
</RightPanel>
|
||||
{token && <MobileBalanceSummaryFooter token={token} />}
|
||||
{detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
|
||||
|
||||
<TokenSafetyModal
|
||||
isOpen={openTokenSafetyModal || !!continueSwap}
|
||||
|
@ -31,7 +31,7 @@ import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { DefaultTokens, useSyncWidgetInputs } from './inputs'
|
||||
import { DefaultTokens, SwapTokens, useSyncWidgetInputs } from './inputs'
|
||||
import { useSyncWidgetSettings } from './settings'
|
||||
import { DARK_THEME, LIGHT_THEME } from './theme'
|
||||
import { useSyncWidgetTransactions } from './transactions'
|
||||
@ -47,7 +47,7 @@ function useWidgetTheme() {
|
||||
interface WidgetProps {
|
||||
defaultTokens: DefaultTokens
|
||||
width?: number | string
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
onDefaultTokenChange?: (tokens: SwapTokens) => void
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
const EMPTY_AMOUNT = ''
|
||||
|
||||
type SwapValue = Required<SwapController>['value']
|
||||
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
|
||||
export type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
|
||||
export type DefaultTokens = Partial<SwapTokens>
|
||||
|
||||
function missingDefaultToken(tokens: SwapTokens) {
|
||||
@ -47,7 +47,7 @@ export function useSyncWidgetInputs({
|
||||
onDefaultTokenChange,
|
||||
}: {
|
||||
defaultTokens: DefaultTokens
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
onDefaultTokenChange?: (tokens: SwapTokens) => void
|
||||
}) {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
@ -137,7 +137,10 @@ export function useSyncWidgetInputs({
|
||||
})
|
||||
|
||||
if (missingDefaultToken(update)) {
|
||||
onDefaultTokenChange?.(update[Field.OUTPUT] ?? selectingToken)
|
||||
onDefaultTokenChange?.({
|
||||
...update,
|
||||
default: update[Field.OUTPUT] ?? selectingToken,
|
||||
})
|
||||
return
|
||||
}
|
||||
setTokens(update)
|
||||
|
@ -108,8 +108,19 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
|
||||
|
||||
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
|
||||
|
||||
export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
|
||||
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
|
||||
export function getTokenDetailsURL({
|
||||
address,
|
||||
chain,
|
||||
inputAddress,
|
||||
}: {
|
||||
address?: string | null
|
||||
chain: Chain
|
||||
inputAddress?: string | null
|
||||
}) {
|
||||
const chainName = chain.toLowerCase()
|
||||
const tokenAddress = address ?? NATIVE_CHAIN_ID
|
||||
const inputAddressSuffix = inputAddress ? `?inputCurrency=${inputAddress}` : ''
|
||||
return `/tokens/${chainName}/${tokenAddress}${inputAddressSuffix}`
|
||||
}
|
||||
|
||||
export function unwrapToken<
|
||||
|
@ -3,6 +3,7 @@ import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleto
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
|
||||
import useParsedQueryString from 'hooks/useParsedQueryString'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@ -12,27 +13,36 @@ import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
|
||||
|
||||
export default function TokenDetailsPage() {
|
||||
const { tokenAddress, chainName } = useParams<{ tokenAddress: string; chainName?: string }>()
|
||||
const { tokenAddress, chainName } = useParams<{
|
||||
tokenAddress: string
|
||||
chainName?: string
|
||||
}>()
|
||||
const chain = validateUrlChainParam(chainName)
|
||||
const isNative = tokenAddress === NATIVE_CHAIN_ID
|
||||
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
|
||||
const [address, duration] = useMemo(
|
||||
const [detailedTokenAddress, duration] = useMemo(
|
||||
/* tokenAddress will always be defined in the path for for this page to render, but useParams will always
|
||||
return optional arguments; nullish coalescing operator is present here to appease typechecker */
|
||||
() => [isNative ? getNativeTokenDBAddress(chain) : tokenAddress ?? '', toHistoryDuration(timePeriod)],
|
||||
[chain, isNative, timePeriod, tokenAddress]
|
||||
)
|
||||
|
||||
const parsedQs = useParsedQueryString()
|
||||
|
||||
const parsedInputTokenAddress: string | undefined = useMemo(() => {
|
||||
return typeof parsedQs.inputCurrency === 'string' ? (parsedQs.inputCurrency as string) : undefined
|
||||
}, [parsedQs])
|
||||
|
||||
const { data: tokenQuery } = useTokenQuery({
|
||||
variables: {
|
||||
address,
|
||||
address: detailedTokenAddress,
|
||||
chain,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: tokenPriceQuery } = useTokenPriceQuery({
|
||||
variables: {
|
||||
address,
|
||||
address: detailedTokenAddress,
|
||||
chain,
|
||||
duration,
|
||||
},
|
||||
@ -53,6 +63,7 @@ export default function TokenDetailsPage() {
|
||||
tokenQuery={tokenQuery}
|
||||
tokenPriceQuery={currentPriceQuery}
|
||||
onChangeTimePeriod={setTimePeriod}
|
||||
inputTokenAddress={parsedInputTokenAddress}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user