mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Implement price impact acknowledgement button (#10347)
This commit is contained in:
parent
efd280172f
commit
eeca0af5b9
@ -1735,6 +1735,12 @@
|
|||||||
"message": "You are about to swap $1 $2 (~$3) for $4 $5 (~$6).",
|
"message": "You are about to swap $1 $2 (~$3) for $4 $5 (~$6).",
|
||||||
"description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts."
|
"description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts."
|
||||||
},
|
},
|
||||||
|
"swapPriceDifferenceAcknowledgement": {
|
||||||
|
"message": "I'm aware"
|
||||||
|
},
|
||||||
|
"swapPriceDifferenceAcknowledgementNoFiat": {
|
||||||
|
"message": "Continue"
|
||||||
|
},
|
||||||
"swapPriceDifferenceTitle": {
|
"swapPriceDifferenceTitle": {
|
||||||
"message": "Price difference of ~$1%",
|
"message": "Price difference of ~$1%",
|
||||||
"description": "$1 is a number (ex: 1.23) that represents the price difference."
|
"description": "$1 is a number (ex: 1.23) that represents the price difference."
|
||||||
|
@ -15,6 +15,11 @@
|
|||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&_modal > div:not(.view-quote__warning-wrapper) {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 576px) {
|
@media screen and (max-width: 576px) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 428px;
|
max-height: 428px;
|
||||||
@ -98,14 +103,30 @@
|
|||||||
.actionable-message__message {
|
.actionable-message__message {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: $Yellow-500;
|
||||||
|
border-radius: 42px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.high .actionable-message {
|
&.high {
|
||||||
border-color: $Red-500;
|
.actionable-message {
|
||||||
background: $Red-100;
|
border-color: $Red-500;
|
||||||
|
background: $Red-100;
|
||||||
|
|
||||||
.actionable-message__message {
|
.actionable-message__message {
|
||||||
color: $Red-500;
|
color: $Red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: $Red-500;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 42px;
|
||||||
|
|
||||||
|
/* Offsets the width of ActionableMessage icon */
|
||||||
|
margin-right: -22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +139,17 @@
|
|||||||
|
|
||||||
&-contents {
|
&-contents {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-actions {
|
||||||
|
text-align: end;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
margin-inline-start: 10px;
|
margin-inline-start: 10px;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ describe('View Price Quote Difference', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('displays a fiat error when calculationError is present', function () {
|
it('displays a fiat error when calculationError is present', function () {
|
||||||
const props = { ...DEFAULT_PROPS }
|
const props = { ...DEFAULT_PROPS, priceSlippageUnknownFiatValue: true }
|
||||||
props.usedQuote.priceSlippage.calculationError =
|
props.usedQuote.priceSlippage.calculationError =
|
||||||
'Could not determine price.'
|
'Could not determine price.'
|
||||||
|
|
||||||
|
@ -2,64 +2,37 @@ import React, { useContext } from 'react'
|
|||||||
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import BigNumber from 'bignumber.js'
|
|
||||||
import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'
|
|
||||||
import { I18nContext } from '../../../contexts/i18n'
|
import { I18nContext } from '../../../contexts/i18n'
|
||||||
|
|
||||||
import ActionableMessage from '../actionable-message'
|
import ActionableMessage from '../actionable-message'
|
||||||
import Tooltip from '../../../components/ui/tooltip'
|
import Tooltip from '../../../components/ui/tooltip'
|
||||||
|
|
||||||
export default function ViewQuotePriceDifference(props) {
|
export default function ViewQuotePriceDifference(props) {
|
||||||
const { usedQuote, sourceTokenValue, destinationTokenValue } = props
|
const {
|
||||||
|
usedQuote,
|
||||||
|
sourceTokenValue,
|
||||||
|
destinationTokenValue,
|
||||||
|
onAcknowledgementClick,
|
||||||
|
acknowledged,
|
||||||
|
priceSlippageFromSource,
|
||||||
|
priceSlippageFromDestination,
|
||||||
|
priceDifferencePercentage,
|
||||||
|
priceSlippageUnknownFiatValue,
|
||||||
|
} = props
|
||||||
|
|
||||||
const t = useContext(I18nContext)
|
const t = useContext(I18nContext)
|
||||||
|
|
||||||
const priceSlippageFromSource = useEthFiatAmount(
|
|
||||||
usedQuote?.priceSlippage?.sourceAmountInETH || 0,
|
|
||||||
)
|
|
||||||
const priceSlippageFromDestination = useEthFiatAmount(
|
|
||||||
usedQuote?.priceSlippage?.destinationAmountInEth || 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!usedQuote || !usedQuote.priceSlippage) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { priceSlippage } = usedQuote
|
|
||||||
|
|
||||||
// We cannot present fiat value if there is a calculation error or no slippage
|
|
||||||
// from source or destination
|
|
||||||
const priceSlippageUnknownFiatValue =
|
|
||||||
!priceSlippageFromSource ||
|
|
||||||
!priceSlippageFromDestination ||
|
|
||||||
priceSlippage.calculationError
|
|
||||||
|
|
||||||
let priceDifferencePercentage = 0
|
|
||||||
if (priceSlippage.ratio) {
|
|
||||||
priceDifferencePercentage = parseFloat(
|
|
||||||
new BigNumber(priceSlippage.ratio, 10)
|
|
||||||
.minus(1, 10)
|
|
||||||
.times(100, 10)
|
|
||||||
.toFixed(2),
|
|
||||||
10,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldShowPriceDifferenceWarning =
|
|
||||||
['high', 'medium'].includes(priceSlippage.bucket) ||
|
|
||||||
priceSlippageUnknownFiatValue
|
|
||||||
|
|
||||||
if (!shouldShowPriceDifferenceWarning) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let priceDifferenceTitle = ''
|
let priceDifferenceTitle = ''
|
||||||
let priceDifferenceMessage = ''
|
let priceDifferenceMessage = ''
|
||||||
let priceDifferenceClass = ''
|
let priceDifferenceClass = ''
|
||||||
|
let priceDifferenceAcknowledgementText = ''
|
||||||
if (priceSlippageUnknownFiatValue) {
|
if (priceSlippageUnknownFiatValue) {
|
||||||
// A calculation error signals we cannot determine dollar value
|
// A calculation error signals we cannot determine dollar value
|
||||||
priceDifferenceMessage = t('swapPriceDifferenceUnavailable')
|
priceDifferenceMessage = t('swapPriceDifferenceUnavailable')
|
||||||
priceDifferenceClass = 'fiat-error'
|
priceDifferenceClass = 'fiat-error'
|
||||||
|
priceDifferenceAcknowledgementText = t(
|
||||||
|
'swapPriceDifferenceAcknowledgementNoFiat',
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
priceDifferenceTitle = t('swapPriceDifferenceTitle', [
|
priceDifferenceTitle = t('swapPriceDifferenceTitle', [
|
||||||
priceDifferencePercentage,
|
priceDifferencePercentage,
|
||||||
@ -72,7 +45,8 @@ export default function ViewQuotePriceDifference(props) {
|
|||||||
usedQuote.destinationTokenInfo.symbol, // Destination token symbol,
|
usedQuote.destinationTokenInfo.symbol, // Destination token symbol,
|
||||||
priceSlippageFromDestination, // Destination tokens total value
|
priceSlippageFromDestination, // Destination tokens total value
|
||||||
])
|
])
|
||||||
priceDifferenceClass = priceSlippage.bucket
|
priceDifferenceClass = usedQuote.priceSlippage.bucket
|
||||||
|
priceDifferenceAcknowledgementText = t('swapPriceDifferenceAcknowledgement')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -92,6 +66,17 @@ export default function ViewQuotePriceDifference(props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{priceDifferenceMessage}
|
{priceDifferenceMessage}
|
||||||
|
{!acknowledged && (
|
||||||
|
<div className="view-quote__price-difference-warning-contents-actions">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onAcknowledgementClick()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{priceDifferenceAcknowledgementText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="bottom"
|
position="bottom"
|
||||||
@ -111,4 +96,10 @@ ViewQuotePriceDifference.propTypes = {
|
|||||||
usedQuote: PropTypes.object,
|
usedQuote: PropTypes.object,
|
||||||
sourceTokenValue: PropTypes.string,
|
sourceTokenValue: PropTypes.string,
|
||||||
destinationTokenValue: PropTypes.string,
|
destinationTokenValue: PropTypes.string,
|
||||||
|
onAcknowledgementClick: PropTypes.func,
|
||||||
|
acknowledged: PropTypes.bool,
|
||||||
|
priceSlippageFromSource: PropTypes.string,
|
||||||
|
priceSlippageFromDestination: PropTypes.string,
|
||||||
|
priceDifferencePercentage: PropTypes.number,
|
||||||
|
priceSlippageUnknownFiatValue: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { isEqual } from 'lodash'
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { I18nContext } from '../../../contexts/i18n'
|
import { I18nContext } from '../../../contexts/i18n'
|
||||||
import SelectQuotePopover from '../select-quote-popover'
|
import SelectQuotePopover from '../select-quote-popover'
|
||||||
|
import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'
|
||||||
import { useEqualityCheck } from '../../../hooks/useEqualityCheck'
|
import { useEqualityCheck } from '../../../hooks/useEqualityCheck'
|
||||||
import { useNewMetricEvent } from '../../../hooks/useMetricEvent'
|
import { useNewMetricEvent } from '../../../hooks/useMetricEvent'
|
||||||
import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken'
|
import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken'
|
||||||
@ -88,6 +89,11 @@ export default function ViewQuote() {
|
|||||||
const [warningHidden, setWarningHidden] = useState(false)
|
const [warningHidden, setWarningHidden] = useState(false)
|
||||||
const [originalApproveAmount, setOriginalApproveAmount] = useState(null)
|
const [originalApproveAmount, setOriginalApproveAmount] = useState(null)
|
||||||
|
|
||||||
|
const [
|
||||||
|
acknowledgedPriceDifference,
|
||||||
|
setAcknowledgedPriceDifference,
|
||||||
|
] = useState(false)
|
||||||
|
|
||||||
const routeState = useSelector(getBackgroundSwapRouteState)
|
const routeState = useSelector(getBackgroundSwapRouteState)
|
||||||
const quotes = useSelector(getQuotes, isEqual)
|
const quotes = useSelector(getQuotes, isEqual)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -474,20 +480,70 @@ export default function ViewQuote() {
|
|||||||
: 'ETH',
|
: 'ETH',
|
||||||
])
|
])
|
||||||
|
|
||||||
const viewQuotePriceDifferenceComponent = (
|
// Price difference warning
|
||||||
<ViewQuotePriceDifference
|
let viewQuotePriceDifferenceComponent = null
|
||||||
usedQuote={usedQuote}
|
const priceSlippageFromSource = useEthFiatAmount(
|
||||||
sourceTokenValue={sourceTokenValue}
|
usedQuote?.priceSlippage?.sourceAmountInETH || 0,
|
||||||
destinationTokenValue={destinationTokenValue}
|
)
|
||||||
/>
|
const priceSlippageFromDestination = useEthFiatAmount(
|
||||||
|
usedQuote?.priceSlippage?.destinationAmountInEth || 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// We cannot present fiat value if there is a calculation error or no slippage
|
||||||
|
// from source or destination
|
||||||
|
const priceSlippageUnknownFiatValue =
|
||||||
|
!priceSlippageFromSource ||
|
||||||
|
!priceSlippageFromDestination ||
|
||||||
|
usedQuote?.priceSlippage?.calculationError
|
||||||
|
|
||||||
|
let priceDifferencePercentage = 0
|
||||||
|
if (usedQuote?.priceSlippage?.ratio) {
|
||||||
|
priceDifferencePercentage = parseFloat(
|
||||||
|
new BigNumber(usedQuote.priceSlippage.ratio, 10)
|
||||||
|
.minus(1, 10)
|
||||||
|
.times(100, 10)
|
||||||
|
.toFixed(2),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowPriceDifferenceWarning =
|
||||||
|
!showInsufficientWarning &&
|
||||||
|
usedQuote &&
|
||||||
|
(['high', 'medium'].includes(usedQuote.priceSlippage.bucket) ||
|
||||||
|
priceSlippageUnknownFiatValue)
|
||||||
|
|
||||||
|
if (shouldShowPriceDifferenceWarning) {
|
||||||
|
viewQuotePriceDifferenceComponent = (
|
||||||
|
<ViewQuotePriceDifference
|
||||||
|
usedQuote={usedQuote}
|
||||||
|
sourceTokenValue={sourceTokenValue}
|
||||||
|
destinationTokenValue={destinationTokenValue}
|
||||||
|
priceSlippageFromSource={priceSlippageFromSource}
|
||||||
|
priceSlippageFromDestination={priceSlippageFromDestination}
|
||||||
|
priceDifferencePercentage={priceDifferencePercentage}
|
||||||
|
priceSlippageUnknownFiatValue={priceSlippageUnknownFiatValue}
|
||||||
|
onAcknowledgementClick={() => {
|
||||||
|
setAcknowledgedPriceDifference(true)
|
||||||
|
}}
|
||||||
|
acknowledged={acknowledgedPriceDifference}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableSubmissionDueToPriceWarning =
|
||||||
|
shouldShowPriceDifferenceWarning && !acknowledgedPriceDifference
|
||||||
|
|
||||||
const isShowingWarning =
|
const isShowingWarning =
|
||||||
showInsufficientWarning || viewQuotePriceDifferenceComponent !== null
|
showInsufficientWarning || shouldShowPriceDifferenceWarning
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="view-quote">
|
<div className="view-quote">
|
||||||
<div className="view-quote__content">
|
<div
|
||||||
|
className={classnames('view-quote__content', {
|
||||||
|
'view-quote__content_modal': disableSubmissionDueToPriceWarning,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{selectQuotePopoverShown && (
|
{selectQuotePopoverShown && (
|
||||||
<SelectQuotePopover
|
<SelectQuotePopover
|
||||||
quoteDataRows={renderablePopoverData}
|
quoteDataRows={renderablePopoverData}
|
||||||
@ -503,7 +559,7 @@ export default function ViewQuote() {
|
|||||||
'view-quote__warning-wrapper--thin': !isShowingWarning,
|
'view-quote__warning-wrapper--thin': !isShowingWarning,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showInsufficientWarning && viewQuotePriceDifferenceComponent}
|
{viewQuotePriceDifferenceComponent}
|
||||||
{showInsufficientWarning && (
|
{showInsufficientWarning && (
|
||||||
<ActionableMessage
|
<ActionableMessage
|
||||||
message={actionableInsufficientMessage}
|
message={actionableInsufficientMessage}
|
||||||
@ -585,6 +641,7 @@ export default function ViewQuote() {
|
|||||||
disabled={
|
disabled={
|
||||||
submitClicked ||
|
submitClicked ||
|
||||||
balanceError ||
|
balanceError ||
|
||||||
|
disableSubmissionDueToPriceWarning ||
|
||||||
gasPrice === null ||
|
gasPrice === null ||
|
||||||
gasPrice === undefined
|
gasPrice === undefined
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user