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
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Tooltip from 'components/Tooltip'
import { isSupportedChain } from 'constants/chains'
import ms from 'ms'
import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
import { forwardRef, ReactNode, useCallback, useEffect, useState } from 'react'
import { Lock } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
......@@ -58,6 +60,7 @@ const CurrencySelect = styled(ButtonGray)<{
selected: boolean
hideInput?: boolean
disabled?: boolean
animateShake?: boolean
}>`
align-items: center;
background-color: ${({ selected, theme }) => (selected ? theme.surface1 : theme.accent1)};
......@@ -105,6 +108,34 @@ const CurrencySelect = styled(ButtonGray)<{
}
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`
......@@ -206,162 +237,204 @@ interface SwapCurrencyInputPanelProps {
locked?: boolean
loading?: boolean
disabled?: boolean
numericalInputSettings?: {
disabled?: boolean
onDisabledClick?: () => void
disabledTooltipBody?: ReactNode
}
}
export default function SwapCurrencyInputPanel({
value,
onUserInput,
onMax,
showMaxButton,
onCurrencySelect,
currency,
otherCurrency,
id,
showCommonBases,
showCurrencyAmount,
disableNonToken,
renderBalance,
fiatValue,
priceImpact,
hideBalance = false,
pair = null, // used for double token logo
hideInput = false,
locked = false,
loading = false,
disabled = false,
label,
...rest
}: SwapCurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
const { account, chainId } = useWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useTheme()
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
}, [setModalOpen])
const chainAllowed = isSupportedChain(chainId)
return (
<InputPanel id={id} hideInput={hideInput} {...rest}>
{locked && (
<FixedContainer>
<AutoColumn gap="sm" justify="center">
<Lock />
<ThemedText.BodySecondary fontSize="12px" textAlign="center" padding="0 12px">
<Trans>The market price is outside your specified price range. Single-asset deposit only.</Trans>
</ThemedText.BodySecondary>
</AutoColumn>
</FixedContainer>
)}
<Container hideInput={hideInput}>
<ThemedText.SubHeaderSmall style={{ userSelect: 'none' }}>{label}</ThemedText.SubHeaderSmall>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}>
{!hideInput && (
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
disabled={!chainAllowed || disabled}
$loading={loading}
/>
)}
<PrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen}>
<CurrencySelect
disabled={!chainAllowed || disabled}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
setModalOpen(true)
}
}}
>
<Aligner>
<RowFixed>
{pair ? (
<span style={{ marginRight: '0.5rem' }}>
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : currency ? (
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size="24px" />
) : null}
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
</PrefetchBalancesWrapper>
</InputRow>
{Boolean(!hideInput && !hideBalance) && (
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
{fiatValue && <FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />}
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '16px' }}>
<ThemedText.DeprecatedBody
data-testid="balance-text"
color={theme.neutral2}
fontWeight={485}
fontSize={14}
style={{ display: 'inline' }}
>
{!hideBalance && currency && selectedCurrencyBalance ? (
renderBalance ? (
renderBalance(selectedCurrencyBalance)
const SwapCurrencyInputPanel = forwardRef<HTMLInputElement, SwapCurrencyInputPanelProps>(
(
{
value,
onUserInput,
onMax,
showMaxButton,
onCurrencySelect,
currency,
otherCurrency,
id,
showCommonBases,
showCurrencyAmount,
disableNonToken,
renderBalance,
fiatValue,
priceImpact,
hideBalance = false,
pair = null, // used for double token logo
hideInput = false,
locked = false,
loading = false,
disabled = false,
numericalInputSettings,
label,
...rest
},
ref
) => {
const [modalOpen, setModalOpen] = useState(false)
const { account, chainId } = useWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useTheme()
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
}, [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)
// reset tooltip state when currency changes
useEffect(() => setTooltipVisible(false), [currency])
return (
<InputPanel id={id} hideInput={hideInput} {...rest}>
{locked && (
<FixedContainer>
<AutoColumn gap="sm" justify="center">
<Lock />
<ThemedText.BodySecondary fontSize="12px" textAlign="center" padding="0 12px">
<Trans>The market price is outside your specified price range. Single-asset deposit only.</Trans>
</ThemedText.BodySecondary>
</AutoColumn>
</FixedContainer>
)}
<Container hideInput={hideInput}>
<ThemedText.SubHeaderSmall style={{ userSelect: 'none' }}>{label}</ThemedText.SubHeaderSmall>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}>
{!hideInput && (
<div style={{ display: 'flex', flexGrow: '1' }} onClick={handleDisabledNumericalInputClick}>
<StyledNumericalInput
className="token-amount-input"
value={value}
onUserInput={onUserInput}
disabled={!chainAllowed || disabled || numericalInputSettings?.disabled}
$loading={loading}
id={id}
ref={ref}
/>
</div>
)}
<PrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen}>
<Tooltip
show={tooltipVisible && !modalOpen}
placement="bottom"
offsetY={14}
text={numericalInputSettings?.disabledTooltipBody}
>
<CurrencySelect
disabled={!chainAllowed || disabled}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
setModalOpen(true)
}
}}
animateShake={tooltipVisible}
>
<Aligner>
<RowFixed>
{pair ? (
<span style={{ marginRight: '0.5rem' }}>
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : currency ? (
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size="24px" />
) : null}
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, NumberType.TokenNonTx)}</Trans>
)
) : null}
</ThemedText.DeprecatedBody>
{showMaxButton && selectedCurrencyBalance ? (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
<StyledTokenName
className="token-symbol-container"
active={Boolean(currency && currency.symbol)}
>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
</Tooltip>
</PrefetchBalancesWrapper>
</InputRow>
{Boolean(!hideInput && !hideBalance) && (
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
{fiatValue && <FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />}
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '16px' }}>
<ThemedText.DeprecatedBody
data-testid="balance-text"
color={theme.neutral2}
fontWeight={485}
fontSize={14}
style={{ display: 'inline' }}
>
<StyledBalanceMax onClick={onMax}>
<Trans>Max</Trans>
</StyledBalanceMax>
</TraceEvent>
) : null}
</RowFixed>
) : (
<span />
)}
</RowBetween>
</FiatRow>
{!hideBalance && currency && selectedCurrencyBalance ? (
renderBalance ? (
renderBalance(selectedCurrencyBalance)
) : (
<Trans>Balance: {formatCurrencyAmount(selectedCurrencyBalance, NumberType.TokenNonTx)}</Trans>
)
) : null}
</ThemedText.DeprecatedBody>
{showMaxButton && selectedCurrencyBalance ? (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED}
element={InterfaceElementName.MAX_TOKEN_AMOUNT_BUTTON}
>
<StyledBalanceMax onClick={onMax}>
<Trans>Max</Trans>
</StyledBalanceMax>
</TraceEvent>
) : null}
</RowFixed>
) : (
<span />
)}
</RowBetween>
</FiatRow>
)}
</Container>
{onCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onCurrencySelect={onCurrencySelect}
selectedCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
/>
)}
</Container>
{onCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onCurrencySelect={onCurrencySelect}
selectedCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
/>
)}
</InputPanel>
)
}
</InputPanel>
)
}
)
SwapCurrencyInputPanel.displayName = 'SwapCurrencyInputPanel'
export default SwapCurrencyInputPanel
import React from 'react'
import React, { forwardRef } from 'react'
import styled from 'styled-components'
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)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
width: 0;
position: relative;
font-weight: 485;
......@@ -40,60 +41,61 @@ const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: s
const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
export const Input = React.memo(function InnerInput({
value,
onUserInput,
placeholder,
prependSymbol,
...rest
}: {
interface InputProps extends Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'> {
value: string | number
onUserInput: (input: string) => void
error?: boolean
fontSize?: string
align?: 'right' | 'left'
prependSymbol?: string
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) {
const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput)
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ value, onUserInput, placeholder, prependSymbol, ...rest }: InputProps, ref) => {
const enforcer = (nextUserInput: string) => {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(nextUserInput)
}
}
}
return (
<StyledInput
{...rest}
value={prependSymbol && value ? prependSymbol + value : value}
onChange={(event) => {
if (prependSymbol) {
const value = event.target.value
return (
<StyledInput
{...rest}
ref={ref}
value={prependSymbol && value ? prependSymbol + value : value}
onChange={(event) => {
if (prependSymbol) {
const value = event.target.value
// cut off prepended symbol
const formattedValue = value.toString().includes(prependSymbol)
? value.toString().slice(1, value.toString().length + 1)
: value
// cut off prepended symbol
const formattedValue = value.toString().includes(prependSymbol)
? value.toString().slice(1, value.toString().length + 1)
: value
// replace commas with periods, because uniswap exclusively uses period as the decimal separator
enforcer(formattedValue.replace(/,/g, '.'))
} else {
enforcer(event.target.value.replace(/,/g, '.'))
}
}}
// universal input options
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// text-specific options
type="text"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder={placeholder || '0'}
minLength={1}
maxLength={79}
spellCheck="false"
/>
)
})
// replace commas with periods, because uniswap exclusively uses period as the decimal separator
enforcer(formattedValue.replace(/,/g, '.'))
} else {
enforcer(event.target.value.replace(/,/g, '.'))
}
}}
// universal input options
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// text-specific options
type="text"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder={placeholder || '0'}
minLength={1}
maxLength={79}
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
import { AutoColumn } from 'components/Column'
import CurrencyInputPanel from 'components/CurrencyInputPanel'
import Input from 'components/NumericalInput'
import { Input } from 'components/NumericalInput'
import styled from 'styled-components'
export const Wrapper = styled.div`
......
......@@ -765,6 +765,7 @@ exports[`disable nft on landing page does not render nft information and card 1`
.c27 {
color: #222222;
pointer-events: auto;
width: 0;
position: relative;
font-weight: 485;
......@@ -864,6 +865,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible;
-webkit-animation: none;
animation: none;
}
.c30:hover,
......@@ -919,6 +922,8 @@ exports[`disable nft on landing page does not render nft information and card 1`
margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible;
-webkit-animation: none;
animation: none;
}
.c44:hover,
......@@ -1826,56 +1831,65 @@ exports[`disable nft on landing page does not render nft information and card 1`
<div
class="c26"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
<div
style="display: flex; flex-grow: 1;"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
id="swap-currency-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
</div>
<div>
<button
class="c14 c15 c29 c30 open-currency-select-button"
<div
class="c13"
>
<span
class="c31"
<button
class="c14 c15 c29 c30 open-currency-select-button"
>
<div
class="c6 c7 c10"
<span
class="c31"
>
<div
class="c32"
style="height: 24px; width: 24px; margin-right: 2px;"
class="c6 c7 c10"
>
<div
class="c33"
class="c32"
style="height: 24px; width: 24px; margin-right: 2px;"
>
<img
alt="ETH logo"
class="c34"
src="ethereum-logo.png"
/>
<div
class="c33"
>
<img
alt="ETH logo"
class="c34"
src="ethereum-logo.png"
/>
</div>
</div>
<span
class="c35 token-symbol-container"
>
ETH
</span>
</div>
<span
class="c35 token-symbol-container"
<svg
class="c36"
>
ETH
</span>
</div>
<svg
class="c36"
>
dropdown.svg
</svg>
</span>
</button>
dropdown.svg
</svg>
</span>
</button>
</div>
</div>
</div>
<div
......@@ -1948,42 +1962,51 @@ exports[`disable nft on landing page does not render nft information and card 1`
<div
class="c26"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
<div
style="display: flex; flex-grow: 1;"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
id="swap-currency-output"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
</div>
<div>
<button
class="c14 c15 c29 c44 open-currency-select-button"
<div
class="c13"
>
<span
class="c31"
<button
class="c14 c15 c29 c44 open-currency-select-button"
>
<div
class="c6 c7 c10"
<span
class="c31"
>
<span
class="c35 token-symbol-container"
<div
class="c6 c7 c10"
>
Select token
</span>
</div>
<svg
class="c45"
>
dropdown.svg
</svg>
</span>
</button>
<span
class="c35 token-symbol-container"
>
Select token
</span>
</div>
<svg
class="c45"
>
dropdown.svg
</svg>
</span>
</button>
</div>
</div>
</div>
<div
......@@ -3345,6 +3368,7 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
.c27 {
color: #222222;
pointer-events: auto;
width: 0;
position: relative;
font-weight: 485;
......@@ -3444,6 +3468,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible;
-webkit-animation: none;
animation: none;
}
.c30:hover,
......@@ -3499,6 +3525,8 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
margin-left: 12px;
box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04);
visibility: visible;
-webkit-animation: none;
animation: none;
}
.c44:hover,
......@@ -4418,56 +4446,65 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
<div
class="c26"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
<div
style="display: flex; flex-grow: 1;"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
id="swap-currency-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
</div>
<div>
<button
class="c14 c15 c29 c30 open-currency-select-button"
<div
class="c13"
>
<span
class="c31"
<button
class="c14 c15 c29 c30 open-currency-select-button"
>
<div
class="c6 c7 c10"
<span
class="c31"
>
<div
class="c32"
style="height: 24px; width: 24px; margin-right: 2px;"
class="c6 c7 c10"
>
<div
class="c33"
class="c32"
style="height: 24px; width: 24px; margin-right: 2px;"
>
<img
alt="ETH logo"
class="c34"
src="ethereum-logo.png"
/>
<div
class="c33"
>
<img
alt="ETH logo"
class="c34"
src="ethereum-logo.png"
/>
</div>
</div>
<span
class="c35 token-symbol-container"
>
ETH
</span>
</div>
<span
class="c35 token-symbol-container"
<svg
class="c36"
>
ETH
</span>
</div>
<svg
class="c36"
>
dropdown.svg
</svg>
</span>
</button>
dropdown.svg
</svg>
</span>
</button>
</div>
</div>
</div>
<div
......@@ -4540,42 +4577,51 @@ exports[`disable nft on landing page renders nft information and card 1`] = `
<div
class="c26"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
<div
style="display: flex; flex-grow: 1;"
>
<input
autocomplete="off"
autocorrect="off"
class="c27 c28 token-amount-input"
id="swap-currency-output"
inputmode="decimal"
maxlength="79"
minlength="1"
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0"
spellcheck="false"
type="text"
value=""
/>
</div>
<div>
<button
class="c14 c15 c29 c44 open-currency-select-button"
<div
class="c13"
>
<span
class="c31"
<button
class="c14 c15 c29 c44 open-currency-select-button"
>
<div
class="c6 c7 c10"
<span
class="c31"
>
<span
class="c35 token-symbol-container"
<div
class="c6 c7 c10"
>
Select token
</span>
</div>
<svg
class="c45"
>
dropdown.svg
</svg>
</span>
</button>
<span
class="c35 token-symbol-container"
>
Select token
</span>
</div>
<svg
class="c45"
>
dropdown.svg
</svg>
</span>
</button>
</div>
</div>
</div>
<div
......
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'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain, isSupportedChain } from 'constants/chains'
import { getInputTax, getOutputTax } from 'constants/tax'
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens'
import { useCurrency, useDefaultActiveTokens } from 'hooks/Tokens'
import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported'
......@@ -43,14 +44,14 @@ import { useUSDPrice } from 'hooks/useUSDPrice'
import useWrapCallback, { WrapErrorText, WrapType } from 'hooks/useWrapCallback'
import JSBI from 'jsbi'
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 { useLocation, useNavigate } from 'react-router-dom'
import { Text } from 'rebass'
import { useAppSelector } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types'
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 swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer'
import styled, { useTheme } from 'styled-components'
......@@ -64,6 +65,7 @@ import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import { useScreenSize } from '../../hooks/useScreenSize'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { OutputTaxTooltipBody } from './TaxTooltipBody'
import { UniswapXOptIn } from './UniswapXOptIn'
export const ArrowContainer = styled.div`
......@@ -279,6 +281,19 @@ export function Swap({
inputError: swapInputError,
} = 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 {
wrapType,
execute: onWrap,
......@@ -510,6 +525,7 @@ export function Swap({
},
[onCurrencyChange, onCurrencySelection, state, trace]
)
const inputCurrencyNumericalInputRef = useRef<HTMLInputElement>(null)
const handleMaxInput = useCallback(() => {
maxInputAmount && onUserInput(Field.INPUT, maxInputAmount.toExact())
......@@ -608,6 +624,7 @@ export function Swap({
showCommonBases
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
ref={inputCurrencyNumericalInputRef}
/>
</Trace>
</SwapSection>
......@@ -621,7 +638,7 @@ export function Swap({
data-testid="swap-currency-button"
onClick={() => {
if (disableTokenInputs) return
onSwitchTokens()
onSwitchTokens(inputTokenHasTax, formattedAmounts[dependentField])
maybeLogFirstSwapAction(trace)
}}
color={theme.neutral1}
......@@ -650,6 +667,13 @@ export function Swap({
showCommonBases
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
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>
{recipient !== null && !showWrap ? (
......
......@@ -6,7 +6,10 @@ export enum Field {
}
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 replaceSwapState = createAction<{
field: Field
......
......@@ -23,7 +23,7 @@ import { SwapState } from './reducer'
export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): {
onCurrencySelection: (field: Field, currency: Currency) => void
onSwitchTokens: () => void
onSwitchTokens: (newOutputHasTax: boolean, previouslyEstimatedOutput: string) => void
onUserInput: (field: Field, typedValue: string) => void
onChangeRecipient: (recipient: string | null) => void
} {
......@@ -39,9 +39,12 @@ export function useSwapActionHandlers(dispatch: React.Dispatch<AnyAction>): {
[dispatch]
)
const onSwitchTokens = useCallback(() => {
dispatch(switchCurrencies())
}, [dispatch])
const onSwitchTokens = useCallback(
(newOutputHasTax: boolean, previouslyEstimatedOutput: string) => {
dispatch(switchCurrencies({ newOutputHasTax, previouslyEstimatedOutput }))
},
[dispatch]
)
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
......
import { createReducer } from '@reduxjs/toolkit'
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'
export interface SwapState {
......@@ -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 {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
......@@ -63,6 +81,13 @@ export default createReducer<SwapState>(initialState, (builder) =>
[Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId },
}
})
.addCase(forceExactInput, (state) => {
return {
...state,
independentField: Field.INPUT,
typedValue: '',
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...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