feat: slippage warning ux (#3211)
* feat: setting input spacings * feat: popover icon props * fix: slippage input border * feat: slippage input warning ux * feat: slippage summary warning ux * fix: summary layout * fix: large icon compatibility * fix: input option style * fix: large icon compatibility * fix: popover dimensions * feat: tooltip hook * fix: better max slippage popovers * feat: error color input on invalid slippage * fix: use default tx ttl * fix: type userDeadline
This commit is contained in:
parent
c82b4fae64
commit
4b762ef5c9
@ -23,6 +23,8 @@ const Reference = styled.div`
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const SQRT_8 = Math.sqrt(8)
|
||||||
|
|
||||||
const Arrow = styled.div`
|
const Arrow = styled.div`
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -30,9 +32,7 @@ const Arrow = styled.div`
|
|||||||
|
|
||||||
::before {
|
::before {
|
||||||
background: ${({ theme }) => theme.dialog};
|
background: ${({ theme }) => theme.dialog};
|
||||||
border: 1px solid ${({ theme }) => theme.outline};
|
|
||||||
content: '';
|
content: '';
|
||||||
|
|
||||||
height: 8px;
|
height: 8px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
@ -40,34 +40,30 @@ const Arrow = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.arrow-top {
|
&.arrow-top {
|
||||||
bottom: -5px;
|
bottom: -${SQRT_8}px;
|
||||||
::before {
|
::before {
|
||||||
border-left: none;
|
border-bottom-right-radius: 1px;
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.arrow-bottom {
|
&.arrow-bottom {
|
||||||
top: -5px;
|
top: -${SQRT_8}px;
|
||||||
::before {
|
::before {
|
||||||
border-bottom: none;
|
border-top-left-radius: 1px;
|
||||||
border-right: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.arrow-left {
|
&.arrow-left {
|
||||||
right: -5px;
|
right: -${SQRT_8}px;
|
||||||
::before {
|
::before {
|
||||||
border-bottom: none;
|
border-top-right-radius: 1px;
|
||||||
border-left: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.arrow-right {
|
&.arrow-right {
|
||||||
left: -5px;
|
left: -${SQRT_8}px;
|
||||||
::before {
|
::before {
|
||||||
border-right: none;
|
border-bottom-left-radius: 1px;
|
||||||
border-top: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -77,10 +73,11 @@ export interface PopoverProps {
|
|||||||
show: boolean
|
show: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
placement: Placement
|
placement: Placement
|
||||||
|
offset?: number
|
||||||
contained?: true
|
contained?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Popover({ content, show, children, placement, contained }: PopoverProps) {
|
export default function Popover({ content, show, children, placement, offset, contained }: PopoverProps) {
|
||||||
const boundary = useContext(BoundaryContext)
|
const boundary = useContext(BoundaryContext)
|
||||||
const reference = useRef<HTMLDivElement>(null)
|
const reference = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@ -90,8 +87,8 @@ export default function Popover({ content, show, children, placement, contained
|
|||||||
|
|
||||||
const options = useMemo((): Options => {
|
const options = useMemo((): Options => {
|
||||||
const modifiers: Options['modifiers'] = [
|
const modifiers: Options['modifiers'] = [
|
||||||
{ name: 'offset', options: { offset: [5, 5] } },
|
{ name: 'offset', options: { offset: [4, offset || 4] } },
|
||||||
{ name: 'arrow', options: { element: arrow, padding: 6 } },
|
{ name: 'arrow', options: { element: arrow, padding: 4 } },
|
||||||
]
|
]
|
||||||
if (contained) {
|
if (contained) {
|
||||||
modifiers.push(
|
modifiers.push(
|
||||||
@ -118,7 +115,7 @@ export default function Popover({ content, show, children, placement, contained
|
|||||||
strategy: 'absolute',
|
strategy: 'absolute',
|
||||||
modifiers,
|
modifiers,
|
||||||
}
|
}
|
||||||
}, [arrow, boundary, placement, contained])
|
}, [offset, arrow, contained, placement, boundary])
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(reference.current, popover, options)
|
const { styles, attributes } = usePopper(reference.current, popover, options)
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { t, Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { Percent } from '@uniswap/sdk-core'
|
import { Percent } from '@uniswap/sdk-core'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { Check, LargeIcon } from 'lib/icons'
|
import Popover from 'lib/components/Popover'
|
||||||
import { maxSlippageAtom } from 'lib/state/settings'
|
import { TooltipHandlers, useTooltip } from 'lib/components/Tooltip'
|
||||||
import styled, { ThemedText } from 'lib/theme'
|
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
|
||||||
import { PropsWithChildren, useCallback, useRef, useState } from 'react'
|
import { MAX_VALID_SLIPPAGE, maxSlippageAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||||
|
import styled, { Color, ThemedText } from 'lib/theme'
|
||||||
|
import { memo, PropsWithChildren, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { BaseButton, TextButton } from '../../Button'
|
import { BaseButton, TextButton } from '../../Button'
|
||||||
import Column from '../../Column'
|
import Column from '../../Column'
|
||||||
@ -15,6 +17,10 @@ import { Label, optionCss } from './components'
|
|||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
|
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
|
||||||
)
|
)
|
||||||
|
const highSlippage = <Trans>High slippage increases the risk of price movement</Trans>
|
||||||
|
const invalidSlippage = <Trans>Please enter a valid slippage %</Trans>
|
||||||
|
|
||||||
|
const placeholder = '0.10'
|
||||||
|
|
||||||
const Button = styled(TextButton)<{ selected: boolean }>`
|
const Button = styled(TextButton)<{ selected: boolean }>`
|
||||||
${({ selected }) => optionCss(selected)}
|
${({ selected }) => optionCss(selected)}
|
||||||
@ -23,27 +29,74 @@ const Button = styled(TextButton)<{ selected: boolean }>`
|
|||||||
const Custom = styled(BaseButton)<{ selected: boolean }>`
|
const Custom = styled(BaseButton)<{ selected: boolean }>`
|
||||||
${({ selected }) => optionCss(selected)}
|
${({ selected }) => optionCss(selected)}
|
||||||
${inputCss}
|
${inputCss}
|
||||||
border-color: ${({ selected, theme }) => (selected ? theme.active : 'transparent')} !important;
|
|
||||||
padding: calc(0.75em - 3px) 0.625em;
|
padding: calc(0.75em - 3px) 0.625em;
|
||||||
`
|
`
|
||||||
|
|
||||||
interface OptionProps {
|
interface OptionProps extends Partial<TooltipHandlers> {
|
||||||
wrapper: typeof Button | typeof Custom
|
wrapper: typeof Button | typeof Custom
|
||||||
selected: boolean
|
selected: boolean
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
|
icon?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function Option({ wrapper: Wrapper, children, selected, onSelect }: PropsWithChildren<OptionProps>) {
|
function Option({
|
||||||
|
wrapper: Wrapper,
|
||||||
|
children,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
icon,
|
||||||
|
...tooltipHandlers
|
||||||
|
}: PropsWithChildren<OptionProps>) {
|
||||||
return (
|
return (
|
||||||
<Wrapper selected={selected} onClick={onSelect}>
|
<Wrapper selected={selected} onClick={onSelect} {...tooltipHandlers}>
|
||||||
<Row gap={0.5}>
|
<Row gap={0.5}>
|
||||||
{children}
|
{children}
|
||||||
<span style={{ width: '1.2em' }}>{selected && <LargeIcon icon={Check} />}</span>
|
{icon ? icon : <LargeIcon icon={selected ? Check : undefined} size={1.25} />}
|
||||||
</Row>
|
</Row>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WarningState {
|
||||||
|
NONE,
|
||||||
|
HIGH_SLIPPAGE,
|
||||||
|
INVALID_SLIPPAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Warning = memo(function Warning({ state, showTooltip }: { state: WarningState; showTooltip: boolean }) {
|
||||||
|
let icon: Icon
|
||||||
|
let color: Color
|
||||||
|
let content: ReactNode
|
||||||
|
let show = showTooltip
|
||||||
|
switch (state) {
|
||||||
|
case WarningState.INVALID_SLIPPAGE:
|
||||||
|
icon = XOctagon
|
||||||
|
color = 'error'
|
||||||
|
content = invalidSlippage
|
||||||
|
show = true
|
||||||
|
break
|
||||||
|
case WarningState.HIGH_SLIPPAGE:
|
||||||
|
icon = AlertTriangle
|
||||||
|
color = 'warning'
|
||||||
|
content = highSlippage
|
||||||
|
break
|
||||||
|
case WarningState.NONE:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
key={state}
|
||||||
|
content={<ThemedText.Caption>{content}</ThemedText.Caption>}
|
||||||
|
show={show}
|
||||||
|
placement="top"
|
||||||
|
offset={16}
|
||||||
|
contained
|
||||||
|
>
|
||||||
|
<LargeIcon icon={icon} color={color} size={1.25} />
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default function MaxSlippageSelect() {
|
export default function MaxSlippageSelect() {
|
||||||
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
|
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
|
||||||
|
|
||||||
@ -51,22 +104,33 @@ export default function MaxSlippageSelect() {
|
|||||||
const input = useRef<HTMLInputElement>(null)
|
const input = useRef<HTMLInputElement>(null)
|
||||||
const focus = useCallback(() => input.current?.focus(), [input])
|
const focus = useCallback(() => input.current?.focus(), [input])
|
||||||
|
|
||||||
const onInputChange = useCallback(
|
const [warning, setWarning] = useState(WarningState.NONE)
|
||||||
(custom: string) => {
|
const [showTooltip, setShowTooltip, tooltipProps] = useTooltip()
|
||||||
setCustom(custom)
|
useEffect(() => setShowTooltip(true), [warning, setShowTooltip]) // enables the tooltip if a warning is set
|
||||||
const numerator = Math.floor(+custom * 100)
|
|
||||||
if (numerator) {
|
const processInput = useCallback(() => {
|
||||||
setMaxSlippage(new Percent(numerator, 10_000))
|
const numerator = Math.floor(+custom * 100)
|
||||||
} else {
|
if (numerator) {
|
||||||
|
const percent = new Percent(numerator, 10_000)
|
||||||
|
if (percent.greaterThan(MAX_VALID_SLIPPAGE)) {
|
||||||
|
setWarning(WarningState.INVALID_SLIPPAGE)
|
||||||
setMaxSlippage('auto')
|
setMaxSlippage('auto')
|
||||||
|
} else if (percent.greaterThan(MIN_HIGH_SLIPPAGE)) {
|
||||||
|
setWarning(WarningState.HIGH_SLIPPAGE)
|
||||||
|
setMaxSlippage(percent)
|
||||||
|
} else {
|
||||||
|
setWarning(WarningState.NONE)
|
||||||
|
setMaxSlippage(percent)
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
[setMaxSlippage]
|
setMaxSlippage('auto')
|
||||||
)
|
}
|
||||||
|
}, [custom, setMaxSlippage])
|
||||||
|
useEffect(processInput, [processInput])
|
||||||
const onInputSelect = useCallback(() => {
|
const onInputSelect = useCallback(() => {
|
||||||
focus()
|
focus()
|
||||||
onInputChange(custom)
|
processInput()
|
||||||
}, [custom, focus, onInputChange])
|
}, [focus, processInput])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap={0.75}>
|
<Column gap={0.75}>
|
||||||
@ -77,9 +141,22 @@ export default function MaxSlippageSelect() {
|
|||||||
<Trans>Auto</Trans>
|
<Trans>Auto</Trans>
|
||||||
</ThemedText.ButtonMedium>
|
</ThemedText.ButtonMedium>
|
||||||
</Option>
|
</Option>
|
||||||
<Option wrapper={Custom} onSelect={onInputSelect} selected={maxSlippage !== 'auto'}>
|
<Option
|
||||||
<Row>
|
wrapper={Custom}
|
||||||
<DecimalInput value={custom} onChange={onInputChange} placeholder={t`Custom`} ref={input} />%
|
selected={maxSlippage !== 'auto'}
|
||||||
|
onSelect={onInputSelect}
|
||||||
|
icon={<Warning state={warning} showTooltip={showTooltip} />}
|
||||||
|
{...tooltipProps}
|
||||||
|
>
|
||||||
|
<Row color={warning === WarningState.INVALID_SLIPPAGE ? 'error' : undefined}>
|
||||||
|
<DecimalInput
|
||||||
|
size={Math.max(custom.length, 3)}
|
||||||
|
value={custom}
|
||||||
|
onChange={setCustom}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ref={input}
|
||||||
|
/>
|
||||||
|
%
|
||||||
</Row>
|
</Row>
|
||||||
</Option>
|
</Option>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -11,6 +11,8 @@ import { Label } from './components'
|
|||||||
|
|
||||||
const tooltip = <Trans>Your transaction will revert if it has been pending for longer than this period of time.</Trans>
|
const tooltip = <Trans>Your transaction will revert if it has been pending for longer than this period of time.</Trans>
|
||||||
|
|
||||||
|
const placeholder = TRANSACTION_TTL_DEFAULT.toString()
|
||||||
|
|
||||||
const Input = styled(Row)`
|
const Input = styled(Row)`
|
||||||
${inputCss}
|
${inputCss}
|
||||||
`
|
`
|
||||||
@ -22,11 +24,12 @@ export default function TransactionTtlInput() {
|
|||||||
<Column gap={0.75}>
|
<Column gap={0.75}>
|
||||||
<Label name={<Trans>Transaction deadline</Trans>} tooltip={tooltip} />
|
<Label name={<Trans>Transaction deadline</Trans>} tooltip={tooltip} />
|
||||||
<ThemedText.Body1>
|
<ThemedText.Body1>
|
||||||
<Input onClick={() => input.current?.focus()}>
|
<Input justify="start" onClick={() => input.current?.focus()}>
|
||||||
<IntegerInput
|
<IntegerInput
|
||||||
placeholder={TRANSACTION_TTL_DEFAULT.toString()}
|
placeholder={placeholder}
|
||||||
value={transactionTtl?.toString() ?? ''}
|
value={transactionTtl?.toString() ?? ''}
|
||||||
onChange={(value) => setTransactionTtl(value ? parseFloat(value) : 0)}
|
onChange={(value) => setTransactionTtl(value ? parseFloat(value) : 0)}
|
||||||
|
size={Math.max(transactionTtl?.toString().length || 0, placeholder.length)}
|
||||||
ref={input}
|
ref={input}
|
||||||
/>
|
/>
|
||||||
<Trans>minutes</Trans>
|
<Trans>minutes</Trans>
|
||||||
|
@ -17,6 +17,10 @@ export const optionCss = (selected: boolean) => css`
|
|||||||
:enabled:hover {
|
:enabled:hover {
|
||||||
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
|
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:enabled:focus-within {
|
||||||
|
border-color: ${({ theme }) => theme.active};
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export function value(Value: AnyStyledComponent) {
|
export function value(Value: AnyStyledComponent) {
|
||||||
|
@ -2,8 +2,8 @@ import { t } from '@lingui/macro'
|
|||||||
import { Trade } from '@uniswap/router-sdk'
|
import { Trade } from '@uniswap/router-sdk'
|
||||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { integratorFeeAtom } from 'lib/state/settings'
|
import { integratorFeeAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||||
import { ThemedText } from 'lib/theme'
|
import { Color, ThemedText } from 'lib/theme'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { currencyId } from 'utils/currencyId'
|
import { currencyId } from 'utils/currencyId'
|
||||||
import { computeRealizedLPFeePercent } from 'utils/prices'
|
import { computeRealizedLPFeePercent } from 'utils/prices'
|
||||||
@ -13,11 +13,12 @@ import Row from '../../Row'
|
|||||||
interface DetailProps {
|
interface DetailProps {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
|
color?: Color
|
||||||
}
|
}
|
||||||
|
|
||||||
function Detail({ label, value }: DetailProps) {
|
function Detail({ label, value, color }: DetailProps) {
|
||||||
return (
|
return (
|
||||||
<ThemedText.Caption>
|
<ThemedText.Caption color={color}>
|
||||||
<Row gap={2}>
|
<Row gap={2}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>{value}</span>
|
<span style={{ whiteSpace: 'nowrap' }}>{value}</span>
|
||||||
@ -44,7 +45,7 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
|||||||
return trade.priceImpact.subtract(realizedLpFeePercent)
|
return trade.priceImpact.subtract(realizedLpFeePercent)
|
||||||
}, [trade])
|
}, [trade])
|
||||||
|
|
||||||
const details = useMemo((): [string, string][] => {
|
const details = useMemo(() => {
|
||||||
// @TODO(ianlapham): Check that provider fee is even a valid list item
|
// @TODO(ianlapham): Check that provider fee is even a valid list item
|
||||||
return [
|
return [
|
||||||
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
|
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
|
||||||
@ -56,17 +57,21 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
|||||||
trade.tradeType === TradeType.EXACT_OUTPUT
|
trade.tradeType === TradeType.EXACT_OUTPUT
|
||||||
? [t`Minimum received`, `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${outputCurrency.symbol}`]
|
? [t`Minimum received`, `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${outputCurrency.symbol}`]
|
||||||
: [],
|
: [],
|
||||||
[t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`],
|
[
|
||||||
|
t`Slippage tolerance`,
|
||||||
|
`${allowedSlippage.toFixed(2)}%`,
|
||||||
|
allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE) && 'warning',
|
||||||
|
],
|
||||||
].filter(isDetail)
|
].filter(isDetail)
|
||||||
|
|
||||||
function isDetail(detail: unknown[]): detail is [string, string] {
|
function isDetail(detail: unknown[]): detail is [string, string, Color | undefined] {
|
||||||
return Boolean(detail[1])
|
return Boolean(detail[1])
|
||||||
}
|
}
|
||||||
}, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade])
|
}, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{details.map(([label, detail]) => (
|
{details.map(([label, detail, color]) => (
|
||||||
<Detail key={label} label={label} value={detail} />
|
<Detail key={label} label={label} value={detail} color={color} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,8 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
|||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { IconButton } from 'lib/components/Button'
|
import { IconButton } from 'lib/components/Button'
|
||||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||||
import { Expando, Info } from 'lib/icons'
|
import { AlertTriangle, Expando, Info } from 'lib/icons'
|
||||||
|
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||||
import { Field, independentFieldAtom } from 'lib/state/swap'
|
import { Field, independentFieldAtom } from 'lib/state/swap'
|
||||||
import styled, { ThemedText } from 'lib/theme'
|
import styled, { ThemedText } from 'lib/theme'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
@ -44,6 +45,7 @@ const Body = styled(Column)<{ open: boolean }>`
|
|||||||
|
|
||||||
${Column} {
|
${Column} {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
grid-template-rows: repeat(auto-fill, 1em);
|
||||||
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
||||||
transition: padding 0.25s;
|
transition: padding 0.25s;
|
||||||
|
|
||||||
@ -61,6 +63,7 @@ const Body = styled(Column)<{ open: boolean }>`
|
|||||||
|
|
||||||
${Estimate} {
|
${Estimate} {
|
||||||
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
|
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
|
||||||
|
min-height: 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: ${({ open }) => (open ? 0 : '1em 0')};
|
padding: ${({ open }) => (open ? 0 : '1em 0')};
|
||||||
transition: ${({ open }) =>
|
transition: ${({ open }) =>
|
||||||
@ -87,6 +90,8 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
|
|
||||||
const independentField = useAtomValue(independentFieldAtom)
|
const independentField = useAtomValue(independentFieldAtom)
|
||||||
|
|
||||||
|
const slippageWarning = useMemo(() => allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE), [allowedSlippage])
|
||||||
|
|
||||||
const [confirmedTrade, setConfirmedTrade] = useState(trade)
|
const [confirmedTrade, setConfirmedTrade] = useState(trade)
|
||||||
const doesTradeDiffer = useMemo(
|
const doesTradeDiffer = useMemo(
|
||||||
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
|
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
|
||||||
@ -115,7 +120,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
<Rule />
|
<Rule />
|
||||||
<Row>
|
<Row>
|
||||||
<Row gap={0.5}>
|
<Row gap={0.5}>
|
||||||
<Info color="secondary" />
|
{slippageWarning ? <AlertTriangle color="warning" /> : <Info color="secondary" />}
|
||||||
<ThemedText.Subhead2 color="secondary">
|
<ThemedText.Subhead2 color="secondary">
|
||||||
<Trans>Swap details</Trans>
|
<Trans>Swap details</Trans>
|
||||||
</ThemedText.Subhead2>
|
</ThemedText.Subhead2>
|
||||||
|
@ -1,40 +1,52 @@
|
|||||||
import { Placement } from '@popperjs/core'
|
import { Placement } from '@popperjs/core'
|
||||||
import { HelpCircle, Icon } from 'lib/icons'
|
import { HelpCircle, Icon } from 'lib/icons'
|
||||||
import styled from 'lib/theme'
|
import styled from 'lib/theme'
|
||||||
import { ReactNode, useState } from 'react'
|
import { ComponentProps, ReactNode, useCallback, useState } from 'react'
|
||||||
|
|
||||||
import { IconButton } from './Button'
|
import { IconButton } from './Button'
|
||||||
import Popover from './Popover'
|
import Popover from './Popover'
|
||||||
|
|
||||||
|
export interface TooltipHandlers {
|
||||||
|
onMouseEnter: () => void
|
||||||
|
onMouseLeave: () => void
|
||||||
|
onFocus: () => void
|
||||||
|
onBlur: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTooltip(): [boolean, (show: boolean) => void, TooltipHandlers] {
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
const enable = useCallback(() => setShow(true), [])
|
||||||
|
const disable = useCallback(() => setShow(false), [])
|
||||||
|
return [show, setShow, { onMouseEnter: enable, onMouseLeave: disable, onFocus: enable, onBlur: disable }]
|
||||||
|
}
|
||||||
|
|
||||||
const IconTooltip = styled(IconButton)`
|
const IconTooltip = styled(IconButton)`
|
||||||
:hover {
|
:hover {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
interface TooltipInterface {
|
interface TooltipProps {
|
||||||
icon?: Icon
|
icon?: Icon
|
||||||
|
iconProps?: ComponentProps<Icon>
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
placement: Placement
|
placement?: Placement
|
||||||
|
offset?: number
|
||||||
contained?: true
|
contained?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tooltip({
|
export default function Tooltip({
|
||||||
icon: Icon = HelpCircle,
|
icon: Icon = HelpCircle,
|
||||||
|
iconProps,
|
||||||
children,
|
children,
|
||||||
placement = 'auto',
|
placement = 'auto',
|
||||||
|
offset,
|
||||||
contained,
|
contained,
|
||||||
}: TooltipInterface) {
|
}: TooltipProps) {
|
||||||
const [show, setShow] = useState(false)
|
const [showTooltip, , tooltipProps] = useTooltip()
|
||||||
return (
|
return (
|
||||||
<Popover content={children} show={show} placement={placement} contained={contained}>
|
<Popover content={children} show={showTooltip} placement={placement} offset={offset} contained={contained}>
|
||||||
<IconTooltip
|
<IconTooltip icon={Icon} iconProps={iconProps} {...tooltipProps} />
|
||||||
onMouseEnter={() => setShow(true)}
|
|
||||||
onMouseLeave={() => setShow(false)}
|
|
||||||
onFocus={() => setShow(true)}
|
|
||||||
onBlur={() => setShow(false)}
|
|
||||||
icon={Icon}
|
|
||||||
/>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { L2_CHAIN_IDS } from 'constants/chains'
|
|||||||
import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
|
import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { transactionTtlAtom } from 'lib/state/settings'
|
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/settings'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import useActiveWeb3React from './useActiveWeb3React'
|
import useActiveWeb3React from './useActiveWeb3React'
|
||||||
@ -11,7 +11,7 @@ import useActiveWeb3React from './useActiveWeb3React'
|
|||||||
// combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction
|
// combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction
|
||||||
export default function useTransactionDeadline(): BigNumber | undefined {
|
export default function useTransactionDeadline(): BigNumber | undefined {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const userDeadline = useAtomValue(transactionTtlAtom)
|
const userDeadline: number = useAtomValue(transactionTtlAtom) || TRANSACTION_TTL_DEFAULT
|
||||||
const blockTimestamp = useCurrentBlockTimestamp()
|
const blockTimestamp = useCurrentBlockTimestamp()
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (blockTimestamp && chainId && L2_CHAIN_IDS.includes(chainId)) return blockTimestamp.add(L2_DEADLINE_FROM_NOW)
|
if (blockTimestamp && chainId && L2_CHAIN_IDS.includes(chainId)) return blockTimestamp.add(L2_DEADLINE_FROM_NOW)
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
Slash as SlashIcon,
|
Slash as SlashIcon,
|
||||||
Trash2 as Trash2Icon,
|
Trash2 as Trash2Icon,
|
||||||
X as XIcon,
|
X as XIcon,
|
||||||
|
XOctagon as XOctagonIcon,
|
||||||
} from 'react-feather'
|
} from 'react-feather'
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from '../assets/svg/check.svg'
|
import { ReactComponent as CheckIcon } from '../assets/svg/check.svg'
|
||||||
@ -36,8 +37,6 @@ function icon(Icon: FeatherIcon | SVGIcon) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Icon = ReturnType<typeof icon>
|
|
||||||
|
|
||||||
export const largeIconCss = css<{ iconSize: number }>`
|
export const largeIconCss = css<{ iconSize: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@ -50,11 +49,14 @@ export const largeIconCss = css<{ iconSize: number }>`
|
|||||||
|
|
||||||
const LargeWrapper = styled.div<{ iconSize: number }>`
|
const LargeWrapper = styled.div<{ iconSize: number }>`
|
||||||
height: 1em;
|
height: 1em;
|
||||||
|
width: ${({ iconSize }) => iconSize}em;
|
||||||
${largeIconCss}
|
${largeIconCss}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export type Icon = ReturnType<typeof icon> | typeof LargeIcon
|
||||||
|
|
||||||
interface LargeIconProps {
|
interface LargeIconProps {
|
||||||
icon: Icon
|
icon?: Icon
|
||||||
color?: Color
|
color?: Color
|
||||||
size?: number
|
size?: number
|
||||||
className?: string
|
className?: string
|
||||||
@ -63,7 +65,7 @@ interface LargeIconProps {
|
|||||||
export function LargeIcon({ icon: Icon, color, size = 1.2, className }: LargeIconProps) {
|
export function LargeIcon({ icon: Icon, color, size = 1.2, className }: LargeIconProps) {
|
||||||
return (
|
return (
|
||||||
<LargeWrapper color={color} iconSize={size} className={className}>
|
<LargeWrapper color={color} iconSize={size} className={className}>
|
||||||
<Icon color={color} />
|
{Icon && <Icon color={color} />}
|
||||||
</LargeWrapper>
|
</LargeWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -83,6 +85,7 @@ export const Settings = icon(SettingsIcon)
|
|||||||
export const Slash = icon(SlashIcon)
|
export const Slash = icon(SlashIcon)
|
||||||
export const Trash2 = icon(Trash2Icon)
|
export const Trash2 = icon(Trash2Icon)
|
||||||
export const X = icon(XIcon)
|
export const X = icon(XIcon)
|
||||||
|
export const XOctagon = icon(XOctagonIcon)
|
||||||
|
|
||||||
export const Check = styled(icon(CheckIcon))`
|
export const Check = styled(icon(CheckIcon))`
|
||||||
circle {
|
circle {
|
||||||
|
@ -3,6 +3,9 @@ import { atomWithReset } from 'jotai/utils'
|
|||||||
|
|
||||||
import { pickAtom, setTogglable } from './atoms'
|
import { pickAtom, setTogglable } from './atoms'
|
||||||
|
|
||||||
|
export const MAX_VALID_SLIPPAGE = new Percent(1, 2)
|
||||||
|
export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
|
||||||
|
|
||||||
// transaction deadline in minutes, needs to be adjusted to seconds before use
|
// transaction deadline in minutes, needs to be adjusted to seconds before use
|
||||||
// @TODO(ianlapham): update this to be stored as seconds
|
// @TODO(ianlapham): update this to be stored as seconds
|
||||||
export const TRANSACTION_TTL_DEFAULT = 30
|
export const TRANSACTION_TTL_DEFAULT = 30
|
||||||
@ -17,7 +20,7 @@ interface Settings {
|
|||||||
|
|
||||||
const initialSettings: Settings = {
|
const initialSettings: Settings = {
|
||||||
maxSlippage: 'auto',
|
maxSlippage: 'auto',
|
||||||
transactionTtl: TRANSACTION_TTL_DEFAULT,
|
transactionTtl: undefined,
|
||||||
integratorFee: undefined,
|
integratorFee: undefined,
|
||||||
mockTogglable: true,
|
mockTogglable: true,
|
||||||
clientSideRouter: false,
|
clientSideRouter: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user