feat: pending approval ui (#3186)
* feat: track approval txs * refactor: update transactions * feat: pending approval ui * chore: fix pending approval doc * fix: clarify optimized trade * fix: use relative path for data uri assets
This commit is contained in:
parent
c7633d910b
commit
8d145b908e
@ -1,9 +1,9 @@
|
||||
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
|
||||
import arbitrumLogoUrl from 'assets/svg/arbitrum_logo.svg'
|
||||
import optimismLogoUrl from 'assets/svg/optimistic_ethereum.svg'
|
||||
import polygonMaticLogo from 'assets/svg/polygon-matic-logo.svg'
|
||||
import ms from 'ms.macro'
|
||||
|
||||
import ethereumLogoUrl from '../assets/images/ethereum-logo.png'
|
||||
import arbitrumLogoUrl from '../assets/svg/arbitrum_logo.svg'
|
||||
import optimismLogoUrl from '../assets/svg/optimistic_ethereum.svg'
|
||||
import polygonMaticLogo from '../assets/svg/polygon-matic-logo.svg'
|
||||
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
|
||||
import { ARBITRUM_LIST, OPTIMISM_LIST } from './lists'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AlertTriangle, LargeIcon } from 'lib/icons'
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
@ -9,6 +9,10 @@ const StyledButton = styled(Button)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
|
||||
|
||||
:disabled {
|
||||
margin: -1px;
|
||||
}
|
||||
`
|
||||
|
||||
const UpdateRow = styled(Row)``
|
||||
@ -24,7 +28,7 @@ const grow = keyframes`
|
||||
}
|
||||
`
|
||||
|
||||
const updatedCss = css`
|
||||
const updateCss = css`
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
padding: calc(0.25em - 1px);
|
||||
padding-left: calc(0.75em - 1px);
|
||||
@ -41,19 +45,19 @@ const updatedCss = css`
|
||||
}
|
||||
`
|
||||
|
||||
export const Overlay = styled(Row)<{ updated?: boolean }>`
|
||||
export const Overlay = styled(Row)<{ update?: boolean }>`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-direction: row-reverse;
|
||||
min-height: 3.5em;
|
||||
transition: padding 0.25s ease-out;
|
||||
|
||||
${({ updated }) => updated && updatedCss}
|
||||
${({ update }) => update && updateCss}
|
||||
`
|
||||
|
||||
export interface ActionButtonProps {
|
||||
color?: Color
|
||||
disabled?: boolean
|
||||
updated?: { message: ReactNode; action: ReactNode }
|
||||
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
|
||||
onClick: () => void
|
||||
onUpdate?: () => void
|
||||
children: ReactNode
|
||||
@ -62,22 +66,22 @@ export interface ActionButtonProps {
|
||||
export default function ActionButton({
|
||||
color = 'accent',
|
||||
disabled,
|
||||
updated,
|
||||
update,
|
||||
onClick,
|
||||
onUpdate,
|
||||
children,
|
||||
}: ActionButtonProps) {
|
||||
return (
|
||||
<Overlay updated={Boolean(updated)} flex align="stretch">
|
||||
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
|
||||
{updated ? updated.action : children}
|
||||
<Overlay update={Boolean(update)} flex align="stretch">
|
||||
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color="currentColor">
|
||||
{update ? update.action : children}
|
||||
</ThemedText.TransitionButton>
|
||||
</StyledButton>
|
||||
{updated && (
|
||||
{update && (
|
||||
<UpdateRow gap={0.5}>
|
||||
<LargeIcon icon={AlertTriangle} />
|
||||
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
|
||||
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
|
||||
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
|
||||
</UpdateRow>
|
||||
)}
|
||||
</Overlay>
|
||||
|
@ -147,7 +147,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
||||
<ActionButton
|
||||
onClick={onConfirm}
|
||||
onUpdate={() => setConfirmedTrade(trade)}
|
||||
updated={doesTradeDiffer ? priceUpdate : undefined}
|
||||
update={doesTradeDiffer ? priceUpdate : undefined}
|
||||
>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
|
@ -1,29 +1,50 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
||||
import useSwapApproval, {
|
||||
ApprovalState,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import { useIsPendingApproval } from 'lib/hooks/transactions'
|
||||
import { usePendingApproval } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { Link, Spinner } from 'lib/icons'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import styled from 'lib/theme'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import ActionButton from '../ActionButton'
|
||||
import Dialog from '../Dialog'
|
||||
import Row from '../Row'
|
||||
import { SummaryDialog } from './Summary'
|
||||
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const EtherscanA = styled.a`
|
||||
color: currentColor;
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const {
|
||||
trade,
|
||||
allowedSlippage,
|
||||
currencies: { [Field.INPUT]: inputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
|
||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>(undefined)
|
||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
|
||||
useEffect(() => {
|
||||
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
||||
}, [trade])
|
||||
@ -33,6 +54,10 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
|
||||
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
|
||||
const approvalHash = usePendingApproval(
|
||||
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||
useSwapRouterAddress(optimizedTrade)
|
||||
)
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const addApprovalTransaction = useCallback(() => {
|
||||
@ -46,13 +71,27 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const actionProps = useMemo(() => {
|
||||
if (disabled) return { disabled: true }
|
||||
|
||||
if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
|
||||
// TODO(zzmp): Update UI for pending approvals.
|
||||
if (chainId && inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
return { disabled: true }
|
||||
return {
|
||||
disabled: true,
|
||||
update: {
|
||||
message: (
|
||||
<EtherscanA href={approvalHash && `${CHAIN_INFO[chainId].explorer}tx/${approvalHash}`} target="_blank">
|
||||
<Row gap={0.25}>
|
||||
<Trans>
|
||||
Approval pending <Link />
|
||||
</Trans>
|
||||
</Row>
|
||||
</EtherscanA>
|
||||
),
|
||||
action: <Trans>Approve</Trans>,
|
||||
icon: Spinner,
|
||||
},
|
||||
}
|
||||
} else if (approval === ApprovalState.NOT_APPROVED) {
|
||||
return {
|
||||
updated: {
|
||||
update: {
|
||||
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
|
||||
action: <Trans>Approve</Trans>,
|
||||
},
|
||||
@ -62,7 +101,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
}
|
||||
|
||||
return { disabled: true }
|
||||
}, [approval, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
||||
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
// TODO(zzmp): Transact the trade.
|
||||
|
@ -34,22 +34,15 @@ function useSwapApprovalStates(
|
||||
return useMemo(() => ({ v2, v3, v2V3 }), [v2, v2V3, v3])
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
export default function useSwapApproval(
|
||||
export function useSwapRouterAddress(
|
||||
trade:
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined,
|
||||
allowedSlippage: Percent,
|
||||
useIsPendingApproval: (token?: Token, spender?: string) => boolean
|
||||
| undefined
|
||||
) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const amountToApprove = useMemo(
|
||||
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
const spender = useMemo(
|
||||
return useMemo(
|
||||
() =>
|
||||
chainId
|
||||
? trade instanceof V2Trade
|
||||
@ -60,6 +53,23 @@ export default function useSwapApproval(
|
||||
: undefined,
|
||||
[chainId, trade]
|
||||
)
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
export default function useSwapApproval(
|
||||
trade:
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined,
|
||||
allowedSlippage: Percent,
|
||||
useIsPendingApproval: (token?: Token, spender?: string) => boolean
|
||||
) {
|
||||
const amountToApprove = useMemo(
|
||||
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
const spender = useSwapRouterAddress(trade)
|
||||
|
||||
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
|
||||
if (trade instanceof V2Trade || trade instanceof V3Trade) {
|
||||
|
@ -40,15 +40,16 @@ export function useAddTransaction() {
|
||||
)
|
||||
}
|
||||
|
||||
export function useIsPendingApproval(token?: Token, spender?: string) {
|
||||
/** Returns the hash of a pending approval transaction, if it exists. */
|
||||
export function usePendingApproval(token?: Token, spender?: string): string | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const txs = useAtomValue(transactionsAtom)
|
||||
if (!chainId || !token || !spender) return false
|
||||
if (!chainId || !token || !spender) return undefined
|
||||
|
||||
const chainTxs = txs[chainId]
|
||||
if (!chainTxs) return false
|
||||
if (!chainTxs) return undefined
|
||||
|
||||
return Object.values(chainTxs).some(
|
||||
return Object.values(chainTxs).find(
|
||||
(tx) =>
|
||||
tx &&
|
||||
tx.receipt === undefined &&
|
||||
@ -56,7 +57,7 @@ export function useIsPendingApproval(token?: Token, spender?: string) {
|
||||
tx.info.tokenAddress === token.address &&
|
||||
tx.info.spenderAddress === spender &&
|
||||
isTransactionRecent(tx)
|
||||
)
|
||||
)?.info.response.hash
|
||||
}
|
||||
|
||||
export function TransactionsUpdater() {
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
ChevronDown as ChevronDownIcon,
|
||||
Clock as ClockIcon,
|
||||
CreditCard as CreditCardIcon,
|
||||
ExternalLink as LinkIcon,
|
||||
HelpCircle as HelpCircleIcon,
|
||||
Info as InfoIcon,
|
||||
Settings as SettingsIcon,
|
||||
@ -77,6 +78,7 @@ export const Clock = icon(ClockIcon)
|
||||
export const CreditCard = icon(CreditCardIcon)
|
||||
export const HelpCircle = icon(HelpCircleIcon)
|
||||
export const Info = icon(InfoIcon)
|
||||
export const Link = icon(LinkIcon)
|
||||
export const Settings = icon(SettingsIcon)
|
||||
export const Slash = icon(SlashIcon)
|
||||
export const Trash2 = icon(Trash2Icon)
|
||||
|
Loading…
Reference in New Issue
Block a user