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
feature/default_network_editable
Dan J Miller 4 years ago committed by GitHub
parent 6a6b27a04d
commit 33ab480fbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      app/_locales/en/messages.json
  2. 32
      ui/app/pages/swaps/actionable-message/actionable-message.js
  3. 38
      ui/app/pages/swaps/actionable-message/index.scss
  4. 62
      ui/app/pages/swaps/build-quote/build-quote.js
  5. 15
      ui/app/pages/swaps/build-quote/index.scss
  6. 4
      ui/app/pages/swaps/view-quote/view-quote-price-difference.js

@ -364,6 +364,9 @@
"contactsSettingsDescription": {
"message": "Add, edit, remove, and manage your contacts"
},
"continue": {
"message": "Continue"
},
"continueToWyre": {
"message": "Continue to Wyre"
},
@ -1735,9 +1738,6 @@
"swapPriceDifferenceAcknowledgement": {
"message": "I'm aware"
},
"swapPriceDifferenceAcknowledgementNoFiat": {
"message": "Continue"
},
"swapPriceDifferenceTitle": {
"message": "Price difference of ~$1%",
"description": "$1 is a number (ex: 1.23) that represents the price difference."
@ -1845,6 +1845,17 @@
"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."
},
"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": {
"message": "Transaction complete"
},

@ -1,15 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
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({
message = '',
primaryAction = null,
secondaryAction = null,
className = '',
infoTooltipText = '',
withRightButton = false,
type = false,
}) {
const actionableMessageClassName = classnames(
'actionable-message',
typeHash[type],
withRightButton ? CLASSNAME_WITH_RIGHT_BUTTON : null,
className,
);
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>
{(primaryAction || secondaryAction) && (
<div className="actionable-message__actions">
@ -52,4 +79,7 @@ ActionableMessage.propTypes = {
onClick: PropTypes.func,
}),
className: PropTypes.string,
type: PropTypes.string,
withRightButton: PropTypes.boolean,
infoTooltipText: PropTypes.string,
};

@ -7,6 +7,7 @@
display: flex;
flex-flow: column;
align-items: center;
position: relative;
@include H7;
@ -29,6 +30,12 @@
cursor: pointer;
}
&__info-tooltip-wrapper {
position: absolute;
right: 4px;
top: 8px;
}
&--warning {
background: $Yellow-100;
border: 1px solid $Yellow-500;
@ -57,9 +64,40 @@
&--left-aligned {
.actionable-message__message,
.actionable-message__actions {
}
}
&--with-right-button {
padding: 12px;
.actionable-message__message {
justify-content: flex-start;
text-align: left;
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;
}
}

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

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

@ -30,9 +30,7 @@ export default function ViewQuotePriceDifference(props) {
// A calculation error signals we cannot determine dollar value
priceDifferenceMessage = t('swapPriceDifferenceUnavailable');
priceDifferenceClass = 'fiat-error';
priceDifferenceAcknowledgementText = t(
'swapPriceDifferenceAcknowledgementNoFiat',
);
priceDifferenceAcknowledgementText = t('continue');
} else {
priceDifferenceTitle = t('swapPriceDifferenceTitle', [
priceDifferencePercentage,

Loading…
Cancel
Save