Commit 8c2a0f19 authored by eddie's avatar eddie Committed by GitHub

feat: update content in Swap Submission Modal (#6577)

* test: swap flow cypress tests

* fix: use default parameter

* feat: use Swap Component on TDP

* feat: auto nav for TDP tokens

* chore: merge

* chore: merge

* chore: merge

* chore: merge

* fix: remove extra inputCurrency URL parsing logic

* fix: undo last change

* fix: pass expected chain id to swap component

* fix: search for default tokens on unconnected networks if needed

* test: e2e test for l2 token

* fix: delete irrelevant tests

* fix: address comments

* fix: lint error

* test: update TDP e2e tests

* fix: use pageChainId for filter

* fix: rename chainId

* fix: typecheck

* fix: chainId bug

* fix: chainId required fixes

* fix: bad merge in e2e test

* fix: remove unused test util

* fix: remove unnecessary variable

* fix: token defaults

* fix: address comments

* fix: address comments and fix tests

* fix: e2e test formatting, remove Maybe<>

* fix: remove unused variable

* fix: use feature flag for swap component on TDP

* fix: back button

* feat: copy review screen UI from widgetg

* fix: modal padding

* feat: add final detail row

* fix: remove widget comment

* fix: update unit tests

* fix: code style consistency

* fix: remove padding from AutoColumn

* fix: update snapshots

* fix: use semantic gaps

* fix: more px and gaps

* fix: design feedbacks

* fix: button radius in summary modal

* fix: design nits

* feat: update design of summary modal

* fix: font weight and vertical spacing

* fix: update snapshots

* fix: css nits

* wip: move approval to summary modal

* wip: not working

* feat: working

* fix: fix flow

* feat: simplify states and build new modal UI

* feat: todos and differs fix

* feat: update tx status modal

* feat: split up approve and permit

* feat: error state

* feat: update success and error states

* feat: undo changes to TxConfirmationModal

* feat: re-order functions

* wip: move approval to summary modal

* wip: not working

* feat: update permit2 e2e tests

* feat: tests passing

* fix: swap test

* fix: bad merge

* wip: move approval to summary modal

* wip: not working

* feat: PendingModalContent tests

* feat: useMaxAmountIn

* fix: bad merge

* fix: naming

* fix: modal flicker when refetching trade

* wip: move approval to summary modal

* wip: not working

* feat: working

* fix: fix flow

* feat: simplify states and build new modal UI

* feat: todos and differs fix

* feat: update tx status modal

* feat: split up approve and permit

* feat: error state

* feat: update success and error states

* feat: undo changes to TxConfirmationModal

* feat: remove step indicators when only one step

* feat: move content into PendingModalContent component

* fix: lint

* chore: merge

* fix: update tests for new modal

* feat: add l2 chain logo to modal

* feat: add unit test

* fix: correct modal state when moving between steps

* fix: correct modal state when moving between steps

* fix: proper error handling of user rejection of swap

* feat: update e2e test

* fix: typecheck

* feat: design updates, state updates

* fix: comments

* fix: code style improvements

* feat: require trade to be defined

* fix: remove extra props from ThemedTexts

* fix: one more trans

* fix: remove unused export

* feat: remove undefined checks and other fixes

* fix: update test

* fix: add missing dollar sign

* fix: remove null check and update test

* fix: remove max width from detail row value

* fix: remove isOpen prop

* fix: isopen

* feat: refactor approval flow into a hook

* fix: custom error type

* fix: testid fix

* fix: text colors

* fix: add comment

* fix: tradeMeaningfullyDiffers improvement and prepareFlow fix

* fix: address  comments

* fix: headerContent prop

* fix: change tooltip to external link

* feat: add comments explaining async state

* fix: test updates

* fix: nits

* fix: reduce nesting

* fix: address comments

* test: remove line from test for debugging

* fix: update tests

* fix: address  comments

* fix: comments

* fix: update tests

* fix: update tests

* fix: more nesting in test

* fix: update test

* fix: update e2e test

* fix: update error test

* fix: update content in test

* fix: reorganize test code
parent 682fba21
......@@ -60,10 +60,10 @@ describe('Permit2', () => {
it('swaps after completing full permit2 approval process', () => {
initiateSwap()
cy.contains('Approve permit').should('exist')
cy.contains('Allow trading DAI on Uniswap').should('exist')
cy.contains('Approved').should('exist')
cy.contains('Approve DAI').should('exist')
cy.contains('Unlock DAI for swapping').should('exist')
cy.contains('Confirm Swap').should('exist')
cy.then(() => {
......@@ -96,7 +96,7 @@ describe('Permit2', () => {
})
cy.get(getTestSelector('confirm-swap-button')).click()
cy.contains('Approve permit').should('exist')
cy.contains('Allow trading DAI on Uniswap').should('exist')
cy.contains('Approved').should('exist')
// permitApprovalStub should reject here, and the modal should revert to the review state.
......@@ -200,7 +200,7 @@ describe('Permit2', () => {
.then(() => {
initiateSwap()
const approvalTime = Date.now()
cy.contains('Approve permit').should('exist')
cy.contains('Allow trading DAI on Uniswap').should('exist')
cy.contains('Confirm Swap').should('exist')
cy.contains('Swapped').should('exist')
......
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2601 18.3918C12.9161 18.5558 12.5141 18.6779 12.0591 18.7599L5.48907 19.9099C3.29907 20.2999 2.00896 19.3999 1.62896 17.2099L0.0891657 8.45987C-0.300834 6.26987 0.599117 4.97988 2.78912 4.58988L4.58697 4.27384C4.79197 4.23784 4.97114 4.41686 4.93414 4.62186L3.75811 11.2799C3.22811 14.3099 4.65803 16.3499 7.67803 16.8799C7.67803 16.8799 12.8971 17.7969 13.1661 17.8489C13.4981 17.9088 13.5511 18.2628 13.2601 18.3918ZM19.9131 5.10587L18.3689 13.8598C17.997 15.9678 16.7811 16.8688 14.7361 16.5888C14.6581 16.5778 14.5881 16.5779 14.5071 16.5639L7.94195 15.4059C5.75295 15.0199 4.85209 13.7329 5.23809 11.5439L6.58111 3.92783L6.78204 2.78983C7.16804 0.600828 8.4551 -0.300151 10.6441 0.0858488L17.21 1.24387C19.398 1.62987 20.2991 2.91787 19.9131 5.10587ZM13.554 11.8958C13.626 11.4878 13.3541 11.0988 12.9461 11.0268L8.8421 10.3028C8.4361 10.2298 8.04518 10.5039 7.97418 10.9109C7.90218 11.3189 8.17409 11.7079 8.58209 11.7799L12.6861 12.5039C12.7301 12.5119 12.7739 12.5149 12.8169 12.5149C13.1739 12.5159 13.49 12.2598 13.554 11.8958ZM16.597 9.03482C16.669 8.62682 16.3971 8.23787 15.9891 8.16587L9.42413 7.00785C9.02013 6.93685 8.62696 7.20888 8.55596 7.61588C8.48396 8.02388 8.75612 8.41284 9.16412 8.48484L15.7291 9.64286C15.7731 9.65086 15.8172 9.65384 15.8602 9.65384C16.2172 9.65384 16.533 9.39782 16.597 9.03482ZM17.2972 5.77286C17.3692 5.36486 17.097 4.97584 16.689 4.90384L10.1241 3.74582C9.72008 3.67382 9.32716 3.94685 9.25616 4.35385C9.18416 4.76185 9.45607 5.15087 9.86407 5.22287L16.429 6.38083C16.473 6.38883 16.5171 6.39188 16.5601 6.39188C16.9171 6.39288 17.2332 6.13686 17.2972 5.77286Z" fill="#F5F6FC" />
</svg>
......@@ -258,8 +258,9 @@ export default function ConfirmSwapModal({
hideStepIndicators={pendingModalSteps.length === 1}
steps={pendingModalSteps}
currentStep={confirmModalState}
approvalCurrency={trade?.inputAmount?.currency}
txHash={txHash}
trade={trade}
swapTxHash={txHash}
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
/>
)
}, [
......@@ -268,6 +269,7 @@ export default function ConfirmSwapModal({
pendingModalSteps,
trade,
txHash,
allowance,
allowedSlippage,
swapQuoteReceivedDate,
fiatValueInput,
......
import { DAI_MAINNET } from '@uniswap/smart-order-router'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import { TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { mocked } from 'test-utils/mocked'
import { render, screen } from 'test-utils/render'
......@@ -24,15 +24,12 @@ describe('PendingModalContent', () => {
<PendingModalContent
steps={[ConfirmModalState.APPROVING_TOKEN]}
currentStep={ConfirmModalState.APPROVING_TOKEN}
approvalCurrency={DAI_MAINNET}
trade={TEST_TRADE_EXACT_INPUT}
/>
)
expect(screen.getByText('Approve permit')).toBeInTheDocument()
expect(screen.getByText('Proceed in wallet')).toBeInTheDocument()
expect(screen.getByText('Why are permits required?')).toBeInTheDocument()
expect(
screen.getByText('Permit2 allows token approvals to be shared and managed across different applications.')
).toBeInTheDocument()
expect(screen.getByText('Allow trading ABC on Uniswap')).toBeInTheDocument()
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
})
describe('renders the correct step when there are multiple', () => {
......@@ -45,22 +42,13 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION,
]}
currentStep={ConfirmModalState.APPROVING_TOKEN}
approvalCurrency={DAI_MAINNET}
trade={TEST_TRADE_EXACT_INPUT}
/>
)
expect(screen.getByText('Approve permit')).toBeInTheDocument()
expect(screen.getByText('Proceed in wallet')).toBeInTheDocument()
expect(screen.getByText('Why are permits required?')).toBeInTheDocument()
expect(
screen.getByText('Permit2 allows token approvals to be shared and managed across different applications.')
).toBeInTheDocument()
expect(screen.queryByText('Approve DAI')).toBeNull()
expect(screen.queryByText('Why are approvals required?')).toBeNull()
expect(
screen.queryByText(
'This provides the Uniswap protocol access to your token for trading. For security, this will expire after 30 days.'
)
).toBeNull()
expect(screen.getByText('Allow trading ABC on Uniswap')).toBeInTheDocument()
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
expect(screen.queryByText('Unlock ABC for swapping')).not.toBeInTheDocument()
})
it('renders the second step with activeStepIndex=1', () => {
......@@ -72,22 +60,13 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION,
]}
currentStep={ConfirmModalState.PERMITTING}
approvalCurrency={DAI_MAINNET}
trade={TEST_TRADE_EXACT_INPUT}
/>
)
expect(screen.queryByText('Approve permit')).toBeNull()
expect(screen.queryByText('Why are permits required?')).toBeNull()
expect(
screen.queryByText('Permit2 allows token approvals to be shared and managed across different applications.')
).toBeNull()
expect(screen.queryByText('Approve DAI')).toBeInTheDocument()
expect(screen.queryByText('Proceed in wallet')).toBeInTheDocument()
expect(screen.queryByText('Why are approvals required?')).toBeInTheDocument()
expect(
screen.queryByText(
'This provides the Uniswap protocol access to your token for trading. For security, this will expire after 30 days.'
)
).toBeInTheDocument()
expect(screen.getByText('Unlock ABC for swapping')).toBeInTheDocument()
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
expect(screen.queryByText('Allow trading ABC on Uniswap')).not.toBeInTheDocument()
})
})
......@@ -101,10 +80,10 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION,
]}
currentStep={ConfirmModalState.APPROVING_TOKEN}
approvalCurrency={DAI_MAINNET}
trade={TEST_TRADE_EXACT_INPUT}
/>
)
expect(screen.getByTestId('pending-modal-currency-logo-loader-DAI')).toBeInTheDocument()
expect(screen.getByTestId('papers-icon-container-ABC')).toBeInTheDocument()
expect(screen.queryByTestId('pending-modal-failure-icon')).toBeNull()
})
......@@ -125,7 +104,6 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION,
]}
currentStep={ConfirmModalState.PENDING_CONFIRMATION}
approvalCurrency={DAI_MAINNET}
/>
)
expect(screen.queryByTestId('pending-modal-failure-icon')).toBeNull()
......
import { t, Trans } from '@lingui/macro'
import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { NumberType } from '@uniswap/conedison/format'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ButtonPrimary } from 'components/Button'
import { ColumnCenter } from 'components/Column'
import Loader from 'components/Icons/LoadingSpinner'
......@@ -7,18 +10,26 @@ import CurrencyLogo from 'components/Logo/CurrencyLogo'
import QuestionHelper from 'components/QuestionHelper'
import Row from 'components/Row'
import AnimatedConfirmation from 'components/TransactionConfirmationModal/AnimatedConfirmation'
import { SupportedChainId } from 'constants/chains'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { AlertTriangle, ArrowRight } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
import { ExternalLink } from 'theme'
import { ThemedText } from 'theme/components/text'
import { ReactComponent as PapersIcon } from '../../assets/svg/papers-text.svg'
import { ConfirmModalState } from './ConfirmSwapModal'
const Container = styled(ColumnCenter)`
margin: 48px 0 28px;
`
const HeaderContainer = styled(ColumnCenter)<{ $disabled?: boolean }>`
${({ $disabled }) => $disabled && `opacity: 0.5;`}
`
const LogoContainer = styled.div`
position: relative;
display: flex;
......@@ -52,6 +63,70 @@ const LoadingIndicator = styled(Loader)`
position: absolute;
`
function CurrencyLoader({ currency }: { currency?: Currency }) {
const theme = useTheme()
return (
<LogoContainer data-testid={`pending-modal-currency-logo-loader-${currency?.symbol}`}>
<LogoLayer>
<CurrencyLogo currency={currency} size="48px" />
</LogoLayer>
<LoadingIndicator stroke={theme.textTertiary} />
</LogoContainer>
)
}
const PinkCircle = styled(LogoContainer)`
display: flex;
height: 48px;
width: 48px;
align-items: center;
justify-content: center;
background-color: ${({ theme }) => theme.userThemeColor};
z-index: 1;
`
function PaperIcon({ currency, loading }: { currency?: Currency; loading: boolean }) {
const theme = useTheme()
return (
<LogoContainer data-testid={`papers-icon-container-${currency?.symbol}`}>
<PinkCircle>
<PapersIcon />
<CurrencyLogo
currency={currency}
size="20px"
style={{
position: 'absolute',
bottom: '-4px',
right: '-4px',
outline: `2px solid ${theme.background}`,
borderRadius: '50%',
}}
/>
</PinkCircle>
{loading && <LoadingIndicator stroke={theme.textTertiary} />}
</LogoContainer>
)
}
function TradeSummary({ trade }: { trade: InterfaceTrade }) {
const theme = useTheme()
return (
<Row gap="sm">
<CurrencyLogo currency={trade.inputAmount.currency} size="16px" />
<ThemedText.LabelSmall color="textPrimary">
{formatCurrencyAmount(trade.inputAmount, NumberType.SwapTradeAmount)}
</ThemedText.LabelSmall>
<ThemedText.LabelSmall color="textPrimary">{trade.inputAmount.currency.symbol}</ThemedText.LabelSmall>
<ArrowRight color={theme.textPrimary} size="12px" />
<CurrencyLogo currency={trade.outputAmount.currency} size="16px" />
<ThemedText.LabelSmall color="textPrimary">
{formatCurrencyAmount(trade.outputAmount, NumberType.SwapTradeAmount)}
</ThemedText.LabelSmall>
<ThemedText.LabelSmall color="textPrimary">{trade.outputAmount.currency.symbol}</ThemedText.LabelSmall>
</Row>
)
}
// This component is used for all steps after ConfirmModalState.REVIEWING
export type PendingConfirmModalState = Extract<
ConfirmModalState,
......@@ -70,51 +145,73 @@ interface PendingModalStep {
interface PendingModalContentProps {
steps: PendingConfirmModalState[]
currentStep: PendingConfirmModalState
approvalCurrency?: Currency
trade?: InterfaceTrade
swapTxHash?: string
hideStepIndicators?: boolean
txHash?: string
tokenApprovalPending?: boolean
}
function CurrencyLoader({ currency }: { currency?: Currency }) {
const theme = useTheme()
return (
<LogoContainer data-testid={`pending-modal-currency-logo-loader-${currency?.symbol}`}>
<LogoLayer>
<CurrencyLogo currency={currency} size="48px" />
</LogoLayer>
<LoadingIndicator stroke={theme.textTertiary} />
</LogoContainer>
)
interface ContentArgs {
chainId?: number
step: PendingConfirmModalState
approvalCurrency?: Currency
trade?: InterfaceTrade
swapConfirmed: boolean
swapPending: boolean
tokenApprovalPending: boolean
theme: DefaultTheme
swapTxHash?: string
}
function getContent(
step: PendingConfirmModalState,
approvalCurrency: Currency | undefined,
confirmed: boolean,
theme: DefaultTheme
): PendingModalStep {
function getContent(args: ContentArgs): PendingModalStep {
const { chainId, step, approvalCurrency, swapConfirmed, swapPending, tokenApprovalPending, theme, trade } = args
switch (step) {
case ConfirmModalState.APPROVING_TOKEN:
return {
title: t`Approve permit`,
subtitle: t`Proceed in wallet`,
label: t`Why are permits required?`,
tooltipText: t`Permit2 allows token approvals to be shared and managed across different applications.`,
logo: <CurrencyLoader currency={approvalCurrency} />,
title: t`Allow trading ${approvalCurrency?.symbol ?? 'token'} on Uniswap`,
subtitle: (
<>
<Trans>First, we need your permission to use your DAI for swapping.</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/8120520483085">
<Trans>Why is this required?</Trans>
</ExternalLink>
</>
),
label: tokenApprovalPending ? t`Pending...` : t`Proceed in your wallet`,
logo: <PaperIcon currency={approvalCurrency} loading={tokenApprovalPending} />,
}
case ConfirmModalState.PERMITTING:
return {
title: t`Approve ${approvalCurrency?.symbol ?? 'token'}`,
subtitle: t`Proceed in wallet`,
label: t`Why are approvals required?`,
tooltipText: t`This provides the Uniswap protocol access to your token for trading. For security, this will expire after 30 days.`,
title: t`Unlock ${approvalCurrency?.symbol ?? 'token'} for swapping`,
subtitle: (
<>
<Trans>This will expire after 30 days for your security.</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/360056642192">
<Trans>Why is this required?</Trans>
</ExternalLink>
</>
),
label: t`Proceed in your wallet`,
logo: <CurrencyLoader currency={approvalCurrency} />,
}
case ConfirmModalState.PENDING_CONFIRMATION:
return {
title: t`Confirm Swap`,
subtitle: t`Proceed in wallet`,
logo: confirmed ? <SizedAnimatedConfirmation /> : <Loader stroke={theme.textTertiary} size="48px" />,
title: swapPending ? t`Transaction submitted` : swapConfirmed ? t`Success` : t`Confirm Swap`,
subtitle: swapConfirmed ? (
<ExternalLink href={`https://etherscan.io/tx/${swapConfirmed}`} color="textSecondary">
<Trans>View on Explorer</Trans>
</ExternalLink>
) : trade ? (
<TradeSummary trade={trade} />
) : null,
label: !swapPending && !swapConfirmed ? t`Proceed in your wallet` : null,
logo:
// On mainnet, we show the success icon once the tx is sent, since it takes longer to confirm than on L2s.
swapConfirmed || (swapPending && chainId === SupportedChainId.MAINNET) ? (
<SizedAnimatedConfirmation />
) : (
<Loader stroke={theme.textTertiary} size="48px" />
),
}
}
}
......@@ -122,39 +219,50 @@ function getContent(
export function PendingModalContent({
steps,
currentStep,
approvalCurrency,
txHash,
trade,
swapTxHash,
hideStepIndicators,
tokenApprovalPending = false,
}: PendingModalContentProps) {
const theme = useTheme()
const confirmed = useIsTransactionConfirmed(txHash)
const { logo, title, subtitle, label, tooltipText, button } = getContent(
currentStep,
approvalCurrency,
confirmed,
theme
)
const { chainId } = useWeb3React()
const swapConfirmed = useIsTransactionConfirmed(swapTxHash)
const swapPending = swapTxHash !== undefined && !swapConfirmed
const { logo, title, subtitle, label, button } = getContent({
chainId,
step: currentStep,
approvalCurrency: trade?.inputAmount.currency,
swapConfirmed,
swapPending,
tokenApprovalPending,
theme,
swapTxHash,
trade,
})
if (steps.length === 0) {
return null
}
return (
<Container gap="lg">
{logo}
{/* TODO: implement animations between title/subtitles of each step. */}
<ColumnCenter gap="md">
<HeaderContainer gap="md" $disabled={tokenApprovalPending || swapPending}>
<ThemedText.HeadlineSmall data-testid="PendingModalContent-title">{title}</ThemedText.HeadlineSmall>
{subtitle && (
<ThemedText.LabelSmall data-testid="PendingModalContent-subtitle">{subtitle}</ThemedText.LabelSmall>
<ThemedText.LabelSmall textAlign="center" data-testid="PendingModalContent-subtitle">
{subtitle}
</ThemedText.LabelSmall>
)}
<Row justify="center">
<Row justify="center" marginTop="32px">
{label && (
<ThemedText.Caption color="textSecondary" data-testid="PendingModalContent-label">
{label}
</ThemedText.Caption>
)}
{tooltipText && <QuestionHelper text={tooltipText} />}
</Row>
</ColumnCenter>
</HeaderContainer>
{button && (
<Row justify="center" data-testid="PendingModalContent-button">
{button}
......@@ -198,7 +306,7 @@ function getErrorContent(errorType: PendingModalError) {
}
case PendingModalError.CONFIRMATION_ERROR:
return {
title: t`Confirmation failed`,
title: t`Swap failed`,
}
}
}
......
......@@ -24,6 +24,7 @@ interface AllowanceRequired {
state: AllowanceState.REQUIRED
token: Token
isApprovalLoading: boolean
isApprovalPending: boolean
approveAndPermit: () => Promise<void>
approve: () => Promise<void>
permit: () => Promise<void>
......@@ -127,6 +128,7 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
token,
state: AllowanceState.REQUIRED,
isApprovalLoading: false,
isApprovalPending,
approveAndPermit,
approve,
permit,
......@@ -138,6 +140,7 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
token,
state: AllowanceState.REQUIRED,
isApprovalLoading,
isApprovalPending,
approveAndPermit,
approve,
permit,
......@@ -157,6 +160,7 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
approve,
approveAndPermit,
isApprovalLoading,
isApprovalPending,
isApproved,
isPermitted,
isSigned,
......
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