Commit 83bc6db7 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: clean up widget token lock (#5017)

* feat: add decimals, wrapper to token query

* fix: construct token in details page

* fix: clean widget default behavior

* fix: actual defaulting again
parent 058aa52f
......@@ -20,7 +20,7 @@ import {
getTokenAddress,
} from 'analytics/utils'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useState } from 'react'
import { useIsDarkMode } from 'state/user/hooks'
import { DARK_THEME, LIGHT_THEME } from 'theme/widget'
import { computeRealizedPriceImpact } from 'utils/prices'
......@@ -39,30 +39,19 @@ function useWidgetTheme() {
}
export interface WidgetProps {
defaultToken?: Currency
onTokensChange?: (input: Currency | undefined, output: Currency | undefined) => void
token?: Currency
onTokenChange?: (token: Currency) => void
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ defaultToken, onTokensChange, onReviewSwapClick }: WidgetProps) {
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
const { connector, provider } = useWeb3React()
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs(defaultToken)
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
const { settings } = useSyncWidgetSettings()
const { transactions } = useSyncWidgetTransactions()
const lastValue = useRef([inputs.value.INPUT, inputs.value.OUTPUT])
useEffect(() => {
const [input, output] = [inputs.value.INPUT, inputs.value.OUTPUT]
const [lastInput, lastOutput] = lastValue.current
// Avoid calling onTokensChange if only the handler has changed.
if (input === lastInput && output === lastOutput) return
if (input && lastInput && input.equals(lastInput) && output && lastOutput && lastOutput.equals(lastOutput)) return
lastValue.current = [input, output]
onTokensChange?.(inputs.value.INPUT, inputs.value.OUTPUT)
}, [inputs.value.INPUT, inputs.value.OUTPUT, onTokensChange])
const onSwitchChain = useCallback(
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
......@@ -143,7 +132,7 @@ export default function Widget({ defaultToken, onTokensChange, onReviewSwapClick
[initialQuoteDate, trace]
)
if (!inputs.value.INPUT && !inputs.value.OUTPUT) {
if (!(inputs.value.INPUT || inputs.value.OUTPUT)) {
return <WidgetSkeleton />
}
......
......@@ -7,15 +7,36 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
const EMPTY_AMOUNT = ''
type SwapValue = Required<SwapController>['value']
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT>
/**
* Integrates the Widget's inputs.
* Treats the Widget as a controlled component, using the app's own token selector for selection.
* Enforces that token is a part of the returned value.
*/
export function useSyncWidgetInputs(defaultToken?: Currency) {
export function useSyncWidgetInputs({
token: defaultToken,
onTokenChange,
}: {
token?: Currency
onTokenChange?: (token: Currency) => void
}) {
const trace = useTrace({ section: SectionName.WIDGET })
const [type, setType] = useState(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState(EMPTY_AMOUNT)
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: defaultToken })
const shouldDefault = useCallback(
(tokens: SwapTokens) => defaultToken && !Object.values(tokens).some((token) => token?.equals(defaultToken)),
[defaultToken]
)
useEffect(
() => setTokens((tokens) => (shouldDefault(tokens) ? { [Field.OUTPUT]: defaultToken } : tokens)),
[defaultToken, shouldDefault]
)
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {
if (origin === 'max') {
......@@ -27,20 +48,6 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
[trace]
)
const [tokens, setTokens] = useState<{ [Field.INPUT]?: Currency; [Field.OUTPUT]?: Currency }>({
[Field.OUTPUT]: defaultToken,
})
useEffect(() => {
// Avoid overwriting tokens if none are specified, so that a loading token does not cause layout flashing.
if (!defaultToken) return
setTokens((tokens) =>
// Avoid overwriting tokens if the default is already included, so that the widget does not spuriously reset.
Object.values(tokens).some((token) => token?.equals(defaultToken)) ? tokens : { [Field.OUTPUT]: defaultToken }
)
setAmount(EMPTY_AMOUNT)
}, [defaultToken])
const onSwitchTokens = useCallback(() => {
sendAnalyticsEvent(EventName.SWAP_TOKENS_REVERSED, { ...trace })
setType((type) => invertTradeType(type))
......@@ -51,47 +58,65 @@ export function useSyncWidgetInputs(defaultToken?: Currency) {
}, [trace])
const [selectingField, setSelectingField] = useState<Field>()
const otherField = useMemo(() => (selectingField === Field.INPUT ? Field.OUTPUT : Field.INPUT), [selectingField])
const [selectingToken, otherToken] = useMemo(() => {
if (selectingField === undefined) return [undefined, undefined]
return [tokens[selectingField], tokens[otherField]]
}, [otherField, selectingField, tokens])
const onTokenSelectorClick = useCallback((field: Field) => {
setSelectingField(field)
return false
}, [])
const onTokenSelect = useCallback(
(token: Currency) => {
if (selectingField === undefined) return
setType(TradeType.EXACT_INPUT)
setTokens(() => {
return {
[otherField]: otherToken?.equals(token) ? selectingToken : otherToken,
[selectingField]: token,
}
})
setType(toTradeType(selectingField))
const otherField = invertField(selectingField)
let otherToken = tokens[otherField]
otherToken = otherToken?.equals(token) ? tokens[selectingField] : otherToken
const update = {
[selectingField]: token,
[otherField]: otherToken,
}
if (shouldDefault(update)) {
onTokenChange?.(update[Field.OUTPUT] || update[Field.INPUT] || token)
}
setTokens(update)
},
[otherField, otherToken, selectingField, selectingToken]
[onTokenChange, selectingField, shouldDefault, tokens]
)
const tokenSelector = (
<CurrencySearchModal
isOpen={selectingField !== undefined}
onDismiss={() => setSelectingField(undefined)}
selectedCurrency={selectingToken}
otherSelectedCurrency={otherToken}
selectedCurrency={selectingField && tokens[selectingField]}
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
onCurrencySelect={onTokenSelect}
/>
)
const value: SwapController['value'] = useMemo(() => ({ type, amount, ...tokens }), [amount, tokens, type])
const value: SwapValue = useMemo(
() => ({
type,
amount,
...tokens,
}),
[amount, tokens, type]
)
const valueHandlers: SwapEventHandlers = useMemo(
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
[onAmountChange, onSwitchTokens, onTokenSelectorClick]
)
return { inputs: { value, ...valueHandlers }, tokenSelector }
}
// TODO(zzmp): Move to @uniswap/widgets.
function invertField(field: Field) {
switch (field) {
case Field.INPUT:
return Field.OUTPUT
case Field.OUTPUT:
return Field.INPUT
}
}
// TODO(zzmp): Move to @uniswap/widgets.
function toTradeType(modifiedField: Field) {
switch (modifiedField) {
......
......@@ -67,13 +67,11 @@ export default function TokenDetails() {
)
useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback(
(input: Currency | undefined, output: Currency | undefined) => {
const update = output || input
if (!token || !update || input?.equals(token) || output?.equals(token)) return
const address = update.isNative ? NATIVE_CHAIN_ID : update.address
(token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTransition(() => navigate(`/tokens/${chainName}/${address}`))
},
[chainName, navigate, token]
[chainName, navigate]
)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
......@@ -132,8 +130,8 @@ export default function TokenDetails() {
<RightPanel>
<Widget
defaultToken={token ?? nativeCurrency}
onTokensChange={navigateToWidgetSelectedToken}
token={token ?? nativeCurrency}
onTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenAddress ?? ''} warning={tokenWarning} />}
......
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