diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 5e6ef82f4..10d92d3f4 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -52,6 +52,23 @@
"addAlias": {
"message": "Add alias"
},
+ "addEthereumChainConfirmationDescription": {
+ "message": "This will allow this network to be used within MetaMask."
+ },
+ "addEthereumChainConfirmationRisks": {
+ "message": "MetaMask does not verify custom networks."
+ },
+ "addEthereumChainConfirmationRisksLearnMore": {
+ "message": "Learn about $1.",
+ "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key"
+ },
+ "addEthereumChainConfirmationRisksLearnMoreLink": {
+ "message": "scams and network security risks",
+ "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key"
+ },
+ "addEthereumChainConfirmationTitle": {
+ "message": "Allow this site to add a network?"
+ },
"addNetwork": {
"message": "Add Network"
},
@@ -150,6 +167,9 @@
"approve": {
"message": "Approve spend limit"
},
+ "approveButtonText": {
+ "message": "Approve"
+ },
"approveSpendLimit": {
"message": "Approve $1 spend limit",
"description": "The token symbol that is being approved"
@@ -212,7 +232,10 @@
"message": "Basic"
},
"blockExplorerUrl": {
- "message": "Block Explorer"
+ "message": "Block Explorer URL"
+ },
+ "blockExplorerUrlDefinition": {
+ "message": "The URL used as the block explorer for this network."
},
"blockExplorerView": {
"message": "View account at $1",
@@ -254,6 +277,9 @@
"chainId": {
"message": "Chain ID"
},
+ "chainIdDefinition": {
+ "message": "The chain ID used to sign transactions for this network."
+ },
"chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
},
@@ -409,6 +435,12 @@
"currencyConversion": {
"message": "Currency Conversion"
},
+ "currencySymbol": {
+ "message": "Currency Symbol"
+ },
+ "currencySymbolDefinition": {
+ "message": "The ticker symbol displayed for this network’s currency."
+ },
"currentAccountNotConnected": {
"message": "Your current account is not connected"
},
@@ -1005,6 +1037,14 @@
"metametricsOptInDescription": {
"message": "MetaMask would like to gather usage data to better understand how our users interact with the extension. This data will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem."
},
+ "mismatchedChain": {
+ "message": "This network details for this Chain ID do not match our records. We recommend that you $1 before proceeding.",
+ "description": "$1 is a clickable link with text defined by the 'mismatchedChainLinkText' key"
+ },
+ "mismatchedChainLinkText": {
+ "message": "verify the network details",
+ "description": "Serves as link text for the 'mismatchedChain' key. This text will be embedded inside the translation for that key."
+ },
"mobileSyncText": {
"message": "Please enter your password to confirm it's you!"
},
@@ -1030,15 +1070,27 @@
"negativeETH": {
"message": "Can not send negative amounts of ETH."
},
+ "networkDetails": {
+ "message": "Network Details"
+ },
"networkName": {
"message": "Network Name"
},
+ "networkNameDefinition": {
+ "message": "The name associated with this network."
+ },
"networkSettingsChainIdDescription": {
"message": "The chain ID is used for signing transactions. It must match the chain ID returned by the network. You can enter a decimal or '0x'-prefixed hexadecimal number, but we will display the number in decimal."
},
"networkSettingsDescription": {
"message": "Add and edit custom RPC networks"
},
+ "networkURL": {
+ "message": "Network URL"
+ },
+ "networkURLDefinition": {
+ "message": "The URL used to access this network."
+ },
"networks": {
"message": "Networks"
},
@@ -1892,12 +1944,24 @@
"swapsViewInActivity": {
"message": "View in activity"
},
+ "switchEthereumChainConfirmationDescription": {
+ "message": "This will will switch the selected network within MetaMask to a previously added network:"
+ },
+ "switchEthereumChainConfirmationTitle": {
+ "message": "Allow this site to switch the network?"
+ },
+ "switchNetwork": {
+ "message": "Switch network"
+ },
"switchNetworks": {
"message": "Switch Networks"
},
"switchToThisAccount": {
"message": "Switch to this account"
},
+ "switchingNetworksCancelsPendingConfirmations": {
+ "message": "Switching networks will cancel all pending confirmations"
+ },
"symbol": {
"message": "Symbol"
},
@@ -2073,6 +2137,14 @@
"unlockMessage": {
"message": "The decentralized web awaits"
},
+ "unrecognizedChain": {
+ "message": "This custom network is not recognized. We recommend that you $1 before proceeding",
+ "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details."
+ },
+ "unrecognizedChainLinkText": {
+ "message": "verify the network details",
+ "description": "Serves as link text for the 'unrecognizedChain' key. This text will be embedded inside the translation for that key."
+ },
"updatedWithDate": {
"message": "Updated $1"
},
@@ -2145,6 +2217,10 @@
"message": "$1 of $2",
"description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total"
},
+ "xOfYPending": {
+ "message": "$1 of $2 pending",
+ "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total"
+ },
"yesLetsTry": {
"message": "Yes, let's try"
},
diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js
index 1a0b6825c..2e7b7584b 100644
--- a/development/verify-locale-strings.js
+++ b/development/verify-locale-strings.js
@@ -172,6 +172,7 @@ async function verifyEnglishLocale() {
// and gradually phase out the key based search
const globsToStrictSearch = [
'ui/app/components/app/metamask-translation/*.js',
+ 'ui/app/pages/confirmation/templates/*.js',
];
const testGlob = '**/*.test.js';
const javascriptFiles = await glob(['ui/**/*.js', 'shared/**/*.js'], {
diff --git a/package.json b/package.json
index 6140d2881..01d69b02e 100644
--- a/package.json
+++ b/package.json
@@ -135,6 +135,7 @@
"fuse.js": "^3.2.0",
"globalthis": "^1.0.1",
"human-standard-token-abi": "^2.0.0",
+ "immer": "^8.0.1",
"json-rpc-engine": "^6.1.0",
"json-rpc-middleware-stream": "^2.1.1",
"jsonschema": "^1.2.4",
diff --git a/ui/app/components/app/metamask-template-renderer/safe-component-list.js b/ui/app/components/app/metamask-template-renderer/safe-component-list.js
index b17dbad82..72571832b 100644
--- a/ui/app/components/app/metamask-template-renderer/safe-component-list.js
+++ b/ui/app/components/app/metamask-template-renderer/safe-component-list.js
@@ -6,9 +6,11 @@ import Popover from '../../ui/popover';
import Typography from '../../ui/typography';
import Box from '../../ui/box';
import MetaMaskTranslation from '../metamask-translation';
+import NetworkDisplay from '../network-display';
export const safeComponentList = {
MetaMaskTranslation,
+ a: 'a',
b: 'b',
p: 'p',
div: 'div',
@@ -20,4 +22,5 @@ export const safeComponentList = {
Button,
Popover,
Box,
+ NetworkDisplay,
};
diff --git a/ui/app/components/app/modals/tests/account-details-modal.test.js b/ui/app/components/app/modals/tests/account-details-modal.test.js
index f4da817e4..304b76d7c 100644
--- a/ui/app/components/app/modals/tests/account-details-modal.test.js
+++ b/ui/app/components/app/modals/tests/account-details-modal.test.js
@@ -70,11 +70,8 @@ describe('Account Details Modal', function () {
wrapper.setProps({ rpcPrefs: { blockExplorerUrl } });
const modalButton = wrapper.find('.account-details-modal__button');
- const blockExplorerLink = modalButton.first();
+ const blockExplorerLink = modalButton.first().shallow();
- assert.strictEqual(
- blockExplorerLink.html(),
- '',
- );
+ assert.strictEqual(blockExplorerLink.text(), 'blockExplorerView');
});
});
diff --git a/ui/app/components/app/network-display/network-display.js b/ui/app/components/app/network-display/network-display.js
index beda83498..65060abdf 100644
--- a/ui/app/components/app/network-display/network-display.js
+++ b/ui/app/components/app/network-display/network-display.js
@@ -2,7 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useSelector } from 'react-redux';
-import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network';
+import {
+ NETWORK_TYPE_RPC,
+ NETWORK_TYPE_TO_ID_MAP,
+} from '../../../../../shared/constants/network';
import LoadingIndicator from '../../ui/loading-indicator';
import ColorIndicator from '../../ui/color-indicator';
@@ -21,15 +24,19 @@ export default function NetworkDisplay({
indicatorSize,
disabled,
labelProps,
+ targetNetwork,
onClick,
}) {
- const { network, networkNickname, networkType } = useSelector((state) => ({
+ const currentNetwork = useSelector((state) => ({
network: state.metamask.network,
- networkNickname: state.metamask.provider.nickname,
- networkType: state.metamask.provider.type,
+ nickname: state.metamask.provider.nickname,
+ type: state.metamask.provider.type,
}));
const t = useI18nContext();
+ const { network = '', nickname: networkNickname, type: networkType } =
+ targetNetwork ?? currentNetwork;
+
return (
@@ -75,8 +84,13 @@ export default function NetworkDisplay({
NetworkDisplay.propTypes = {
colored: PropTypes.bool,
indicatorSize: PropTypes.oneOf(Object.values(SIZES)),
- labelProps: PropTypes.shape({
- ...Chip.propTypes.labelProps,
+ labelProps: Chip.propTypes.labelProps,
+ targetNetwork: PropTypes.shape({
+ type: PropTypes.oneOf([
+ ...Object.values(NETWORK_TYPE_TO_ID_MAP),
+ NETWORK_TYPE_RPC,
+ ]),
+ nickname: PropTypes.string,
}),
outline: PropTypes.bool,
disabled: PropTypes.bool,
diff --git a/ui/app/components/app/signature-request-original/signature-request-original.component.js b/ui/app/components/app/signature-request-original/signature-request-original.component.js
index ab7bd6c50..5dbcf9eb7 100644
--- a/ui/app/components/app/signature-request-original/signature-request-original.component.js
+++ b/ui/app/components/app/signature-request-original/signature-request-original.component.js
@@ -166,7 +166,7 @@ export default class SignatureRequestOriginal extends Component {
{originMetadata?.icon ? (
) : null}
diff --git a/ui/app/components/ui/button/button.component.js b/ui/app/components/ui/button/button.component.js
index a1af4da43..4313a301a 100644
--- a/ui/app/components/ui/button/button.component.js
+++ b/ui/app/components/ui/button/button.component.js
@@ -45,6 +45,15 @@ const Button = ({
} else if (submit) {
buttonProps.type = 'submit';
}
+ if (typeof buttonProps.onClick === 'function') {
+ buttonProps.onKeyUp ??= (event) => {
+ if (event.key === 'Enter') {
+ buttonProps.onClick();
+ }
+ };
+ buttonProps.role ??= 'button';
+ buttonProps.tabIndex ??= 0;
+ }
return (
{leftIcon &&
{leftIcon}
}
{children ?? (
diff --git a/ui/app/components/ui/tooltip/tooltip.js b/ui/app/components/ui/tooltip/tooltip.js
index 4c01eaade..9ae60d2a5 100644
--- a/ui/app/components/ui/tooltip/tooltip.js
+++ b/ui/app/components/ui/tooltip/tooltip.js
@@ -14,7 +14,7 @@ export default class Tooltip extends PureComponent {
offset: 0,
size: 'small',
title: null,
- trigger: 'mouseenter',
+ trigger: 'mouseenter focus',
wrapperClassName: undefined,
theme: '',
};
@@ -35,6 +35,7 @@ export default class Tooltip extends PureComponent {
wrapperClassName: PropTypes.string,
style: PropTypes.object,
theme: PropTypes.string,
+ tabIndex: PropTypes.number,
};
render() {
@@ -54,6 +55,7 @@ export default class Tooltip extends PureComponent {
wrapperClassName,
style,
theme,
+ tabIndex,
} = this.props;
if (!title && !html) {
@@ -77,6 +79,7 @@ export default class Tooltip extends PureComponent {
title={title}
trigger={trigger}
theme={theme}
+ tabIndex={tabIndex || 0}
>
{children}
diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js
index 5b92c3fd5..2079dcf40 100644
--- a/ui/app/helpers/constants/routes.js
+++ b/ui/app/helpers/constants/routes.js
@@ -64,6 +64,7 @@ const CONFIRM_TOKEN_METHOD_PATH = '/token-method';
const SIGNATURE_REQUEST_PATH = '/signature-request';
const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request';
const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request';
+const CONFIRMATION_V_NEXT_ROUTE = '/confirmation';
// Used to pull a convenient name for analytics tracking events. The key must
// be react-router ready path, and can include params such as :id for popup windows
@@ -170,6 +171,7 @@ export {
SIGNATURE_REQUEST_PATH,
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
+ CONFIRMATION_V_NEXT_ROUTE,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
ADVANCED_ROUTE,
SECURITY_ROUTE,
diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js
index 3d0686a5f..a7c06c052 100644
--- a/ui/app/helpers/utils/util.js
+++ b/ui/app/helpers/utils/util.js
@@ -350,6 +350,17 @@ export function stripHttpSchemes(urlString) {
return urlString.replace(/^https?:\/\//u, '');
}
+/**
+ * Strips the following schemes from URL strings:
+ * - https
+ *
+ * @param {string} urlString - The URL string to strip the scheme from.
+ * @returns {string} The URL string, without the scheme, if it was stripped.
+ */
+export function stripHttpsScheme(urlString) {
+ return urlString.replace(/^https:\/\//u, '');
+}
+
/**
* Checks whether a URL-like value (object or string) is an extension URL.
*
diff --git a/ui/app/hooks/useOriginMetadata.js b/ui/app/hooks/useOriginMetadata.js
new file mode 100644
index 000000000..31fb405d7
--- /dev/null
+++ b/ui/app/hooks/useOriginMetadata.js
@@ -0,0 +1,42 @@
+import { useSelector } from 'react-redux';
+import { getDomainMetadata } from '../selectors';
+
+/**
+ * @typedef {Object} OriginMetadata
+ * @property {string} host - The host of the origin
+ * @property {string} hostname - The hostname of the origin (host + port)
+ * @property {string} origin - The original origin string itself
+ * @property {string} [icon] - The origin's site icon if available
+ * @property {number} [lastUpdated] - Timestamp of the last update to the
+ * origin's metadata
+ * @property {string} [name] - The registered name of the origin if available
+ */
+
+/**
+ * Gets origin metadata from redux and formats it appropriately.
+ * @param {string} origin - The fully formed url of the site interacting with
+ * MetaMask
+ * @returns {OriginMetadata | null} - The origin metadata available for the
+ * current origin
+ */
+export function useOriginMetadata(origin) {
+ const domainMetaData = useSelector(getDomainMetadata);
+ if (!origin) {
+ return null;
+ }
+ const url = new URL(origin);
+
+ const minimumOriginMetadata = {
+ host: url.host,
+ hostname: url.hostname,
+ origin,
+ };
+
+ if (domainMetaData?.[origin]) {
+ return {
+ ...minimumOriginMetadata,
+ ...domainMetaData[origin],
+ };
+ }
+ return minimumOriginMetadata;
+}
diff --git a/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
index bae3e22eb..3ed8f67c2 100644
--- a/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
+++ b/ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
@@ -179,7 +179,7 @@ export default class ConfirmDecryptMessage extends Component {
const { t } = this.context;
const originMetadata = domainMetadata[txData.msgParams.origin];
- const name = originMetadata?.name || txData.msgParams.origin;
+ const name = originMetadata?.hostname || txData.msgParams.origin;
const notice = t('decryptMessageNotice', [txData.msgParams.origin]);
const {
diff --git a/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
index 45e192da6..fb4100832 100644
--- a/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
+++ b/ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
@@ -160,7 +160,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
const originMetadata = domainMetadata[txData.origin];
const notice = t('encryptionPublicKeyNotice', [txData.origin]);
- const name = originMetadata?.name || txData.origin;
+ const name = originMetadata?.hostname || txData.origin;
return (
diff --git a/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.js b/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.js
new file mode 100644
index 000000000..9574a13a4
--- /dev/null
+++ b/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Button from '../../../../components/ui/button';
+
+export default function ConfirmationFooter({
+ onApprove,
+ onCancel,
+ approveText,
+ cancelText,
+ alerts,
+}) {
+ return (
+
+ {alerts}
+
+
+
+
+
+ );
+}
+
+ConfirmationFooter.propTypes = {
+ alerts: PropTypes.node,
+ onApprove: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ approveText: PropTypes.string.isRequired,
+ cancelText: PropTypes.string.isRequired,
+};
diff --git a/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.scss b/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.scss
new file mode 100644
index 000000000..9bad79296
--- /dev/null
+++ b/ui/app/pages/confirmation/components/confirmation-footer/confirmation-footer.scss
@@ -0,0 +1,14 @@
+.confirmation-footer {
+ grid-area: footer;
+
+ &__actions {
+ display: flex;
+ border-top: 1px solid $ui-2;
+ background-color: white;
+ padding: 16px;
+
+ & .button:first-child {
+ margin-right: 16px;
+ }
+ }
+}
diff --git a/ui/app/pages/confirmation/components/confirmation-footer/index.js b/ui/app/pages/confirmation/components/confirmation-footer/index.js
new file mode 100644
index 000000000..e2f17c87a
--- /dev/null
+++ b/ui/app/pages/confirmation/components/confirmation-footer/index.js
@@ -0,0 +1 @@
+export { default } from './confirmation-footer';
diff --git a/ui/app/pages/confirmation/confirmation.js b/ui/app/pages/confirmation/confirmation.js
new file mode 100644
index 000000000..7d47fc3a8
--- /dev/null
+++ b/ui/app/pages/confirmation/confirmation.js
@@ -0,0 +1,240 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useState,
+} from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import { isEqual } from 'lodash';
+import { produce } from 'immer';
+import { getEnvironmentType } from '../../../../app/scripts/lib/util';
+import {
+ ENVIRONMENT_TYPE_FULLSCREEN,
+ ENVIRONMENT_TYPE_POPUP,
+} from '../../../../shared/constants/app';
+import Box from '../../components/ui/box';
+import Chip from '../../components/ui/chip';
+import MetaMaskTemplateRenderer from '../../components/app/metamask-template-renderer';
+import SiteIcon from '../../components/ui/site-icon';
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
+import { stripHttpsScheme } from '../../helpers/utils/util';
+import { useI18nContext } from '../../hooks/useI18nContext';
+import { useOriginMetadata } from '../../hooks/useOriginMetadata';
+import { getUnapprovedConfirmations } from '../../selectors';
+import NetworkDisplay from '../../components/app/network-display/network-display';
+import { COLORS, SIZES } from '../../helpers/constants/design-system';
+import Callout from '../../components/ui/callout';
+import ConfirmationFooter from './components/confirmation-footer';
+import { getTemplateValues, getTemplateAlerts } from './templates';
+
+/**
+ * a very simple reducer using produce from Immer to keep state manipulation
+ * immutable and painless. This state is not stored in redux state because it
+ * should persist only for the lifespan of the current session, and will only
+ * be used on this page. Dismissing alerts for confirmations should persist
+ * while the user pages back and forth between confirmations. However, if the
+ * user closes the confirmation window and later reopens the extension they
+ * should be displayed the alerts again.
+ */
+const alertStateReducer = produce((state, action) => {
+ switch (action.type) {
+ case 'dismiss':
+ if (state?.[action.confirmationId]?.[action.alertId]) {
+ state[action.confirmationId][action.alertId].dismissed = true;
+ }
+ break;
+ case 'set':
+ if (!state[action.confirmationId]) {
+ state[action.confirmationId] = {};
+ }
+ action.alerts.forEach((alert) => {
+ state[action.confirmationId][alert.id] = {
+ ...alert,
+ dismissed: false,
+ };
+ });
+ break;
+ default:
+ throw new Error(
+ 'You must provide a type when dispatching an action for alertState',
+ );
+ }
+});
+
+/**
+ * Encapsulates the state and effects needed to manage alert state for the
+ * confirmation page in a custom hook. This hook is not likely to be used
+ * outside of this file, but it helps to reduce complexity of the primary
+ * component.
+ * @param {Object} pendingConfirmation - a pending confirmation waiting for
+ * user approval
+ * @returns {[alertState: Object, dismissAlert: Function]} - tuple with
+ * the current alert state and function to dismiss an alert by id
+ */
+function useAlertState(pendingConfirmation) {
+ const [alertState, dispatch] = useReducer(alertStateReducer, {});
+
+ /**
+ * Computation of the current alert state happens every time the current
+ * pendingConfirmation changes. The async function getTemplateAlerts is
+ * responsible for returning alert state. Setting state on unmounted
+ * components is an anti-pattern, so we use a isMounted variable to keep
+ * track of the current state of the component. Returning a function that
+ * sets isMounted to false when the component is unmounted.
+ */
+ useEffect(() => {
+ let isMounted = true;
+ if (pendingConfirmation) {
+ getTemplateAlerts(pendingConfirmation).then((alerts) => {
+ if (isMounted && alerts) {
+ dispatch({
+ type: 'set',
+ confirmationId: pendingConfirmation.id,
+ alerts,
+ });
+ }
+ });
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [pendingConfirmation]);
+
+ const dismissAlert = useCallback(
+ (alertId) => {
+ dispatch({
+ type: 'dismiss',
+ confirmationId: pendingConfirmation.id,
+ alertId,
+ });
+ },
+ [pendingConfirmation],
+ );
+
+ return [alertState, dismissAlert];
+}
+
+export default function ConfirmationPage() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+ const history = useHistory();
+ const pendingConfirmations = useSelector(getUnapprovedConfirmations, isEqual);
+ const [currentPendingConfirmation, setCurrentPendingConfirmation] = useState(
+ 0,
+ );
+ const pendingConfirmation = pendingConfirmations[currentPendingConfirmation];
+ const originMetadata = useOriginMetadata(pendingConfirmation?.origin);
+ const [alertState, dismissAlert] = useAlertState(pendingConfirmation);
+
+ // Generating templatedValues is potentially expensive, and if done on every render
+ // will result in a new object. Avoiding calling this generation unnecessarily will
+ // improve performance and prevent unnecessary draws.
+ const templatedValues = useMemo(() => {
+ return pendingConfirmation
+ ? getTemplateValues(pendingConfirmation, t, dispatch)
+ : {};
+ }, [pendingConfirmation, t, dispatch]);
+
+ useEffect(() => {
+ const environmentType = getEnvironmentType();
+ // If the number of pending confirmations reduces to zero when the user
+ // is in the fullscreen or popup UI, return them to the default route.
+ // Otherwise, if the number of pending confirmations reduces to a number
+ // that is less than the currently viewed index, reset the index.
+ if (
+ pendingConfirmations.length === 0 &&
+ (environmentType === ENVIRONMENT_TYPE_FULLSCREEN ||
+ environmentType === ENVIRONMENT_TYPE_POPUP)
+ ) {
+ history.push(DEFAULT_ROUTE);
+ } else if (pendingConfirmations.length <= currentPendingConfirmation) {
+ setCurrentPendingConfirmation(pendingConfirmations.length - 1);
+ }
+ }, [pendingConfirmations, history, currentPendingConfirmation]);
+ if (!pendingConfirmation) {
+ return null;
+ }
+
+ return (
+