You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
8.8 KiB
236 lines
8.8 KiB
import EventEmitter from 'events'
|
|
import React, { useState, useEffect, useRef, useContext } from 'react'
|
|
import { useDispatch, useSelector } from 'react-redux'
|
|
import PropTypes from 'prop-types'
|
|
import { shuffle } from 'lodash'
|
|
import { useHistory } from 'react-router-dom'
|
|
import classnames from 'classnames'
|
|
import { navigateBackToBuildQuote, getFetchParams, getQuotesFetchStartTime } from '../../../ducks/swaps/swaps'
|
|
import { I18nContext } from '../../../contexts/i18n'
|
|
import { MetaMetricsContext } from '../../../contexts/metametrics.new'
|
|
import Mascot from '../../../components/ui/mascot'
|
|
import SwapsFooter from '../swaps-footer'
|
|
import BackgroundAnimation from './background-animation'
|
|
import AggregatorLogo from './aggregator-logo'
|
|
|
|
// These locations reference where we want the top-left corner of the logo div to appear in relation to the
|
|
// centre point of the fox
|
|
const AGGREGATOR_LOCATIONS = [
|
|
{ x: -125, y: -75 },
|
|
{ x: 30, y: -75 },
|
|
{ x: -145, y: 0 },
|
|
{ x: 50, y: 0 },
|
|
{ x: -135, y: 46 },
|
|
{ x: 40, y: 46 },
|
|
]
|
|
|
|
function getRandomLocations (numberOfLocations) {
|
|
const randomLocations = shuffle(AGGREGATOR_LOCATIONS)
|
|
if (numberOfLocations <= AGGREGATOR_LOCATIONS.length) {
|
|
return randomLocations.slice(0, numberOfLocations)
|
|
}
|
|
const numberOfExtraLocations = numberOfLocations - AGGREGATOR_LOCATIONS.length
|
|
return [...randomLocations, ...getRandomLocations(numberOfExtraLocations)]
|
|
}
|
|
|
|
function getMascotTarget (aggregatorName, centerPoint, aggregatorLocationMap) {
|
|
const location = aggregatorLocationMap[aggregatorName]
|
|
|
|
if (!location || !centerPoint) {
|
|
return centerPoint ?? {}
|
|
}
|
|
|
|
// The aggregator logos are 94px x 40px. For the fox to look at the center of each logo, the target needs to be
|
|
// the coordinates for the centre point of the fox + the desired top and left coordinates of the logo + half
|
|
// the height and width of the logo.
|
|
return {
|
|
x: location.x + centerPoint.x + 47,
|
|
y: location.y + centerPoint.y + 20,
|
|
}
|
|
}
|
|
|
|
export default function LoadingSwapsQuotes ({
|
|
aggregatorMetadata,
|
|
loadingComplete,
|
|
onDone,
|
|
}) {
|
|
const t = useContext(I18nContext)
|
|
const metaMetricsEvent = useContext(MetaMetricsContext)
|
|
const dispatch = useDispatch()
|
|
const history = useHistory()
|
|
const animationEventEmitter = useRef(new EventEmitter())
|
|
|
|
const fetchParams = useSelector(getFetchParams)
|
|
const quotesFetchStartTime = useSelector(getQuotesFetchStartTime)
|
|
const quotesRequestCancelledEventConfig = {
|
|
event: 'Quotes Request Cancelled',
|
|
category: 'swaps',
|
|
}
|
|
const anonymousQuotesRequestCancelledEventConfig = {
|
|
event: 'Quotes Request Cancelled',
|
|
category: 'swaps',
|
|
excludeMetaMetricsId: true,
|
|
properties: {
|
|
token_from: fetchParams?.sourceTokenInfo?.symbol,
|
|
token_from_amount: fetchParams?.value,
|
|
request_type: fetchParams?.balanceError,
|
|
token_to: fetchParams?.destinationTokenInfo?.symbol,
|
|
slippage: fetchParams?.slippage,
|
|
custom_slippage: fetchParams?.slippage !== 2,
|
|
response_time: Date.now() - quotesFetchStartTime,
|
|
},
|
|
}
|
|
|
|
const [aggregatorNames] = useState(() => shuffle(Object.keys(aggregatorMetadata)))
|
|
const numberOfQuotes = aggregatorNames.length
|
|
const mascotContainer = useRef()
|
|
const currentMascotContainer = mascotContainer.current
|
|
|
|
const [quoteCount, updateQuoteCount] = useState(0)
|
|
// is an array of randomized items from AGGREGATOR_LOCATIONS, containing
|
|
// numberOfQuotes number of items it is randomized so that the order in
|
|
// which the fox looks at locations is random
|
|
const [aggregatorLocations] = useState(() => getRandomLocations(numberOfQuotes))
|
|
const _aggregatorLocationMap = aggregatorNames.reduce((nameLocationMap, name, index) => ({
|
|
...nameLocationMap,
|
|
[name]: aggregatorLocations[index],
|
|
}), {})
|
|
const [aggregatorLocationMap] = useState(_aggregatorLocationMap)
|
|
const [midPointTarget, setMidpointTarget] = useState(null)
|
|
|
|
useEffect(() => {
|
|
let timeoutLength
|
|
|
|
// The below logic simulates a sequential loading of the aggregator quotes, even though we are fetching them all with a single call.
|
|
// This is to give the user a sense of progress. The callback passed to `setTimeout` updates the quoteCount and therefore causes
|
|
// a new logo to be shown, the fox to look at that logo, the logo bar and aggregator name to update.
|
|
|
|
// If loading is complete and all logos + aggregator names have been shown, give the user 1.2 seconds to read the
|
|
// "Finalizing message" and prepare for the screen change
|
|
if (quoteCount === numberOfQuotes && loadingComplete) {
|
|
timeoutLength = 1200
|
|
} else if (loadingComplete) {
|
|
// If loading is complete, but the quoteCount is not, we quickly display the remaining logos/names/fox looks. 0.5s each
|
|
timeoutLength = 500
|
|
} else {
|
|
// If loading is not complete, we display remaining logos/names/fox looks at random intervals between 0.5s and 2s, to simulate the
|
|
// sort of loading a user would experience in most async scenarios
|
|
timeoutLength = 500 + Math.floor(Math.random() * 1500)
|
|
}
|
|
const quoteCountTimeout = setTimeout(() => {
|
|
if (quoteCount < numberOfQuotes) {
|
|
updateQuoteCount(quoteCount + 1)
|
|
} else if (quoteCount === numberOfQuotes && loadingComplete) {
|
|
onDone()
|
|
}
|
|
}, timeoutLength)
|
|
|
|
return function cleanup () {
|
|
clearTimeout(quoteCountTimeout)
|
|
}
|
|
}, [quoteCount, loadingComplete, onDone, numberOfQuotes])
|
|
|
|
useEffect(() => {
|
|
if (currentMascotContainer) {
|
|
const { top, left, width, height } = currentMascotContainer.getBoundingClientRect()
|
|
const center = { x: left + (width / 2), y: top + (height / 2) }
|
|
setMidpointTarget(center)
|
|
}
|
|
}, [currentMascotContainer])
|
|
|
|
return (
|
|
<div className="loading-swaps-quotes">
|
|
<div className="loading-swaps-quotes__content">
|
|
<>
|
|
<div className="loading-swaps-quotes__quote-counter">
|
|
<span>
|
|
{
|
|
t('swapQuoteNofN', [
|
|
Math.min(quoteCount + 1, numberOfQuotes),
|
|
numberOfQuotes,
|
|
])
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="loading-swaps-quotes__quote-name-check">
|
|
<span>
|
|
{
|
|
quoteCount === numberOfQuotes
|
|
? t('swapFinalizing')
|
|
: t('swapCheckingQuote', [
|
|
aggregatorMetadata[aggregatorNames[quoteCount]].title,
|
|
])
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="loading-swaps-quotes__loading-bar-container">
|
|
<div
|
|
className="loading-swaps-quotes__loading-bar"
|
|
style={{
|
|
width: `${(100 / numberOfQuotes) * quoteCount}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
<div className="loading-swaps-quotes__animation">
|
|
<BackgroundAnimation />
|
|
<div
|
|
className="loading-swaps-quotes__mascot-container"
|
|
ref={mascotContainer}
|
|
>
|
|
<Mascot
|
|
animationEventEmitter={animationEventEmitter.current}
|
|
width="90"
|
|
height="90"
|
|
followMouse={false}
|
|
lookAtTarget={
|
|
getMascotTarget(
|
|
aggregatorNames[quoteCount],
|
|
midPointTarget,
|
|
aggregatorLocationMap,
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
{currentMascotContainer && midPointTarget && aggregatorNames.map((aggName) => (
|
|
<div
|
|
className={classnames('loading-swaps-quotes__logo', {
|
|
'loading-swaps-quotes__logo--transition': aggName === aggregatorNames[quoteCount],
|
|
})}
|
|
style={{
|
|
opacity: aggName === aggregatorNames[quoteCount] ? 1 : 0,
|
|
top: aggregatorLocationMap[aggName]?.y + midPointTarget?.y ?? 0,
|
|
left: aggregatorLocationMap[aggName]?.x + midPointTarget?.x ?? 0,
|
|
}}
|
|
key={`aggregator-logo-${aggName}`}
|
|
>
|
|
<AggregatorLogo
|
|
aggregatorName={aggName}
|
|
icon={aggregatorMetadata[aggName]?.icon}
|
|
color={aggregatorMetadata[aggName]?.color}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<SwapsFooter
|
|
submitText={t('back')}
|
|
onSubmit={async () => {
|
|
metaMetricsEvent(quotesRequestCancelledEventConfig)
|
|
metaMetricsEvent(anonymousQuotesRequestCancelledEventConfig)
|
|
await dispatch(navigateBackToBuildQuote(history))
|
|
}}
|
|
hideCancel
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
LoadingSwapsQuotes.propTypes = {
|
|
loadingComplete: PropTypes.bool.isRequired,
|
|
onDone: PropTypes.func.isRequired,
|
|
aggregatorMetadata: PropTypes.objectOf(PropTypes.shape({
|
|
color: PropTypes.string,
|
|
icon: PropTypes.string,
|
|
})),
|
|
}
|
|
|