Commit 26d2ab53 authored by cartcrom's avatar cartcrom Committed by GitHub

feat: disable exact_output FOT swaps (#7237)

* feat: disable exact_output FOT swaps

* fix: pr comments

* test: update snapshots
parent e50ecc83
...@@ -8,9 +8,11 @@ import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWr ...@@ -8,9 +8,11 @@ import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWr
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled' import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import CurrencyLogo from 'components/Logo/CurrencyLogo' import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Tooltip from 'components/Tooltip'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import ms from 'ms'
import { darken } from 'polished' import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react' import { forwardRef, ReactNode, useCallback, useEffect, useState } from 'react'
import { Lock } from 'react-feather' import { Lock } from 'react-feather'
import styled, { useTheme } from 'styled-components' import styled, { useTheme } from 'styled-components'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
...@@ -58,6 +60,7 @@ const CurrencySelect = styled(ButtonGray)<{ ...@@ -58,6 +60,7 @@ const CurrencySelect = styled(ButtonGray)<{
selected: boolean selected: boolean
hideInput?: boolean hideInput?: boolean
disabled?: boolean disabled?: boolean
animateShake?: boolean
}>` }>`
align-items: center; align-items: center;
background-color: ${({ selected, theme }) => (selected ? theme.surface1 : theme.accent1)}; background-color: ${({ selected, theme }) => (selected ? theme.surface1 : theme.accent1)};
...@@ -105,6 +108,34 @@ const CurrencySelect = styled(ButtonGray)<{ ...@@ -105,6 +108,34 @@ const CurrencySelect = styled(ButtonGray)<{
} }
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')}; visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
animation-timing-function: ease-in-out;
}
20% {
transform: translateX(10px);
animation-timing-function: ease-in-out;
}
40% {
transform: translateX(-10px);
animation-timing-function: ease-in-out;
}
60% {
transform: translateX(10px);
animation-timing-function: ease-in-out;
}
80% {
transform: translateX(-10px);
animation-timing-function: ease-in-out;
}
100% {
transform: translateX(0);
animation-timing-function: ease-in-out;
}
}
animation: ${({ animateShake }) => (animateShake ? 'horizontal-shaking 300ms' : 'none')};
` `
const InputRow = styled.div` const InputRow = styled.div`
...@@ -206,9 +237,16 @@ interface SwapCurrencyInputPanelProps { ...@@ -206,9 +237,16 @@ interface SwapCurrencyInputPanelProps {
locked?: boolean locked?: boolean
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean
numericalInputSettings?: {
disabled?: boolean
onDisabledClick?: () => void
disabledTooltipBody?: ReactNode
}
} }
export default function SwapCurrencyInputPanel({ const SwapCurrencyInputPanel = forwardRef<HTMLInputElement, SwapCurrencyInputPanelProps>(
(
{
value, value,
onUserInput, onUserInput,
onMax, onMax,
...@@ -229,9 +267,12 @@ export default function SwapCurrencyInputPanel({ ...@@ -229,9 +267,12 @@ export default function SwapCurrencyInputPanel({
locked = false, locked = false,
loading = false, loading = false,
disabled = false, disabled = false,
numericalInputSettings,
label, label,
...rest ...rest
}: SwapCurrencyInputPanelProps) { },
ref
) => {
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const { account, chainId } = useWeb3React() const { account, chainId } = useWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined) const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
...@@ -241,8 +282,20 @@ export default function SwapCurrencyInputPanel({ ...@@ -241,8 +282,20 @@ export default function SwapCurrencyInputPanel({
setModalOpen(false) setModalOpen(false)
}, [setModalOpen]) }, [setModalOpen])
const [tooltipVisible, setTooltipVisible] = useState(false)
const handleDisabledNumericalInputClick = useCallback(() => {
if (numericalInputSettings?.disabled && !tooltipVisible) {
setTooltipVisible(true)
setTimeout(() => setTooltipVisible(false), ms('4s')) // reset shake animation state after 4s
numericalInputSettings.onDisabledClick?.()
}
}, [tooltipVisible, numericalInputSettings])
const chainAllowed = isSupportedChain(chainId) const chainAllowed = isSupportedChain(chainId)
// reset tooltip state when currency changes
useEffect(() => setTooltipVisible(false), [currency])
return ( return (
<InputPanel id={id} hideInput={hideInput} {...rest}> <InputPanel id={id} hideInput={hideInput} {...rest}>
{locked && ( {locked && (
...@@ -255,19 +308,30 @@ export default function SwapCurrencyInputPanel({ ...@@ -255,19 +308,30 @@ export default function SwapCurrencyInputPanel({
</AutoColumn> </AutoColumn>
</FixedContainer> </FixedContainer>
)} )}
<Container hideInput={hideInput}> <Container hideInput={hideInput}>
<ThemedText.SubHeaderSmall style={{ userSelect: 'none' }}>{label}</ThemedText.SubHeaderSmall> <ThemedText.SubHeaderSmall style={{ userSelect: 'none' }}>{label}</ThemedText.SubHeaderSmall>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}> <InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}>
{!hideInput && ( {!hideInput && (
<div style={{ display: 'flex', flexGrow: '1' }} onClick={handleDisabledNumericalInputClick}>
<StyledNumericalInput <StyledNumericalInput
className="token-amount-input" className="token-amount-input"
value={value} value={value}
onUserInput={onUserInput} onUserInput={onUserInput}
disabled={!chainAllowed || disabled} disabled={!chainAllowed || disabled || numericalInputSettings?.disabled}
$loading={loading} $loading={loading}
id={id}
ref={ref}
/> />
</div>
)} )}
<PrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen}> <PrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen}>
<Tooltip
show={tooltipVisible && !modalOpen}
placement="bottom"
offsetY={14}
text={numericalInputSettings?.disabledTooltipBody}
>
<CurrencySelect <CurrencySelect
disabled={!chainAllowed || disabled} disabled={!chainAllowed || disabled}
visible={currency !== undefined} visible={currency !== undefined}
...@@ -279,6 +343,7 @@ export default function SwapCurrencyInputPanel({ ...@@ -279,6 +343,7 @@ export default function SwapCurrencyInputPanel({
setModalOpen(true) setModalOpen(true)
} }
}} }}
animateShake={tooltipVisible}
> >
<Aligner> <Aligner>
<RowFixed> <RowFixed>
...@@ -294,7 +359,10 @@ export default function SwapCurrencyInputPanel({ ...@@ -294,7 +359,10 @@ export default function SwapCurrencyInputPanel({
{pair?.token0.symbol}:{pair?.token1.symbol} {pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName> </StyledTokenName>
) : ( ) : (
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}> <StyledTokenName
className="token-symbol-container"
active={Boolean(currency && currency.symbol)}
>
{(currency && currency.symbol && currency.symbol.length > 20 {(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) + ? currency.symbol.slice(0, 4) +
'...' + '...' +
...@@ -306,6 +374,7 @@ export default function SwapCurrencyInputPanel({ ...@@ -306,6 +374,7 @@ export default function SwapCurrencyInputPanel({
{onCurrencySelect && <StyledDropDown selected={!!currency} />} {onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner> </Aligner>
</CurrencySelect> </CurrencySelect>
</Tooltip>
</PrefetchBalancesWrapper> </PrefetchBalancesWrapper>
</InputRow> </InputRow>
{Boolean(!hideInput && !hideBalance) && ( {Boolean(!hideInput && !hideBalance) && (
...@@ -364,4 +433,8 @@ export default function SwapCurrencyInputPanel({ ...@@ -364,4 +433,8 @@ export default function SwapCurrencyInputPanel({
)} )}
</InputPanel> </InputPanel>
) )
} }
)
SwapCurrencyInputPanel.displayName = 'SwapCurrencyInputPanel'
export default SwapCurrencyInputPanel
import React from 'react' import React, { forwardRef } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { escapeRegExp } from 'utils' import { escapeRegExp } from 'utils'
const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>` const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string; disabled?: boolean }>`
color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
width: 0; width: 0;
position: relative; position: relative;
font-weight: 485; font-weight: 485;
...@@ -40,20 +41,17 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s ...@@ -40,20 +41,17 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
export const Input = React.memo(function InnerInput({ interface InputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'> {
value,
onUserInput,
placeholder,
prependSymbol,
...rest
}: {
value: string | number value: string | number
onUserInput: (input: string) => void onUserInput: (input: string) => void
error?: boolean error?: boolean
fontSize?: string fontSize?: string
align?: 'right' | 'left' align?: 'right' | 'left'
prependSymbol?: string prependSymbol?: string
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) { }
const Input = forwardRef<HTMLInputElement, InputProps>(
({ value, onUserInput, placeholder, prependSymbol, ...rest }: InputProps, ref) => {
const enforcer = (nextUserInput: string) => { const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput) onUserInput(nextUserInput)
...@@ -63,6 +61,7 @@ export const Input = React.memo(function InnerInput({ ...@@ -63,6 +61,7 @@ export const Input = React.memo(function InnerInput({
return ( return (
<StyledInput <StyledInput
{...rest} {...rest}
ref={ref}
value={prependSymbol && value ? prependSymbol + value : value} value={prependSymbol && value ? prependSymbol + value : value}
onChange={(event) => { onChange={(event) => {
if (prependSymbol) { if (prependSymbol) {
...@@ -92,8 +91,11 @@ export const Input = React.memo(function InnerInput({ ...@@ -92,8 +91,11 @@ export const Input = React.memo(function InnerInput({
spellCheck="false" spellCheck="false"
/> />
) )
}) }
)
export default Input Input.displayName = 'Input'
const MemoizedInput = React.memo(Input)
export { MemoizedInput as Input }
// const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group // const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import CurrencyInputPanel from 'components/CurrencyInputPanel' import CurrencyInputPanel from 'components/CurrencyInputPanel'
import Input from 'components/NumericalInput' import { Input } from 'components/NumericalInput'
import styled from 'styled-components' import styled from 'styled-components'
export const Wrapper = styled.div` export const Wrapper = styled.div`
......
...@@ -765,6 +765,7 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -765,6 +765,7 @@ exports[`disable nft on landing page does not render nft information and card 1`
.c27 { .c27 {
color: #222222; color: #222222;
pointer-events: auto;
width: 0; width: 0;
position: relative; position: relative;
font-weight: 485; font-weight: 485;
...@@ -864,6 +865,8 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -864,6 +865,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
margin-left: 12px; margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04); box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible; visibility: visible;
-webkit-animation: none;
animation: none;
} }
.c30:hover, .c30:hover,
...@@ -919,6 +922,8 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -919,6 +922,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
margin-left: 12px; margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04); box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible; visibility: visible;
-webkit-animation: none;
animation: none;
} }
.c44:hover, .c44:hover,
...@@ -1825,11 +1830,15 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1825,11 +1830,15 @@ exports[`disable nft on landing page does not render nft information and card 1`
</div> </div>
<div <div
class="c26" class="c26"
>
<div
style="display: flex; flex-grow: 1;"
> >
<input <input
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
class="c27 c28 token-amount-input" class="c27 c28 token-amount-input"
id="swap-currency-input"
inputmode="decimal" inputmode="decimal"
maxlength="79" maxlength="79"
minlength="1" minlength="1"
...@@ -1839,7 +1848,11 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1839,7 +1848,11 @@ exports[`disable nft on landing page does not render nft information and card 1`
type="text" type="text"
value="" value=""
/> />
</div>
<div> <div>
<div
class="c13"
>
<button <button
class="c14 c15 c29 c30 open-currency-select-button" class="c14 c15 c29 c30 open-currency-select-button"
> >
...@@ -1878,6 +1891,7 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1878,6 +1891,7 @@ exports[`disable nft on landing page does not render nft information and card 1`
</button> </button>
</div> </div>
</div> </div>
</div>
<div <div
class="c37 c38" class="c37 c38"
> >
...@@ -1947,11 +1961,15 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1947,11 +1961,15 @@ exports[`disable nft on landing page does not render nft information and card 1`
</div> </div>
<div <div
class="c26" class="c26"
>
<div
style="display: flex; flex-grow: 1;"
> >
<input <input
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
class="c27 c28 token-amount-input" class="c27 c28 token-amount-input"
id="swap-currency-output"
inputmode="decimal" inputmode="decimal"
maxlength="79" maxlength="79"
minlength="1" minlength="1"
...@@ -1961,7 +1979,11 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1961,7 +1979,11 @@ exports[`disable nft on landing page does not render nft information and card 1`
type="text" type="text"
value="" value=""
/> />
</div>
<div> <div>
<div
class="c13"
>
<button <button
class="c14 c15 c29 c44 open-currency-select-button" class="c14 c15 c29 c44 open-currency-select-button"
> >
...@@ -1986,6 +2008,7 @@ exports[`disable nft on landing page does not render nft information and card 1` ...@@ -1986,6 +2008,7 @@ exports[`disable nft on landing page does not render nft information and card 1`
</button> </button>
</div> </div>
</div> </div>
</div>
<div <div
class="c37 c38" class="c37 c38"
> >
...@@ -3345,6 +3368,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -3345,6 +3368,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
.c27 { .c27 {
color: #222222; color: #222222;
pointer-events: auto;
width: 0; width: 0;
position: relative; position: relative;
font-weight: 485; font-weight: 485;
...@@ -3444,6 +3468,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -3444,6 +3468,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
margin-left: 12px; margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04); box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible; visibility: visible;
-webkit-animation: none;
animation: none;
} }
.c30:hover, .c30:hover,
...@@ -3499,6 +3525,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -3499,6 +3525,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
margin-left: 12px; margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04); box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible; visibility: visible;
-webkit-animation: none;
animation: none;
} }
.c44:hover, .c44:hover,
...@@ -4417,11 +4445,15 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4417,11 +4445,15 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
</div> </div>
<div <div
class="c26" class="c26"
>
<div
style="display: flex; flex-grow: 1;"
> >
<input <input
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
class="c27 c28 token-amount-input" class="c27 c28 token-amount-input"
id="swap-currency-input"
inputmode="decimal" inputmode="decimal"
maxlength="79" maxlength="79"
minlength="1" minlength="1"
...@@ -4431,7 +4463,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4431,7 +4463,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
type="text" type="text"
value="" value=""
/> />
</div>
<div> <div>
<div
class="c13"
>
<button <button
class="c14 c15 c29 c30 open-currency-select-button" class="c14 c15 c29 c30 open-currency-select-button"
> >
...@@ -4470,6 +4506,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4470,6 +4506,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
</button> </button>
</div> </div>
</div> </div>
</div>
<div <div
class="c37 c38" class="c37 c38"
> >
...@@ -4539,11 +4576,15 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4539,11 +4576,15 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
</div> </div>
<div <div
class="c26" class="c26"
>
<div
style="display: flex; flex-grow: 1;"
> >
<input <input
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
class="c27 c28 token-amount-input" class="c27 c28 token-amount-input"
id="swap-currency-output"
inputmode="decimal" inputmode="decimal"
maxlength="79" maxlength="79"
minlength="1" minlength="1"
...@@ -4553,7 +4594,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4553,7 +4594,11 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
type="text" type="text"
value="" value=""
/> />
</div>
<div> <div>
<div
class="c13"
>
<button <button
class="c14 c15 c29 c44 open-currency-select-button" class="c14 c15 c29 c44 open-currency-select-button"
> >
...@@ -4578,6 +4623,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = ` ...@@ -4578,6 +4623,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
</button> </button>
</div> </div>
</div> </div>
</div>
<div <div
class="c37 c38" class="c37 c38"
> >
......
import { Trans } from '@lingui/macro'
import styled from 'styled-components'
import { ThemedText } from 'theme'
const Divider = styled.div`
width: 100%;
height: 1px;
border-width: 0;
margin: 12px 0;
background-color: ${({ theme }) => theme.surface3};
`
export function OutputTaxTooltipBody({ currencySymbol }: { currencySymbol?: string }) {
return (
<>
<ThemedText.SubHeaderSmall color="textPrimary">
<Trans>Exact input only</Trans>
</ThemedText.SubHeaderSmall>
<Divider />
<ThemedText.LabelMicro color="textPrimary">
{currencySymbol ? (
<Trans>
{currencySymbol} fees don&apos;t allow for accurate exact outputs. Use the `You pay` field instead.
</Trans>
) : (
<Trans>
Fees on the selected output token don&apos;t allow for accurate exact outputs. Use the `You pay` field
instead.
</Trans>
)}
</ThemedText.LabelMicro>
</>
)
}
...@@ -31,6 +31,7 @@ import { SwitchLocaleLink } from 'components/SwitchLocaleLink' ...@@ -31,6 +31,7 @@ import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain, isSupportedChain } from 'constants/chains' import { asSupportedChain, isSupportedChain } from 'constants/chains'
import { getInputTax, getOutputTax } from 'constants/tax'
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens' import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens'
import { useCurrency, useDefaultActiveTokens } from 'hooks/Tokens' import { useCurrency, useDefaultActiveTokens } from 'hooks/Tokens'
import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported' import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported'
...@@ -43,14 +44,14 @@ import { useUSDPrice } from 'hooks/useUSDPrice' ...@@ -43,14 +44,14 @@ import { useUSDPrice } from 'hooks/useUSDPrice'
import useWrapCallback, { WrapErrorText, WrapType } from 'hooks/useWrapCallback' import useWrapCallback, { WrapErrorText, WrapType } from 'hooks/useWrapCallback'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics' import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics'
import { ReactNode, useCallback, useEffect, useMemo, useReducer, useState } from 'react' import { ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { ArrowDown } from 'react-feather' import { ArrowDown } from 'react-feather'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { useAppSelector } from 'state/hooks' import { useAppSelector } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils' import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { Field, replaceSwapState } from 'state/swap/actions' import { Field, forceExactInput, replaceSwapState } from 'state/swap/actions'
import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks' import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks'
import swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer' import swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer'
import styled, { useTheme } from 'styled-components' import styled, { useTheme } from 'styled-components'
...@@ -64,6 +65,7 @@ import { didUserReject } from 'utils/swapErrorToUserReadableMessage' ...@@ -64,6 +65,7 @@ import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import { useScreenSize } from '../../hooks/useScreenSize' import { useScreenSize } from '../../hooks/useScreenSize'
import { useIsDarkMode } from '../../theme/components/ThemeToggle' import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { OutputTaxTooltipBody } from './TaxTooltipBody'
import { UniswapXOptIn } from './UniswapXOptIn' import { UniswapXOptIn } from './UniswapXOptIn'
export const ArrowContainer = styled.div` export const ArrowContainer = styled.div`
...@@ -279,6 +281,19 @@ export function Swap({ ...@@ -279,6 +281,19 @@ export function Swap({
inputError: swapInputError, inputError: swapInputError,
} = swapInfo } = swapInfo
const [inputTokenHasTax, outputTokenHasTax] = useMemo(
() => [
!!currencies[Field.INPUT] && !getInputTax(currencies[Field.INPUT]).equalTo(0),
!!currencies[Field.OUTPUT] && !getOutputTax(currencies[Field.OUTPUT]).equalTo(0),
],
[currencies]
)
useEffect(() => {
// Force exact input if the user switches to an output token with tax
if (outputTokenHasTax && independentField === Field.OUTPUT) dispatch(forceExactInput())
}, [independentField, outputTokenHasTax, trade?.outputAmount])
const { const {
wrapType, wrapType,
execute: onWrap, execute: onWrap,
...@@ -510,6 +525,7 @@ export function Swap({ ...@@ -510,6 +525,7 @@ export function Swap({
}, },
[onCurrencyChange, onCurrencySelection, state, trace] [onCurrencyChange, onCurrencySelection, state, trace]
) )
const inputCurrencyNumericalInputRef = useRef<HTMLInputElement>(null)
const handleMaxInput = useCallback(() => { const handleMaxInput = useCallback(() => {
maxInputAmount && onUserInput(Field.INPUT, maxInputAmount.toExact()) maxInputAmount && onUserInput(Field.INPUT, maxInputAmount.toExact())
...@@ -608,6 +624,7 @@ export function Swap({ ...@@ -608,6 +624,7 @@ export function Swap({
showCommonBases showCommonBases
id={InterfaceSectionName.CURRENCY_INPUT_PANEL} id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing} loading={independentField === Field.OUTPUT && routeIsSyncing}
ref={inputCurrencyNumericalInputRef}
/> />
</Trace> </Trace>
</SwapSection> </SwapSection>
...@@ -621,7 +638,7 @@ export function Swap({ ...@@ -621,7 +638,7 @@ export function Swap({
data-testid="swap-currency-button" data-testid="swap-currency-button"
onClick={() => { onClick={() => {
if (disableTokenInputs) return if (disableTokenInputs) return
onSwitchTokens() onSwitchTokens(inputTokenHasTax, formattedAmounts[dependentField])
maybeLogFirstSwapAction(trace) maybeLogFirstSwapAction(trace)
}} }}
color={theme.neutral1} color={theme.neutral1}
...@@ -650,6 +667,13 @@ export function Swap({ ...@@ -650,6 +667,13 @@ export function Swap({
showCommonBases showCommonBases
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL} id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing} loading={independentField === Field.INPUT && routeIsSyncing}
numericalInputSettings={{
// We disable numerical input here if the selected token has tax, since we cannot guarantee exact_outputs for FOT tokens
disabled: outputTokenHasTax,
// Focus the input currency panel if the user tries to type into the disabled output currency panel
onDisabledClick: () => inputCurrencyNumericalInputRef.current?.focus(),
disabledTooltipBody: <OutputTaxTooltipBody currencySymbol={currencies[Field.OUTPUT]?.symbol} />,
}}
/> />
</Trace> </Trace>
{recipient !== null && !showWrap ? ( {recipient !== null && !showWrap ? (
......
...@@ -6,7 +6,10 @@ export enum Field { ...@@ -6,7 +6,10 @@ export enum Field {
} }
export const selectCurrency = createAction<{ field: Field; currencyId: string }>('swap/selectCurrency') export const selectCurrency = createAction<{ field: Field; currencyId: string }>('swap/selectCurrency')
export const switchCurrencies = createAction<void>('swap/switchCurrencies') export const switchCurrencies = createAction<{ newOutputHasTax: boolean; previouslyEstimatedOutput: string }>(
'swap/switchCurrencies'
)
export const forceExactInput = createAction<void>('swap/forceExactInput')
export const typeInput = createAction<{ field: Field; typedValue: string }>('swap/typeInput') export const typeInput = createAction<{ field: Field; typedValue: string }>('swap/typeInput')
export const replaceSwapState = createAction<{ export const replaceSwapState = createAction<{
field: Field field: Field
......
...@@ -23,7 +23,7 @@ import { SwapState } from './reducer' ...@@ -23,7 +23,7 @@ import { SwapState } from './reducer'
export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): { export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): {
onCurrencySelection: (field: Field, currency: Currency) => void onCurrencySelection: (field: Field, currency: Currency) => void
onSwitchTokens: () => void onSwitchTokens: (newOutputHasTax: boolean, previouslyEstimatedOutput: string) => void
onUserInput: (field: Field, typedValue: string) => void onUserInput: (field: Field, typedValue: string) => void
onChangeRecipient: (recipient: string | null) => void onChangeRecipient: (recipient: string | null) => void
} { } {
...@@ -39,9 +39,12 @@ export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): { ...@@ -39,9 +39,12 @@ export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): {
[dispatch] [dispatch]
) )
const onSwitchTokens = useCallback(() => { const onSwitchTokens = useCallback(
dispatch(switchCurrencies()) (newOutputHasTax: boolean, previouslyEstimatedOutput: string) => {
}, [dispatch]) dispatch(switchCurrencies({ newOutputHasTax, previouslyEstimatedOutput }))
},
[dispatch]
)
const onUserInput = useCallback( const onUserInput = useCallback(
(field: Field, typedValue: string) => { (field: Field, typedValue: string) => {
......
import { createReducer } from '@reduxjs/toolkit' import { createReducer } from '@reduxjs/toolkit'
import { parsedQueryString } from 'hooks/useParsedQueryString' import { parsedQueryString } from 'hooks/useParsedQueryString'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions' import {
Field,
forceExactInput,
replaceSwapState,
selectCurrency,
setRecipient,
switchCurrencies,
typeInput,
} from './actions'
import { queryParametersToSwapState } from './hooks' import { queryParametersToSwapState } from './hooks'
export interface SwapState { export interface SwapState {
...@@ -55,7 +63,17 @@ export default createReducer<SwapState>(initialState, (builder) => ...@@ -55,7 +63,17 @@ export default createReducer<SwapState>(initialState, (builder) =>
} }
} }
}) })
.addCase(switchCurrencies, (state) => { .addCase(switchCurrencies, (state, { payload: { newOutputHasTax, previouslyEstimatedOutput } }) => {
if (newOutputHasTax && state.independentField === Field.INPUT) {
// To prevent swaps with FOT tokens as exact-outputs, we leave it as an exact-in swap and use the previously estimated output amount as the new exact-in amount.
return {
...state,
[Field.INPUT]: { currencyId: state[Field.OUTPUT].currencyId },
[Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId },
typedValue: previouslyEstimatedOutput,
}
}
return { return {
...state, ...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT, independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
...@@ -63,6 +81,13 @@ export default createReducer<SwapState>(initialState, (builder) => ...@@ -63,6 +81,13 @@ export default createReducer<SwapState>(initialState, (builder) =>
[Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId }, [Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId },
} }
}) })
.addCase(forceExactInput, (state) => {
return {
...state,
independentField: Field.INPUT,
typedValue: '',
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => { .addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return { return {
...state, ...state,
......
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