From 8d145b908ea840505f1866f5e9ac9f973ada4e42 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 25 Jan 2022 16:24:36 -0800 Subject: [PATCH] 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 --- src/constants/chainInfo.ts | 8 ++-- src/lib/components/ActionButton.tsx | 30 +++++++------ src/lib/components/Swap/Summary/index.tsx | 2 +- src/lib/components/Swap/SwapButton.tsx | 55 +++++++++++++++++++---- src/lib/hooks/swap/useSwapApproval.ts | 30 ++++++++----- src/lib/hooks/transactions/index.tsx | 11 ++--- src/lib/icons/index.tsx | 2 + 7 files changed, 97 insertions(+), 41 deletions(-) diff --git a/src/constants/chainInfo.ts b/src/constants/chainInfo.ts index bfc1594158..261ba9d74d 100644 --- a/src/constants/chainInfo.ts +++ b/src/constants/chainInfo.ts @@ -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' diff --git a/src/lib/components/ActionButton.tsx b/src/lib/components/ActionButton.tsx index f5cf0921a0..61f8b61381 100644 --- a/src/lib/components/ActionButton.tsx +++ b/src/lib/components/ActionButton.tsx @@ -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 ( - - - - {updated ? updated.action : children} + + + + {update ? update.action : children} - {updated && ( + {update && ( - - {updated?.message} + + {update?.message} )} diff --git a/src/lib/components/Swap/Summary/index.tsx b/src/lib/components/Swap/Summary/index.tsx index 32c026452f..1a9b986ede 100644 --- a/src/lib/components/Swap/Summary/index.tsx +++ b/src/lib/components/Swap/Summary/index.tsx @@ -147,7 +147,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial setConfirmedTrade(trade)} - updated={doesTradeDiffer ? priceUpdate : undefined} + update={doesTradeDiffer ? priceUpdate : undefined} > Confirm swap diff --git a/src/lib/components/Swap/SwapButton.tsx b/src/lib/components/Swap/SwapButton.tsx index 2c047860b6..1f2d991ca9 100644 --- a/src/lib/components/Swap/SwapButton.tsx +++ b/src/lib/components/Swap/SwapButton.tsx @@ -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(undefined) + const [activeTrade, setActiveTrade] = useState() 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: ( + + + + Approval pending + + + + ), + action: Approve, + icon: Spinner, + }, + } } else if (approval === ApprovalState.NOT_APPROVED) { return { - updated: { + update: { message: Approve {inputCurrencyAmount.currency.symbol} first, action: Approve, }, @@ -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. diff --git a/src/lib/hooks/swap/useSwapApproval.ts b/src/lib/hooks/swap/useSwapApproval.ts index 30be3bce7e..ed975a6c10 100644 --- a/src/lib/hooks/swap/useSwapApproval.ts +++ b/src/lib/hooks/swap/useSwapApproval.ts @@ -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 | V3Trade | Trade - | 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 + | V3Trade + | Trade + | 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) { diff --git a/src/lib/hooks/transactions/index.tsx b/src/lib/hooks/transactions/index.tsx index 560b50c9e8..ddf208c853 100644 --- a/src/lib/hooks/transactions/index.tsx +++ b/src/lib/hooks/transactions/index.tsx @@ -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() { diff --git a/src/lib/icons/index.tsx b/src/lib/icons/index.tsx index c3eb2533cc..3e3a5d626f 100644 --- a/src/lib/icons/index.tsx +++ b/src/lib/icons/index.tsx @@ -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)