From 86e449f6c28fa430c54a91b615bb495e14c189d0 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 24 Jul 2024 20:25:38 +0530 Subject: [PATCH] Display toast message if user quickly sends transaction on different networks --- app/_locales/en/messages.json | 4 + app/scripts/controllers/app-state.js | 21 +++++ app/scripts/metamask-controller.js | 10 +++ .../confirm/network-change-toast/index.scss | 11 +++ .../confirm/network-change-toast/index.tsx | 2 + .../network-change-toast-inner.test.tsx | 57 +++++++++++++ .../network-change-toast-inner.tsx | 85 +++++++++++++++++++ .../network-change-toast.tsx | 14 +++ ui/pages/confirmations/components/index.scss | 1 + .../signature-request-original.component.js | 3 +- .../signature-request-siwe.js | 5 +- .../signature-request/signature-request.js | 7 +- .../confirm-approve/confirm-approve.js | 2 + .../confirm-transaction-base.component.js | 3 +- ui/pages/confirmations/confirm/confirm.tsx | 47 +++++----- ui/pages/confirmations/types/confirm.ts | 6 ++ ui/store/actions.ts | 50 +++++++++++ 17 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 ui/pages/confirmations/components/confirm/network-change-toast/index.scss create mode 100644 ui/pages/confirmations/components/confirm/network-change-toast/index.tsx create mode 100644 ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.tsx create mode 100644 ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 046fdb5423e6..48294bb484ed 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2940,6 +2940,10 @@ "message": "We can't connect to $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Network switched to $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Network URL" }, diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 256dfba38438..637d5c4543a0 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -73,6 +73,7 @@ export default class AppStateController extends EventEmitter { switchedNetworkDetails: null, switchedNetworkNeverShowMessage: false, currentExtensionPopupId: 0, + lastInteractedConfirmationInfo: undefined, }); this.timer = null; @@ -560,6 +561,26 @@ export default class AppStateController extends EventEmitter { }); } + /** + * The function returns information about the last confirmation user interacted with + * + * @returns {lastInteractedConfirmationInfo}: Information about the last confirmation user interacted with. + */ + getLastInteractedConfirmationInfo() { + return this.store.getState().lastInteractedConfirmationInfo; + } + + /** + * Update the information about the last confirmation user interacted with + * + * @param lastInteractedConfirmationInfo + */ + setLastInteractedConfirmationInfo(lastInteractedConfirmationInfo) { + this.store.updateState({ + lastInteractedConfirmationInfo, + }); + } + /** * A getter to retrieve currentPopupId saved in the appState */ diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6ab4b9b85879..3aee28fee7dd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3683,6 +3683,16 @@ export default class MetamaskController extends EventEmitter { resolvePendingApproval: this.resolvePendingApproval, rejectPendingApproval: this.rejectPendingApproval, + getLastInteractedConfirmationInfo: + this.appStateController.getLastInteractedConfirmationInfo.bind( + this.appStateController, + ), + + setLastInteractedConfirmationInfo: + this.appStateController.setLastInteractedConfirmationInfo.bind( + this.appStateController, + ), + // Notifications resetViewedNotifications: announcementController.resetViewed.bind( announcementController, diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/index.scss b/ui/pages/confirmations/components/confirm/network-change-toast/index.scss new file mode 100644 index 000000000000..4b679dc6339a --- /dev/null +++ b/ui/pages/confirmations/components/confirm/network-change-toast/index.scss @@ -0,0 +1,11 @@ +@use "design-system"; + +.toast_wrapper { + bottom: 70px; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 10px; + position: fixed; + width: 100%; + z-index: design-system.$modal-z-index; +} diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/index.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/index.tsx new file mode 100644 index 000000000000..dd8d146031de --- /dev/null +++ b/ui/pages/confirmations/components/confirm/network-change-toast/index.tsx @@ -0,0 +1,2 @@ +export { default as NetworkChangeToast } from './network-change-toast'; +export { default as NetworkChangeToastLegacy } from './network-change-toast-inner'; diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.test.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.test.tsx new file mode 100644 index 000000000000..ed6cc468ad7f --- /dev/null +++ b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import configureStore from 'redux-mock-store'; +import { + TransactionStatus, + TransactionType, +} from '@metamask/transaction-controller'; + +import mockState from '../../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../../test/lib/render-helpers'; + +import NetworkChangeToastInner from './network-change-toast-inner'; + +const render = () => { + const currentConfirmationMock = { + id: '1', + status: TransactionStatus.unapproved, + time: new Date().getTime(), + type: TransactionType.personalSign, + chainId: '0x1', + }; + + const mockExpectedState = { + ...mockState, + metamask: { + ...mockState.metamask, + unapprovedPersonalMsgs: { + '1': { ...currentConfirmationMock, msgParams: {} }, + }, + pendingApprovals: { + '1': { + ...currentConfirmationMock, + origin: 'origin', + requestData: {}, + requestState: null, + expectsResult: false, + }, + }, + preferences: { redesignedConfirmationsEnabled: true }, + }, + confirm: { currentConfirmation: currentConfirmationMock }, + }; + + const defaultStore = configureStore()(mockExpectedState); + return renderWithProvider( + , + defaultStore, + ); +}; + +describe('NetworkChangeToast', () => { + it('render without throwing error', () => { + expect(() => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }).not.toThrow(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.tsx new file mode 100644 index 000000000000..19b7d2e2d3ad --- /dev/null +++ b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-inner.tsx @@ -0,0 +1,85 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { Box } from '../../../../../components/component-library'; +import { Toast } from '../../../../../components/multichain'; +import { + getLastInteractedConfirmationInfo, + setLastInteractedConfirmationInfo, +} from '../../../../../store/actions'; +import { getCurrentChainId } from '../../../../../selectors'; +import { NETWORK_TO_NAME_MAP } from '../../../../../../shared/constants/network'; +import { useI18nContext } from '../../../../../hooks/useI18nContext'; + +const MILLISECONDS_IN_ONE_MINUTES = 60000; +const MILLISECONDS_IN_FIVE_SECONDS = 5000; + +const NetworkChangeToastInner = ({ + confirmation, +}: { + confirmation: { id: string }; +}) => { + const chainId = useSelector(getCurrentChainId); + const [toastVisible, setToastVisible] = useState(false); + const t = useI18nContext(); + + const hideToast = useCallback(() => { + setToastVisible(false); + }, [setToastVisible]); + + useEffect(() => { + let isMounted = true; + if (!confirmation) { + return undefined; + } + (async () => { + const lastInteractedConfirmationInfo = + await getLastInteractedConfirmationInfo(); + const currentTimestamp = new Date().getTime(); + if ( + lastInteractedConfirmationInfo && + lastInteractedConfirmationInfo.chainId !== chainId && + currentTimestamp - lastInteractedConfirmationInfo.timestamp <= + MILLISECONDS_IN_ONE_MINUTES && + isMounted + ) { + setToastVisible(true); + setTimeout(() => { + hideToast(); + }, MILLISECONDS_IN_FIVE_SECONDS); + } + if ( + (!lastInteractedConfirmationInfo || + lastInteractedConfirmationInfo?.id !== confirmation.id) && + isMounted + ) { + setLastInteractedConfirmationInfo({ + id: confirmation.id, + chainId, + timestamp: new Date().getTime(), + }); + } + })(); + return () => { + isMounted = false; + }; + }, [confirmation?.id]); + + if (!toastVisible) { + return null; + } + + return ( + + )[chainId], + ])} + startAdornment={null} + /> + + ); +}; + +export default NetworkChangeToastInner; diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast.tsx new file mode 100644 index 000000000000..c3bf3095714d --- /dev/null +++ b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import useCurrentConfirmation from '../../../hooks/useCurrentConfirmation'; +import NetworkChangeToastInner from './network-change-toast-inner'; + +// The component has been broken into NetworkChangeToast and NetworkChangeToastInner +// to suffice need of old and re-designed confirmation pages. +// These can be merged once we get rid of old confirmation pages. +const NetworkChangeToast = () => { + const { currentConfirmation } = useCurrentConfirmation(); + return ; +}; + +export default NetworkChangeToast; diff --git a/ui/pages/confirmations/components/index.scss b/ui/pages/confirmations/components/index.scss index 10bf10d15bfd..c38aaba012df 100644 --- a/ui/pages/confirmations/components/index.scss +++ b/ui/pages/confirmations/components/index.scss @@ -14,6 +14,7 @@ @import 'confirm/header/header.scss'; @import 'confirm/scroll-to-bottom'; @import 'confirm/nav/nav.scss'; +@import "confirm/network-change-toast/index"; @import 'contract-details-modal/index'; @import 'custom-nonce/index'; @import 'edit-gas-display/index'; diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js index 08fe9b727889..2f06792a7b51 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.component.js @@ -57,7 +57,7 @@ import SnapLegacyAuthorshipHeader from '../../../../components/app/snaps/snap-le import InsightWarnings from '../../../../components/app/snaps/insight-warnings'; ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; -import { BlockaidUnavailableBannerAlert } from '../blockaid-unavailable-banner-alert/blockaid-unavailable-banner-alert'; +import { NetworkChangeToastLegacy } from '../confirm/network-change-toast'; import SignatureRequestOriginalWarning from './signature-request-original-warning'; export default class SignatureRequestOriginal extends Component { @@ -452,6 +452,7 @@ export default class SignatureRequestOriginal extends Component { {rejectNText} ) : null} + ); }; diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js index 2d2470f7ed09..ece23ed68271 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.js @@ -48,6 +48,7 @@ import SignatureRequestHeader from '../signature-request-header'; import InsightWarnings from '../../../../components/app/snaps/insight-warnings'; ///: END:ONLY_INCLUDE_IF import { BlockaidResultType } from '../../../../../shared/constants/security-provider'; +import { NetworkChangeToastLegacy } from '../confirm/network-change-toast'; import Header from './signature-request-siwe-header'; import Message from './signature-request-siwe-message'; @@ -299,9 +300,7 @@ export default function SignatureRequestSIWE({ }} /> )} - { - ///: END:ONLY_INCLUDE_IF - } + ); } diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index 73b0fa64fe5c..77bbb23f8444 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -82,8 +82,7 @@ import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-bann ///: BEGIN:ONLY_INCLUDE_IF(snaps) import InsightWarnings from '../../../../components/app/snaps/insight-warnings'; -///: END:ONLY_INCLUDE_IF -import { BlockaidUnavailableBannerAlert } from '../blockaid-unavailable-banner-alert/blockaid-unavailable-banner-alert'; +import { NetworkChangeToastLegacy } from '../confirm/network-change-toast'; import Message from './signature-request-message'; import Footer from './signature-request-footer'; @@ -382,9 +381,7 @@ const SignatureRequest = ({ }} /> )} - { - ///: END:ONLY_INCLUDE_IF - } + ); }; diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 8e7c6423d842..cd7b1b1099d6 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -39,6 +39,7 @@ import { parseStandardTokenTransactionData } from '../../../../shared/modules/tr import { TokenStandard } from '../../../../shared/constants/transaction'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; import TokenAllowance from '../token-allowance/token-allowance'; +import { NetworkChangeToastLegacy } from '../components/confirm/network-change-toast'; import { getCustomTxParamsData } from './confirm-approve.util'; import ConfirmApproveContent from './confirm-approve-content'; @@ -229,6 +230,7 @@ export default function ConfirmApprove({ )} + ); } diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index b8546e2953e5..b116576b5336 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -69,7 +69,7 @@ import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import FeeDetailsComponent from '../components/fee-details-component/fee-details-component'; import { SimulationDetails } from '../components/simulation-details'; import { fetchSwapsFeatureFlags } from '../../swaps/swaps.util'; -import { BlockaidUnavailableBannerAlert } from '../components/blockaid-unavailable-banner-alert/blockaid-unavailable-banner-alert'; +import { NetworkChangeToastLegacy } from '../components/confirm/network-change-toast'; export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -1201,6 +1201,7 @@ export default class ConfirmTransactionBase extends Component { txData={txData} displayAccountBalanceHeader={displayAccountBalanceHeader} /> + ); } diff --git a/ui/pages/confirmations/confirm/confirm.tsx b/ui/pages/confirmations/confirm/confirm.tsx index 3f2520667ddd..a113e915becc 100644 --- a/ui/pages/confirmations/confirm/confirm.tsx +++ b/ui/pages/confirmations/confirm/confirm.tsx @@ -11,7 +11,8 @@ import { MMISignatureMismatchBanner } from '../../../components/app/mmi-signatur ///: END:ONLY_INCLUDE_IF import { Nav } from '../components/confirm/nav'; import { Title } from '../components/confirm/title'; -import { Page } from '../../../components/multichain/pages/page'; +import EditGasFeePopover from '../components/edit-gas-fee-popover'; +import { NetworkChangeToast } from '../components/confirm/network-change-toast'; import setCurrentConfirmation from '../hooks/setCurrentConfirmation'; import syncConfirmPath from '../hooks/syncConfirmPath'; import { LedgerInfo } from '../components/confirm/ledger-info'; @@ -25,24 +26,32 @@ const Confirm = () => { const processAction = useConfirmationAlertActions(); return ( - - -