feat: uniswapx deadline (#7376)
* feat: uniswapX time-to-sign * fix: animation timing * fix: bug * fix: improve props and remove memo
This commit is contained in:
parent
d63bdf1887
commit
e6362212c6
@ -28,6 +28,7 @@ import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { isL2ChainId } from 'utils/chains'
|
||||
import { SignatureExpiredError } from 'utils/errors'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
import { formatSwapPriceUpdatedEventProperties } from 'utils/loggingFormatters'
|
||||
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
|
||||
@ -265,6 +266,7 @@ export default function ConfirmSwapModal({
|
||||
onAcceptChanges,
|
||||
allowedSlippage,
|
||||
allowance,
|
||||
clearSwapState,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
onCurrencySelection,
|
||||
@ -280,6 +282,7 @@ export default function ConfirmSwapModal({
|
||||
allowedSlippage: Percent
|
||||
allowance: Allowance
|
||||
onAcceptChanges: () => void
|
||||
clearSwapState: () => void
|
||||
onConfirm: () => void
|
||||
swapError?: Error
|
||||
onDismiss: () => void
|
||||
@ -293,7 +296,10 @@ export default function ConfirmSwapModal({
|
||||
useConfirmModalState({
|
||||
trade,
|
||||
allowedSlippage,
|
||||
onSwap: onConfirm,
|
||||
onSwap: () => {
|
||||
clearSwapState()
|
||||
onConfirm()
|
||||
},
|
||||
onCurrencySelection,
|
||||
allowance,
|
||||
doesTradeDiffer: Boolean(doesTradeDiffer),
|
||||
@ -376,6 +382,8 @@ export default function ConfirmSwapModal({
|
||||
wrapTxHash={wrapTxHash}
|
||||
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
|
||||
revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending}
|
||||
swapError={swapError}
|
||||
onRetryUniswapXSignature={onConfirm}
|
||||
/>
|
||||
)
|
||||
}, [
|
||||
@ -386,13 +394,14 @@ export default function ConfirmSwapModal({
|
||||
swapResult,
|
||||
wrapTxHash,
|
||||
allowance,
|
||||
swapError,
|
||||
startSwapFlow,
|
||||
allowedSlippage,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
onAcceptChanges,
|
||||
swapFailed,
|
||||
swapError?.message,
|
||||
startSwapFlow,
|
||||
onConfirm,
|
||||
])
|
||||
|
||||
const l2Badge = () => {
|
||||
@ -410,14 +419,20 @@ export default function ConfirmSwapModal({
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getErrorType = () => {
|
||||
if (approvalError) return approvalError
|
||||
// SignatureExpiredError is a special case. The UI is shown in the PendingModalContent component.
|
||||
if (swapError instanceof SignatureExpiredError) return
|
||||
if (swapError && !didUserReject(swapError)) return PendingModalError.CONFIRMATION_ERROR
|
||||
return
|
||||
}
|
||||
const errorType = getErrorType()
|
||||
|
||||
return (
|
||||
<Trace modal={InterfaceModalName.CONFIRM_SWAP}>
|
||||
<Modal isOpen $scrollOverlay onDismiss={onModalDismiss} maxHeight={90}>
|
||||
{approvalError || swapFailed ? (
|
||||
<ErrorModalContent
|
||||
errorType={approvalError ?? PendingModalError.CONFIRMATION_ERROR}
|
||||
onRetry={startSwapFlow}
|
||||
/>
|
||||
{errorType ? (
|
||||
<ErrorModalContent errorType={errorType} onRetry={startSwapFlow} />
|
||||
) : (
|
||||
<ConfirmationModalContent
|
||||
title={confirmModalState === ConfirmModalState.REVIEWING ? <Trans>Review swap</Trans> : undefined}
|
||||
|
81
src/components/swap/PendingModalContent/TransitionText.tsx
Normal file
81
src/components/swap/PendingModalContent/TransitionText.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { slideInAnimation, slideOutAnimation } from './animations'
|
||||
import { AnimationType } from './Logos'
|
||||
|
||||
interface TransitionTextProps {
|
||||
initialText: ReactNode
|
||||
transitionText: ReactNode
|
||||
transitionTimeMs?: number
|
||||
onTransition?: () => void
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
`
|
||||
|
||||
const InitialTextContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: display ${({ theme }) => `${theme.transition.duration.fast} ${theme.transition.timing.inOut}`};
|
||||
${slideInAnimation}
|
||||
&.${AnimationType.EXITING} {
|
||||
${slideOutAnimation}
|
||||
}
|
||||
`
|
||||
|
||||
const TransitionTextContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: display ${({ theme }) => `${theme.transition.duration.fast} ${theme.transition.timing.inOut}`};
|
||||
${slideInAnimation}
|
||||
&.${AnimationType.EXITING} {
|
||||
${slideOutAnimation}
|
||||
}
|
||||
`
|
||||
|
||||
export function TransitionText({
|
||||
initialText,
|
||||
transitionText,
|
||||
transitionTimeMs = 1500,
|
||||
onTransition,
|
||||
}: TransitionTextProps) {
|
||||
const [transitioned, setTransitioned] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Transition from initial text to transition text.
|
||||
const timeout = setTimeout(() => {
|
||||
if (!transitioned) {
|
||||
setTransitioned(true)
|
||||
onTransition?.()
|
||||
}
|
||||
}, transitionTimeMs)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [onTransition, transitionTimeMs, transitioned])
|
||||
|
||||
const initialTextRef = useRef<HTMLDivElement>(null)
|
||||
useUnmountingAnimation(initialTextRef, () => AnimationType.EXITING)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{!transitioned && (
|
||||
<InitialTextContainer ref={initialTextRef}>
|
||||
<Trans>{initialText}</Trans>
|
||||
</InitialTextContainer>
|
||||
)}
|
||||
{transitioned && (
|
||||
<TransitionTextContainer>
|
||||
<Trans>{transitionText}</Trans>
|
||||
</TransitionTextContainer>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
16
src/components/swap/PendingModalContent/animations.ts
Normal file
16
src/components/swap/PendingModalContent/animations.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { css, keyframes } from 'styled-components'
|
||||
|
||||
const slideIn = keyframes`
|
||||
from { opacity: 0; transform: translateX(40px) }
|
||||
to { opacity: 1; transform: translateX(0px) }
|
||||
`
|
||||
export const slideInAnimation = css`
|
||||
animation: ${slideIn} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
`
|
||||
const slideOut = keyframes`
|
||||
from { opacity: 1; transform: translateX(0px) }
|
||||
to { opacity: 0; transform: translateX(-40px) }
|
||||
`
|
||||
export const slideOutAnimation = css`
|
||||
animation: ${slideOut} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
`
|
@ -14,13 +14,15 @@ import { InterfaceTrade, TradeFillType } from 'state/routing/types'
|
||||
import { useOrder } from 'state/signatures/hooks'
|
||||
import { UniswapXOrderDetails } from 'state/signatures/types'
|
||||
import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks'
|
||||
import styled, { css, keyframes } from 'styled-components'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { ExternalLink } from 'theme/components'
|
||||
import { ThemedText } from 'theme/components/text'
|
||||
import { SignatureExpiredError } from 'utils/errors'
|
||||
import { getExplorerLink } from 'utils/getExplorerLink'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
|
||||
import { ConfirmModalState } from '../ConfirmSwapModal'
|
||||
import { slideInAnimation, slideOutAnimation } from './animations'
|
||||
import {
|
||||
AnimatedEntranceConfirmationIcon,
|
||||
AnimatedEntranceSubmittedIcon,
|
||||
@ -31,6 +33,7 @@ import {
|
||||
PaperIcon,
|
||||
} from './Logos'
|
||||
import { TradeSummary } from './TradeSummary'
|
||||
import { TransitionText } from './TransitionText'
|
||||
|
||||
export const PendingModalContainer = styled(ColumnCenter)`
|
||||
margin: 48px 0 8px;
|
||||
@ -51,21 +54,6 @@ const StepCircle = styled.div<{ active: boolean }>`
|
||||
transition: background-color ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
`
|
||||
|
||||
const slideIn = keyframes`
|
||||
from { opacity: 0; transform: translateX(40px) }
|
||||
to { opacity: 1; transform: translateX(0px) }
|
||||
`
|
||||
const slideInAnimation = css`
|
||||
animation: ${slideIn} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
`
|
||||
const slideOut = keyframes`
|
||||
from { opacity: 1; transform: translateX(0px) }
|
||||
to { opacity: 0; transform: translateX(-40px) }
|
||||
`
|
||||
const slideOutAnimation = css`
|
||||
animation: ${slideOut} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
`
|
||||
|
||||
const AnimationWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -77,7 +65,10 @@ const AnimationWrapper = styled.div`
|
||||
const StepTitleAnimationContainer = styled(Column)<{ disableEntranceAnimation?: boolean }>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: display ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.inOut}`};
|
||||
${({ disableEntranceAnimation }) =>
|
||||
!disableEntranceAnimation &&
|
||||
@ -117,6 +108,8 @@ interface PendingModalContentProps {
|
||||
hideStepIndicators?: boolean
|
||||
tokenApprovalPending?: boolean
|
||||
revocationPending?: boolean
|
||||
swapError?: Error | string
|
||||
onRetryUniswapXSignature?: () => void
|
||||
}
|
||||
|
||||
interface ContentArgs {
|
||||
@ -130,6 +123,8 @@ interface ContentArgs {
|
||||
swapResult?: SwapResult
|
||||
chainId?: number
|
||||
order?: UniswapXOrderDetails
|
||||
swapError?: Error | string
|
||||
onRetryUniswapXSignature?: () => void
|
||||
}
|
||||
|
||||
function getPendingConfirmationContent({
|
||||
@ -138,7 +133,12 @@ function getPendingConfirmationContent({
|
||||
trade,
|
||||
chainId,
|
||||
swapResult,
|
||||
}: Pick<ContentArgs, 'swapConfirmed' | 'swapPending' | 'trade' | 'chainId' | 'swapResult'>): PendingModalStep {
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
}: Pick<
|
||||
ContentArgs,
|
||||
'swapConfirmed' | 'swapPending' | 'trade' | 'chainId' | 'swapResult' | 'swapError' | 'onRetryUniswapXSignature'
|
||||
>): PendingModalStep {
|
||||
const title = swapPending ? t`Swap submitted` : swapConfirmed ? t`Swap success!` : t`Confirm Swap`
|
||||
const tradeSummary = trade ? <TradeSummary trade={trade} /> : null
|
||||
if (swapPending && trade?.fillType === TradeFillType.UniswapX) {
|
||||
@ -174,6 +174,19 @@ function getPendingConfirmationContent({
|
||||
bottomLabel: null,
|
||||
}
|
||||
}
|
||||
} else if (swapError instanceof SignatureExpiredError) {
|
||||
return {
|
||||
title: (
|
||||
<TransitionText
|
||||
key={swapError.id}
|
||||
initialText={<Trans>Time expired</Trans>}
|
||||
transitionText={<Trans>Retry confirmation</Trans>}
|
||||
onTransition={onRetryUniswapXSignature}
|
||||
/>
|
||||
),
|
||||
subtitle: tradeSummary,
|
||||
bottomLabel: t`Proceed in your wallet`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title,
|
||||
@ -194,6 +207,8 @@ function useStepContents(args: ContentArgs): Record<PendingConfirmModalState, Pe
|
||||
trade,
|
||||
swapResult,
|
||||
chainId,
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
} = args
|
||||
|
||||
return useMemo(
|
||||
@ -236,6 +251,8 @@ function useStepContents(args: ContentArgs): Record<PendingConfirmModalState, Pe
|
||||
swapPending,
|
||||
swapResult,
|
||||
trade,
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
}),
|
||||
}),
|
||||
[
|
||||
@ -248,6 +265,8 @@ function useStepContents(args: ContentArgs): Record<PendingConfirmModalState, Pe
|
||||
tokenApprovalPending,
|
||||
trade,
|
||||
wrapPending,
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
]
|
||||
)
|
||||
}
|
||||
@ -261,6 +280,8 @@ export function PendingModalContent({
|
||||
hideStepIndicators,
|
||||
tokenApprovalPending = false,
|
||||
revocationPending = false,
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
}: PendingModalContentProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
@ -283,6 +304,8 @@ export function PendingModalContent({
|
||||
swapResult,
|
||||
trade,
|
||||
chainId,
|
||||
swapError,
|
||||
onRetryUniswapXSignature,
|
||||
})
|
||||
|
||||
const currentStepContainerRef = useRef<HTMLDivElement>(null)
|
||||
@ -341,7 +364,7 @@ export function PendingModalContent({
|
||||
key={step}
|
||||
ref={step === currentStep ? currentStepContainerRef : undefined}
|
||||
>
|
||||
<ThemedText.SubHeaderLarge textAlign="center" data-testid="pending-modal-content-title">
|
||||
<ThemedText.SubHeaderLarge width="100%" textAlign="center" data-testid="pending-modal-content-title">
|
||||
{stepContents[step].title}
|
||||
</ThemedText.SubHeaderLarge>
|
||||
<ThemedText.LabelSmall textAlign="center">{stepContents[step].subtitle}</ThemedText.LabelSmall>
|
||||
|
@ -9,7 +9,7 @@ import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
|
||||
import { useCallback } from 'react'
|
||||
import { DutchOrderTrade, TradeFillType } from 'state/routing/types'
|
||||
import { trace } from 'tracing/trace'
|
||||
import { UserRejectedRequestError } from 'utils/errors'
|
||||
import { SignatureExpiredError, UserRejectedRequestError } from 'utils/errors'
|
||||
import { signTypedData } from 'utils/signing'
|
||||
import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
|
||||
|
||||
@ -90,11 +90,14 @@ export function useUniswapXSwapCallback({
|
||||
const { domain, types, values } = updatedOrder.permitData()
|
||||
|
||||
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
|
||||
if (deadline < Math.floor(Date.now() / 1000)) {
|
||||
return signDutchOrder()
|
||||
if (startTime < Math.floor(Date.now() / 1000)) {
|
||||
throw new SignatureExpiredError()
|
||||
}
|
||||
return { signature, updatedOrder }
|
||||
} catch (swapError) {
|
||||
if (swapError instanceof SignatureExpiredError) {
|
||||
throw swapError
|
||||
}
|
||||
if (didUserReject(swapError)) {
|
||||
setTraceStatus('cancelled')
|
||||
throw new UserRejectedRequestError(swapErrorToUserReadableMessage(swapError))
|
||||
|
@ -457,6 +457,14 @@ export function Swap({
|
||||
})
|
||||
}, [trade])
|
||||
|
||||
const clearSwapState = useCallback(() => {
|
||||
setSwapState((currentState) => ({
|
||||
...currentState,
|
||||
swapError: undefined,
|
||||
swapResult: undefined,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const handleSwap = useCallback(() => {
|
||||
if (!swapCallback) {
|
||||
return
|
||||
@ -464,11 +472,6 @@ export function Swap({
|
||||
if (preTaxStablecoinPriceImpact && !confirmPriceImpactWithoutFee(preTaxStablecoinPriceImpact)) {
|
||||
return
|
||||
}
|
||||
setSwapState((currentState) => ({
|
||||
...currentState,
|
||||
swapError: undefined,
|
||||
swapResult: undefined,
|
||||
}))
|
||||
swapCallback()
|
||||
.then((result) => {
|
||||
setSwapState((currentState) => ({
|
||||
@ -613,6 +616,7 @@ export function Swap({
|
||||
onCurrencySelection={onCurrencySelection}
|
||||
swapResult={swapResult}
|
||||
allowedSlippage={allowedSlippage}
|
||||
clearSwapState={clearSwapState}
|
||||
onConfirm={handleSwap}
|
||||
allowance={allowance}
|
||||
swapError={swapError}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// You may throw an instance of this class when the user rejects a request in their wallet.
|
||||
|
||||
import { t } from '@lingui/macro'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
// You may throw an instance of this class when the user rejects a request in their wallet.
|
||||
// The benefit is that you can distinguish this error from other errors using didUserReject().
|
||||
export class UserRejectedRequestError extends Error {
|
||||
constructor(message: string) {
|
||||
@ -23,3 +23,16 @@ export class WrongChainError extends Error {
|
||||
super(t`Your wallet is connected to the wrong network.`)
|
||||
}
|
||||
}
|
||||
|
||||
export class SignatureExpiredError extends Error {
|
||||
private _id: string
|
||||
constructor() {
|
||||
super(t`Your signature has expired.`)
|
||||
this.name = 'SignatureExpiredError'
|
||||
this._id = `SignatureExpiredError-${uuid()}`
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user