1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-21 00:43:21 +01:00
metamask-extension/ui/app/pages/swaps/loading-swaps-quotes/loading-swaps-quotes.js

248 lines
8.6 KiB
JavaScript

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',
sensitiveProperties: {
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)
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,
}),
),
}