diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 00160d07b..3c539e810 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1620,6 +1620,9 @@ "message": "You need $1 more $2 to complete this swap", "description": "Tells the user how many more of a given token they need for a specific swap. $1 is an amount of tokens and $2 is the token symbol." }, + "swapBetterQuoteAvailable": { + "message": "A better quote is available" + }, "swapBuildQuotePlaceHolderText": { "message": "No tokens available matching $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" @@ -1726,8 +1729,8 @@ "message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.", "description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number." }, - "swapNQuotesAvailable": { - "message": "$1 quotes available", + "swapNQuotes": { + "message": "$1 quotes", "description": "$1 is the number of quotes that the user can select from when opening the list of quotes on the 'view quote' screen" }, "swapNetworkFeeSummary": { @@ -1788,6 +1791,10 @@ "swapRequestForQuotation": { "message": "Request for quotation" }, + "swapSaving": { + "message": "Saving $1 $2", + "description": "Tells the user their average savings for the selected best quote. $1 is replaced by a tilde sign, to shown approximation. $2 is replaced by the approximate amount of money the user will save, in fiat" + }, "swapSearchForAToken": { "message": "Search for a token" }, @@ -1841,6 +1848,9 @@ "swapUnknown": { "message": "Unknown" }, + "swapUsingBestQuote": { + "message": "Using the best quote" + }, "swapViewToken": { "message": "View $1" }, diff --git a/ui/app/pages/swaps/fee-card/fee-card.js b/ui/app/pages/swaps/fee-card/fee-card.js index 146115130..b6c7a7545 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.js +++ b/ui/app/pages/swaps/fee-card/fee-card.js @@ -1,7 +1,12 @@ import React, { useContext } from 'react' import PropTypes from 'prop-types' +import BigNumber from 'bignumber.js' +import classnames from 'classnames' import { I18nContext } from '../../../contexts/i18n' import InfoTooltip from '../../../components/ui/info-tooltip' +import { decEthToConvertedCurrency } from '../../../helpers/utils/conversions.util' +import { formatCurrency } from '../../../helpers/utils/confirm-tx.util' +import PigIcon from './pig-icon' export default function FeeCard({ primaryFee, @@ -11,11 +16,88 @@ export default function FeeCard({ tokenApprovalTextComponent, tokenApprovalSourceTokenSymbol, onTokenApprovalClick, + metaMaskFee, + savings, + isBestQuote, + numberOfQuotes, + onQuotesClick, + conversionRate, + currentCurrency, + tokenConversionRate, }) { const t = useContext(I18nContext) + const savingAmount = + isBestQuote && savings?.total + ? formatCurrency( + decEthToConvertedCurrency( + savings.total, + currentCurrency, + conversionRate, + ), + currentCurrency, + ) + : null + const savingsIsPositive = + savings?.total && new BigNumber(savings.total, 10).gt(0) + + const inDevelopment = process.env.NODE_ENV === 'development' + const shouldDisplaySavings = + inDevelopment && isBestQuote && tokenConversionRate && savingsIsPositive + + let savingsText = '' + if (inDevelopment && shouldDisplaySavings) { + savingsText = t('swapSaving', [ + + ~ + , + savingAmount, + ]) + } else if (inDevelopment && isBestQuote && tokenConversionRate) { + savingsText = t('swapUsingBestQuote') + } else if (inDevelopment && savingsIsPositive && tokenConversionRate) { + savingsText = t('swapBetterQuoteAvailable') + } + return (
+
+
+
+
+ {shouldDisplaySavings && ( +
+ +
+ )} +
+ {savingsText && ( +

{savingsText}

+ )} +
+

+ {t('swapNQuotes', [numberOfQuotes])} +

+
+ +
+
+
+
@@ -83,26 +165,39 @@ export default function FeeCard({
{!hideTokenApprovalRow && ( -
+
{t('swapThisWillAllowApprove', [tokenApprovalTextComponent])}
-
onTokenApprovalClick()} - > - {t('swapEditLimit')} -
+
onTokenApprovalClick()} + > + {t('swapEditLimit')} +
)} +
+
+
+ {t('swapQuoteIncludesRate', [metaMaskFee])} +
+ +
+
) @@ -122,4 +217,12 @@ FeeCard.propTypes = { tokenApprovalTextComponent: PropTypes.node, tokenApprovalSourceTokenSymbol: PropTypes.string, onTokenApprovalClick: PropTypes.func, + metaMaskFee: PropTypes.string.isRequired, + savings: PropTypes.object, + isBestQuote: PropTypes.bool, + onQuotesClick: PropTypes.func.isRequired, + numberOfQuotes: PropTypes.number.isRequired, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + tokenConversionRate: PropTypes.number, } diff --git a/ui/app/pages/swaps/fee-card/fee-card.stories.js b/ui/app/pages/swaps/fee-card/fee-card.stories.js index 6b316caab..43adbc48f 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.stories.js +++ b/ui/app/pages/swaps/fee-card/fee-card.stories.js @@ -1,6 +1,6 @@ import React from 'react' import { action } from '@storybook/addon-actions' -import { text } from '@storybook/addon-knobs/react' +import { text, boolean, number, object } from '@storybook/addon-knobs/react' import FeeCard from './fee-card' const tokenApprovalTextComponent = ( @@ -35,6 +35,13 @@ export const WithAllProps = () => { tokenApprovalSourceTokenSymbol="ABC" onTokenApprovalClick={action('Clicked third row link')} hideTokenApprovalRow={false} + metaMaskFee="0.875" + savings={object('savings 1', { total: '8.55' })} + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={number('numberOfQuotes', 6)} + isBestQuote={boolean('isBestQuote', true)} + conversionRate={300} + currentCurrency="usd" />
) @@ -55,6 +62,11 @@ export const WithoutThirdRow = () => { }} onFeeCardMaxRowClick={action('Clicked max fee row link')} hideTokenApprovalRow + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={number('numberOfQuotes', 1)} + isBestQuote={boolean('isBestQuote', true)} + savings={object('savings 1', { total: '8.55' })} + metaMaskFee="0.875" />
) @@ -70,6 +82,9 @@ export const WithOnlyRequiredProps = () => { }} onFeeCardMaxRowClick={action('Clicked max fee row link')} hideTokenApprovalRow + metaMaskFee="0.875" + onQuotesClick={action('Clicked quotes link')} + numberOfQuotes={2} />
) diff --git a/ui/app/pages/swaps/fee-card/index.scss b/ui/app/pages/swaps/fee-card/index.scss index 0a86f6ab7..ce42ca676 100644 --- a/ui/app/pages/swaps/fee-card/index.scss +++ b/ui/app/pages/swaps/fee-card/index.scss @@ -1,11 +1,103 @@ .fee-card { - border-radius: 8px; - border: 1px solid $Grey-100; - width: 100%; - @include H7; + &__savings-and-quotes-header { + display: flex; + position: relative; + align-items: center; + } + + &__savings-and-quotes-header-first-part, + &__savings-and-quotes-header-second-part, + &__savings-and-quotes-header-third-part { + height: 39px; + background: $Blue-000; + border: 1px solid $Blue-500; + } + + &__savings-and-quotes-header-first-part { + width: 22px; + border-top-left-radius: 8px; + border-bottom: none; + border-right: none; + } + + &__savings-and-quotes-header-second-part { + width: 18px; + border: none; + + &--top-border { + border-top: 1px solid $Blue-500; + } + } + + &__savings-and-quotes-header-third-part { + width: 271px; + border-top-right-radius: 8px; + border-bottom: none; + border-left: none; + } + + &__pig-icon-container { + position: absolute; + left: 14.5px; + bottom: 4px; + } + + &__savings-and-quotes-row { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 234px; + width: 100%; + position: absolute; + left: 58px; + + &--align-left { + left: 16px; + max-width: 272px; + } + } + + &__savings-text { + @include H6; + + font-weight: bold; + color: $Blue-500; + } + + &__quote-link-container { + display: flex; + align-items: center; + cursor: pointer; + } + + &__quote-link-text { + @include H7; + + color: $Blue-500; + } + + &__caret-right { + color: $Blue-500; + width: 6px; + height: 6px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 6px; + + i { + transform: rotate(90deg); + } + } + &__main { + border: 1px solid $Blue-500; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + width: 100%; + max-width: 311px; padding: 16px 16px 12px 16px; } @@ -31,6 +123,10 @@ cursor: pointer; } + &__row-header-text--bold { + color: $Black-100; + } + &__row, &__top-bordered-row { display: flex; @@ -51,7 +147,6 @@ img { height: 10px; width: 10px; - margin-left: 4px; cursor: pointer; } } @@ -60,7 +155,12 @@ height: 10px; width: 10px; justify-content: center; - margin-top: 2px; + + div { + // Needed to override the style property added by the react-tippy library + display: flex !important; + height: 10px; + } } &__info-tooltip-paragraph { @@ -111,11 +211,14 @@ margin-right: 12px; } - &__row-header-primary, - &__row-header-primary--bold { + &__row-header-primary { color: $Grey-500; } + &__row-header-primary--bold { + color: $Black-100; + } + &__row-header-text--bold, &__row-header-secondary--bold, &__row-header-primary--bold { @@ -125,6 +228,11 @@ &__bold { font-weight: bold; } + + &__tilde { + font-family: Roboto, Helvetica, Arial, sans-serif; + margin-right: -3.5px; + } } .info-tooltip { diff --git a/ui/app/pages/swaps/fee-card/pig-icon.js b/ui/app/pages/swaps/fee-card/pig-icon.js new file mode 100644 index 000000000..ef7677ae4 --- /dev/null +++ b/ui/app/pages/swaps/fee-card/pig-icon.js @@ -0,0 +1,54 @@ +import React from 'react' + +export default function PigIcon() { + return ( + + + + + + + + + + ) +} diff --git a/ui/app/pages/swaps/main-quote-summary/index.scss b/ui/app/pages/swaps/main-quote-summary/index.scss index 650f0717c..dfdc882f1 100644 --- a/ui/app/pages/swaps/main-quote-summary/index.scss +++ b/ui/app/pages/swaps/main-quote-summary/index.scss @@ -1,8 +1,11 @@ .main-quote-summary { display: flex; flex-flow: column; + justify-content: center; align-items: center; position: relative; + max-height: 196px; + min-height: 196px; width: 100%; color: $Black-100; diff --git a/ui/app/pages/swaps/swaps-footer/swaps-footer.js b/ui/app/pages/swaps/swaps-footer/swaps-footer.js index c158c21fb..c5a90ba34 100644 --- a/ui/app/pages/swaps/swaps-footer/swaps-footer.js +++ b/ui/app/pages/swaps/swaps-footer/swaps-footer.js @@ -13,6 +13,7 @@ export default function SwapsFooter({ disabled, showTermsOfService, showTopBorder, + className, }) { const t = useContext(I18nContext) @@ -30,7 +31,10 @@ export default function SwapsFooter({ onSubmit={onSubmit} submitText={submitText} submitButtonType="confirm" - footerClassName="swaps-footer__custom-page-container-footer-class" + footerClassName={classnames( + 'swaps-footer__custom-page-container-footer-class', + className, + )} footerButtonClassName={classnames( 'swaps-footer__custom-page-container-footer-button-class', { @@ -62,4 +66,5 @@ SwapsFooter.propTypes = { disabled: PropTypes.bool, showTermsOfService: PropTypes.bool, showTopBorder: PropTypes.bool, + className: PropTypes.string, } diff --git a/ui/app/pages/swaps/view-quote/index.scss b/ui/app/pages/swaps/view-quote/index.scss index a7fef1f42..013f57557 100644 --- a/ui/app/pages/swaps/view-quote/index.scss +++ b/ui/app/pages/swaps/view-quote/index.scss @@ -105,38 +105,25 @@ } &__countdown-timer-container { - @media screen and (max-width: 576px) { - margin-top: 12px; - margin-bottom: 16px; - - &--thin { - margin-top: 8px; - margin-bottom: 8px; - - > div { - margin-top: 0; - margin-bottom: 0; - } - } - } - - @media screen and (min-width: 576px) { - &--thin { - margin-top: 6px; - } - } + width: 152px; + min-height: 32px; + display: flex; + justify-content: center; + border-radius: 42px; + background: #f2f3f4; + margin-top: 16px; } &__fee-card-container { + display: flex; + align-items: center; + min-height: 172px; width: 100%; + max-width: 311px; margin-bottom: 8px; @media screen and (min-width: 576px) { margin-bottom: 0; - - &--three-rows { - margin-bottom: -16px; - } } } @@ -153,4 +140,10 @@ &__metamask-rate-info-icon { margin-left: 4px; } + + &__thin-swaps-footer { + @media screen and (min-width: 576px) { + height: 72px; + } + } } diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 71279aeaf..c4375219b 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -73,7 +73,6 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker' import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps' import CountdownTimer from '../countdown-timer' import SwapsFooter from '../swaps-footer' -import InfoTooltip from '../../../components/ui/info-tooltip' export default function ViewQuote() { const history = useHistory() @@ -115,6 +114,8 @@ export default function ViewQuote() { const usedQuote = selectedQuote || topQuote const tradeValue = usedQuote?.trade?.value ?? '0x0' + const { isBestQuote } = usedQuote + const fetchParamsSourceToken = fetchParams?.sourceToken const usedGasLimit = @@ -483,11 +484,7 @@ export default function ViewQuote() { /> )}
-
+
-
-
- {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
{ - allAvailableQuotesOpened() - setSelectQuotePopoverShown(true) - }} - > - {t('swapNQuotesAvailable', [Object.values(quotes).length])} - -
-
-
-

- {t('swapQuoteIncludesRate', [metaMaskFee])} -

- -
{ + allAvailableQuotesOpened() + setSelectQuotePopoverShown(true) + }} + savings={usedQuote?.savings} + conversionRate={conversionRate} + currentCurrency={currentCurrency} + tokenConversionRate={ + destinationTokenSymbol === 'ETH' + ? 1 + : memoizedTokenConversionRates[destinationToken.address] + } />
@@ -573,6 +559,7 @@ export default function ViewQuote() { submitText={t('swap')} onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} disabled={balanceError || gasPrice === null || gasPrice === undefined} + className={showWarning && 'view-quote__thin-swaps-footer'} showTermsOfService showTopBorder />