-
-
-
+ />
diff --git a/ui/components/app/name/name-details/name-details.test.tsx b/ui/components/app/name/name-details/name-details.test.tsx
index 9e93384adcd6..0f0df9f5b5f6 100644
--- a/ui/components/app/name/name-details/name-details.test.tsx
+++ b/ui/components/app/name/name-details/name-details.test.tsx
@@ -11,8 +11,8 @@ import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
-import { mockNetworkState } from '../../../../../test/stub/networks';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
+import { mockNetworkState } from '../../../../../test/stub/networks';
import NameDetails from './name-details';
jest.mock('../../../../store/actions', () => ({
@@ -37,11 +37,11 @@ const SOURCE_ID_MOCK = 'ens';
const SOURCE_ID_2_MOCK = 'some_snap';
const PROPOSED_NAME_MOCK = 'TestProposedName';
const PROPOSED_NAME_2_MOCK = 'TestProposedName2';
+const VARIATION_MOCK = CHAIN_ID_MOCK;
const STATE_MOCK = {
metamask: {
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
-
nameSources: {
[SOURCE_ID_2_MOCK]: { label: 'Super Name Resolution Snap' },
},
@@ -85,13 +85,17 @@ const STATE_MOCK = {
},
},
useTokenDetection: true,
- tokenList: {
- '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': {
- address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d',
- symbol: 'IUSD',
- name: 'iZUMi Bond USD',
- iconUrl:
- 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png',
+ tokensChainsCache: {
+ [VARIATION_MOCK]: {
+ data: {
+ '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': {
+ address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d',
+ symbol: 'IUSD',
+ name: 'iZUMi Bond USD',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png',
+ },
+ },
},
},
},
@@ -157,6 +161,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -170,6 +175,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -183,6 +189,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -196,6 +203,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -209,6 +217,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -229,6 +238,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -251,6 +261,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -273,6 +284,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -295,6 +307,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -317,6 +330,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -336,6 +350,7 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -373,6 +388,7 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -399,6 +415,7 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -426,6 +443,7 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -454,6 +472,7 @@ describe('NameDetails', () => {
undefined}
/>
,
diff --git a/ui/components/app/name/name-details/name-details.tsx b/ui/components/app/name/name-details/name-details.tsx
index 22b0445a0ad5..1bb2b1f1e478 100644
--- a/ui/components/app/name/name-details/name-details.tsx
+++ b/ui/components/app/name/name-details/name-details.tsx
@@ -46,7 +46,7 @@ import Name from '../name';
import FormComboField, {
FormComboFieldOption,
} from '../../../ui/form-combo-field/form-combo-field';
-import { getCurrentChainId, getNameSources } from '../../../../selectors';
+import { getNameSources } from '../../../../selectors';
import {
setName as saveName,
updateProposedNames,
@@ -64,6 +64,7 @@ export type NameDetailsProps = {
sourcePriority?: string[];
type: NameType;
value: string;
+ variation: string;
};
type ProposedNameOption = Required & {
@@ -157,12 +158,14 @@ function getInitialSources(
return [...resultSources, ...stateSources].sort();
}
-function useProposedNames(value: string, type: NameType, chainId: string) {
+function useProposedNames(value: string, type: NameType, variation: string) {
const dispatch = useDispatch();
- const { proposedNames } = useName(value, type);
+ const { proposedNames } = useName(value, type, variation);
+
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateInterval = useRef();
+
const [initialSources, setInitialSources] = useState();
useEffect(() => {
@@ -178,7 +181,7 @@ function useProposedNames(value: string, type: NameType, chainId: string) {
value,
type,
onlyUpdateAfterDelay: true,
- variation: chainId,
+ variation,
}),
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -196,7 +199,7 @@ function useProposedNames(value: string, type: NameType, chainId: string) {
updateInterval.current = setInterval(update, UPDATE_DELAY);
return reset;
- }, [value, type, chainId, dispatch, initialSources, setInitialSources]);
+ }, [value, type, variation, dispatch, initialSources, setInitialSources]);
return { proposedNames, initialSources };
}
@@ -205,13 +208,20 @@ export default function NameDetails({
onClose,
type,
value,
+ variation,
}: NameDetailsProps) {
- const chainId = useSelector(getCurrentChainId);
- const { name: savedPetname, sourceId: savedSourceId } = useName(value, type);
- const { name: displayName, hasPetname: hasSavedPetname } = useDisplayName(
+ const { name: savedPetname, sourceId: savedSourceId } = useName(
value,
type,
+ variation,
);
+
+ const { name: displayName, hasPetname: hasSavedPetname } = useDisplayName({
+ value,
+ type,
+ variation,
+ });
+
const nameSources = useSelector(getNameSources, isEqual);
const [name, setName] = useState('');
const [openMetricSent, setOpenMetricSent] = useState(false);
@@ -226,7 +236,7 @@ export default function NameDetails({
const { proposedNames, initialSources } = useProposedNames(
value,
type,
- chainId,
+ variation,
);
const [copiedAddress, handleCopyAddress] = useCopyToClipboard() as [
@@ -275,12 +285,12 @@ export default function NameDetails({
type,
name: name?.length ? name : null,
sourceId: selectedSourceId,
- variation: chainId,
+ variation,
}),
);
onClose();
- }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, chainId]);
+ }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, variation]);
const handleClose = useCallback(() => {
onClose();
@@ -333,6 +343,7 @@ export default function NameDetails({
diff --git a/ui/components/app/name/name.test.tsx b/ui/components/app/name/name.test.tsx
index 061d39e670de..33648e98e38c 100644
--- a/ui/components/app/name/name.test.tsx
+++ b/ui/components/app/name/name.test.tsx
@@ -22,6 +22,7 @@ jest.mock('react-redux', () => ({
const ADDRESS_NO_SAVED_NAME_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54977';
const ADDRESS_SAVED_NAME_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979';
const SAVED_NAME_MOCK = 'TestName';
+const VARIATION_MOCK = 'testVariation';
const STATE_MOCK = {
metamask: {
@@ -44,7 +45,11 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -61,6 +66,7 @@ describe('Name', () => {
,
store,
);
@@ -75,7 +81,11 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -90,7 +100,11 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -114,7 +128,11 @@ describe('Name', () => {
renderWithProvider(
-
+
,
store,
);
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx
index 5af2851c8885..2097d21faf07 100644
--- a/ui/components/app/name/name.tsx
+++ b/ui/components/app/name/name.tsx
@@ -38,6 +38,12 @@ export type NameProps = {
/** The raw value to display the name of. */
value: string;
+
+ /**
+ * The variation of the value.
+ * Such as the chain ID if the `type` is an Ethereum address.
+ */
+ variation: string;
};
function formatValue(value: string, type: NameType): string {
@@ -61,15 +67,17 @@ const Name = memo(
disableEdit,
internal,
preferContractSymbol = false,
+ variation,
}: NameProps) => {
const [modalOpen, setModalOpen] = useState(false);
const trackEvent = useContext(MetaMetricsContext);
- const { name, hasPetname, image } = useDisplayName(
+ const { name, hasPetname, image } = useDisplayName({
value,
type,
preferContractSymbol,
- );
+ variation,
+ });
useEffect(() => {
if (internal) {
@@ -100,7 +108,12 @@ const Name = memo(
return (
{!disableEdit && modalOpen && (
-
+
)}
= ({
const shortenedAddress = shortenAddress(transformedAddress);
return (
-
+
{shortenedAddress}
diff --git a/ui/components/app/snaps/snap-ui-link/index.scss b/ui/components/app/snaps/snap-ui-link/index.scss
new file mode 100644
index 000000000000..7d3f75f0e372
--- /dev/null
+++ b/ui/components/app/snaps/snap-ui-link/index.scss
@@ -0,0 +1,11 @@
+.snap-ui-renderer__link {
+ & .snap-ui-renderer__address {
+ // Fixes an issue where the link end icon would wrap
+ display: inline-flex;
+ }
+
+ .snap-ui-renderer__address + .mm-icon {
+ // This fixes an issue where the icon would be misaligned with the Address component
+ top: 0;
+ }
+}
diff --git a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
index a1289543fd45..58a22008a52a 100644
--- a/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
+++ b/ui/components/app/snaps/snap-ui-link/snap-ui-link.js
@@ -34,7 +34,7 @@ export const SnapUILink = ({ href, children }) => {
{children}
@@ -51,7 +51,14 @@ export const SnapUILink = ({ href, children }) => {
externalLink
size={ButtonLinkSize.Inherit}
display={Display.Inline}
- className="snap-ui-link"
+ className="snap-ui-renderer__link"
+ style={{
+ // Prevents the link from taking up the full width of the parent.
+ width: 'fit-content',
+ }}
+ textProps={{
+ display: Display.Inline,
+ }}
>
{children}
diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss
index 7e18e72c917f..d32edf726479 100644
--- a/ui/components/app/snaps/snap-ui-renderer/index.scss
+++ b/ui/components/app/snaps/snap-ui-renderer/index.scss
@@ -34,6 +34,10 @@
border-radius: 8px;
border-color: var(--color-border-muted);
+ & .mm-icon {
+ top: 0;
+ }
+
.mm-text--overflow-wrap-anywhere {
overflow-wrap: normal;
}
@@ -48,10 +52,6 @@
&__panel {
gap: 8px;
-
- .mm-icon--size-inherit {
- top: 0;
- }
}
&__text {
diff --git a/ui/components/app/toast-master/selectors.ts b/ui/components/app/toast-master/selectors.ts
new file mode 100644
index 000000000000..b88762c3bc19
--- /dev/null
+++ b/ui/components/app/toast-master/selectors.ts
@@ -0,0 +1,108 @@
+import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api';
+import { getAlertEnabledness } from '../../../ducks/metamask/metamask';
+import { PRIVACY_POLICY_DATE } from '../../../helpers/constants/privacy-policy';
+import {
+ SURVEY_DATE,
+ SURVEY_END_TIME,
+ SURVEY_START_TIME,
+} from '../../../helpers/constants/survey';
+import { getPermittedAccountsForCurrentTab } from '../../../selectors';
+import { MetaMaskReduxState } from '../../../store/store';
+import { getIsPrivacyToastRecent } from './utils';
+
+// TODO: get this into one of the larger definitions of state type
+type State = Omit & {
+ appState: {
+ showNftDetectionEnablementToast?: boolean;
+ };
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed?: boolean;
+ newPrivacyPolicyToastShownDate?: number;
+ onboardingDate?: number;
+ showNftDetectionEnablementToast?: boolean;
+ surveyLinkLastClickedOrClosed?: number;
+ switchedNetworkNeverShowMessage?: boolean;
+ };
+};
+
+/**
+ * Determines if the survey toast should be shown based on the current time, survey start and end times, and whether the survey link was last clicked or closed.
+ *
+ * @param state - The application state containing the necessary survey data.
+ * @returns True if the current time is between the survey start and end times and the survey link was not last clicked or closed. False otherwise.
+ */
+export function selectShowSurveyToast(state: State): boolean {
+ if (state.metamask?.surveyLinkLastClickedOrClosed) {
+ return false;
+ }
+
+ const startTime = new Date(`${SURVEY_DATE} ${SURVEY_START_TIME}`).getTime();
+ const endTime = new Date(`${SURVEY_DATE} ${SURVEY_END_TIME}`).getTime();
+ const now = Date.now();
+
+ return now > startTime && now < endTime;
+}
+
+/**
+ * Determines if the privacy policy toast should be shown based on the current date and whether the new privacy policy toast was clicked or closed.
+ *
+ * @param state - The application state containing the privacy policy data.
+ * @returns Boolean is True if the toast should be shown, and the number is the date the toast was last shown.
+ */
+export function selectShowPrivacyPolicyToast(state: State): {
+ showPrivacyPolicyToast: boolean;
+ newPrivacyPolicyToastShownDate?: number;
+} {
+ const {
+ newPrivacyPolicyToastClickedOrClosed,
+ newPrivacyPolicyToastShownDate,
+ onboardingDate,
+ } = state.metamask || {};
+ const newPrivacyPolicyDate = new Date(PRIVACY_POLICY_DATE);
+ const currentDate = new Date(Date.now());
+
+ const showPrivacyPolicyToast =
+ !newPrivacyPolicyToastClickedOrClosed &&
+ currentDate >= newPrivacyPolicyDate &&
+ getIsPrivacyToastRecent(newPrivacyPolicyToastShownDate) &&
+ // users who onboarded before the privacy policy date should see the notice
+ // and
+ // old users who don't have onboardingDate set should see the notice
+ (!onboardingDate || onboardingDate < newPrivacyPolicyDate.valueOf());
+
+ return { showPrivacyPolicyToast, newPrivacyPolicyToastShownDate };
+}
+
+export function selectNftDetectionEnablementToast(state: State): boolean {
+ return Boolean(state.appState?.showNftDetectionEnablementToast);
+}
+
+// If there is more than one connected account to activeTabOrigin,
+// *BUT* the current account is not one of them, show the banner
+export function selectShowConnectAccountToast(
+ state: State,
+ account: InternalAccount,
+): boolean {
+ const allowShowAccountSetting = getAlertEnabledness(state).unconnectedAccount;
+ const connectedAccounts = getPermittedAccountsForCurrentTab(state);
+ const isEvmAccount = isEvmAccountType(account?.type);
+
+ return (
+ allowShowAccountSetting &&
+ account &&
+ state.activeTab?.origin &&
+ isEvmAccount &&
+ connectedAccounts.length > 0 &&
+ !connectedAccounts.some((address) => address === account.address)
+ );
+}
+
+/**
+ * Retrieves user preference to never see the "Switched Network" toast
+ *
+ * @param state - Redux state object.
+ * @returns Boolean preference value
+ */
+export function selectSwitchedNetworkNeverShowMessage(state: State): boolean {
+ return Boolean(state.metamask.switchedNetworkNeverShowMessage);
+}
diff --git a/ui/components/app/toast-master/toast-master.js b/ui/components/app/toast-master/toast-master.js
new file mode 100644
index 000000000000..584f1cc25983
--- /dev/null
+++ b/ui/components/app/toast-master/toast-master.js
@@ -0,0 +1,299 @@
+/* eslint-disable react/prop-types -- TODO: upgrade to TypeScript */
+
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory, useLocation } from 'react-router-dom';
+import { MILLISECOND, SECOND } from '../../../../shared/constants/time';
+import {
+ PRIVACY_POLICY_LINK,
+ SURVEY_LINK,
+} from '../../../../shared/lib/ui-utils';
+import {
+ BorderColor,
+ BorderRadius,
+ IconColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import {
+ DEFAULT_ROUTE,
+ REVIEW_PERMISSIONS,
+} from '../../../helpers/constants/routes';
+import { getURLHost } from '../../../helpers/utils/util';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { usePrevious } from '../../../hooks/usePrevious';
+import {
+ getCurrentNetwork,
+ getOriginOfCurrentTab,
+ getSelectedAccount,
+ getSwitchedNetworkDetails,
+ getUseNftDetection,
+} from '../../../selectors';
+import {
+ addPermittedAccount,
+ clearSwitchedNetworkDetails,
+ hidePermittedNetworkToast,
+} from '../../../store/actions';
+import {
+ AvatarAccount,
+ AvatarAccountSize,
+ AvatarNetwork,
+ Icon,
+ IconName,
+} from '../../component-library';
+import { Toast, ToastContainer } from '../../multichain';
+import { SurveyToast } from '../../ui/survey-toast';
+import {
+ selectNftDetectionEnablementToast,
+ selectShowConnectAccountToast,
+ selectShowPrivacyPolicyToast,
+ selectShowSurveyToast,
+ selectSwitchedNetworkNeverShowMessage,
+} from './selectors';
+import {
+ setNewPrivacyPolicyToastClickedOrClosed,
+ setNewPrivacyPolicyToastShownDate,
+ setShowNftDetectionEnablementToast,
+ setSurveyLinkLastClickedOrClosed,
+ setSwitchedNetworkNeverShowMessage,
+} from './utils';
+
+export function ToastMaster() {
+ const location = useLocation();
+
+ const onHomeScreen = location.pathname === DEFAULT_ROUTE;
+
+ return (
+ onHomeScreen && (
+
+
+
+
+
+
+
+
+
+ )
+ );
+}
+
+function ConnectAccountToast() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+
+ const [hideConnectAccountToast, setHideConnectAccountToast] = useState(false);
+ const account = useSelector(getSelectedAccount);
+
+ // If the account has changed, allow the connect account toast again
+ const prevAccountAddress = usePrevious(account?.address);
+ if (account?.address !== prevAccountAddress && hideConnectAccountToast) {
+ setHideConnectAccountToast(false);
+ }
+
+ const showConnectAccountToast = useSelector((state) =>
+ selectShowConnectAccountToast(state, account),
+ );
+
+ const activeTabOrigin = useSelector(getOriginOfCurrentTab);
+
+ return (
+ Boolean(!hideConnectAccountToast && showConnectAccountToast) && (
+
+ }
+ text={t('accountIsntConnectedToastText', [
+ account?.metadata?.name,
+ getURLHost(activeTabOrigin),
+ ])}
+ actionText={t('connectAccount')}
+ onActionClick={() => {
+ // Connect this account
+ dispatch(addPermittedAccount(activeTabOrigin, account.address));
+ // Use setTimeout to prevent React re-render from
+ // hiding the tooltip
+ setTimeout(() => {
+ // Trigger a mouseenter on the header's connection icon
+ // to display the informative connection tooltip
+ document
+ .querySelector(
+ '[data-testid="connection-menu"] [data-tooltipped]',
+ )
+ ?.dispatchEvent(new CustomEvent('mouseenter', {}));
+ }, 250 * MILLISECOND);
+ }}
+ onClose={() => setHideConnectAccountToast(true)}
+ />
+ )
+ );
+}
+
+function SurveyToastMayDelete() {
+ const t = useI18nContext();
+
+ const showSurveyToast = useSelector(selectShowSurveyToast);
+
+ return (
+ showSurveyToast && (
+
+ }
+ text={t('surveyTitle')}
+ actionText={t('surveyConversion')}
+ onActionClick={() => {
+ global.platform.openTab({
+ url: SURVEY_LINK,
+ });
+ setSurveyLinkLastClickedOrClosed(Date.now());
+ }}
+ onClose={() => {
+ setSurveyLinkLastClickedOrClosed(Date.now());
+ }}
+ />
+ )
+ );
+}
+
+function PrivacyPolicyToast() {
+ const t = useI18nContext();
+
+ const { showPrivacyPolicyToast, newPrivacyPolicyToastShownDate } =
+ useSelector(selectShowPrivacyPolicyToast);
+
+ // If the privacy policy toast is shown, and there is no date set, set it
+ if (showPrivacyPolicyToast && !newPrivacyPolicyToastShownDate) {
+ setNewPrivacyPolicyToastShownDate(Date.now());
+ }
+
+ return (
+ showPrivacyPolicyToast && (
+
+ }
+ text={t('newPrivacyPolicyTitle')}
+ actionText={t('newPrivacyPolicyActionButton')}
+ onActionClick={() => {
+ global.platform.openTab({
+ url: PRIVACY_POLICY_LINK,
+ });
+ setNewPrivacyPolicyToastClickedOrClosed();
+ }}
+ onClose={setNewPrivacyPolicyToastClickedOrClosed}
+ />
+ )
+ );
+}
+
+function SwitchedNetworkToast() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+
+ const switchedNetworkDetails = useSelector(getSwitchedNetworkDetails);
+ const switchedNetworkNeverShowMessage = useSelector(
+ selectSwitchedNetworkNeverShowMessage,
+ );
+
+ const isShown = switchedNetworkDetails && !switchedNetworkNeverShowMessage;
+
+ return (
+ isShown && (
+
+ }
+ text={t('switchedNetworkToastMessage', [
+ switchedNetworkDetails.nickname,
+ getURLHost(switchedNetworkDetails.origin),
+ ])}
+ actionText={t('switchedNetworkToastDecline')}
+ onActionClick={setSwitchedNetworkNeverShowMessage}
+ onClose={() => dispatch(clearSwitchedNetworkDetails())}
+ />
+ )
+ );
+}
+
+function NftEnablementToast() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+
+ const showNftEnablementToast = useSelector(selectNftDetectionEnablementToast);
+ const useNftDetection = useSelector(getUseNftDetection);
+
+ const autoHideToastDelay = 5 * SECOND;
+
+ return (
+ showNftEnablementToast &&
+ useNftDetection && (
+
+ }
+ text={t('nftAutoDetectionEnabled')}
+ borderRadius={BorderRadius.LG}
+ textVariant={TextVariant.bodyMd}
+ autoHideTime={autoHideToastDelay}
+ onAutoHideToast={() =>
+ dispatch(setShowNftDetectionEnablementToast(false))
+ }
+ />
+ )
+ );
+}
+
+function PermittedNetworkToast() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+
+ const isPermittedNetworkToastOpen = useSelector(
+ (state) => state.appState.showPermittedNetworkToastOpen,
+ );
+
+ const currentNetwork = useSelector(getCurrentNetwork);
+ const activeTabOrigin = useSelector(getOriginOfCurrentTab);
+ const safeEncodedHost = encodeURIComponent(activeTabOrigin);
+ const history = useHistory();
+
+ return (
+ isPermittedNetworkToastOpen && (
+
+ }
+ text={t('permittedChainToastUpdate', [
+ getURLHost(activeTabOrigin),
+ currentNetwork?.nickname,
+ ])}
+ actionText={t('editPermissions')}
+ onActionClick={() => {
+ dispatch(hidePermittedNetworkToast());
+ history.push(`${REVIEW_PERMISSIONS}/${safeEncodedHost}`);
+ }}
+ onClose={() => dispatch(hidePermittedNetworkToast())}
+ />
+ )
+ );
+}
diff --git a/ui/components/app/toast-master/toast-master.test.ts b/ui/components/app/toast-master/toast-master.test.ts
new file mode 100644
index 000000000000..8b29f20a240d
--- /dev/null
+++ b/ui/components/app/toast-master/toast-master.test.ts
@@ -0,0 +1,206 @@
+import { PRIVACY_POLICY_DATE } from '../../../helpers/constants/privacy-policy';
+import { SURVEY_DATE, SURVEY_GMT } from '../../../helpers/constants/survey';
+import {
+ selectShowPrivacyPolicyToast,
+ selectShowSurveyToast,
+} from './selectors';
+
+describe('#getShowSurveyToast', () => {
+ const realDateNow = Date.now;
+
+ afterEach(() => {
+ Date.now = realDateNow;
+ });
+
+ it('shows the survey link when not yet seen and within time bounds', () => {
+ Date.now = () =>
+ new Date(`${SURVEY_DATE} 12:25:00 ${SURVEY_GMT}`).getTime();
+ const result = selectShowSurveyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ surveyLinkLastClickedOrClosed: undefined,
+ },
+ });
+ expect(result).toStrictEqual(true);
+ });
+
+ it('does not show the survey link when seen and within time bounds', () => {
+ Date.now = () =>
+ new Date(`${SURVEY_DATE} 12:25:00 ${SURVEY_GMT}`).getTime();
+ const result = selectShowSurveyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ surveyLinkLastClickedOrClosed: 123456789,
+ },
+ });
+ expect(result).toStrictEqual(false);
+ });
+
+ it('does not show the survey link before time bounds', () => {
+ Date.now = () =>
+ new Date(`${SURVEY_DATE} 11:25:00 ${SURVEY_GMT}`).getTime();
+ const result = selectShowSurveyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ surveyLinkLastClickedOrClosed: undefined,
+ },
+ });
+ expect(result).toStrictEqual(false);
+ });
+
+ it('does not show the survey link after time bounds', () => {
+ Date.now = () =>
+ new Date(`${SURVEY_DATE} 14:25:00 ${SURVEY_GMT}`).getTime();
+ const result = selectShowSurveyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ surveyLinkLastClickedOrClosed: undefined,
+ },
+ });
+ expect(result).toStrictEqual(false);
+ });
+});
+
+describe('#getShowPrivacyPolicyToast', () => {
+ let dateNowSpy: jest.SpyInstance;
+
+ describe('mock one day after', () => {
+ beforeEach(() => {
+ const dayAfterPolicyDate = new Date(PRIVACY_POLICY_DATE);
+ dayAfterPolicyDate.setDate(dayAfterPolicyDate.getDate() + 1);
+
+ dateNowSpy = jest
+ .spyOn(Date, 'now')
+ .mockReturnValue(dayAfterPolicyDate.getTime());
+ });
+
+ afterEach(() => {
+ dateNowSpy.mockRestore();
+ });
+
+ it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is before the policy date', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate(
+ new Date(PRIVACY_POLICY_DATE).getDate() - 2,
+ ),
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(true);
+ });
+
+ it('does not show the privacy policy toast when seen, even if on or after the policy date and onboardingDate is before the policy date', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: true,
+ onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate(
+ new Date(PRIVACY_POLICY_DATE).getDate() - 2,
+ ),
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(false);
+ });
+
+ it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is not set', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: undefined,
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(true);
+ });
+ });
+
+ describe('mock same day', () => {
+ beforeEach(() => {
+ dateNowSpy = jest
+ .spyOn(Date, 'now')
+ .mockReturnValue(new Date(PRIVACY_POLICY_DATE).getTime());
+ });
+
+ afterEach(() => {
+ dateNowSpy.mockRestore();
+ });
+
+ it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is before the policy date', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate(
+ new Date(PRIVACY_POLICY_DATE).getDate() - 2,
+ ),
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(true);
+ });
+
+ it('does not show the privacy policy toast when seen, even if on or after the policy date and onboardingDate is before the policy date', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: true,
+ onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate(
+ new Date(PRIVACY_POLICY_DATE).getDate() - 2,
+ ),
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(false);
+ });
+
+ it('shows the privacy policy toast when not yet seen, on or after the policy date, and onboardingDate is not set', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: undefined,
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(true);
+ });
+ });
+
+ describe('mock day before', () => {
+ beforeEach(() => {
+ const dayBeforePolicyDate = new Date(PRIVACY_POLICY_DATE);
+ dayBeforePolicyDate.setDate(dayBeforePolicyDate.getDate() - 1);
+
+ dateNowSpy = jest
+ .spyOn(Date, 'now')
+ .mockReturnValue(dayBeforePolicyDate.getTime());
+ });
+
+ afterEach(() => {
+ dateNowSpy.mockRestore();
+ });
+
+ it('does not show the privacy policy toast before the policy date', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: new Date(PRIVACY_POLICY_DATE).setDate(
+ new Date(PRIVACY_POLICY_DATE).getDate() - 2,
+ ),
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(false);
+ });
+
+ it('does not show the privacy policy toast before the policy date even if onboardingDate is not set', () => {
+ const result = selectShowPrivacyPolicyToast({
+ // @ts-expect-error: intentionally passing incomplete input
+ metamask: {
+ newPrivacyPolicyToastClickedOrClosed: false,
+ onboardingDate: undefined,
+ },
+ });
+ expect(result.showPrivacyPolicyToast).toBe(false);
+ });
+ });
+});
diff --git a/ui/components/app/toast-master/utils.ts b/ui/components/app/toast-master/utils.ts
new file mode 100644
index 000000000000..d6544707f45d
--- /dev/null
+++ b/ui/components/app/toast-master/utils.ts
@@ -0,0 +1,69 @@
+import { PayloadAction } from '@reduxjs/toolkit';
+import { ReactFragment } from 'react';
+import { SHOW_NFT_DETECTION_ENABLEMENT_TOAST } from '../../../store/actionConstants';
+import { submitRequestToBackground } from '../../../store/background-connection';
+
+/**
+ * Returns true if the privacy policy toast was shown either never, or less than a day ago.
+ *
+ * @param newPrivacyPolicyToastShownDate
+ * @returns true if the privacy policy toast was shown either never, or less than a day ago
+ */
+export function getIsPrivacyToastRecent(
+ newPrivacyPolicyToastShownDate?: number,
+): boolean {
+ if (!newPrivacyPolicyToastShownDate) {
+ return true;
+ }
+
+ const currentDate = new Date();
+ const oneDayInMilliseconds = 24 * 60 * 60 * 1000;
+ const newPrivacyPolicyToastShownDateObj = new Date(
+ newPrivacyPolicyToastShownDate,
+ );
+ const toastWasShownLessThanADayAgo =
+ currentDate.valueOf() - newPrivacyPolicyToastShownDateObj.valueOf() <
+ oneDayInMilliseconds;
+
+ return toastWasShownLessThanADayAgo;
+}
+
+export function setNewPrivacyPolicyToastShownDate(time: number) {
+ submitRequestToBackgroundAndCatch('setNewPrivacyPolicyToastShownDate', [
+ time,
+ ]);
+}
+
+export function setNewPrivacyPolicyToastClickedOrClosed() {
+ submitRequestToBackgroundAndCatch('setNewPrivacyPolicyToastClickedOrClosed');
+}
+
+export function setShowNftDetectionEnablementToast(
+ value: boolean,
+): PayloadAction {
+ return {
+ type: SHOW_NFT_DETECTION_ENABLEMENT_TOAST,
+ payload: value,
+ };
+}
+
+export function setSwitchedNetworkNeverShowMessage() {
+ submitRequestToBackgroundAndCatch('setSwitchedNetworkNeverShowMessage', [
+ true,
+ ]);
+}
+
+export function setSurveyLinkLastClickedOrClosed(time: number) {
+ submitRequestToBackgroundAndCatch('setSurveyLinkLastClickedOrClosed', [time]);
+}
+
+// May move this to a different file after discussion with team
+export function submitRequestToBackgroundAndCatch(
+ method: string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ args?: any[],
+) {
+ submitRequestToBackground(method, args)?.catch((error) => {
+ console.error('Error caught in submitRequestToBackground', error);
+ });
+}
diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js
index 751b0f53e73a..226a2a9113c0 100644
--- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js
+++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js
@@ -214,7 +214,7 @@ export default class TransactionListItemDetails extends PureComponent {
primaryTransaction: transaction,
initialTransaction: { type },
} = transactionGroup;
- const { hash } = transaction;
+ const { chainId, hash } = transaction;
return (
@@ -332,6 +332,7 @@ export default class TransactionListItemDetails extends PureComponent {
recipientMetadataName={recipientMetadataName}
senderName={senderNickname}
senderAddress={senderAddress}
+ chainId={chainId}
onRecipientClick={() => {
this.context.trackEvent({
category: MetaMetricsEventCategory.Navigation,
diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js
index 517639b1c86e..9e070b33954b 100644
--- a/ui/components/multichain/account-list-item/account-list-item.js
+++ b/ui/components/multichain/account-list-item/account-list-item.js
@@ -86,6 +86,7 @@ const AccountListItem = ({
isActive = false,
startAccessory,
onActionClick,
+ shouldScrollToWhenSelected = true,
}) => {
const t = useI18nContext();
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false);
@@ -128,10 +129,10 @@ const AccountListItem = ({
// scroll the item into view
const itemRef = useRef(null);
useEffect(() => {
- if (selected) {
+ if (selected && shouldScrollToWhenSelected) {
itemRef.current?.scrollIntoView?.();
}
- }, [itemRef, selected]);
+ }, [itemRef, selected, shouldScrollToWhenSelected]);
const trackEvent = useContext(MetaMetricsContext);
const primaryTokenImage = useMultichainSelector(
@@ -502,6 +503,10 @@ AccountListItem.propTypes = {
* Represents start accessory
*/
startAccessory: PropTypes.node,
+ /**
+ * Determines if list item should be scrolled to when selected
+ */
+ shouldScrollToWhenSelected: PropTypes.bool,
};
AccountListItem.displayName = 'AccountListItem';
diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx
index 5376dc17859e..518f9f19387a 100644
--- a/ui/components/multichain/network-list-menu/network-list-menu.tsx
+++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx
@@ -251,10 +251,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => {
const generateNetworkListItem = (network: NetworkConfiguration) => {
const isCurrentNetwork = network.chainId === currentChainId;
const canDeleteNetwork =
- isUnlocked &&
- !isCurrentNetwork &&
- network.chainId !== CHAIN_IDS.MAINNET &&
- network.chainId !== CHAIN_IDS.LINEA_MAINNET;
+ isUnlocked && !isCurrentNetwork && network.chainId !== CHAIN_IDS.MAINNET;
return (
= ({
setShowEditAccountsModal(true);
trackEvent({
category: MetaMetricsEventCategory.Navigation,
- event: MetaMetricsEventName.TokenImportButtonClicked,
+ event: MetaMetricsEventName.ViewPermissionedAccounts,
properties: {
location: 'Connect view, Permissions toast, Permissions (dapp)',
},
@@ -133,7 +133,7 @@ export const SiteCell: React.FC = ({
setShowEditNetworksModal(true);
trackEvent({
category: MetaMetricsEventCategory.Navigation,
- event: MetaMetricsEventName.TokenImportButtonClicked,
+ event: MetaMetricsEventName.ViewPermissionedNetworks,
properties: {
location: 'Connect view, Permissions toast, Permissions (dapp)',
},
diff --git a/ui/components/multichain/pages/send/components/your-accounts.tsx b/ui/components/multichain/pages/send/components/your-accounts.tsx
index f53d6603cb78..e59d0aa2d5a1 100644
--- a/ui/components/multichain/pages/send/components/your-accounts.tsx
+++ b/ui/components/multichain/pages/send/components/your-accounts.tsx
@@ -57,6 +57,7 @@ export const SendPageYourAccounts = ({
selected={selectedAccount.address === account.address}
key={account.address}
isPinned={Boolean(account.pinned)}
+ shouldScrollToWhenSelected={false}
onClick={() => {
dispatch(
addHistoryEntry(
diff --git a/ui/components/ui/definition-list/definition-list.js b/ui/components/ui/definition-list/definition-list.js
index 84a23325b37a..84d3f48135ab 100644
--- a/ui/components/ui/definition-list/definition-list.js
+++ b/ui/components/ui/definition-list/definition-list.js
@@ -32,7 +32,7 @@ export default function DefinitionList({
{Object.entries(dictionary).map(([term, definition]) => (
) : (
) : (
@@ -292,4 +297,5 @@ SenderToRecipient.propTypes = {
onSenderClick: PropTypes.func,
warnUserOnAccountMismatch: PropTypes.bool,
recipientIsOwnedAccount: PropTypes.bool,
+ chainId: PropTypes.string,
};
diff --git a/ui/components/ui/truncated-definition-list/truncated-definition-list.js b/ui/components/ui/truncated-definition-list/truncated-definition-list.js
index ae1782979866..2db9784dad8e 100644
--- a/ui/components/ui/truncated-definition-list/truncated-definition-list.js
+++ b/ui/components/ui/truncated-definition-list/truncated-definition-list.js
@@ -5,7 +5,6 @@ import { BorderColor, Size } from '../../../helpers/constants/design-system';
import Box from '../box';
import Button from '../button';
import DefinitionList from '../definition-list/definition-list';
-import Popover from '../popover';
import { useI18nContext } from '../../../hooks/useI18nContext';
export default function TruncatedDefinitionList({
@@ -13,7 +12,6 @@ export default function TruncatedDefinitionList({
tooltips,
warnings,
prefaceKeys,
- title,
}) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const t = useI18nContext();
@@ -33,55 +31,27 @@ export default function TruncatedDefinitionList({
type="link"
onClick={() => setIsPopoverOpen(true)}
>
- {t(process.env.CHAIN_PERMISSIONS ? 'seeDetails' : 'viewAllDetails')}
+ {t('seeDetails')}
);
- const renderPopover = () =>
- isPopoverOpen && (
-
setIsPopoverOpen(false)}
- footer={
-
- }
- >
-
- {renderDefinitionList(true)}
-
-
- );
-
const renderContent = () => {
- if (process.env.CHAIN_PERMISSIONS) {
- return isPopoverOpen ? (
- renderDefinitionList(true)
- ) : (
- <>
- {renderDefinitionList(false)}
- {renderButton()}
- >
- );
- }
- return (
+ return isPopoverOpen ? (
+ renderDefinitionList(true)
+ ) : (
<>
{renderDefinitionList(false)}
{renderButton()}
- {renderPopover()}
>
);
};
return (
{
{ srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] },
{ toChainId: '0xe708' },
{},
- { ...mockNetworkState(FEATURED_RPCS[0]) },
+ { ...mockNetworkState(FEATURED_RPCS[1]) },
);
const result = getFromChain(state as never);
@@ -89,7 +89,7 @@ describe('Bridge selectors', () => {
);
const result = getAllBridgeableNetworks(state as never);
- expect(result).toHaveLength(7);
+ expect(result).toHaveLength(8);
expect(result[0]).toStrictEqual(
expect.objectContaining({ chainId: FEATURED_RPCS[0].chainId }),
);
@@ -190,21 +190,19 @@ describe('Bridge selectors', () => {
},
{},
{},
- mockNetworkState(...FEATURED_RPCS, {
- chainId: CHAIN_IDS.LINEA_MAINNET,
- }),
+ mockNetworkState(...FEATURED_RPCS),
);
const result = getToChains(state as never);
expect(result).toHaveLength(3);
expect(result[0]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
+ expect.objectContaining({ chainId: CHAIN_IDS.ARBITRUM }),
);
expect(result[1]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }),
+ expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
);
expect(result[2]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }),
+ expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }),
);
});
@@ -297,7 +295,9 @@ describe('Bridge selectors', () => {
{
...mockNetworkState(
...Object.values(BUILT_IN_NETWORKS),
- ...FEATURED_RPCS,
+ ...FEATURED_RPCS.filter(
+ (network) => network.chainId !== CHAIN_IDS.LINEA_MAINNET, // Linea mainnet is both a built in network, as well as featured RPC
+ ),
),
useExternalServices: true,
},
diff --git a/ui/helpers/utils/notification.utils.test.ts b/ui/helpers/utils/notification.utils.test.ts
index f82f8532cb58..2f4e66c26504 100644
--- a/ui/helpers/utils/notification.utils.test.ts
+++ b/ui/helpers/utils/notification.utils.test.ts
@@ -9,7 +9,7 @@ import {
describe('formatMenuItemDate', () => {
beforeAll(() => {
jest.useFakeTimers();
- jest.setSystemTime(new Date('2024-06-07T09:40:00Z'));
+ jest.setSystemTime(new Date(Date.UTC(2024, 5, 7, 9, 40, 0))); // 2024-06-07T09:40:00Z
});
afterAll(() => {
@@ -28,7 +28,7 @@ describe('formatMenuItemDate', () => {
// assert 1 hour ago
assertToday((testDate) => {
- testDate.setHours(testDate.getHours() - 1);
+ testDate.setUTCHours(testDate.getUTCHours() - 1);
return testDate;
});
});
@@ -42,14 +42,14 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 day ago
assertYesterday((testDate) => {
- testDate.setDate(testDate.getDate() - 1);
+ testDate.setUTCDate(testDate.getUTCDate() - 1);
});
// assert almost a day ago, but was still yesterday
// E.g. if Today way 09:40AM, but date to test was 23 hours ago (yesterday at 10:40AM), we still want to to show yesterday
assertYesterday((testDate) => {
- testDate.setDate(testDate.getDate() - 1);
- testDate.setHours(testDate.getHours() + 1);
+ testDate.setUTCDate(testDate.getUTCDate() - 1);
+ testDate.setUTCHours(testDate.getUTCHours() + 1);
});
});
@@ -62,18 +62,18 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 month ago
assertMonthsAgo((testDate) => {
- testDate.setMonth(testDate.getMonth() - 1);
+ testDate.setUTCMonth(testDate.getUTCMonth() - 1);
});
// assert 2 months ago
assertMonthsAgo((testDate) => {
- testDate.setMonth(testDate.getMonth() - 2);
+ testDate.setUTCMonth(testDate.getUTCMonth() - 2);
});
// assert almost a month ago (where it is a new month, but not 30 days)
assertMonthsAgo(() => {
// jest mock date is set in july, so we will test with month may
- return new Date('2024-05-20T09:40:00Z');
+ return new Date(Date.UTC(2024, 4, 20, 9, 40, 0)); // 2024-05-20T09:40:00Z
});
});
@@ -86,18 +86,18 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 year ago
assertYearsAgo((testDate) => {
- testDate.setFullYear(testDate.getFullYear() - 1);
+ testDate.setUTCFullYear(testDate.getUTCFullYear() - 1);
});
// assert 2 years ago
assertYearsAgo((testDate) => {
- testDate.setFullYear(testDate.getFullYear() - 2);
+ testDate.setUTCFullYear(testDate.getUTCFullYear() - 2);
});
// assert almost a year ago (where it is a new year, but not 365 days ago)
assertYearsAgo(() => {
// jest mock date is set in 2024, so we will test with year 2023
- return new Date('2023-11-20T09:40:00Z');
+ return new Date(Date.UTC(2023, 10, 20, 9, 40, 0)); // 2023-11-20T09:40:00Z
});
});
});
diff --git a/ui/hooks/metamask-notifications/useNotifications.ts b/ui/hooks/metamask-notifications/useNotifications.ts
index 62367cdbe310..9724253a8671 100644
--- a/ui/hooks/metamask-notifications/useNotifications.ts
+++ b/ui/hooks/metamask-notifications/useNotifications.ts
@@ -54,8 +54,13 @@ export function useListNotifications(): {
setLoading(true);
setError(null);
+ const urlParams = new URLSearchParams(window.location.search);
+ const previewToken = urlParams.get('previewToken');
+
try {
- const data = await dispatch(fetchAndUpdateMetamaskNotifications());
+ const data = await dispatch(
+ fetchAndUpdateMetamaskNotifications(previewToken ?? undefined),
+ );
setNotificationsData(data as unknown as Notification[]);
return data as unknown as Notification[];
} catch (e) {
diff --git a/ui/hooks/useDisplayName.test.ts b/ui/hooks/useDisplayName.test.ts
index 1d6fb22b5e69..5c36b0a97ed2 100644
--- a/ui/hooks/useDisplayName.test.ts
+++ b/ui/hooks/useDisplayName.test.ts
@@ -1,218 +1,533 @@
-import { NameEntry, NameType } from '@metamask/name-controller';
-import { NftContract } from '@metamask/assets-controllers';
-import { renderHook } from '@testing-library/react-hooks';
-import { getRemoteTokens } from '../selectors';
-import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft';
+import { NameType } from '@metamask/name-controller';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+import { cloneDeep } from 'lodash';
+import { Hex } from '@metamask/utils';
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import mockState from '../../test/data/mock-state.json';
+import {
+ EXPERIENCES_TYPE,
+ FIRST_PARTY_CONTRACT_NAMES,
+} from '../../shared/constants/first-party-contracts';
import { useDisplayName } from './useDisplayName';
-import { useNames } from './useName';
-import { useFirstPartyContractNames } from './useFirstPartyContractName';
import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
+import { useNames } from './useName';
-jest.mock('react-redux', () => ({
- // TODO: Replace `any` with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- useSelector: (selector: any) => selector(),
-}));
-
-jest.mock('./useName', () => ({
- useNames: jest.fn(),
-}));
-
-jest.mock('./useFirstPartyContractName', () => ({
- useFirstPartyContractNames: jest.fn(),
-}));
-
-jest.mock('./useNftCollectionsMetadata', () => ({
- useNftCollectionsMetadata: jest.fn(),
-}));
-
-jest.mock('../selectors', () => ({
- getRemoteTokens: jest.fn(),
- getCurrentChainId: jest.fn(),
-}));
-
-jest.mock('../selectors/nft', () => ({
- getNftContractsByAddressOnCurrentChain: jest.fn(),
-}));
-
-const VALUE_MOCK = '0xabc123';
-const TYPE_MOCK = NameType.ETHEREUM_ADDRESS;
-const NAME_MOCK = 'TestName';
-const CONTRACT_NAME_MOCK = 'TestContractName';
-const FIRST_PARTY_CONTRACT_NAME_MOCK = 'MetaMask Bridge';
-const WATCHED_NFT_NAME_MOCK = 'TestWatchedNFTName';
-
-const NO_PETNAME_FOUND_RETURN_VALUE = {
- name: null,
-} as NameEntry;
-const NO_CONTRACT_NAME_FOUND_RETURN_VALUE = undefined;
-const NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE = null;
-const NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE = {};
-
-const PETNAME_FOUND_RETURN_VALUE = {
- name: NAME_MOCK,
-} as NameEntry;
-
-const WATCHED_NFT_FOUND_RETURN_VALUE = {
- [VALUE_MOCK]: {
- name: WATCHED_NFT_NAME_MOCK,
- } as NftContract,
-};
+jest.mock('./useName');
+jest.mock('./useNftCollectionsMetadata');
+
+const VALUE_MOCK = 'testvalue';
+const VARIATION_MOCK = CHAIN_IDS.GOERLI;
+const PETNAME_MOCK = 'testName1';
+const ERC20_TOKEN_NAME_MOCK = 'testName2';
+const WATCHED_NFT_NAME_MOCK = 'testName3';
+const NFT_NAME_MOCK = 'testName4';
+const FIRST_PARTY_CONTRACT_NAME_MOCK = 'testName5';
+const SYMBOL_MOCK = 'tes';
+const NFT_IMAGE_MOCK = 'testNftImage';
+const ERC20_IMAGE_MOCK = 'testImage';
+const OTHER_NAME_TYPE = 'test' as NameType;
describe('useDisplayName', () => {
const useNamesMock = jest.mocked(useNames);
- const getRemoteTokensMock = jest.mocked(getRemoteTokens);
- const useFirstPartyContractNamesMock = jest.mocked(
- useFirstPartyContractNames,
- );
- const getNftContractsByAddressOnCurrentChainMock = jest.mocked(
- getNftContractsByAddressOnCurrentChain,
- );
const useNftCollectionsMetadataMock = jest.mocked(useNftCollectionsMetadata);
- beforeEach(() => {
- jest.resetAllMocks();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let state: any;
- useNamesMock.mockReturnValue([NO_PETNAME_FOUND_RETURN_VALUE]);
- useFirstPartyContractNamesMock.mockReturnValue([
- NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE,
- ]);
- getRemoteTokensMock.mockReturnValue([
+ function mockPetname(name: string) {
+ useNamesMock.mockReturnValue([
{
- name: NO_CONTRACT_NAME_FOUND_RETURN_VALUE,
+ name,
+ sourceId: null,
+ proposedNames: {},
+ origin: null,
},
]);
- getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
- NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE,
- );
- useNftCollectionsMetadataMock.mockReturnValue({});
- });
+ }
+
+ function mockERC20Token(
+ value: string,
+ variation: string,
+ name: string,
+ symbol: string,
+ image: string,
+ ) {
+ state.metamask.tokensChainsCache = {
+ [variation]: {
+ data: {
+ [value]: {
+ name,
+ symbol,
+ iconUrl: image,
+ },
+ },
+ },
+ };
+ }
- it('handles no name found', () => {
- const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
- expect(result.current).toEqual({
- name: null,
- hasPetname: false,
+ function mockWatchedNFTName(value: string, variation: string, name: string) {
+ state.metamask.allNftContracts = {
+ '0x123': {
+ [variation]: [{ address: value, name }],
+ },
+ };
+ }
+
+ function mockNFT(
+ value: string,
+ variation: string,
+ name: string,
+ image: string,
+ isSpam: boolean,
+ ) {
+ useNftCollectionsMetadataMock.mockReturnValue({
+ [variation]: {
+ [value]: { name, image, isSpam },
+ },
});
- });
+ }
+
+ function mockFirstPartyContractName(
+ value: string,
+ variation: string,
+ name: string,
+ ) {
+ FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE] = {
+ [variation as Hex]: value as Hex,
+ };
+ }
- it('prioritizes a petname over all else', () => {
- useNamesMock.mockReturnValue([PETNAME_FOUND_RETURN_VALUE]);
- useFirstPartyContractNamesMock.mockReturnValue([
- FIRST_PARTY_CONTRACT_NAME_MOCK,
- ]);
- getRemoteTokensMock.mockReturnValue([
+ beforeEach(() => {
+ jest.resetAllMocks();
+
+ useNftCollectionsMetadataMock.mockReturnValue({});
+
+ useNamesMock.mockReturnValue([
{
- name: CONTRACT_NAME_MOCK,
+ name: null,
+ sourceId: null,
+ proposedNames: {},
+ origin: null,
},
]);
- getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
- WATCHED_NFT_FOUND_RETURN_VALUE,
- );
- const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+ state = cloneDeep(mockState);
- expect(result.current).toEqual({
- name: NAME_MOCK,
- hasPetname: true,
- contractDisplayName: CONTRACT_NAME_MOCK,
- });
+ delete FIRST_PARTY_CONTRACT_NAMES[
+ FIRST_PARTY_CONTRACT_NAME_MOCK as EXPERIENCES_TYPE
+ ];
});
- it('prioritizes a first-party contract name over a contract name and watched NFT name', () => {
- useFirstPartyContractNamesMock.mockReturnValue([
- FIRST_PARTY_CONTRACT_NAME_MOCK,
- ]);
- getRemoteTokensMock.mockReturnValue({
- name: CONTRACT_NAME_MOCK,
- });
- getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
- WATCHED_NFT_FOUND_RETURN_VALUE,
+ it('returns no name if no defaults found', () => {
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
);
- const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
-
- expect(result.current).toEqual({
- name: FIRST_PARTY_CONTRACT_NAME_MOCK,
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
hasPetname: false,
+ image: undefined,
+ name: null,
});
});
- it('prioritizes a contract name over a watched NFT name', () => {
- getRemoteTokensMock.mockReturnValue([
- {
- name: CONTRACT_NAME_MOCK,
- },
- ]);
- getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
- WATCHED_NFT_FOUND_RETURN_VALUE,
- );
+ describe('Petname', () => {
+ it('returns petname', () => {
+ mockPetname(PETNAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: true,
+ image: undefined,
+ name: PETNAME_MOCK,
+ });
+ });
+ });
- const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+ describe('ERC-20 Token', () => {
+ it('returns ERC-20 token name and image', () => {
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: ERC20_TOKEN_NAME_MOCK,
+ hasPetname: false,
+ image: ERC20_IMAGE_MOCK,
+ name: ERC20_TOKEN_NAME_MOCK,
+ });
+ });
- expect(result.current).toEqual({
- name: CONTRACT_NAME_MOCK,
- hasPetname: false,
- contractDisplayName: CONTRACT_NAME_MOCK,
+ it('returns ERC-20 token symbol', () => {
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: CHAIN_IDS.GOERLI,
+ preferContractSymbol: true,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: SYMBOL_MOCK,
+ hasPetname: false,
+ image: ERC20_IMAGE_MOCK,
+ name: SYMBOL_MOCK,
+ });
});
- });
- it('returns a watched NFT name if no other name is found', () => {
- getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
- WATCHED_NFT_FOUND_RETURN_VALUE,
- );
+ it('returns no name if type not address', () => {
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: OTHER_NAME_TYPE,
+ variation: CHAIN_IDS.GOERLI,
+ preferContractSymbol: true,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
+ });
+ });
- const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+ describe('First-party Contract', () => {
+ it('returns first-party contract name', () => {
+ mockFirstPartyContractName(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ FIRST_PARTY_CONTRACT_NAME_MOCK,
+ );
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: FIRST_PARTY_CONTRACT_NAME_MOCK,
+ });
+ });
- expect(result.current).toEqual({
- name: WATCHED_NFT_NAME_MOCK,
- hasPetname: false,
+ it('returns no name if type is not address', () => {
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value:
+ FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][
+ CHAIN_IDS.OPTIMISM
+ ],
+ type: OTHER_NAME_TYPE,
+ variation: CHAIN_IDS.OPTIMISM,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
});
});
- it('returns nft collection name from metadata if no other name is found', () => {
- const IMAGE_MOCK = 'url';
+ describe('Watched NFT', () => {
+ it('returns watched NFT name', () => {
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: WATCHED_NFT_NAME_MOCK,
+ });
+ });
- useNftCollectionsMetadataMock.mockReturnValue({
- [VALUE_MOCK.toLowerCase()]: {
- name: CONTRACT_NAME_MOCK,
- image: IMAGE_MOCK,
- isSpam: false,
- },
+ it('returns no name if type is not address', () => {
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: OTHER_NAME_TYPE,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
+ });
+ });
+
+ describe('NFT', () => {
+ it('returns NFT name and image', () => {
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: NFT_IMAGE_MOCK,
+ name: NFT_NAME_MOCK,
+ });
});
- const { result } = renderHook(() =>
- useDisplayName(VALUE_MOCK, TYPE_MOCK, false),
- );
+ it('returns no name if NFT collection is spam', () => {
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, true);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
+ });
- expect(result.current).toEqual({
- name: CONTRACT_NAME_MOCK,
- hasPetname: false,
- contractDisplayName: undefined,
- image: IMAGE_MOCK,
+ it('returns no name if type not address', () => {
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: OTHER_NAME_TYPE,
+ variation: VARIATION_MOCK,
+ }),
+ mockState,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: undefined,
+ hasPetname: false,
+ image: undefined,
+ name: null,
+ });
});
});
- it('does not return nft collection name if collection is marked as spam', () => {
- const IMAGE_MOCK = 'url';
+ describe('Priority', () => {
+ it('uses petname as first priority', () => {
+ mockPetname(PETNAME_MOCK);
+ mockFirstPartyContractName(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ FIRST_PARTY_CONTRACT_NAME_MOCK,
+ );
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false);
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: ERC20_TOKEN_NAME_MOCK,
+ hasPetname: true,
+ image: NFT_IMAGE_MOCK,
+ name: PETNAME_MOCK,
+ });
+ });
- useNftCollectionsMetadataMock.mockReturnValue({
- [VALUE_MOCK.toLowerCase()]: {
- name: CONTRACT_NAME_MOCK,
- image: IMAGE_MOCK,
- isSpam: true,
- },
+ it('uses first-party contract name as second priority', () => {
+ mockFirstPartyContractName(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ FIRST_PARTY_CONTRACT_NAME_MOCK,
+ );
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false);
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: ERC20_TOKEN_NAME_MOCK,
+ hasPetname: false,
+ image: NFT_IMAGE_MOCK,
+ name: FIRST_PARTY_CONTRACT_NAME_MOCK,
+ });
});
- const { result } = renderHook(() =>
- useDisplayName(VALUE_MOCK, TYPE_MOCK, false),
- );
+ it('uses NFT name as third priority', () => {
+ mockNFT(VALUE_MOCK, VARIATION_MOCK, NFT_NAME_MOCK, NFT_IMAGE_MOCK, false);
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: ERC20_TOKEN_NAME_MOCK,
+ hasPetname: false,
+ image: NFT_IMAGE_MOCK,
+ name: NFT_NAME_MOCK,
+ });
+ });
- expect(result.current).toEqual(
- expect.objectContaining({
- name: null,
- image: undefined,
- }),
- );
+ it('uses ERC-20 token name as fourth priority', () => {
+ mockERC20Token(
+ VALUE_MOCK,
+ VARIATION_MOCK,
+ ERC20_TOKEN_NAME_MOCK,
+ SYMBOL_MOCK,
+ ERC20_IMAGE_MOCK,
+ );
+ mockWatchedNFTName(VALUE_MOCK, VARIATION_MOCK, WATCHED_NFT_NAME_MOCK);
+
+ const { result } = renderHookWithProvider(
+ () =>
+ useDisplayName({
+ value: VALUE_MOCK,
+ type: NameType.ETHEREUM_ADDRESS,
+ variation: VARIATION_MOCK,
+ }),
+ state,
+ );
+
+ expect(result.current).toStrictEqual({
+ contractDisplayName: ERC20_TOKEN_NAME_MOCK,
+ hasPetname: false,
+ image: ERC20_IMAGE_MOCK,
+ name: ERC20_TOKEN_NAME_MOCK,
+ });
+ });
});
});
diff --git a/ui/hooks/useDisplayName.ts b/ui/hooks/useDisplayName.ts
index 64a878d2e357..7b7429c7a0d4 100644
--- a/ui/hooks/useDisplayName.ts
+++ b/ui/hooks/useDisplayName.ts
@@ -1,16 +1,20 @@
-import { useMemo } from 'react';
import { NameType } from '@metamask/name-controller';
import { useSelector } from 'react-redux';
-import { getRemoteTokens } from '../selectors';
-import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft';
+import { Hex } from '@metamask/utils';
+import { selectERC20TokensByChain } from '../selectors';
+import { getNftContractsByAddressByChain } from '../selectors/nft';
+import {
+ EXPERIENCES_TYPE,
+ FIRST_PARTY_CONTRACT_NAMES,
+} from '../../shared/constants/first-party-contracts';
import { useNames } from './useName';
-import { useFirstPartyContractNames } from './useFirstPartyContractName';
import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
export type UseDisplayNameRequest = {
- value: string;
preferContractSymbol?: boolean;
type: NameType;
+ value: string;
+ variation: string;
};
export type UseDisplayNameResponse = {
@@ -23,79 +27,145 @@ export type UseDisplayNameResponse = {
export function useDisplayNames(
requests: UseDisplayNameRequest[],
): UseDisplayNameResponse[] {
- const nameRequests = useMemo(
- () => requests.map(({ value, type }) => ({ value, type })),
- [requests],
- );
-
- const nameEntries = useNames(nameRequests);
- const firstPartyContractNames = useFirstPartyContractNames(nameRequests);
- const nftCollections = useNftCollectionsMetadata(nameRequests);
- const values = requests.map(({ value }) => value);
-
- const contractInfo = useSelector((state) =>
- // TODO: Replace `any` with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (getRemoteTokens as any)(state, values),
- );
+ const nameEntries = useNames(requests);
+ const firstPartyContractNames = useFirstPartyContractNames(requests);
+ const erc20Tokens = useERC20Tokens(requests);
+ const watchedNFTNames = useWatchedNFTNames(requests);
+ const nfts = useNFTs(requests);
- const watchedNftNames = useSelector(getNftContractsByAddressOnCurrentChain);
-
- return requests.map(({ value, preferContractSymbol }, index) => {
+ return requests.map((_request, index) => {
const nameEntry = nameEntries[index];
const firstPartyContractName = firstPartyContractNames[index];
- const singleContractInfo = contractInfo[index];
- const watchedNftName = watchedNftNames[value.toLowerCase()]?.name;
- const nftCollectionProperties = nftCollections[value.toLowerCase()];
-
- const isNotSpam = nftCollectionProperties?.isSpam === false;
-
- const nftCollectionName = isNotSpam
- ? nftCollectionProperties?.name
- : undefined;
- const nftCollectionImage = isNotSpam
- ? nftCollectionProperties?.image
- : undefined;
-
- const contractDisplayName =
- preferContractSymbol && singleContractInfo?.symbol
- ? singleContractInfo.symbol
- : singleContractInfo?.name;
+ const erc20Token = erc20Tokens[index];
+ const watchedNftName = watchedNFTNames[index];
+ const nft = nfts[index];
const name =
nameEntry?.name ||
firstPartyContractName ||
- nftCollectionName ||
- contractDisplayName ||
+ nft?.name ||
+ erc20Token?.name ||
watchedNftName ||
null;
+ const image = nft?.image || erc20Token?.image;
+
const hasPetname = Boolean(nameEntry?.name);
return {
name,
hasPetname,
- contractDisplayName,
- image: nftCollectionImage,
+ contractDisplayName: erc20Token?.name,
+ image,
};
});
}
-/**
- * Attempts to resolve the name for the given parameters.
- *
- * @param value - The address or contract address to resolve.
- * @param type - The type of value, e.g. NameType.ETHEREUM_ADDRESS.
- * @param preferContractSymbol - Applies to recognized contracts when no petname is saved:
- * If true the contract symbol (e.g. WBTC) will be used instead of the contract name.
- * @returns An object with two properties:
- * - `name` {string|null} - The display name, if it can be resolved, otherwise null.
- * - `hasPetname` {boolean} - True if there is a petname for the given address.
- */
export function useDisplayName(
- value: string,
- type: NameType,
- preferContractSymbol: boolean = false,
+ request: UseDisplayNameRequest,
): UseDisplayNameResponse {
- return useDisplayNames([{ preferContractSymbol, type, value }])[0];
+ return useDisplayNames([request])[0];
+}
+
+function useERC20Tokens(
+ nameRequests: UseDisplayNameRequest[],
+): ({ name?: string; image?: string } | undefined)[] {
+ const erc20TokensByChain = useSelector(selectERC20TokensByChain);
+
+ return nameRequests.map(
+ ({ preferContractSymbol, type, value, variation }) => {
+ if (type !== NameType.ETHEREUM_ADDRESS) {
+ return undefined;
+ }
+
+ const contractAddress = value.toLowerCase();
+
+ const {
+ iconUrl: image,
+ name: tokenName,
+ symbol,
+ } = erc20TokensByChain?.[variation]?.data?.[contractAddress] ?? {};
+
+ const name = preferContractSymbol && symbol ? symbol : tokenName;
+
+ return { name, image };
+ },
+ );
+}
+
+function useWatchedNFTNames(
+ nameRequests: UseDisplayNameRequest[],
+): (string | undefined)[] {
+ const watchedNftNamesByAddressByChain = useSelector(
+ getNftContractsByAddressByChain,
+ );
+
+ return nameRequests.map(({ type, value, variation }) => {
+ if (type !== NameType.ETHEREUM_ADDRESS) {
+ return undefined;
+ }
+
+ const contractAddress = value.toLowerCase();
+ const watchedNftNamesByAddress = watchedNftNamesByAddressByChain[variation];
+ return watchedNftNamesByAddress?.[contractAddress]?.name;
+ });
+}
+
+function useNFTs(
+ nameRequests: UseDisplayNameRequest[],
+): ({ name?: string; image?: string } | undefined)[] {
+ const requests = nameRequests
+ .filter(({ type }) => type === NameType.ETHEREUM_ADDRESS)
+ .map(({ value, variation }) => ({
+ chainId: variation,
+ contractAddress: value,
+ }));
+
+ const nftCollectionsByAddressByChain = useNftCollectionsMetadata(requests);
+
+ return nameRequests.map(
+ ({ type, value: contractAddress, variation: chainId }) => {
+ if (type !== NameType.ETHEREUM_ADDRESS) {
+ return undefined;
+ }
+
+ const nftCollectionProperties =
+ nftCollectionsByAddressByChain[chainId]?.[
+ contractAddress.toLowerCase()
+ ];
+
+ const isSpam = nftCollectionProperties?.isSpam !== false;
+
+ if (!nftCollectionProperties || isSpam) {
+ return undefined;
+ }
+
+ const { name, image } = nftCollectionProperties;
+
+ return { name, image };
+ },
+ );
+}
+
+function useFirstPartyContractNames(nameRequests: UseDisplayNameRequest[]) {
+ return nameRequests.map(({ type, value, variation }) => {
+ if (type !== NameType.ETHEREUM_ADDRESS) {
+ return undefined;
+ }
+
+ const normalizedContractAddress = value.toLowerCase();
+
+ const contractNames = Object.keys(
+ FIRST_PARTY_CONTRACT_NAMES,
+ ) as EXPERIENCES_TYPE[];
+
+ return contractNames.find((contractName) => {
+ const currentContractAddress =
+ FIRST_PARTY_CONTRACT_NAMES[contractName]?.[variation as Hex];
+
+ return (
+ currentContractAddress?.toLowerCase() === normalizedContractAddress
+ );
+ });
+ });
}
diff --git a/ui/hooks/useFirstPartyContractName.test.ts b/ui/hooks/useFirstPartyContractName.test.ts
deleted file mode 100644
index 14d0cd429e6f..000000000000
--- a/ui/hooks/useFirstPartyContractName.test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { NameType } from '@metamask/name-controller';
-import { getCurrentChainId } from '../selectors';
-import { CHAIN_IDS } from '../../shared/constants/network';
-import { useFirstPartyContractName } from './useFirstPartyContractName';
-
-jest.mock('react-redux', () => ({
- // TODO: Replace `any` with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- useSelector: (selector: any) => selector(),
-}));
-
-jest.mock('../selectors', () => ({
- getCurrentChainId: jest.fn(),
- getNames: jest.fn(),
-}));
-
-const BRIDGE_NAME_MOCK = 'MetaMask Bridge';
-const BRIDGE_MAINNET_ADDRESS_MOCK =
- '0x0439e60F02a8900a951603950d8D4527f400C3f1';
-const BRIDGE_OPTIMISM_ADDRESS_MOCK =
- '0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e';
-const UNKNOWN_ADDRESS_MOCK = '0xabc123';
-
-describe('useFirstPartyContractName', () => {
- const getCurrentChainIdMock = jest.mocked(getCurrentChainId);
- beforeEach(() => {
- jest.resetAllMocks();
-
- getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET);
- });
-
- it('returns null if no name found', () => {
- const name = useFirstPartyContractName(
- UNKNOWN_ADDRESS_MOCK,
- NameType.ETHEREUM_ADDRESS,
- );
-
- expect(name).toBe(null);
- });
-
- it('returns name if found', () => {
- const name = useFirstPartyContractName(
- BRIDGE_MAINNET_ADDRESS_MOCK,
- NameType.ETHEREUM_ADDRESS,
- );
- expect(name).toBe(BRIDGE_NAME_MOCK);
- });
-
- it('uses variation if specified', () => {
- const name = useFirstPartyContractName(
- BRIDGE_OPTIMISM_ADDRESS_MOCK,
- NameType.ETHEREUM_ADDRESS,
- CHAIN_IDS.OPTIMISM,
- );
-
- expect(name).toBe(BRIDGE_NAME_MOCK);
- });
-
- it('returns null if type is not address', () => {
- const alternateType = 'alternateType' as NameType;
-
- const name = useFirstPartyContractName(
- BRIDGE_MAINNET_ADDRESS_MOCK,
- alternateType,
- );
-
- expect(name).toBe(null);
- });
-
- it('normalizes addresses to lowercase', () => {
- const name = useFirstPartyContractName(
- BRIDGE_MAINNET_ADDRESS_MOCK.toUpperCase(),
- NameType.ETHEREUM_ADDRESS,
- );
-
- expect(name).toBe(BRIDGE_NAME_MOCK);
- });
-});
diff --git a/ui/hooks/useFirstPartyContractName.ts b/ui/hooks/useFirstPartyContractName.ts
deleted file mode 100644
index 47468b472955..000000000000
--- a/ui/hooks/useFirstPartyContractName.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { NameType } from '@metamask/name-controller';
-import { useSelector } from 'react-redux';
-import { getCurrentChainId } from '../selectors';
-import {
- EXPERIENCES_TYPE,
- FIRST_PARTY_CONTRACT_NAMES,
-} from '../../shared/constants/first-party-contracts';
-
-export type UseFirstPartyContractNameRequest = {
- value: string;
- type: NameType;
- variation?: string;
-};
-
-export function useFirstPartyContractNames(
- requests: UseFirstPartyContractNameRequest[],
-): (string | null)[] {
- const currentChainId = useSelector(getCurrentChainId);
-
- return requests.map(({ type, value, variation }) => {
- if (type !== NameType.ETHEREUM_ADDRESS) {
- return null;
- }
-
- const chainId = variation ?? currentChainId;
- const normalizedValue = value.toLowerCase();
-
- return (
- Object.keys(FIRST_PARTY_CONTRACT_NAMES).find(
- (name) =>
- FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE]?.[
- chainId
- ]?.toLowerCase() === normalizedValue,
- ) ?? null
- );
- });
-}
-
-export function useFirstPartyContractName(
- value: string,
- type: NameType,
- variation?: string,
-): string | null {
- return useFirstPartyContractNames([{ value, type, variation }])[0];
-}
diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts
index 76bd5dc593ad..f746c4bb6267 100644
--- a/ui/hooks/useName.test.ts
+++ b/ui/hooks/useName.test.ts
@@ -5,7 +5,7 @@ import {
NameOrigin,
NameType,
} from '@metamask/name-controller';
-import { getCurrentChainId, getNames } from '../selectors';
+import { getNames } from '../selectors';
import { useName } from './useName';
jest.mock('react-redux', () => ({
@@ -19,13 +19,14 @@ jest.mock('../selectors', () => ({
getNames: jest.fn(),
}));
-const CHAIN_ID_MOCK = '0x1';
-const CHAIN_ID_2_MOCK = '0x2';
+const VARIATION_MOCK = '0x1';
+const VARIATION_2_MOCK = '0x2';
const VALUE_MOCK = '0xabc123';
const TYPE_MOCK = NameType.ETHEREUM_ADDRESS;
const NAME_MOCK = 'TestName';
const SOURCE_ID_MOCK = 'TestSourceId';
const ORIGIN_MOCK = NameOrigin.API;
+
const PROPOSED_NAMES_MOCK = {
[SOURCE_ID_MOCK]: {
proposedNames: ['TestProposedName', 'TestProposedName2'],
@@ -35,7 +36,6 @@ const PROPOSED_NAMES_MOCK = {
};
describe('useName', () => {
- const getCurrentChainIdMock = jest.mocked(getCurrentChainId);
const getNamesMock =
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -43,14 +43,12 @@ describe('useName', () => {
beforeEach(() => {
jest.resetAllMocks();
-
- getCurrentChainIdMock.mockReturnValue(CHAIN_ID_MOCK);
});
it('returns default values if no state', () => {
getNamesMock.mockReturnValue({} as NameControllerState['names']);
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK);
expect(nameEntry).toStrictEqual({
name: null,
@@ -64,7 +62,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [CHAIN_ID_2_MOCK]: {
+ [VARIATION_2_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -74,7 +72,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK);
expect(nameEntry).toStrictEqual({
name: null,
@@ -88,7 +86,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [CHAIN_ID_MOCK]: {
+ [VARIATION_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -98,7 +96,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -112,7 +110,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [CHAIN_ID_2_MOCK]: {
+ [VARIATION_2_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -122,7 +120,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -147,7 +145,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -161,7 +159,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [CHAIN_ID_2_MOCK]: {
+ [VARIATION_2_MOCK]: {
name: null,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: null,
@@ -177,7 +175,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -188,37 +186,11 @@ describe('useName', () => {
});
});
- it('uses empty string as variation if not specified and type is not address', () => {
- const alternateType = 'alternateType' as NameType;
-
- getNamesMock.mockReturnValue({
- [alternateType]: {
- [VALUE_MOCK]: {
- '': {
- name: NAME_MOCK,
- proposedNames: PROPOSED_NAMES_MOCK,
- sourceId: SOURCE_ID_MOCK,
- origin: ORIGIN_MOCK,
- },
- },
- },
- });
-
- const nameEntry = useName(VALUE_MOCK, alternateType);
-
- expect(nameEntry).toStrictEqual({
- name: NAME_MOCK,
- sourceId: SOURCE_ID_MOCK,
- proposedNames: PROPOSED_NAMES_MOCK,
- origin: ORIGIN_MOCK,
- });
- });
-
it('normalizes addresses to lowercase', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [CHAIN_ID_MOCK]: {
+ [VARIATION_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -228,7 +200,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName('0xAbC123', TYPE_MOCK);
+ const nameEntry = useName('0xAbC123', TYPE_MOCK, VARIATION_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
diff --git a/ui/hooks/useName.ts b/ui/hooks/useName.ts
index 3af9b0457f79..dd587e81abf6 100644
--- a/ui/hooks/useName.ts
+++ b/ui/hooks/useName.ts
@@ -5,32 +5,29 @@ import {
} from '@metamask/name-controller';
import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
-import { getCurrentChainId, getNames } from '../selectors';
+import { getNames } from '../selectors';
export type UseNameRequest = {
value: string;
type: NameType;
- variation?: string;
+ variation: string;
};
export function useName(
value: string,
type: NameType,
- variation?: string,
+ variation: string,
): NameEntry {
return useNames([{ value, type, variation }])[0];
}
export function useNames(requests: UseNameRequest[]): NameEntry[] {
const names = useSelector(getNames, isEqual);
- const chainId = useSelector(getCurrentChainId);
return requests.map(({ value, type, variation }) => {
const normalizedValue = normalizeValue(value, type);
- const typeVariationKey = getVariationKey(type, chainId);
- const variationKey = variation ?? typeVariationKey;
const variationsToNameEntries = names[type]?.[normalizedValue] ?? {};
- const variationEntry = variationsToNameEntries[variationKey];
+ const variationEntry = variationsToNameEntries[variation];
const fallbackEntry = variationsToNameEntries[FALLBACK_VARIATION];
const entry =
@@ -63,13 +60,3 @@ function normalizeValue(value: string, type: string): string {
return value;
}
}
-
-function getVariationKey(type: string, chainId: string): string {
- switch (type) {
- case NameType.ETHEREUM_ADDRESS:
- return chainId;
-
- default:
- return '';
- }
-}
diff --git a/ui/hooks/useNftCollectionsMetadata.test.ts b/ui/hooks/useNftCollectionsMetadata.test.ts
index 4897e449e6ad..e1e2b6745ad1 100644
--- a/ui/hooks/useNftCollectionsMetadata.test.ts
+++ b/ui/hooks/useNftCollectionsMetadata.test.ts
@@ -1,6 +1,5 @@
import { renderHook } from '@testing-library/react-hooks';
import { TokenStandard } from '../../shared/constants/transaction';
-import { getCurrentChainId } from '../selectors';
import {
getNFTContractInfo,
getTokenStandardAndDetails,
@@ -42,7 +41,6 @@ const ERC_721_COLLECTION_2_MOCK = {
};
describe('useNftCollectionsMetadata', () => {
- const mockGetCurrentChainId = jest.mocked(getCurrentChainId);
const mockGetNFTContractInfo = jest.mocked(getNFTContractInfo);
const mockGetTokenStandardAndDetails = jest.mocked(
getTokenStandardAndDetails,
@@ -50,7 +48,6 @@ describe('useNftCollectionsMetadata', () => {
beforeEach(() => {
jest.resetAllMocks();
- mockGetCurrentChainId.mockReturnValue(CHAIN_ID_MOCK);
mockGetNFTContractInfo.mockResolvedValue({
collections: [ERC_721_COLLECTION_1_MOCK, ERC_721_COLLECTION_2_MOCK],
});
@@ -67,10 +64,12 @@ describe('useNftCollectionsMetadata', () => {
const { result, waitForNextUpdate } = renderHook(() =>
useNftCollectionsMetadata([
{
- value: ERC_721_ADDRESS_1,
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: ERC_721_ADDRESS_1,
},
{
- value: ERC_721_ADDRESS_2,
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: ERC_721_ADDRESS_2,
},
]),
);
@@ -79,8 +78,10 @@ describe('useNftCollectionsMetadata', () => {
expect(mockGetNFTContractInfo).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual({
- [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK,
- [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK,
+ [CHAIN_ID_MOCK]: {
+ [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK,
+ [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK,
+ },
});
});
@@ -99,7 +100,8 @@ describe('useNftCollectionsMetadata', () => {
renderHook(() =>
useNftCollectionsMetadata([
{
- value: '0xERC20Address',
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: '0xERC20Address',
},
]),
);
@@ -114,7 +116,8 @@ describe('useNftCollectionsMetadata', () => {
renderHook(() =>
useNftCollectionsMetadata([
{
- value: '0xERC20Address',
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: '0xERC20Address',
},
]),
);
@@ -126,10 +129,12 @@ describe('useNftCollectionsMetadata', () => {
const { waitForNextUpdate, rerender } = renderHook(() =>
useNftCollectionsMetadata([
{
- value: ERC_721_ADDRESS_1,
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: ERC_721_ADDRESS_1,
},
{
- value: ERC_721_ADDRESS_2,
+ chainId: CHAIN_ID_MOCK,
+ contractAddress: ERC_721_ADDRESS_2,
},
]),
);
diff --git a/ui/hooks/useNftCollectionsMetadata.ts b/ui/hooks/useNftCollectionsMetadata.ts
index 641e0fb25dcd..e71216e254c9 100644
--- a/ui/hooks/useNftCollectionsMetadata.ts
+++ b/ui/hooks/useNftCollectionsMetadata.ts
@@ -1,9 +1,5 @@
-import { useMemo } from 'react';
-import { useSelector } from 'react-redux';
import { Collection } from '@metamask/assets-controllers';
-import type { Hex } from '@metamask/utils';
import { TokenStandard } from '../../shared/constants/transaction';
-import { getCurrentChainId } from '../selectors';
import {
getNFTContractInfo,
getTokenStandardAndDetails,
@@ -11,28 +7,62 @@ import {
import { useAsyncResult } from './useAsyncResult';
export type UseNftCollectionsMetadataRequest = {
- value: string;
- chainId?: string;
-};
-
-type CollectionsData = {
- [key: string]: Collection;
+ chainId: string;
+ contractAddress: string;
};
// For now, we only support ERC721 tokens
const SUPPORTED_NFT_TOKEN_STANDARDS = [TokenStandard.ERC721];
-async function fetchCollections(
- memoisedContracts: string[],
+export function useNftCollectionsMetadata(
+ requests: UseNftCollectionsMetadataRequest[],
+): Record> {
+ const { value: collectionsMetadata } = useAsyncResult(
+ () => fetchCollections(requests),
+ [JSON.stringify(requests)],
+ );
+
+ return collectionsMetadata ?? {};
+}
+
+async function fetchCollections(requests: UseNftCollectionsMetadataRequest[]) {
+ const valuesByChainId = requests.reduce>(
+ (acc, { chainId, contractAddress }) => {
+ acc[chainId] = [...(acc[chainId] ?? []), contractAddress.toLowerCase()];
+ return acc;
+ },
+ {},
+ );
+
+ const chainIds = Object.keys(valuesByChainId);
+
+ const responses = await Promise.all(
+ chainIds.map((chainId) => {
+ const contractAddresses = valuesByChainId[chainId];
+ return fetchCollectionsForChain(contractAddresses, chainId);
+ }),
+ );
+
+ return chainIds.reduce>>(
+ (acc, chainId, index) => {
+ acc[chainId] = responses[index];
+ return acc;
+ },
+ {},
+ );
+}
+
+async function fetchCollectionsForChain(
+ contractAddresses: string[],
chainId: string,
-): Promise {
+) {
const contractStandardsResponses = await Promise.all(
- memoisedContracts.map((contractAddress) =>
+ contractAddresses.map((contractAddress) =>
getTokenStandardAndDetails(contractAddress, chainId),
),
);
- const supportedNFTContracts = memoisedContracts.filter(
+ const supportedNFTContracts = contractAddresses.filter(
(_contractAddress, index) =>
SUPPORTED_NFT_TOKEN_STANDARDS.includes(
contractStandardsResponses[index].standard as TokenStandard,
@@ -48,37 +78,16 @@ async function fetchCollections(
chainId,
);
- const collectionsData: CollectionsData = collectionsResult.collections.reduce(
- (acc: CollectionsData, collection, index) => {
- acc[supportedNFTContracts[index]] = {
- name: collection?.name,
- image: collection?.image,
- isSpam: collection?.isSpam,
- };
- return acc;
- },
- {},
- );
+ const collectionsData = collectionsResult.collections.reduce<
+ Record
+ >((acc, collection, index) => {
+ acc[supportedNFTContracts[index]] = {
+ name: collection?.name,
+ image: collection?.image,
+ isSpam: collection?.isSpam,
+ };
+ return acc;
+ }, {});
return collectionsData;
}
-
-export function useNftCollectionsMetadata(
- requests: UseNftCollectionsMetadataRequest[],
- providedChainId?: Hex,
-) {
- const chainId = useSelector(getCurrentChainId) || providedChainId;
-
- const memoisedContracts = useMemo(() => {
- return requests
- .filter(({ value }) => value)
- .map(({ value }) => value.toLowerCase());
- }, [requests]);
-
- const { value: collectionsMetadata } = useAsyncResult(
- () => fetchCollections(memoisedContracts, chainId),
- [JSON.stringify(memoisedContracts), chainId],
- );
-
- return collectionsMetadata || {};
-}
diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js
index d4a92172aed6..c61a4193ccd0 100644
--- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js
+++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.component.js
@@ -218,6 +218,7 @@ const ConfirmPageContainer = (props) => {
recipientEns={toEns}
recipientNickname={toNickname}
recipientIsOwnedAccount={recipientIsOwnedAccount}
+ chainId={currentTransaction.chainId}
/>
)}
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-details/approve-details.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve-details/approve-details.tsx
index cdcaf7267e06..13d455503db6 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/approve-details/approve-details.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/approve-details/approve-details.tsx
@@ -45,6 +45,7 @@ const Spender = ({
}
const spender = value.data[0].params[0].value;
+ const { chainId } = transactionMeta;
if (getIsRevokeSetApprovalForAll(value)) {
return null;
@@ -59,7 +60,7 @@ const Spender = ({
)}
data-testid="confirmation__approve-spender"
>
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx
index bdbe0e6fbae3..f1117e2a22b2 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx
@@ -50,6 +50,8 @@ export const ApproveStaticSimulation = () => {
return null;
}
+ const { chainId } = transactionMeta;
+
const formattedTokenText = (
{
value={transactionMeta.txParams.to as string}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx
index 38ff93ba9b36..77cee271d667 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx
@@ -16,6 +16,8 @@ export const RevokeStaticSimulation = () => {
currentConfirmation: TransactionMeta;
};
+ const { chainId } = transactionMeta;
+
const TokenContractRow = (
@@ -24,6 +26,7 @@ export const RevokeStaticSimulation = () => {
value={transactionMeta.txParams.to as string}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
@@ -38,6 +41,7 @@ export const RevokeStaticSimulation = () => {
value={transactionMeta.txParams.from as string}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
index 28b56c8de9ed..370d5e68261f 100644
--- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
+++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx
@@ -34,6 +34,7 @@ const PersonalSignInfo: React.FC = () => {
const { from } = currentConfirmation.msgParams;
const isSIWE = isSIWESignatureRequest(currentConfirmation);
+ const chainId = currentConfirmation.chainId as string;
return (
<>
@@ -62,7 +63,7 @@ const PersonalSignInfo: React.FC = () => {
label={t('signingInWith')}
ownerId={currentConfirmation.id}
>
-
+
)}
diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx
index 383bf182ac8c..abf93e50f7b4 100644
--- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx
+++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx
@@ -50,7 +50,7 @@ const SIWESignInfo: React.FC = () => {
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx
index 64e90a7066e6..d1746ff85f08 100644
--- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation.tsx
@@ -22,6 +22,8 @@ export const RevokeSetApprovalForAllStaticSimulation = ({
const { currentConfirmation: transactionMeta } =
useConfirmContext();
+ const { chainId } = transactionMeta;
+
const nftsRow = (
@@ -30,6 +32,7 @@ export const RevokeSetApprovalForAllStaticSimulation = ({
value={transactionMeta.txParams.to as string}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
@@ -44,6 +47,7 @@ export const RevokeSetApprovalForAllStaticSimulation = ({
value={spender}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx
index 177ef4080860..56a27d3397be 100644
--- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/set-approval-for-all-static-simulation.tsx
@@ -24,6 +24,8 @@ export const SetApprovalForAllStaticSimulation = () => {
currentConfirmation: TransactionMeta;
};
+ const { chainId } = transactionMeta;
+
const SetApprovalForAllRow = (
@@ -48,6 +50,7 @@ export const SetApprovalForAllStaticSimulation = () => {
value={transactionMeta.txParams.to as string}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
+ variation={chainId}
/>
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.tsx
index 4af44b5a310d..374aec4f26ce 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.tsx
@@ -57,6 +57,7 @@ export const TransactionData = () => {
const { data, source } = value;
const isExpandable = data.length > 1;
+ const { chainId } = currentConfirmation;
return (
@@ -67,6 +68,7 @@ export const TransactionData = () => {
method={method}
source={source}
isExpandable={isExpandable}
+ chainId={chainId}
/>
{index < data.length - 1 && }
@@ -119,10 +121,12 @@ function FunctionContainer({
method,
source,
isExpandable,
+ chainId,
}: {
method: DecodedTransactionDataMethod;
source?: DecodedTransactionDataSource;
isExpandable: boolean;
+ chainId: string;
}) {
const t = useI18nContext();
@@ -134,6 +138,7 @@ function FunctionContainer({
param={param}
index={paramIndex}
source={source}
+ chainId={chainId}
/>
))}
@@ -172,18 +177,20 @@ function FunctionContainer({
function ParamValue({
param,
source,
+ chainId,
}: {
param: DecodedTransactionDataParam;
source?: DecodedTransactionDataSource;
+ chainId: string;
}) {
const { name, type, value } = param;
if (type === 'address') {
- return ;
+ return ;
}
if (name === 'path' && source === DecodedTransactionDataSource.Uniswap) {
- return ;
+ return ;
}
let valueString = value.toString();
@@ -199,10 +206,12 @@ function ParamRow({
param,
index,
source,
+ chainId,
}: {
param: DecodedTransactionDataParam;
index: number;
source?: DecodedTransactionDataSource;
+ chainId: string;
}) {
const { name, type, description } = param;
const label = name ? _.startCase(name) : `Param #${index + 1}`;
@@ -215,20 +224,29 @@ function ParamRow({
param={childParam}
index={childIndex}
source={source}
+ chainId={chainId}
/>
));
return (
<>
- {!childRows?.length && }
+ {!childRows?.length && (
+
+ )}
{childRows && {childRows}}
>
);
}
-function UniswapPath({ pathPools }: { pathPools: UniswapPathPool[] }) {
+function UniswapPath({
+ pathPools,
+ chainId,
+}: {
+ pathPools: UniswapPathPool[];
+ chainId: string;
+}) {
return (
{pathPools.map((pool, index) => (
<>
- {index === 0 && }
+ {index === 0 && (
+
+ )}
-
+
>
))}
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
index 706729a8cc0a..89c2da783a4f 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
@@ -56,13 +56,18 @@ export const RecipientRow = () => {
return null;
}
+ const { chainId } = currentConfirmation;
+
return (
-
+
);
};
@@ -115,7 +120,7 @@ const PaymasterRow = () => {
const t = useI18nContext();
const { currentConfirmation } = useConfirmContext();
- const { id: userOperationId } = currentConfirmation ?? {};
+ const { id: userOperationId, chainId } = currentConfirmation ?? {};
const isUserOperation = Boolean(currentConfirmation?.isUserOperation);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -134,7 +139,7 @@ const PaymasterRow = () => {
label={t('confirmFieldPaymaster')}
tooltip={t('confirmFieldTooltipPaymaster')}
>
-
+
);
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
index 48a5f2dad74c..6d686873ea1c 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-details-section.tsx
@@ -63,7 +63,10 @@ export const TokenDetailsSection = () => {
const tokenRow = (
-
+
);
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
index de0e928c10f8..250b9bffd2cf 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
@@ -34,6 +34,8 @@ export const TransactionFlowSection = () => {
return ;
}
+ const { chainId } = transactionMeta;
+
return (
{
{
color={IconColor.iconMuted}
/>
{recipientAddress && (
-
+
)}
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx
index 35d8e3e6d672..951acbba6782 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx
@@ -23,6 +23,8 @@ const TypedSignV1Info: React.FC = () => {
return null;
}
+ const chainId = currentConfirmation.chainId as string;
+
return (
<>
@@ -41,6 +43,7 @@ const TypedSignV1Info: React.FC = () => {
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
index 44131ec18fbf..855f7837ba42 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
@@ -42,6 +42,7 @@ const PermitSimulation: React.FC
diff --git a/ui/pages/confirmations/components/confirm/row/dataTree.test.tsx b/ui/pages/confirmations/components/confirm/row/dataTree.test.tsx
index b427b6a8ee97..35dafe552cb7 100644
--- a/ui/pages/confirmations/components/confirm/row/dataTree.test.tsx
+++ b/ui/pages/confirmations/components/confirm/row/dataTree.test.tsx
@@ -10,6 +10,8 @@ import configureStore from '../../../../../store/store';
import { DataTree } from './dataTree';
+const CHAIN_ID_MOCK = '0x123';
+
const mockData = {
contents: { value: 'Hello, Bob!', type: 'string' },
from: {
@@ -67,7 +69,7 @@ const store = configureStore(mockState);
describe('DataTree', () => {
it('should match snapshot', () => {
const { container } = renderWithProvider(
- ,
+ ,
store,
);
expect(container).toMatchSnapshot();
@@ -77,6 +79,7 @@ describe('DataTree', () => {
const { container } = renderWithProvider(
,
store,
);
@@ -90,7 +93,7 @@ describe('DataTree', () => {
mockPermitData.message.deadline = '-1';
const { container } = renderWithProvider(
- ,
+ ,
store,
);
expect(container).toMatchSnapshot();
@@ -100,6 +103,7 @@ describe('DataTree', () => {
const { container } = renderWithProvider(
,
store,
);
@@ -114,7 +118,10 @@ describe('DataTree', () => {
},
'A number': { type: 'uint32', value: '1337' },
};
- const { container } = renderWithProvider(, store);
+ const { container } = renderWithProvider(
+ ,
+ store,
+ );
expect(container).toMatchSnapshot();
});
});
diff --git a/ui/pages/confirmations/components/confirm/row/dataTree.tsx b/ui/pages/confirmations/components/confirm/row/dataTree.tsx
index b295f337deb4..9a089a757004 100644
--- a/ui/pages/confirmations/components/confirm/row/dataTree.tsx
+++ b/ui/pages/confirmations/components/confirm/row/dataTree.tsx
@@ -97,10 +97,12 @@ export const DataTree = ({
data,
primaryType,
tokenDecimals: tokenDecimalsProp,
+ chainId,
}: {
data: Record | TreeData[];
primaryType?: PrimaryType;
tokenDecimals?: number;
+ chainId: string;
}) => {
const tokenContract = getTokenContractInDataTree(data);
const { decimalsNumber } = useGetTokenStandardAndDetails(tokenContract);
@@ -125,6 +127,7 @@ export const DataTree = ({
value={value}
type={type}
tokenDecimals={tokenDecimals}
+ chainId={chainId}
/>
}
@@ -150,12 +153,14 @@ const DataField = memo(
type,
value,
tokenDecimals,
+ chainId,
}: {
label: string;
primaryType?: PrimaryType;
type: string;
value: ValueType;
tokenDecimals?: number;
+ chainId: string;
}) => {
const t = useI18nContext();
@@ -165,6 +170,7 @@ const DataField = memo(
data={value}
primaryType={primaryType}
tokenDecimals={tokenDecimals}
+ chainId={chainId}
/>
);
}
@@ -191,7 +197,7 @@ const DataField = memo(
mixedCaseUseChecksum: true,
})
) {
- return
;
+ return
;
}
return
;
diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx
index ecf55e3b574d..e01a42782033 100644
--- a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx
+++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.test.tsx
@@ -8,11 +8,14 @@ import { ConfirmInfoRowTypedSignDataV1 } from './typedSignDataV1';
const mockStore = configureMockStore([])(mockState);
+const CHAIN_ID_MOCK = '0x123';
+
describe('ConfirmInfoRowTypedSignData', () => {
it('should match snapshot', () => {
const { container } = renderWithProvider(
,
mockStore,
);
@@ -21,7 +24,10 @@ describe('ConfirmInfoRowTypedSignData', () => {
it('should return null if data is not defined', () => {
const { container } = renderWithProvider(
-
,
+
,
mockStore,
);
expect(container).toBeEmptyDOMElement();
diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.tsx
index eb7c5c5ecb02..b781a9a6ff3e 100644
--- a/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.tsx
+++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data-v1/typedSignDataV1.tsx
@@ -7,8 +7,10 @@ import { DataTree } from '../dataTree';
export const ConfirmInfoRowTypedSignDataV1 = ({
data,
+ chainId,
}: {
data?: TypedSignDataV1Type;
+ chainId: string;
}) => {
if (!data) {
return null;
@@ -22,7 +24,7 @@ export const ConfirmInfoRowTypedSignDataV1 = ({
return (
-
+
);
diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.test.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.test.tsx
index 436edbecd3e0..6eb1c5b4236f 100644
--- a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.test.tsx
+++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.test.tsx
@@ -8,6 +8,8 @@ import { renderWithProvider } from '../../../../../../../test/lib/render-helpers
import configureStore from '../../../../../../store/store';
import { ConfirmInfoRowTypedSignData } from './typedSignData';
+const CHAIN_ID_MOCK = '0x123';
+
describe('ConfirmInfoRowTypedSignData', () => {
const renderWithComponentData = (
data = unapprovedTypedSignMsgV4.msgParams?.data as string,
@@ -15,7 +17,7 @@ describe('ConfirmInfoRowTypedSignData', () => {
const store = configureStore(mockState);
return renderWithProvider(
-
,
+
,
store,
);
};
diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx
index 828f82f41352..b11b02656e81 100644
--- a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx
+++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx
@@ -13,10 +13,12 @@ import { DataTree } from '../dataTree';
export const ConfirmInfoRowTypedSignData = ({
data,
tokenDecimals,
+ chainId,
}: {
data: string;
isPermit?: boolean;
tokenDecimals?: number;
+ chainId: string;
}) => {
const t = useI18nContext();
@@ -39,6 +41,7 @@ export const ConfirmInfoRowTypedSignData = ({
data={sanitizedMessage.value}
primaryType={primaryType}
tokenDecimals={tokenDecimals}
+ chainId={chainId}
/>
diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
index 68209a68e35d..a0a16cc838c3 100644
--- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
+++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js
@@ -224,7 +224,11 @@ export default function ContractDetailsModal({
{petnamesEnabled ? (
-
+
) : (
{typeof value === 'object' && value !== null ? (
-
+
) : (
{petnamesEnabled ? (
-
+
) : (
{primaryType}
-
+
);
@@ -99,4 +100,5 @@ SignatureRequestMessage.propTypes = {
messageRootRef: PropTypes.object,
messageIsScrollable: PropTypes.bool,
primaryType: PropTypes.string,
+ chainId: PropTypes.string,
};
diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js
index ce6967b50f70..3cf2a54327c9 100644
--- a/ui/pages/confirmations/components/signature-request/signature-request.js
+++ b/ui/pages/confirmations/components/signature-request/signature-request.js
@@ -313,6 +313,7 @@ const SignatureRequest = ({ txData, warnings }) => {
messageRootRef={messageRootRef}
messageIsScrollable={messageIsScrollable}
primaryType={primaryType}
+ chainId={chainId}
/>