feat: confirm price impact (#3288)
* refactor: action button naming * feat: high price impact acknowledgement
This commit is contained in:
parent
b4aac94c2c
commit
8404c6076c
@ -15,7 +15,7 @@ const StyledButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const UpdateRow = styled(Row)``
|
const ActionRow = styled(Row)``
|
||||||
|
|
||||||
const grow = keyframes`
|
const grow = keyframes`
|
||||||
from {
|
from {
|
||||||
@ -28,12 +28,12 @@ const grow = keyframes`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const updateCss = css`
|
const actionCss = css`
|
||||||
border: 1px solid ${({ theme }) => theme.outline};
|
border: 1px solid ${({ theme }) => theme.outline};
|
||||||
padding: calc(0.25em - 1px);
|
padding: calc(0.25em - 1px);
|
||||||
padding-left: calc(0.75em - 1px);
|
padding-left: calc(0.75em - 1px);
|
||||||
|
|
||||||
${UpdateRow} {
|
${ActionRow} {
|
||||||
animation: ${grow} 0.25s ease-in;
|
animation: ${grow} 0.25s ease-in;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@ -45,45 +45,37 @@ const updateCss = css`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Overlay = styled(Row)<{ update?: boolean }>`
|
export const Overlay = styled(Row)<{ action?: boolean }>`
|
||||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
min-height: 3.5em;
|
min-height: 3.5em;
|
||||||
transition: padding 0.25s ease-out;
|
transition: padding 0.25s ease-out;
|
||||||
|
|
||||||
${({ update }) => update && updateCss}
|
${({ action }) => action && actionCss}
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface ActionButtonProps {
|
export interface ActionButtonProps {
|
||||||
color?: Color
|
color?: Color
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
|
action?: { message: ReactNode; icon?: Icon; onClick: () => void; children: ReactNode }
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onUpdate?: () => void
|
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionButton({
|
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
|
||||||
color = 'accent',
|
|
||||||
disabled,
|
|
||||||
update,
|
|
||||||
onClick,
|
|
||||||
onUpdate,
|
|
||||||
children,
|
|
||||||
}: ActionButtonProps) {
|
|
||||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||||
return (
|
return (
|
||||||
<Overlay update={Boolean(update)} flex align="stretch">
|
<Overlay action={Boolean(action)} flex align="stretch">
|
||||||
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
|
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
|
||||||
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color={textColor}>
|
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
|
||||||
{update ? update.action : children}
|
{action ? action.children : children}
|
||||||
</ThemedText.TransitionButton>
|
</ThemedText.TransitionButton>
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
{update && (
|
{action && (
|
||||||
<UpdateRow gap={0.5}>
|
<ActionRow gap={0.5}>
|
||||||
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
|
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
|
||||||
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
|
<ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
|
||||||
</UpdateRow>
|
</ActionRow>
|
||||||
)}
|
)}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
)
|
)
|
||||||
|
@ -36,7 +36,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
|||||||
error={this.state.error}
|
error={this.state.error}
|
||||||
header={<Trans>Something went wrong.</Trans>}
|
header={<Trans>Something went wrong.</Trans>}
|
||||||
action={<Trans>Reload the page</Trans>}
|
action={<Trans>Reload the page</Trans>}
|
||||||
onAction={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
@ -87,10 +87,10 @@ interface ErrorDialogProps {
|
|||||||
header?: ReactNode
|
header?: ReactNode
|
||||||
error: Error
|
error: Error
|
||||||
action: ReactNode
|
action: ReactNode
|
||||||
onAction: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ErrorDialog({ header, error, action, onAction }: ErrorDialogProps) {
|
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||||
const scrollbar = useScrollbar(details)
|
const scrollbar = useScrollbar(details)
|
||||||
@ -123,7 +123,7 @@ export default function ErrorDialog({ header, error, action, onAction }: ErrorDi
|
|||||||
</ThemedText.Code>
|
</ThemedText.Code>
|
||||||
</Column>
|
</Column>
|
||||||
</ErrorColumn>
|
</ErrorColumn>
|
||||||
<ActionButton onClick={onAction}>{action}</ActionButton>
|
<ActionButton onClick={onClick}>{action}</ActionButton>
|
||||||
</ExpandoColumn>
|
</ExpandoColumn>
|
||||||
</Column>
|
</Column>
|
||||||
)
|
)
|
||||||
|
@ -93,7 +93,7 @@ export default function TransactionStatusDialog({ tx, onClose }: TransactionStat
|
|||||||
header={errorMessage}
|
header={errorMessage}
|
||||||
error={new Error('TODO(zzmp)')}
|
error={new Error('TODO(zzmp)')}
|
||||||
action={<Trans>Dismiss</Trans>}
|
action={<Trans>Dismiss</Trans>}
|
||||||
onAction={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TransactionStatus tx={tx} onClose={onClose} />
|
<TransactionStatus tx={tx} onClose={onClose} />
|
||||||
|
@ -6,7 +6,7 @@ import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constant
|
|||||||
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 { AlertTriangle, Expando, Info } from 'lib/icons'
|
import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
|
||||||
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
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'
|
||||||
@ -79,8 +79,6 @@ const Body = styled(Column)<{ open: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const priceUpdate = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
|
|
||||||
|
|
||||||
interface SummaryDialogProps {
|
interface SummaryDialogProps {
|
||||||
trade: Trade<Currency, Currency, TradeType>
|
trade: Trade<Currency, Currency, TradeType>
|
||||||
allowedSlippage: Percent
|
allowedSlippage: Percent
|
||||||
@ -92,8 +90,12 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
const inputCurrency = inputAmount.currency
|
const inputCurrency = inputAmount.currency
|
||||||
const outputCurrency = outputAmount.currency
|
const outputCurrency = outputAmount.currency
|
||||||
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
||||||
|
|
||||||
const independentField = useAtomValue(independentFieldAtom)
|
const independentField = useAtomValue(independentFieldAtom)
|
||||||
|
const { i18n } = useLingui()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||||
|
const scrollbar = useScrollbar(details)
|
||||||
|
|
||||||
const warning = useMemo(() => {
|
const warning = useMemo(() => {
|
||||||
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error'
|
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error'
|
||||||
@ -102,18 +104,31 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
return
|
return
|
||||||
}, [allowedSlippage, priceImpact])
|
}, [allowedSlippage, priceImpact])
|
||||||
|
|
||||||
|
const [ackPriceImpact, setAckPriceImpact] = useState(false)
|
||||||
|
|
||||||
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)),
|
||||||
[confirmedTrade, trade]
|
[confirmedTrade, trade]
|
||||||
)
|
)
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
const action = useMemo(() => {
|
||||||
|
if (doesTradeDiffer) {
|
||||||
const scrollbar = useScrollbar(details)
|
return {
|
||||||
|
message: <Trans>Price updated</Trans>,
|
||||||
const { i18n } = useLingui()
|
icon: BarChart,
|
||||||
|
onClick: () => setConfirmedTrade(trade),
|
||||||
|
children: <Trans>Accept</Trans>,
|
||||||
|
}
|
||||||
|
} else if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH) && !ackPriceImpact) {
|
||||||
|
return {
|
||||||
|
message: <Trans>High price impact</Trans>,
|
||||||
|
onClick: () => setAckPriceImpact(true),
|
||||||
|
children: <Trans>Acknowledge</Trans>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
|
||||||
|
|
||||||
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
|
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
|
||||||
return null
|
return null
|
||||||
@ -163,11 +178,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
|||||||
</Trans>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
</Estimate>
|
</Estimate>
|
||||||
<ActionButton
|
<ActionButton onClick={onConfirm} action={action}>
|
||||||
onClick={onConfirm}
|
|
||||||
onUpdate={() => setConfirmedTrade(trade)}
|
|
||||||
update={doesTradeDiffer ? priceUpdate : undefined}
|
|
||||||
>
|
|
||||||
<Trans>Confirm swap</Trans>
|
<Trans>Confirm swap</Trans>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</ExpandoColumn>
|
</ExpandoColumn>
|
||||||
|
@ -95,8 +95,9 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
</Row>
|
</Row>
|
||||||
</EtherscanLink>
|
</EtherscanLink>
|
||||||
),
|
),
|
||||||
action: <Trans>Approve</Trans>,
|
|
||||||
icon: Spinner,
|
icon: Spinner,
|
||||||
|
onClick: addApprovalTransaction,
|
||||||
|
children: <Trans>Approve</Trans>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if (approval === ApprovalState.NOT_APPROVED) {
|
} else if (approval === ApprovalState.NOT_APPROVED) {
|
||||||
@ -111,7 +112,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { disabled: true }
|
return { disabled: true }
|
||||||
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
}, [addApprovalTransaction, approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
||||||
|
|
||||||
const deadline = useTransactionDeadline()
|
const deadline = useTransactionDeadline()
|
||||||
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
|
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
|
||||||
@ -156,7 +157,6 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
color={tokenColorExtraction ? 'interactive' : 'accent'}
|
color={tokenColorExtraction ? 'interactive' : 'accent'}
|
||||||
onClick={() => setActiveTrade(trade.trade)}
|
onClick={() => setActiveTrade(trade.trade)}
|
||||||
onUpdate={addApprovalTransaction}
|
|
||||||
{...actionProps}
|
{...actionProps}
|
||||||
>
|
>
|
||||||
<Trans>Review swap</Trans>
|
<Trans>Review swap</Trans>
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
ArrowDown as ArrowDownIcon,
|
ArrowDown as ArrowDownIcon,
|
||||||
ArrowRight as ArrowRightIcon,
|
ArrowRight as ArrowRightIcon,
|
||||||
ArrowUp as ArrowUpIcon,
|
ArrowUp as ArrowUpIcon,
|
||||||
|
BarChart2 as BarChart2Icon,
|
||||||
CheckCircle as CheckCircleIcon,
|
CheckCircle as CheckCircleIcon,
|
||||||
ChevronDown as ChevronDownIcon,
|
ChevronDown as ChevronDownIcon,
|
||||||
Clock as ClockIcon,
|
Clock as ClockIcon,
|
||||||
@ -75,6 +76,7 @@ export const ArrowDown = icon(ArrowDownIcon)
|
|||||||
export const ArrowRight = icon(ArrowRightIcon)
|
export const ArrowRight = icon(ArrowRightIcon)
|
||||||
export const ArrowUp = icon(ArrowUpIcon)
|
export const ArrowUp = icon(ArrowUpIcon)
|
||||||
export const CheckCircle = icon(CheckCircleIcon)
|
export const CheckCircle = icon(CheckCircleIcon)
|
||||||
|
export const BarChart = icon(BarChart2Icon)
|
||||||
export const ChevronDown = icon(ChevronDownIcon)
|
export const ChevronDown = icon(ChevronDownIcon)
|
||||||
export const Clock = icon(ClockIcon)
|
export const Clock = icon(ClockIcon)
|
||||||
export const HelpCircle = icon(HelpCircleIcon)
|
export const HelpCircle = icon(HelpCircleIcon)
|
||||||
|
Loading…
Reference in New Issue
Block a user