1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Swaps token sources/verification messaging update (#10346)

* Update standard swaps build quote screen token verification message

* Add actionable warning token verification message to swaps build quote screen

* Simplify swapTokenVerification translations

* Use original verifyThisTokenOn message instead of swapsConfirmTokenAddressOnEtherscan

* Restore verifyThisTokenOn message to hi locale

* Support type and the withRightButton option as parameters on the actionable message component

* Use 'continue' in place of swapPriceDifferenceAcknowledgementNoFiat message

* Use wrapperClassName property on infotooltip in actionable-message

* Remove unnecessary change

* Lint fix
This commit is contained in:
Dan J Miller 2021-02-05 13:41:10 -03:30 committed by GitHub
parent 6a6b27a04d
commit 33ab480fbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 153 additions and 15 deletions

View File

@ -364,6 +364,9 @@
"contactsSettingsDescription": { "contactsSettingsDescription": {
"message": "Add, edit, remove, and manage your contacts" "message": "Add, edit, remove, and manage your contacts"
}, },
"continue": {
"message": "Continue"
},
"continueToWyre": { "continueToWyre": {
"message": "Continue to Wyre" "message": "Continue to Wyre"
}, },
@ -1735,9 +1738,6 @@
"swapPriceDifferenceAcknowledgement": { "swapPriceDifferenceAcknowledgement": {
"message": "I'm aware" "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."
@ -1845,6 +1845,17 @@
"message": "Swap $1 to $2", "message": "Swap $1 to $2",
"description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap."
}, },
"swapTokenVerificationMessage": {
"message": "Always confirm the token address on $1.",
"description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover."
},
"swapTokenVerificationOnlyOneSource": {
"message": "Only verified on 1 source."
},
"swapTokenVerificationSources": {
"message": "Verified on $1 sources.",
"description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number."
},
"swapTransactionComplete": { "swapTransactionComplete": {
"message": "Transaction complete" "message": "Transaction complete"
}, },

View File

@ -1,15 +1,42 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import InfoTooltip from '../../../components/ui/info-tooltip';
const CLASSNAME_WARNING = 'actionable-message--warning';
const CLASSNAME_DANGER = 'actionable-message--danger';
const CLASSNAME_WITH_RIGHT_BUTTON = 'actionable-message--with-right-button';
const typeHash = {
warning: CLASSNAME_WARNING,
danger: CLASSNAME_DANGER,
};
export default function ActionableMessage({ export default function ActionableMessage({
message = '', message = '',
primaryAction = null, primaryAction = null,
secondaryAction = null, secondaryAction = null,
className = '', className = '',
infoTooltipText = '',
withRightButton = false,
type = false,
}) { }) {
const actionableMessageClassName = classnames(
'actionable-message',
typeHash[type],
withRightButton ? CLASSNAME_WITH_RIGHT_BUTTON : null,
className,
);
return ( return (
<div className={classnames('actionable-message', className)}> <div className={actionableMessageClassName}>
{infoTooltipText && (
<InfoTooltip
position="left"
contentText={infoTooltipText}
wrapperClassName="actionable-message__info-tooltip-wrapper"
/>
)}
<div className="actionable-message__message">{message}</div> <div className="actionable-message__message">{message}</div>
{(primaryAction || secondaryAction) && ( {(primaryAction || secondaryAction) && (
<div className="actionable-message__actions"> <div className="actionable-message__actions">
@ -52,4 +79,7 @@ ActionableMessage.propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
}), }),
className: PropTypes.string, className: PropTypes.string,
type: PropTypes.string,
withRightButton: PropTypes.boolean,
infoTooltipText: PropTypes.string,
}; };

View File

@ -7,6 +7,7 @@
display: flex; display: flex;
flex-flow: column; flex-flow: column;
align-items: center; align-items: center;
position: relative;
@include H7; @include H7;
@ -29,6 +30,12 @@
cursor: pointer; cursor: pointer;
} }
&__info-tooltip-wrapper {
position: absolute;
right: 4px;
top: 8px;
}
&--warning { &--warning {
background: $Yellow-100; background: $Yellow-100;
border: 1px solid $Yellow-500; border: 1px solid $Yellow-500;
@ -57,9 +64,40 @@
&--left-aligned { &--left-aligned {
.actionable-message__message, .actionable-message__message,
.actionable-message__actions { .actionable-message__actions {
}
}
&--with-right-button {
padding: 12px;
.actionable-message__message {
justify-content: flex-start; justify-content: flex-start;
text-align: left; text-align: left;
width: 100%; width: 100%;
} }
.actionable-message__actions {
justify-content: flex-end;
width: 100%;
}
.actionable-message__action {
font-weight: normal;
cursor: pointer;
border-radius: 42px;
min-width: 72px;
height: 18px;
display: flex;
justify-content: center;
align-items: center;
@include H8;
}
}
}
.actionable-message--warning.actionable-message--with-right-button {
.actionable-message__action {
background: $Yellow-500;
} }
} }

View File

@ -14,6 +14,7 @@ import DropdownSearchList from '../dropdown-search-list';
import SlippageButtons from '../slippage-buttons'; import SlippageButtons from '../slippage-buttons';
import { getTokens } from '../../../ducks/metamask/metamask'; import { getTokens } from '../../../ducks/metamask/metamask';
import InfoTooltip from '../../../components/ui/info-tooltip'; import InfoTooltip from '../../../components/ui/info-tooltip';
import ActionableMessage from '../actionable-message';
import { import {
fetchQuotesAndSetQuoteState, fetchQuotesAndSetQuoteState,
@ -65,6 +66,7 @@ export default function BuildQuote({
const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = useState( const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = useState(
undefined, undefined,
); );
const [verificationClicked, setVerificationClicked] = useState(false);
const balanceError = useSelector(getBalanceError); const balanceError = useSelector(getBalanceError);
const fetchParams = useSelector(getFetchParams); const fetchParams = useSelector(getFetchParams);
@ -108,6 +110,9 @@ export default function BuildQuote({
const selectedToToken = const selectedToToken =
tokensToSearch.find(({ address }) => address === toToken?.address) || tokensToSearch.find(({ address }) => address === toToken?.address) ||
toToken; toToken;
const toTokenIsNotEth =
selectedToToken?.address &&
selectedToToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address;
const { const {
address: fromTokenAddress, address: fromTokenAddress,
@ -195,6 +200,7 @@ export default function BuildQuote({
dispatch(removeToken(toAddress)); dispatch(removeToken(toAddress));
} }
dispatch(setSwapToToken(token)); dispatch(setSwapToToken(token));
setVerificationClicked(false);
}, },
[dispatch, destinationTokenAddedForSwap, toAddress], [dispatch, destinationTokenAddedForSwap, toAddress],
); );
@ -369,10 +375,52 @@ export default function BuildQuote({
defaultToAll defaultToAll
/> />
</div> </div>
{selectedToToken?.address && {toTokenIsNotEth &&
selectedToToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address && ( (selectedToToken.occurances === 1 ? (
<ActionableMessage
message={
<div className="build-quote__token-verification-warning-message">
<div className="build-quote__bold">
{t('swapTokenVerificationOnlyOneSource')}
</div>
<div>
{t('verifyThisTokenOn', [
<a
className="build-quote__token-etherscan-link build-quote__underline"
key="build-quote-etherscan-link"
href={`https://etherscan.io/token/${selectedToToken.address}`}
target="_blank"
rel="noopener noreferrer"
>
{t('etherscan')}
</a>,
])}
</div>
</div>
}
primaryAction={
verificationClicked
? null
: {
label: t('continue'),
onClick: () => setVerificationClicked(true),
}
}
type="warning"
withRightButton
infoTooltipText={t('swapVerifyTokenExplanation')}
/>
) : (
<div className="build-quote__token-message"> <div className="build-quote__token-message">
{t('verifyThisTokenOn', [ <span
className="build-quote__bold"
key="token-verification-bold-text"
>
{t('swapTokenVerificationSources', [
selectedToToken.occurances,
])}
</span>
{t('swapTokenVerificationMessage', [
<a <a
className="build-quote__token-etherscan-link" className="build-quote__token-etherscan-link"
key="build-quote-etherscan-link" key="build-quote-etherscan-link"
@ -387,9 +435,10 @@ export default function BuildQuote({
position="top" position="top"
contentText={t('swapVerifyTokenExplanation')} contentText={t('swapVerifyTokenExplanation')}
containerClassName="build-quote__token-tooltip-container" containerClassName="build-quote__token-tooltip-container"
key="token-verification-info-tooltip"
/> />
</div> </div>
)} ))}
<div className="build-quote__slippage-buttons-container"> <div className="build-quote__slippage-buttons-container">
<SlippageButtons <SlippageButtons
onSelect={(newSlippage) => { onSelect={(newSlippage) => {
@ -415,7 +464,10 @@ export default function BuildQuote({
!Number(inputValue) || !Number(inputValue) ||
!selectedToToken?.address || !selectedToToken?.address ||
Number(maxSlippage) === 0 || Number(maxSlippage) === 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
(toTokenIsNotEth &&
selectedToToken.occurances === 1 &&
!verificationClicked)
} }
hideCancel hideCancel
showTermsOfService showTermsOfService

View File

@ -122,14 +122,15 @@
width: 100%; width: 100%;
color: $Grey-500; color: $Grey-500;
margin-top: 4px; margin-top: 4px;
display: flex;
align-items: center; .info-tooltip {
display: inline-block;
}
} }
&__token-etherscan-link { &__token-etherscan-link {
color: $Blue-500; color: $Blue-500;
cursor: pointer; cursor: pointer;
margin-right: 4px;
} }
&__token-tooltip-container { &__token-tooltip-container {
@ -137,6 +138,14 @@
display: flex !important; display: flex !important;
} }
&__bold {
font-weight: bold;
}
&__underline {
text-decoration: underline;
}
/* Prevents the swaps "Swap to" field from overflowing */ /* Prevents the swaps "Swap to" field from overflowing */
.dropdown-input-pair__to .dropdown-search-list { .dropdown-input-pair__to .dropdown-search-list {
width: 100%; width: 100%;

View File

@ -30,9 +30,7 @@ export default function ViewQuotePriceDifference(props) {
// 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( priceDifferenceAcknowledgementText = t('continue');
'swapPriceDifferenceAcknowledgementNoFiat',
);
} else { } else {
priceDifferenceTitle = t('swapPriceDifferenceTitle', [ priceDifferenceTitle = t('swapPriceDifferenceTitle', [
priceDifferencePercentage, priceDifferencePercentage,