diff --git a/cypress/e2e/permit2.test.ts b/cypress/e2e/permit2.test.ts index e3fa74af7e..9ad65c2f9f 100644 --- a/cypress/e2e/permit2.test.ts +++ b/cypress/e2e/permit2.test.ts @@ -75,8 +75,9 @@ describe('Permit2', () => { cy.contains('Allow DAI to be used for swapping') cy.wait('@eth_signTypedData_v4') cy.wait('@eth_sendRawTransaction') + cy.contains('Swap submitted') cy.hardhat().then((hardhat) => hardhat.mine()) - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') expectPermit2AllowanceForUniversalRouterToBeMax(DAI) }) @@ -99,7 +100,7 @@ describe('Permit2', () => { // Verify transaction cy.wait('@eth_sendRawTransaction') cy.hardhat().then((hardhat) => hardhat.mine()) - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') }) @@ -142,7 +143,7 @@ describe('Permit2', () => { // Verify transaction cy.wait('@eth_sendRawTransaction') cy.hardhat().then((hardhat) => hardhat.mine()) - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') }) }) @@ -158,7 +159,7 @@ describe('Permit2', () => { initiateSwap() // Verify transaction - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') }) @@ -197,7 +198,7 @@ describe('Permit2', () => { cy.contains('Confirm swap').click() // Verify permit2 approval - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') expectPermit2AllowanceForUniversalRouterToBeMax(DAI) }) @@ -231,7 +232,7 @@ describe('Permit2', () => { // Verify permit2 approval cy.wait('@eth_signTypedData_v4') - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') expectPermit2AllowanceForUniversalRouterToBeMax(DAI) }) @@ -249,7 +250,7 @@ describe('Permit2', () => { // Verify permit2 approval cy.wait('@eth_signTypedData_v4') - cy.contains('Success') + cy.contains('Swap success!') cy.get(getTestSelector('popups')).contains('Swapped') expectPermit2AllowanceForUniversalRouterToBeMax(DAI) }) diff --git a/src/components/swap/PendingModalContent/Logos.tsx b/src/components/swap/PendingModalContent/Logos.tsx index 938e6c510e..efb3c745b0 100644 --- a/src/components/swap/PendingModalContent/Logos.tsx +++ b/src/components/swap/PendingModalContent/Logos.tsx @@ -162,7 +162,38 @@ function ConfirmedIcon({ className }: { className?: string }) { ) } -export const AnimatedEntranceConfirmationIcon = styled(ConfirmedIcon)` +function SubmittedIcon({ className }: { className?: string }) { + const theme = useTheme() + return ( + + + + + + ) +} + +const IconCss = css` + position: absolute; height: 48px; width: 48px; ` + +export const AnimatedEntranceConfirmationIcon = styled(ConfirmedIcon)` + ${IconCss} +` + +export const AnimatedEntranceSubmittedIcon = styled(SubmittedIcon)` + ${IconCss} +` diff --git a/src/components/swap/PendingModalContent/PendingModalContent.test.tsx b/src/components/swap/PendingModalContent/PendingModalContent.test.tsx index 8bd0599ed8..9b1bc5cdb5 100644 --- a/src/components/swap/PendingModalContent/PendingModalContent.test.tsx +++ b/src/components/swap/PendingModalContent/PendingModalContent.test.tsx @@ -128,6 +128,38 @@ describe('PendingModalContent', () => { expect(screen.queryByTestId('pending-modal-currency-logo-loader')).toBeNull() }) + it('renders the submitted icon instead of the given logo on mainnet when the transaction is submitted', () => { + mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Pending) + + render( + + ) + expect(screen.queryByTestId('pending-modal-failure-icon')).toBeNull() + expect(screen.queryByTestId('pending-modal-currency-logo-loader')).toBeNull() + expect(screen.getByTestId('submitted-icon')).toBeInTheDocument() + }) + it('renders the success icon instead of the given logo when confirmed and successful', () => { mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Confirmed) diff --git a/src/components/swap/PendingModalContent/index.tsx b/src/components/swap/PendingModalContent/index.tsx index cb8c800f81..04abaaa4fc 100644 --- a/src/components/swap/PendingModalContent/index.tsx +++ b/src/components/swap/PendingModalContent/index.tsx @@ -9,7 +9,7 @@ import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' import { SwapResult } from 'hooks/useSwapCallback' import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation' import { UniswapXOrderStatus } from 'lib/hooks/orders/types' -import { ReactNode, useRef } from 'react' +import { ReactNode, useMemo, useRef } from 'react' import { InterfaceTrade, TradeFillType } from 'state/routing/types' import { useOrder } from 'state/signatures/hooks' import { UniswapXOrderDetails } from 'state/signatures/types' @@ -23,6 +23,7 @@ import { ExplorerDataType } from 'utils/getExplorerLink' import { ConfirmModalState } from '../ConfirmSwapModal' import { AnimatedEntranceConfirmationIcon, + AnimatedEntranceSubmittedIcon, AnimationType, CurrencyLoader, LoadingIndicatorOverlay, @@ -102,7 +103,7 @@ export type PendingConfirmModalState = Extract< interface PendingModalStep { title: ReactNode subtitle?: ReactNode - label?: ReactNode + bottomLabel?: ReactNode logo?: ReactNode button?: ReactNode } @@ -119,7 +120,6 @@ interface PendingModalContentProps { } interface ContentArgs { - step: PendingConfirmModalState approvalCurrency?: Currency trade?: InterfaceTrade swapConfirmed: boolean @@ -132,9 +132,59 @@ interface ContentArgs { order?: UniswapXOrderDetails } -function getContent(args: ContentArgs): PendingModalStep { +function getPendingConfirmationContent({ + swapConfirmed, + swapPending, + trade, + chainId, + swapResult, +}: Pick): PendingModalStep { + const title = swapPending ? t`Swap submitted` : swapConfirmed ? t`Swap success!` : t`Confirm Swap` + const tradeSummary = trade ? : null + if (swapPending && trade?.fillType === TradeFillType.UniswapX) { + return { + title, + subtitle: tradeSummary, + bottomLabel: ( + + Learn more about swapping with UniswapX + + ), + } + } else if ((swapPending || swapConfirmed) && chainId && swapResult?.type === TradeFillType.Classic) { + const explorerLink = ( + + View on Explorer + + ) + if (swapPending) { + // On Mainnet, we show a "submitted" state while the transaction is pending confirmation. + return { + title, + subtitle: chainId === ChainId.MAINNET ? explorerLink : tradeSummary, + bottomLabel: chainId === ChainId.MAINNET ? t`Transaction pending...` : explorerLink, + } + } else { + return { + title, + subtitle: explorerLink, + bottomLabel: null, + } + } + } else { + return { + title, + subtitle: tradeSummary, + bottomLabel: t`Proceed in your wallet`, + } + } +} + +function useStepContents(args: ContentArgs): Record { const { - step, wrapPending, approvalCurrency, swapConfirmed, @@ -146,70 +196,60 @@ function getContent(args: ContentArgs): PendingModalStep { chainId, } = args - switch (step) { - case ConfirmModalState.WRAPPING: - return { + return useMemo( + () => ({ + [ConfirmModalState.WRAPPING]: { title: t`Wrap ETH`, subtitle: ( Why is this required? ), - label: wrapPending ? t`Pending...` : t`Proceed in your wallet`, - } - case ConfirmModalState.RESETTING_USDT: - return { + bottomLabel: wrapPending ? t`Pending...` : t`Proceed in your wallet`, + }, + [ConfirmModalState.RESETTING_USDT]: { title: t`Reset USDT`, subtitle: t`USDT requires resetting approval when spending limits are too low.`, - label: revocationPending ? t`Pending...` : t`Proceed in your wallet`, - } - case ConfirmModalState.APPROVING_TOKEN: - return { + bottomLabel: revocationPending ? t`Pending...` : t`Proceed in your wallet`, + }, + [ConfirmModalState.APPROVING_TOKEN]: { title: t`Enable spending ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`, subtitle: ( Why is this required? ), - label: tokenApprovalPending ? t`Pending...` : t`Proceed in your wallet`, - } - case ConfirmModalState.PERMITTING: - return { + bottomLabel: tokenApprovalPending ? t`Pending...` : t`Proceed in your wallet`, + }, + [ConfirmModalState.PERMITTING]: { title: t`Allow ${approvalCurrency?.symbol ?? 'this token'} to be used for swapping`, subtitle: ( Why is this required? ), - label: t`Proceed in your wallet`, - } - case ConfirmModalState.PENDING_CONFIRMATION: { - let labelText: string | null = null - let href: string | null = null - - if (swapPending && trade?.fillType === TradeFillType.UniswapX) { - labelText = t`Learn more about swapping with UniswapX` - href = 'https://support.uniswap.org/hc/en-us/articles/17515415311501' - } else if (chainId && (swapConfirmed || swapPending) && swapResult && swapResult.type === TradeFillType.Classic) { - labelText = t`View on Explorer` - href = getExplorerLink(chainId, swapResult.response.hash, ExplorerDataType.TRANSACTION) - } else { - labelText = t`Proceed in your wallet` - } - - return { - title: swapPending ? t`Swap submitted` : swapConfirmed ? t`Success` : t`Confirm Swap`, - subtitle: trade ? : null, - label: href ? ( - - {labelText} - - ) : ( - labelText - ), - } - } - } + bottomLabel: t`Proceed in your wallet`, + }, + [ConfirmModalState.PENDING_CONFIRMATION]: getPendingConfirmationContent({ + chainId, + swapConfirmed, + swapPending, + swapResult, + trade, + }), + }), + [ + approvalCurrency?.symbol, + chainId, + revocationPending, + swapConfirmed, + swapPending, + swapResult, + tokenApprovalPending, + trade, + wrapPending, + ] + ) } export function PendingModalContent({ @@ -236,8 +276,7 @@ export function PendingModalContent({ const swapPending = swapResult !== undefined && !swapConfirmed const wrapPending = wrapTxHash != undefined && !wrapConfirmed - const { label, button } = getContent({ - step: currentStep, + const stepContents = useStepContents({ approvalCurrency: trade?.inputAmount.currency, swapConfirmed, swapPending, @@ -263,8 +302,11 @@ export function PendingModalContent({ return } - // On mainnet, we show the success icon once the tx is sent, since it takes longer to confirm than on L2s. - const showSuccess = swapConfirmed || (swapPending && chainId === ChainId.MAINNET) + // On mainnet, we show a different icon when the transaction is submitted but pending confirmation. + const showSubmitted = swapPending && !swapConfirmed && chainId === ChainId.MAINNET + const showSuccess = swapConfirmed || (chainId !== ChainId.MAINNET && swapPending) + + const transactionPending = revocationPending || tokenApprovalPending || wrapPending || swapPending return ( @@ -282,31 +324,18 @@ export function PendingModalContent({ )} {/* Shown only during the final step under "success" conditions, and scales in. */} {currentStep === ConfirmModalState.PENDING_CONFIRMATION && showSuccess && } - {/* Scales in for the USDT revoke allowance step if the revoke is pending onchain confirmation. */} - {/* Scales in for the setup approval step if the approval is pending onchain confirmation. */} - {/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */} - {((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) || - tokenApprovalPending || - wrapPending || - revocationPending) && } + {/* Shown only during the final step on mainnet, when the transaction is sent but pending confirmation. */} + {currentStep === ConfirmModalState.PENDING_CONFIRMATION && showSubmitted && } + {/* Scales in for any step that waits for an onchain transaction, while the transaction is pending. */} + {/* On the last step, appears while waiting for the transaction to be signed too. */} + {((currentStep !== ConfirmModalState.PENDING_CONFIRMATION && transactionPending) || + (currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess && !showSubmitted)) && ( + + )} - + {steps.map((step) => { - const { title, subtitle } = getContent({ - step, - approvalCurrency: trade?.inputAmount.currency, - swapConfirmed, - swapPending, - wrapPending, - revocationPending, - tokenApprovalPending, - swapResult, - trade, - }) // We only render one step at a time, but looping through the array allows us to keep // the exiting step in the DOM during its animation. return ( @@ -318,19 +347,19 @@ export function PendingModalContent({ ref={step === currentStep ? currentStepContainerRef : undefined} > - {title} + {stepContents[step].title} - {subtitle} + {stepContents[step].subtitle} ) ) })} - {label} + {stepContents[currentStep].bottomLabel} - {button && {button}} + {stepContents[currentStep].button && {stepContents[currentStep].button}} {!hideStepIndicators && !showSuccess && ( {steps.map((_, i) => {