From c63714c4f2a1903e4677871f3880bfa1658a4db3 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Wed, 13 Jul 2022 19:45:38 -0230 Subject: [PATCH] Show users a warning when they are sending directly to a token contract (#13588) * Fix warning dialog when sending tokens to a known token contract address Fixing after rebase Covering missed cases Rebased and ran yarn setup Rebased Fix checkContractAddress condition Lint fix Applied requested changes Fix unit tests Applying requested changes Applied requested changes Refactor and update Lint fix Use V2 of ActionableMessage component Adding Learn More Link Updating warning copy Addressing review feedback Fix up copy changes Simplify validation of pasted addresses Improve detection of whether this is a token contract Refactor to leave updateRecipient unchanged, and to prevent the double calling of update recipient Update tests fix * Fix unit tests * Fix e2e tests * Ensure next button is disabled while recipient type is loading * Add optional chaining and a fallback to getRecipientWarningAcknowledgement * Fix lint * Don't reset recipient warning on asset change, because we should show recipient warnings regardless of asset * Update unit tests * Update unit tests Co-authored-by: Filip Sekulic --- app/_locales/en/messages.json | 4 + test/e2e/tests/send-hex-address.spec.js | 2 + ui/ducks/send/send.js | 151 ++++++++++---- ui/ducks/send/send.test.js | 188 ++++++++++++------ ui/helpers/constants/common.js | 3 + ui/helpers/utils/token-util.js | 2 +- .../add-recipient/add-recipient.component.js | 2 + .../send-content/send-content.component.js | 49 ++++- .../send-content.component.test.js | 28 ++- .../send-content/send-content.container.js | 17 +- ui/pages/send/send.js | 4 +- ui/pages/send/send.scss | 9 + ui/pages/send/send.test.js | 19 ++ 13 files changed, 375 insertions(+), 103 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 78bb505b8..4e3a7ec46 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2891,6 +2891,10 @@ "message": "Sending $1", "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, + "sendingToTokenContractWarning": { + "message": "Warning: you are about to send to a token contract which could result in a loss of funds. $1", + "description": "$1 is a clickable link with text defined by the 'learnMoreUpperCase' key. The link will open to a support article regarding the known contract address warning" + }, "setAdvancedPrivacySettings": { "message": "Set advanced privacy settings" }, diff --git a/test/e2e/tests/send-hex-address.spec.js b/test/e2e/tests/send-hex-address.spec.js index 52ff459a3..ed89901db 100644 --- a/test/e2e/tests/send-hex-address.spec.js +++ b/test/e2e/tests/send-hex-address.spec.js @@ -198,6 +198,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { ); return sendDialogMsgs.length === 1; }, 10000); + await driver.delay(2000); await driver.clickElement({ text: 'Next', tag: 'button' }); // Confirm transaction @@ -296,6 +297,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () { ); return sendDialogMsgs.length === 1; }, 10000); + await driver.delay(2000); await driver.clickElement({ text: 'Next', tag: 'button' }); // Confirm transaction diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index f27a9d6a5..c89d23865 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -69,6 +69,7 @@ import { calcTokenAmount, getTokenAddressParam, getTokenValueParam, + getTokenMetadata, } from '../../helpers/utils/token-util'; import { checkExistingAddresses, @@ -382,6 +383,7 @@ export const draftTransactionInitialState = { error: null, nickname: '', warning: null, + recipientWarningAcknowledged: false, }, status: SEND_STATUSES.VALID, transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY, @@ -952,14 +954,6 @@ const slice = createSlice({ // are no longer valid when sending native currency. draftTransaction.recipient.error = null; } - - if ( - draftTransaction.recipient.warning === KNOWN_RECIPIENT_ADDRESS_WARNING - ) { - // Warning related to sending tokens to a known contract address - // are no longer valid when sending native currency. - draftTransaction.recipient.warning = null; - } } // if amount mode is MAX update amount to max of new asset, otherwise set // to zero. This will revalidate the send amount field. @@ -1154,6 +1148,26 @@ const slice = createSlice({ state.recipientInput = ''; state.recipientMode = action.payload; }, + + updateRecipientWarning: (state, action) => { + const draftTransaction = + state.draftTransactions[state.currentTransactionUUID]; + draftTransaction.recipient.warning = action.payload; + }, + + updateDraftTransactionStatus: (state, action) => { + const draftTransaction = + state.draftTransactions[state.currentTransactionUUID]; + draftTransaction.status = action.payload; + }, + + acknowledgeRecipientWarning: (state) => { + const draftTransaction = + state.draftTransactions[state.currentTransactionUUID]; + draftTransaction.recipient.recipientWarningAcknowledged = true; + slice.caseReducers.validateSendState(state); + }, + /** * Updates the value of the recipientInput key with what the user has * typed into the recipient input field in the UI. @@ -1316,10 +1330,13 @@ const slice = createSlice({ draftTransaction.recipient.error = null; draftTransaction.recipient.warning = null; } else { - const isSendingToken = - draftTransaction.asset.type === ASSET_TYPES.TOKEN || - draftTransaction.asset.type === ASSET_TYPES.COLLECTIBLE; - const { chainId, tokens, tokenAddressList } = action.payload; + const { + chainId, + tokens, + tokenAddressList, + isProbablyAnAssetContract, + } = action.payload; + if ( isBurnAddress(state.recipientInput) || (!isValidHexAddress(state.recipientInput, { @@ -1331,10 +1348,9 @@ const slice = createSlice({ ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR; } else if ( - isSendingToken && isOriginContractAddress( state.recipientInput, - draftTransaction.asset.details.address, + draftTransaction.asset?.details?.address, ) ) { draftTransaction.recipient.error = CONTRACT_ADDRESS_ERROR; @@ -1342,12 +1358,12 @@ const slice = createSlice({ draftTransaction.recipient.error = null; } if ( - isSendingToken && - isValidHexAddress(state.recipientInput) && - (tokenAddressList.find((address) => - isEqualCaseInsensitive(address, state.recipientInput), - ) || - checkExistingAddresses(state.recipientInput, tokens)) + (isValidHexAddress(state.recipientInput) && + (tokenAddressList.find((address) => + isEqualCaseInsensitive(address, state.recipientInput), + ) || + checkExistingAddresses(state.recipientInput, tokens))) || + isProbablyAnAssetContract ) { draftTransaction.recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING; } else { @@ -1355,6 +1371,7 @@ const slice = createSlice({ } } } + slice.caseReducers.validateSendState(state); }, /** * Checks if the draftTransaction is currently valid. The following list of @@ -1392,6 +1409,12 @@ const slice = createSlice({ ): draftTransaction.status = SEND_STATUSES.INVALID; break; + case draftTransaction.recipient.warning === 'loading': + case draftTransaction.recipient.warning === + KNOWN_RECIPIENT_ADDRESS_WARNING && + draftTransaction.recipient.recipientWarningAcknowledged === false: + draftTransaction.status = SEND_STATUSES.INVALID; + break; default: draftTransaction.status = SEND_STATUSES.VALID; } @@ -1589,9 +1612,16 @@ const { validateRecipientUserInput, updateRecipientSearchMode, addHistoryEntry, + acknowledgeRecipientWarning, } = actions; -export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry }; +export { + useDefaultGas, + useCustomGas, + updateGasLimit, + addHistoryEntry, + acknowledgeRecipientWarning, +}; // Action Creators @@ -1601,14 +1631,18 @@ export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry }; * passing in both the dispatch method and the payload to dispatch, which makes * it only applicable for use within action creators. */ -const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => { - dispatch( - addHistoryEntry( - `sendFlow - user typed ${payload.userInput} into recipient input field`, - ), - ); - dispatch(validateRecipientUserInput(payload)); -}, 300); +const debouncedValidateRecipientUserInput = debounce( + (dispatch, payload, resolve) => { + dispatch( + addHistoryEntry( + `sendFlow - user typed ${payload.userInput} into recipient input field`, + ), + ); + dispatch(validateRecipientUserInput(payload)); + resolve(); + }, + 300, +); /** * Begins a new draft transaction, derived from the txParams of an existing @@ -1799,18 +1833,55 @@ export function updateRecipient({ address, nickname }) { */ export function updateRecipientUserInput(userInput) { return async (dispatch, getState) => { + dispatch(actions.updateRecipientWarning('loading')); + dispatch(actions.updateDraftTransactionStatus(SEND_STATUSES.INVALID)); await dispatch(actions.updateRecipientUserInput(userInput)); const state = getState(); + const draftTransaction = + state[name].draftTransactions[state[name].currentTransactionUUID]; + const sendingAddress = + draftTransaction.fromAccount?.address ?? + state[name].selectedAccount.address ?? + getSelectedAddress(state); const chainId = getCurrentChainId(state); const tokens = getTokens(state); const useTokenDetection = getUseTokenDetection(state); - const tokenAddressList = Object.keys(getTokenList(state)); - debouncedValidateRecipientUserInput(dispatch, { - userInput, - chainId, - tokens, - useTokenDetection, - tokenAddressList, + const tokenMap = getTokenList(state); + const tokenAddressList = Object.keys(tokenMap); + + const inputIsValidHexAddress = isValidHexAddress(userInput); + let isProbablyAnAssetContract = false; + if (inputIsValidHexAddress) { + const { symbol, decimals } = getTokenMetadata(userInput, tokenMap) || {}; + + isProbablyAnAssetContract = symbol && decimals !== undefined; + + if (!isProbablyAnAssetContract) { + try { + const { standard } = await getTokenStandardAndDetails( + userInput, + sendingAddress, + ); + isProbablyAnAssetContract = Boolean(standard); + } catch (e) { + console.log(e); + } + } + } + + return new Promise((resolve) => { + debouncedValidateRecipientUserInput( + dispatch, + { + userInput, + chainId, + tokens, + useTokenDetection, + tokenAddressList, + isProbablyAnAssetContract, + }, + resolve, + ); }); }; } @@ -2008,6 +2079,7 @@ export function updateSendHexData(hexData) { await dispatch( addHistoryEntry(`sendFlow - user added custom hexData ${hexData}`), ); + await dispatch(actions.updateUserInputHexData(hexData)); const state = getState(); const draftTransaction = @@ -2486,6 +2558,13 @@ export function getRecipientUserInput(state) { return state[name].recipientInput; } +export function getRecipientWarningAcknowledgement(state) { + return ( + getCurrentDraftTransaction(state).recipient?.recipientWarningAcknowledged ?? + false + ); +} + // Overall validity and stage selectors /** diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 1b7ef3608..339770198 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -87,6 +87,11 @@ jest.mock('./send', () => { }; }); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: (fn) => fn, +})); + setBackgroundConnection({ addPollingTokenToAppState: jest.fn(), addUnapprovedTransaction: jest.fn((_w, _x, _y, _z, cb) => { @@ -495,33 +500,6 @@ describe('Send Slice', () => { expect(draftTransaction.recipient.error).toBeNull(); }); - it('should nullify old known address error when asset types is not TOKEN', () => { - const recipientErrorState = getInitialSendStateWithExistingTxState({ - recipient: { - warning: KNOWN_RECIPIENT_ADDRESS_WARNING, - }, - asset: { - type: ASSET_TYPES.TOKEN, - }, - }); - - const action = { - type: 'send/updateAsset', - payload: { - type: 'New Type', - }, - }; - - const result = sendReducer(recipientErrorState, action); - - const draftTransaction = getTestUUIDTx(result); - - expect(draftTransaction.recipient.warning).not.toStrictEqual( - KNOWN_RECIPIENT_ADDRESS_WARNING, - ); - expect(draftTransaction.recipient.warning).toBeNull(); - }); - it('should update asset type and details to TOKEN payload', () => { const action = { type: 'send/updateAsset', @@ -626,6 +604,12 @@ describe('Send Slice', () => { error: 'someError', warning: 'someWarning', }, + amount: {}, + gas: { + gasLimit: '0x0', + minimumGasLimit: '0x0', + }, + asset: {}, }), recipientInput: '', recipientMode: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, @@ -746,6 +730,82 @@ describe('Send Slice', () => { 'contractAddressError', ); }); + + it('should set a warning when sending to a token address in the token address list', () => { + const tokenAssetTypeState = { + ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + }; + + const action = { + type: 'send/validateRecipientUserInput', + payload: { + chainId: '0x4', + tokens: [], + useTokenDetection: true, + tokenAddressList: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], + }, + }; + + const result = sendReducer(tokenAssetTypeState, action); + + const draftTransaction = getTestUUIDTx(result); + + expect(draftTransaction.recipient.warning).toStrictEqual( + KNOWN_RECIPIENT_ADDRESS_WARNING, + ); + }); + + it('should set a warning when sending to a token address in the token list', () => { + const tokenAssetTypeState = { + ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + }; + + const action = { + type: 'send/validateRecipientUserInput', + payload: { + chainId: '0x4', + tokens: [{ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' }], + useTokenDetection: true, + tokenAddressList: [], + }, + }; + + const result = sendReducer(tokenAssetTypeState, action); + + const draftTransaction = getTestUUIDTx(result); + + expect(draftTransaction.recipient.warning).toStrictEqual( + KNOWN_RECIPIENT_ADDRESS_WARNING, + ); + }); + + it('should set a warning when sending to an address that is probably a token contract', () => { + const tokenAssetTypeState = { + ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + }; + + const action = { + type: 'send/validateRecipientUserInput', + payload: { + chainId: '0x4', + tokens: [{ address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }], + useTokenDetection: true, + tokenAddressList: ['0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'], + isProbablyAnAssetContract: true, + }, + }; + + const result = sendReducer(tokenAssetTypeState, action); + + const draftTransaction = getTestUUIDTx(result); + + expect(draftTransaction.recipient.warning).toStrictEqual( + KNOWN_RECIPIENT_ADDRESS_WARNING, + ); + }); }); describe('updateRecipientSearchMode', () => { @@ -1643,11 +1703,10 @@ describe('Send Slice', () => { }, }, }, + send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, }; it('should create actions for updateRecipientUserInput and checks debounce for validation', async () => { - const clock = sinon.useFakeTimers(); - const store = mockStore(updateRecipientUserInputState); const newUserRecipientInput = 'newUserRecipientInput'; @@ -1655,29 +1714,35 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(1); + expect(actionResult).toHaveLength(5); + expect(actionResult[0].type).toStrictEqual( + 'send/updateRecipientWarning', + ); + expect(actionResult[0].payload).toStrictEqual('loading'); + + expect(actionResult[1].type).toStrictEqual( + 'send/updateDraftTransactionStatus', + ); + + expect(actionResult[2].type).toStrictEqual( 'send/updateRecipientUserInput', ); - expect(actionResult[0].payload).toStrictEqual(newUserRecipientInput); + expect(actionResult[2].payload).toStrictEqual(newUserRecipientInput); - clock.tick(300); // debounce - - const actionResultAfterDebounce = store.getActions(); - expect(actionResultAfterDebounce).toHaveLength(3); - - expect(actionResultAfterDebounce[1]).toMatchObject({ + expect(actionResult[3]).toMatchObject({ type: 'send/addHistoryEntry', payload: `sendFlow - user typed ${newUserRecipientInput} into recipient input field`, }); - expect(actionResultAfterDebounce[2].type).toStrictEqual( + expect(actionResult[4].type).toStrictEqual( 'send/validateRecipientUserInput', ); - expect(actionResultAfterDebounce[2].payload).toStrictEqual({ + expect(actionResult[4].payload).toStrictEqual({ chainId: '', tokens: [], useTokenDetection: true, + isProbablyAnAssetContract: false, userInput: newUserRecipientInput, tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }); @@ -1934,21 +1999,7 @@ describe('Send Slice', () => { }, }, }, - send: { - asset: { - type: '', - }, - recipient: { - address: 'Address', - nickname: 'NickName', - }, - gas: { - gasPrice: '0x1', - }, - amount: { - value: '0x1', - }, - }, + send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, }; const store = mockStore(updateRecipientState); @@ -1956,24 +2007,36 @@ describe('Send Slice', () => { await store.dispatch(resetRecipientInput()); const actionResult = store.getActions(); - expect(actionResult).toHaveLength(7); + expect(actionResult).toHaveLength(11); expect(actionResult[0]).toMatchObject({ type: 'send/addHistoryEntry', payload: 'sendFlow - user cleared recipient input', }); expect(actionResult[1].type).toStrictEqual( + 'send/updateRecipientWarning', + ); + expect(actionResult[2].type).toStrictEqual( + 'send/updateDraftTransactionStatus', + ); + + expect(actionResult[3].type).toStrictEqual( 'send/updateRecipientUserInput', ); - expect(actionResult[1].payload).toStrictEqual(''); - expect(actionResult[2].type).toStrictEqual('send/updateRecipient'); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[4].payload).toStrictEqual( + 'sendFlow - user typed into recipient input field', + ); + expect(actionResult[5].type).toStrictEqual( + 'send/validateRecipientUserInput', + ); + expect(actionResult[6].type).toStrictEqual('send/updateRecipient'); + expect(actionResult[7].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[4].type).toStrictEqual( + expect(actionResult[8].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); - expect(actionResult[5].type).toStrictEqual('ENS/resetEnsResolution'); - expect(actionResult[6].type).toStrictEqual( + expect(actionResult[9].type).toStrictEqual('ENS/resetEnsResolution'); + expect(actionResult[10].type).toStrictEqual( 'send/validateRecipientUserInput', ); }); @@ -2326,6 +2389,7 @@ describe('Send Slice', () => { error: null, nickname: '', warning: null, + recipientWarningAcknowledged: false, }, status: SEND_STATUSES.VALID, transactionType: '0x0', @@ -2468,6 +2532,7 @@ describe('Send Slice', () => { error: null, nickname: '', warning: null, + recipientWarningAcknowledged: false, }, status: SEND_STATUSES.VALID, transactionType: '0x0', @@ -2653,6 +2718,7 @@ describe('Send Slice', () => { error: null, warning: null, nickname: '', + recipientWarningAcknowledged: false, }, status: SEND_STATUSES.VALID, transactionType: '0x0', diff --git a/ui/helpers/constants/common.js b/ui/helpers/constants/common.js index 2663c2108..7e980f822 100644 --- a/ui/helpers/constants/common.js +++ b/ui/helpers/constants/common.js @@ -47,6 +47,8 @@ export const GAS_ESTIMATE_TYPES = { let _supportLink = 'https://support.metamask.io'; let _supportRequestLink = 'https://metamask.zendesk.com/hc/en-us/requests/new'; +const _contractAddressLink = + 'https://metamask.zendesk.com/hc/en-us/articles/360020028092-What-is-the-known-contract-address-warning-'; ///: BEGIN:ONLY_INCLUDE_IN(flask) _supportLink = 'https://metamask-flask.zendesk.com/hc'; @@ -56,3 +58,4 @@ _supportRequestLink = export const SUPPORT_LINK = _supportLink; export const SUPPORT_REQUEST_LINK = _supportRequestLink; +export const CONTRACT_ADDRESS_LINK = _contractAddressLink; diff --git a/ui/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index da399c33f..784ceb772 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -43,7 +43,7 @@ async function getDecimalsFromContract(tokenAddress) { } } -function getTokenMetadata(tokenAddress, tokenList) { +export function getTokenMetadata(tokenAddress, tokenList) { const casedTokenList = Object.keys(tokenList).reduce((acc, base) => { return { ...acc, diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.component.js b/ui/pages/send/send-content/add-recipient/add-recipient.component.js index fa2dbbfef..b0f791560 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.component.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.component.js @@ -32,6 +32,7 @@ export default class AddRecipient extends Component { error: PropTypes.string, warning: PropTypes.string, }), + updateRecipientUserInput: PropTypes.func, }; constructor(props) { @@ -70,6 +71,7 @@ export default class AddRecipient extends Component { `sendFlow - User clicked recipient from ${type}. address: ${address}, nickname ${nickname}`, ); this.props.updateRecipient({ address, nickname }); + this.props.updateRecipientUserInput(address); }; searchForContacts = () => { diff --git a/ui/pages/send/send-content/send-content.component.js b/ui/pages/send/send-content/send-content.component.js index 3b54b4a78..a856624d1 100644 --- a/ui/pages/send/send-content/send-content.component.js +++ b/ui/pages/send/send-content/send-content.component.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import PageContainerContent from '../../../components/ui/page-container/page-container-content.component'; import Dialog from '../../../components/ui/dialog'; +import ActionableMessage from '../../../components/ui/actionable-message'; import NicknamePopovers from '../../../components/app/modals/nickname-popovers'; import { ETH_GAS_PRICE_FETCH_WARNING_KEY, @@ -10,6 +11,7 @@ import { INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY, } from '../../../helpers/constants/error-keys'; import { ASSET_TYPES } from '../../../../shared/constants/transaction'; +import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common'; import SendAmountRow from './send-amount-row'; import SendHexDataRow from './send-hex-data-row'; import SendAssetRow from './send-asset-row'; @@ -38,6 +40,9 @@ export default class SendContent extends Component { asset: PropTypes.object, to: PropTypes.string, assetError: PropTypes.string, + recipient: PropTypes.object, + acknowledgeRecipientWarning: PropTypes.func, + recipientWarningAcknowledged: PropTypes.bool, }; render() { @@ -51,6 +56,8 @@ export default class SendContent extends Component { getIsBalanceInsufficient, asset, assetError, + recipient, + recipientWarningAcknowledged, } = this.props; let gasError; @@ -66,6 +73,10 @@ export default class SendContent extends Component { asset.type !== ASSET_TYPES.TOKEN && asset.type !== ASSET_TYPES.COLLECTIBLE; + const showKnownRecipientWarning = + recipient.warning === 'knownAddressRecipient'; + const hideAddContactDialog = recipient.warning === 'loading'; + return (
@@ -76,7 +87,12 @@ export default class SendContent extends Component { : null} {error ? this.renderError(error) : null} {warning ? this.renderWarning() : null} - {this.maybeRenderAddContact()} + {showKnownRecipientWarning && !recipientWarningAcknowledged + ? this.renderRecipientWarning() + : null} + {showKnownRecipientWarning || hideAddContactDialog + ? null + : this.maybeRenderAddContact()} {networkOrAccountNotSupports1559 ? : null} @@ -104,6 +120,7 @@ export default class SendContent extends Component { > {t('newAccountDetectedDialogMessage')} + {showNicknamePopovers ? ( this.setState({ showNicknamePopovers: false })} @@ -124,6 +141,36 @@ export default class SendContent extends Component { ); } + renderRecipientWarning() { + const { acknowledgeRecipientWarning } = this.props; + const { t } = this.context; + return ( +
+ + {t('learnMoreUpperCase')} + , + ])} + roundedButtons + /> +
+ ); + } + renderError(error) { const { t } = this.context; return ( diff --git a/ui/pages/send/send-content/send-content.component.test.js b/ui/pages/send/send-content/send-content.component.test.js index 7b06b50e3..ec8ed3ecf 100644 --- a/ui/pages/send/send-content/send-content.component.test.js +++ b/ui/pages/send/send-content/send-content.component.test.js @@ -16,6 +16,32 @@ describe('SendContent Component', () => { gasIsExcessive: false, networkAndAccountSupports1559: true, asset: { type: 'NATIVE' }, + recipient: { + mode: 'CONTACT_LIST', + userInput: '0x31A2764925BD47CCBd57b2F277702dB46e9C5F66', + address: '0x31A2764925BD47CCBd57b2F277702dB46e9C5F66', + nickname: 'John Doe', + error: null, + warning: null, + }, + tokenAddressList: { + '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { + name: 'BitBase', + symbol: 'BTBS', + decimals: 18, + address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356', + iconUrl: 'BTBS.svg', + occurrences: null, + }, + '0x3fa400483487a489ec9b1db29c4129063eec4654': { + name: 'Cryptokek.com', + symbol: 'KEK', + decimals: 18, + address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654', + iconUrl: 'cryptokek.svg', + occurrences: null, + }, + }, }; beforeEach(() => { @@ -150,7 +176,7 @@ describe('SendContent Component', () => { true, ); expect( - PageContainerContentChild.childAt(1).find( + PageContainerContentChild.childAt(2).find( 'send-v2__asset-dropdown__single-asset', ), ).toHaveLength(0); diff --git a/ui/pages/send/send-content/send-content.container.js b/ui/pages/send/send-content/send-content.container.js index d3e508e9f..53fca7530 100644 --- a/ui/pages/send/send-content/send-content.container.js +++ b/ui/pages/send/send-content/send-content.container.js @@ -11,6 +11,9 @@ import { getSendTo, getSendAsset, getAssetError, + getRecipient, + acknowledgeRecipientWarning, + getRecipientWarningAcknowledgement, } from '../../../ducks/send'; import SendContent from './send-content.component'; @@ -18,6 +21,10 @@ import SendContent from './send-content.component'; function mapStateToProps(state) { const ownedAccounts = accountsWithSendEtherInfoSelector(state); const to = getSendTo(state); + const recipient = getRecipient(state); + const recipientWarningAcknowledged = getRecipientWarningAcknowledgement( + state, + ); return { isOwnedAccount: Boolean( ownedAccounts.find( @@ -34,7 +41,15 @@ function mapStateToProps(state) { getIsBalanceInsufficient: getIsBalanceInsufficient(state), asset: getSendAsset(state), assetError: getAssetError(state), + recipient, + recipientWarningAcknowledged, }; } -export default connect(mapStateToProps)(SendContent); +function mapDispatchToProps(dispatch) { + return { + acknowledgeRecipientWarning: () => dispatch(acknowledgeRecipientWarning()), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(SendContent); diff --git a/ui/pages/send/send.js b/ui/pages/send/send.js index 2c17303b4..060dbfee8 100644 --- a/ui/pages/send/send.js +++ b/ui/pages/send/send.js @@ -91,10 +91,11 @@ export default function SendTransactionScreen() { userInput={userInput} className="send__to-row" onChange={(address) => dispatch(updateRecipientUserInput(address))} - onValidAddressTyped={(address) => { + onValidAddressTyped={async (address) => { dispatch( addHistoryEntry(`sendFlow - Valid address typed ${address}`), ); + await dispatch(updateRecipientUserInput(address)); dispatch(updateRecipient({ address, nickname: '' })); }} internalSearch={isUsingMyAccountsForRecipientSearch} @@ -106,7 +107,6 @@ export default function SendTransactionScreen() { `sendFlow - User pasted ${text} into address field`, ), ); - return dispatch(updateRecipient({ address: text, nickname: '' })); }} onReset={() => dispatch(resetRecipientInput())} scanQrCode={() => { diff --git a/ui/pages/send/send.scss b/ui/pages/send/send.scss index 8d45dfcb7..2735ba89a 100644 --- a/ui/pages/send/send.scss +++ b/ui/pages/send/send.scss @@ -35,6 +35,15 @@ margin: 1rem; } + &__warning-container { + padding-left: 16px; + padding-right: 16px; + + &__link { + color: var(--primary-1); + } + } + &__to-row { margin: 0; padding: 0.5rem; diff --git a/ui/pages/send/send.test.js b/ui/pages/send/send.test.js index 9df773701..af9a21323 100644 --- a/ui/pages/send/send.test.js +++ b/ui/pages/send/send.test.js @@ -80,6 +80,25 @@ const baseStore = { '0x0': { balance: '0x0', address: '0x0' }, }, identities: { '0x0': { address: '0x0' } }, + tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356', + tokenList: { + '0x32e6c34cd57087abbd59b5a4aecc4cb495924356': { + name: 'BitBase', + symbol: 'BTBS', + decimals: 18, + address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356', + iconUrl: 'BTBS.svg', + occurrences: null, + }, + '0x3fa400483487a489ec9b1db29c4129063eec4654': { + name: 'Cryptokek.com', + symbol: 'KEK', + decimals: 18, + address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654', + iconUrl: 'cryptokek.svg', + occurrences: null, + }, + }, }, appState: { sendInputCurrencySwitched: false,