1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-24 12:23:39 +02:00
metamask-extension/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js

342 lines
11 KiB
JavaScript
Raw Normal View History

2020-10-06 20:28:38 +02:00
import EventEmitter from 'events'
import React, { useContext, useRef, useState, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { useHistory } from 'react-router-dom'
import { I18nContext } from '../../../contexts/i18n'
import { useNewMetricEvent } from '../../../hooks/useMetricEvent'
import { MetaMetricsContext } from '../../../contexts/metametrics.new'
import { getCurrentCurrency, conversionRateSelector } from '../../../selectors'
import {
2020-11-03 00:41:28 +01:00
getUsedQuote,
getFetchParams,
getApproveTxParams,
getSwapsTradeTxParams,
2020-10-06 20:28:38 +02:00
fetchQuotesAndSetQuoteState,
navigateBackToBuildQuote,
prepareForRetryGetQuotes,
prepareToLeaveSwaps,
} from '../../../ducks/swaps/swaps'
import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRemaining'
import { usePrevious } from '../../../hooks/usePrevious'
import Mascot from '../../../components/ui/mascot'
import PulseLoader from '../../../components/ui/pulse-loader'
2020-11-03 00:41:28 +01:00
import {
getBlockExplorerUrlForTx,
getStatusKey,
} from '../../../helpers/utils/transactions.util'
2020-10-06 20:28:38 +02:00
import CountdownTimer from '../countdown-timer'
import {
QUOTES_EXPIRED_ERROR,
SWAP_FAILED_ERROR,
ERROR_FETCHING_QUOTES,
QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../../helpers/constants/swaps'
import { SUBMITTED_STATUS } from '../../../helpers/constants/transactions'
2020-10-06 20:28:38 +02:00
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'
import { getRenderableNetworkFeesForQuote } from '../swaps.util'
2020-10-06 20:28:38 +02:00
import SwapsFooter from '../swaps-footer'
import SwapFailureIcon from './swap-failure-icon'
import SwapSuccessIcon from './swap-success-icon'
import QuotesTimeoutIcon from './quotes-timeout-icon'
import ViewOnEtherScanLink from './view-on-ether-scan-link'
2020-11-03 00:41:28 +01:00
export default function AwaitingSwap({
2020-10-06 20:28:38 +02:00
swapComplete,
errorKey,
txHash,
networkId,
tokensReceived,
rpcPrefs,
submittingSwap,
tradeTxData,
usedGasPrice,
inputValue,
maxSlippage,
}) {
const t = useContext(I18nContext)
const metaMetricsEvent = useContext(MetaMetricsContext)
const history = useHistory()
const dispatch = useDispatch()
const animationEventEmitter = useRef(new EventEmitter())
const fetchParams = useSelector(getFetchParams)
const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {}
const usedQuote = useSelector(getUsedQuote)
const approveTxParams = useSelector(getApproveTxParams)
const tradeTxParams = useSelector(getSwapsTradeTxParams)
const currentCurrency = useSelector(getCurrentCurrency)
const conversionRate = useSelector(conversionRateSelector)
const [timeRemainingExpired, setTimeRemainingExpired] = useState(false)
2020-11-03 00:41:28 +01:00
const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState(
false,
)
2020-10-06 20:28:38 +02:00
let feeinFiat
if (usedQuote && tradeTxParams) {
const renderableNetworkFees = getRenderableNetworkFeesForQuote(
usedQuote.gasEstimateWithRefund || usedQuote.averageGas,
approveTxParams?.gas || '0x0',
tradeTxParams.gasPrice,
currentCurrency,
conversionRate,
tradeTxParams.value,
sourceTokenInfo?.symbol,
usedQuote.sourceAmount,
)
feeinFiat = renderableNetworkFees.feeinFiat?.slice(1)
2020-10-06 20:28:38 +02:00
}
const quotesExpiredEvent = useNewMetricEvent({
event: 'Quotes Timed Out',
excludeMetaMetricsId: true,
properties: {
token_from: sourceTokenInfo?.symbol,
token_from_amount: fetchParams?.value,
token_to: destinationTokenInfo?.symbol,
request_type: fetchParams?.balanceError ? 'Quote' : 'Order',
slippage: fetchParams?.slippage,
custom_slippage: fetchParams?.slippage === 2,
gas_fees: feeinFiat,
},
category: 'swaps',
})
2020-11-03 00:41:28 +01:00
const anonymousQuotesExpiredEvent = useNewMetricEvent({
event: 'Quotes Timed Out',
category: 'swaps',
})
2020-10-06 20:28:38 +02:00
2020-11-03 00:41:28 +01:00
const blockExplorerUrl =
txHash && getBlockExplorerUrlForTx(networkId, txHash, rpcPrefs)
2020-10-06 20:28:38 +02:00
const statusKey = tradeTxData && getStatusKey(tradeTxData)
const timeRemaining = useTransactionTimeRemaining(
statusKey === SUBMITTED_STATUS,
true,
tradeTxData?.submittedTime,
usedGasPrice,
true,
true,
)
2020-10-06 20:28:38 +02:00
const previousTimeRemaining = usePrevious(timeRemaining)
2020-11-03 00:41:28 +01:00
const timeRemainingIsNumber =
typeof timeRemaining === 'number' && !isNaN(timeRemaining)
const previousTimeRemainingIsNumber =
typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining)
2020-10-06 20:28:38 +02:00
const estimatedTransactionWaitTime = timeRemaining * 1000 * 60
useEffect(() => {
2020-11-03 00:41:28 +01:00
if (
!timeRemainingIsNumber &&
previousTimeRemainingIsNumber &&
!timeRemainingExpired
) {
2020-10-06 20:28:38 +02:00
setTimeRemainingExpired(true)
}
2020-11-03 00:41:28 +01:00
}, [
timeRemainingIsNumber,
previousTimeRemainingIsNumber,
timeRemainingExpired,
])
2020-10-06 20:28:38 +02:00
let countdownText
2020-11-03 00:41:28 +01:00
if (
timeRemainingIsNumber &&
!timeRemainingExpired &&
tradeTxData?.submittedTime
) {
countdownText = (
<CountdownTimer
key="countdown-timer"
timeStarted={tradeTxData?.submittedTime}
timerBase={estimatedTransactionWaitTime}
timeOnly
/>
)
2020-10-06 20:28:38 +02:00
} else if (tradeTxData?.submittedTime) {
countdownText = t('swapsAlmostDone')
} else {
countdownText = t('swapEstimatedTimeCalculating')
}
let headerText
let statusImage
let descriptionText
let submitText
let content
if (errorKey === OFFLINE_FOR_MAINTENANCE) {
headerText = t('offlineForMaintenance')
descriptionText = t('metamaskSwapsOfflineDescription')
submitText = t('close')
statusImage = <SwapFailureIcon />
} else if (errorKey === SWAP_FAILED_ERROR) {
headerText = t('swapFailedErrorTitle')
descriptionText = t('swapFailedErrorDescription')
submitText = t('tryAgain')
statusImage = <SwapFailureIcon />
content = blockExplorerUrl && (
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
/>
)
} else if (errorKey === QUOTES_EXPIRED_ERROR) {
headerText = t('swapQuotesExpiredErrorTitle')
descriptionText = t('swapQuotesExpiredErrorDescription')
submitText = t('tryAgain')
statusImage = <QuotesTimeoutIcon />
if (!trackedQuotesExpiredEvent) {
setTrackedQuotesExpiredEvent(true)
quotesExpiredEvent()
anonymousQuotesExpiredEvent()
}
} else if (errorKey === ERROR_FETCHING_QUOTES) {
headerText = t('swapFetchingQuotesErrorTitle')
descriptionText = t('swapFetchingQuotesErrorDescription')
submitText = t('back')
statusImage = <SwapFailureIcon />
} else if (errorKey === QUOTES_NOT_AVAILABLE_ERROR) {
headerText = t('swapQuotesNotAvailableErrorTitle')
descriptionText = t('swapQuotesNotAvailableErrorDescription')
submitText = t('tryAgain')
statusImage = <SwapFailureIcon />
} else if (!errorKey && !swapComplete) {
/**
* only show estimated time if the transaction has a submitted time, the swap has
* not yet completed and there isn't an error. If the swap has not completed and
* there is no error, but also has no submitted time (has not yet been published),
* then we apply the invisible class to the time estimate div. This creates consistent
* spacing before and after display of the time estimate.
*/
headerText = t('swapProcessing')
statusImage = <PulseLoader />
submitText = t('swapsViewInActivity')
2020-11-03 00:41:28 +01:00
descriptionText = t('swapOnceTransactionHasProcess', [
<span
key="swapOnceTransactionHasProcess-1"
className="awaiting-swap__amount-and-symbol"
>
{destinationTokenInfo.symbol}
</span>,
])
2020-10-06 20:28:38 +02:00
content = (
<>
<div
className={classnames('awaiting-swap__time-estimate', {
'awaiting-swap__time-estimate--invisible': !tradeTxData?.submittedTime,
})}
>
{t('swapEstimatedTimeFull', [
2020-11-03 00:41:28 +01:00
<span
className="awaiting-swap__time-estimate-text"
key="swapEstimatedTime-1"
>
{t('swapEstimatedTime')}
</span>,
2020-10-06 20:28:38 +02:00
countdownText,
])}
</div>
{blockExplorerUrl && (
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
/>
)}
</>
)
} else if (!errorKey && swapComplete) {
headerText = t('swapTransactionComplete')
statusImage = <SwapSuccessIcon />
submitText = t('swapViewToken', [destinationTokenInfo.symbol])
2020-11-03 00:41:28 +01:00
descriptionText = t('swapTokenAvailable', [
<span
key="swapTokenAvailable-2"
className="awaiting-swap__amount-and-symbol"
>
{`${tokensReceived || ''} ${destinationTokenInfo.symbol}`}
</span>,
])
2020-10-06 20:28:38 +02:00
content = blockExplorerUrl && (
<ViewOnEtherScanLink
txHash={txHash}
blockExplorerUrl={blockExplorerUrl}
isCustomBlockExplorerUrl={Boolean(rpcPrefs.blockExplorerUrl)}
/>
)
}
return (
<div className="awaiting-swap">
<div className="awaiting-swap__content">
{!(swapComplete || errorKey) && (
<Mascot
animationEventEmitter={animationEventEmitter.current}
width="90"
height="90"
/>
)}
2020-11-03 00:41:28 +01:00
<div className="awaiting-swap__status-image">{statusImage}</div>
<div className="awaiting-swap__header">{headerText}</div>
<div className="awaiting-swap__main-descrption">{descriptionText}</div>
2020-10-06 20:28:38 +02:00
{content}
</div>
<SwapsFooter
onSubmit={async () => {
if (errorKey === OFFLINE_FOR_MAINTENANCE) {
await dispatch(prepareToLeaveSwaps())
history.push(DEFAULT_ROUTE)
} else if (errorKey === QUOTES_EXPIRED_ERROR) {
dispatch(prepareForRetryGetQuotes())
2020-11-03 00:41:28 +01:00
await dispatch(
fetchQuotesAndSetQuoteState(
history,
inputValue,
maxSlippage,
metaMetricsEvent,
),
)
2020-10-06 20:28:38 +02:00
} else if (errorKey) {
await dispatch(navigateBackToBuildQuote(history))
} else if (destinationTokenInfo?.symbol === 'ETH') {
history.push(DEFAULT_ROUTE)
} else {
history.push(`${ASSET_ROUTE}/${destinationTokenInfo?.address}`)
}
}}
onCancel={async () => await dispatch(navigateBackToBuildQuote(history))}
submitText={submitText}
disabled={submittingSwap}
hideCancel={errorKey !== QUOTES_EXPIRED_ERROR}
/>
</div>
)
}
AwaitingSwap.propTypes = {
swapComplete: PropTypes.bool,
networkId: PropTypes.string.isRequired,
txHash: PropTypes.string,
tokensReceived: PropTypes.string,
rpcPrefs: PropTypes.object.isRequired,
errorKey: PropTypes.oneOf([
QUOTES_EXPIRED_ERROR,
SWAP_FAILED_ERROR,
ERROR_FETCHING_QUOTES,
QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE,
]),
submittingSwap: PropTypes.bool,
tradeTxData: PropTypes.object,
usedGasPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxSlippage: PropTypes.number,
}