diff --git a/ui/components/app/confirm/info/row/address.test.tsx b/ui/components/app/confirm/info/row/address.test.tsx
index f27e067787b1..08a3561691ff 100644
--- a/ui/components/app/confirm/info/row/address.test.tsx
+++ b/ui/components/app/confirm/info/row/address.test.tsx
@@ -8,8 +8,6 @@ import { mockNetworkState } from '../../../../../../test/stub/networks';
import { ConfirmInfoRowAddress } from './address';
import { TEST_ADDRESS } from './constants';
-const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET;
-
const render = (
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -21,10 +19,7 @@ const render = (
...storeOverrides,
});
- return renderWithProvider(
-
, store);
};
describe('ConfirmInfoRowAddress', () => {
diff --git a/ui/components/app/confirm/info/row/address.tsx b/ui/components/app/confirm/info/row/address.tsx
index 95fabf26652f..7d28851ece92 100644
--- a/ui/components/app/confirm/info/row/address.tsx
+++ b/ui/components/app/confirm/info/row/address.tsx
@@ -22,12 +22,11 @@ import { useFallbackDisplayName } from './hook';
export type ConfirmInfoRowAddressProps = {
address: string;
- chainId: string;
isSnapUsingThis?: boolean;
};
export const ConfirmInfoRowAddress = memo(
- ({ address, chainId, isSnapUsingThis }: ConfirmInfoRowAddressProps) => {
+ ({ address, isSnapUsingThis }: ConfirmInfoRowAddressProps) => {
const isPetNamesEnabled = useSelector(getPetnamesEnabled);
const { displayName, hexAddress } = useFallbackDisplayName(address);
const [isNicknamePopoverShown, setIsNicknamePopoverShown] = useState(false);
@@ -49,7 +48,6 @@ export const ConfirmInfoRowAddress = memo(
value={hexAddress}
type={NameType.ETHEREUM_ADDRESS}
preferContractSymbol
- variation={chainId}
/>
) : (
<>
diff --git a/ui/components/app/confirm/info/row/copy-icon.tsx b/ui/components/app/confirm/info/row/copy-icon.tsx
index 16f6604a53d4..ce349089dac3 100644
--- a/ui/components/app/confirm/info/row/copy-icon.tsx
+++ b/ui/components/app/confirm/info/row/copy-icon.tsx
@@ -1,20 +1,12 @@
-import React, { CSSProperties, useCallback } from 'react';
+import React, { useCallback } from 'react';
import { useCopyToClipboard } from '../../../../../hooks/useCopyToClipboard';
import { IconColor } from '../../../../../helpers/constants/design-system';
-import {
- ButtonIcon,
- ButtonIconSize,
- IconName,
-} from '../../../../component-library';
+import { Icon, IconName, IconSize } from '../../../../component-library';
type CopyCallback = (text: string) => void;
-export const CopyIcon: React.FC<{
- copyText: string;
- color?: IconColor;
- style?: CSSProperties;
-}> = ({ copyText, color, style = {} }) => {
+export const CopyIcon: React.FC<{ copyText: string }> = ({ copyText }) => {
const [copied, handleCopy] = useCopyToClipboard();
const handleClick = useCallback(async () => {
@@ -22,19 +14,12 @@ export const CopyIcon: React.FC<{
}, [copyText]);
return (
-
);
};
diff --git a/ui/components/app/confirm/info/row/row.test.tsx b/ui/components/app/confirm/info/row/row.test.tsx
index 8f0edab1be03..3a6a77e4b354 100644
--- a/ui/components/app/confirm/info/row/row.test.tsx
+++ b/ui/components/app/confirm/info/row/row.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { fireEvent, render, screen } from '@testing-library/react';
+import { render } from '@testing-library/react';
import { Text } from '../../../../component-library';
import { ConfirmInfoRow } from './row';
@@ -22,20 +22,4 @@ describe('ConfirmInfoRow', () => {
);
expect(container).toMatchSnapshot();
});
-
- it('should be expandable when collapsed is true', () => {
- render(
-
,
- );
- expect(screen.queryByText('Some text')).not.toBeInTheDocument();
- fireEvent.click(screen.getByTestId('sectionCollapseButton'));
- expect(screen.queryByText('Some text')).toBeInTheDocument();
- });
});
diff --git a/ui/components/app/confirm/info/row/row.tsx b/ui/components/app/confirm/info/row/row.tsx
index 55821f7080f5..7616ccae5f21 100644
--- a/ui/components/app/confirm/info/row/row.tsx
+++ b/ui/components/app/confirm/info/row/row.tsx
@@ -1,9 +1,7 @@
-import React, { createContext, useState } from 'react';
+import React, { createContext } from 'react';
import Tooltip from '../../../../ui/tooltip/tooltip';
import {
Box,
- ButtonIcon,
- ButtonIconSize,
Icon,
IconName,
IconSize,
@@ -42,7 +40,6 @@ export type ConfirmInfoRowProps = {
copyEnabled?: boolean;
copyText?: string;
'data-testid'?: string;
- collapsed?: boolean;
};
const BACKGROUND_COLORS = {
@@ -82,101 +79,71 @@ export const ConfirmInfoRow: React.FC
= ({
labelChildren,
color,
copyEnabled = false,
- copyText,
+ copyText = undefined,
'data-testid': dataTestId,
- collapsed,
-}) => {
- const [expanded, setExpanded] = useState(!collapsed);
-
- const isCollapsible = collapsed !== undefined;
-
- return (
-
+}) => (
+
+
+ {copyEnabled && }
- {copyEnabled && (
-
- )}
- {isCollapsible && (
- setExpanded(!expanded)}
- data-testid="sectionCollapseButton"
- ariaLabel="collapse-button"
- />
- )}
-
-
-
- {label}
-
- {labelChildren}
- {!labelChildren && tooltip?.length && (
-
-
-
- )}
-
+
+
+ {label}
+
+ {labelChildren}
+ {!labelChildren && tooltip?.length && (
+
+
+
+ )}
- {expanded &&
- (typeof children === 'string' ? (
-
- {children}
-
- ) : (
- children
- ))}
-
- );
-};
+ {typeof children === 'string' ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+
+);
diff --git a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap
index 2482a916a5a9..f9823b5af9ac 100644
--- a/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap
+++ b/ui/components/app/currency-input/__snapshots__/currency-input.test.js.snap
@@ -36,7 +36,6 @@ exports[`CurrencyInput Component rendering should disable unit input 1`] = `
>
$0.00
@@ -90,7 +89,6 @@ exports[`CurrencyInput Component rendering should render properly with a fiat va
>
0.004327880204275946
@@ -185,7 +183,6 @@ exports[`CurrencyInput Component rendering should render properly with an ETH va
>
$231.06
@@ -240,7 +237,6 @@ exports[`CurrencyInput Component rendering should render properly without a suff
>
$0.00
diff --git a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap
index f98b3a231970..179a3821cad4 100644
--- a/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap
+++ b/ui/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/__snapshots__/cancel-transaction-gas-fee.component.test.js.snap
@@ -11,7 +11,6 @@ exports[`CancelTransactionGasFee Component should render 1`] = `
>
<0.000001
@@ -27,7 +26,6 @@ exports[`CancelTransactionGasFee Component should render 1`] = `
>
<0.000001
diff --git a/ui/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/components/app/modals/qr-scanner/qr-scanner.component.js
index 75e1a83417b9..51ae5a89a6ef 100644
--- a/ui/components/app/modals/qr-scanner/qr-scanner.component.js
+++ b/ui/components/app/modals/qr-scanner/qr-scanner.component.js
@@ -22,6 +22,10 @@ const READY_STATE = {
READY: 'READY',
};
+const ethereumPrefix = 'ethereum:';
+// A 0x-prefixed Ethereum address is 42 characters (2 prefix + 40 address)
+const addressLength = 42;
+
const parseContent = (content) => {
let type = 'unknown';
let values = {};
@@ -31,12 +35,18 @@ const parseContent = (content) => {
// For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
// Ethereum address links - fox ex. ethereum:0x.....1111
- if (content.split('ethereum:').length > 1) {
+ if (
+ content.split(ethereumPrefix).length > 1 &&
+ content.length === ethereumPrefix.length + addressLength
+ ) {
type = 'address';
- // uses regex capture groups to match and extract address while ignoring everything else
+ // uses regex capture groups to match and extract address
values = { address: parseScanContent(content) };
// Regular ethereum addresses - fox ex. 0x.....1111
- } else if (content.substring(0, 2).toLowerCase() === '0x') {
+ } else if (
+ content.substring(0, 2).toLowerCase() === '0x' &&
+ content.length === addressLength
+ ) {
type = 'address';
values = { address: content };
}
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
index cfa08b3eee01..a6d0df79843d 100644
--- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
+++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
@@ -706,12 +706,15 @@ exports[`NameDetails renders with recognized name 1`] = `
-
+ >
+
+
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 0f0df9f5b5f6..9e93384adcd6 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 { CHAIN_IDS } from '../../../../../shared/constants/network';
import { mockNetworkState } from '../../../../../test/stub/networks';
+import { CHAIN_IDS } from '../../../../../shared/constants/network';
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,17 +85,13 @@ const STATE_MOCK = {
},
},
useTokenDetection: true,
- 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',
- },
- },
+ tokenList: {
+ '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': {
+ address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d',
+ symbol: 'IUSD',
+ name: 'iZUMi Bond USD',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png',
},
},
},
@@ -161,7 +157,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -175,7 +170,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -189,7 +183,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -203,7 +196,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -217,7 +209,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -238,7 +229,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -261,7 +251,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -284,7 +273,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -307,7 +295,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -330,7 +317,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -350,7 +336,6 @@ describe('NameDetails', () => {
undefined}
/>,
store,
@@ -388,7 +373,6 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -415,7 +399,6 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -443,7 +426,6 @@ describe('NameDetails', () => {
undefined}
/>
,
@@ -472,7 +454,6 @@ 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 1bb2b1f1e478..22b0445a0ad5 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 { getNameSources } from '../../../../selectors';
+import { getCurrentChainId, getNameSources } from '../../../../selectors';
import {
setName as saveName,
updateProposedNames,
@@ -64,7 +64,6 @@ export type NameDetailsProps = {
sourcePriority?: string[];
type: NameType;
value: string;
- variation: string;
};
type ProposedNameOption = Required & {
@@ -158,14 +157,12 @@ function getInitialSources(
return [...resultSources, ...stateSources].sort();
}
-function useProposedNames(value: string, type: NameType, variation: string) {
+function useProposedNames(value: string, type: NameType, chainId: string) {
const dispatch = useDispatch();
- const { proposedNames } = useName(value, type, variation);
-
+ const { proposedNames } = useName(value, type);
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateInterval = useRef();
-
const [initialSources, setInitialSources] = useState();
useEffect(() => {
@@ -181,7 +178,7 @@ function useProposedNames(value: string, type: NameType, variation: string) {
value,
type,
onlyUpdateAfterDelay: true,
- variation,
+ variation: chainId,
}),
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -199,7 +196,7 @@ function useProposedNames(value: string, type: NameType, variation: string) {
updateInterval.current = setInterval(update, UPDATE_DELAY);
return reset;
- }, [value, type, variation, dispatch, initialSources, setInitialSources]);
+ }, [value, type, chainId, dispatch, initialSources, setInitialSources]);
return { proposedNames, initialSources };
}
@@ -208,20 +205,13 @@ export default function NameDetails({
onClose,
type,
value,
- variation,
}: NameDetailsProps) {
- const { name: savedPetname, sourceId: savedSourceId } = useName(
+ const chainId = useSelector(getCurrentChainId);
+ const { name: savedPetname, sourceId: savedSourceId } = useName(value, type);
+ const { name: displayName, hasPetname: hasSavedPetname } = useDisplayName(
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);
@@ -236,7 +226,7 @@ export default function NameDetails({
const { proposedNames, initialSources } = useProposedNames(
value,
type,
- variation,
+ chainId,
);
const [copiedAddress, handleCopyAddress] = useCopyToClipboard() as [
@@ -285,12 +275,12 @@ export default function NameDetails({
type,
name: name?.length ? name : null,
sourceId: selectedSourceId,
- variation,
+ variation: chainId,
}),
);
onClose();
- }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, variation]);
+ }, [name, selectedSourceId, onClose, trackPetnamesSaveEvent, chainId]);
const handleClose = useCallback(() => {
onClose();
@@ -343,7 +333,6 @@ export default function NameDetails({
diff --git a/ui/components/app/name/name.stories.tsx b/ui/components/app/name/name.stories.tsx
index 732c9059b530..fb23334a8776 100644
--- a/ui/components/app/name/name.stories.tsx
+++ b/ui/components/app/name/name.stories.tsx
@@ -3,75 +3,108 @@ import React from 'react';
import { NameType } from '@metamask/name-controller';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
-import Name, { NameProps } from './name';
-import mockState from '../../../../test/data/mock-state.json';
-import {
- EXPERIENCES_TYPE,
- FIRST_PARTY_CONTRACT_NAMES,
-} from '../../../../shared/constants/first-party-contracts';
-import { cloneDeep } from 'lodash';
+import Name from './name';
+import { mockNetworkState } from '../../../../test/stub/networks';
-const ADDRESS_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54978';
-const ADDRESS_NFT_MOCK = '0xc0ffee254729296a45a3885639ac7e10f9d54979';
-const VARIATION_MOCK = '0x1';
-const NAME_MOCK = 'Saved Name';
+const addressNoSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54978';
+const addressSavedNameMock = '0xc0ffee254729296a45a3885639ac7e10f9d54977';
+const addressSavedTokenMock = '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d';
+const addressUnsavedTokenMock = '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8';
+const chainIdMock = '0x1';
-const ADDRESS_FIRST_PARTY_MOCK =
- FIRST_PARTY_CONTRACT_NAMES[EXPERIENCES_TYPE.METAMASK_BRIDGE][
- VARIATION_MOCK
- ].toLowerCase();
-
-const PROPOSED_NAMES_MOCK = {
- ens: {
- proposedNames: ['test.eth'],
- lastRequestTime: 123,
- retryDelay: null,
- },
- etherscan: {
- proposedNames: ['TestContract'],
- lastRequestTime: 123,
- retryDelay: null,
- },
- token: {
- proposedNames: ['Test Token'],
- lastRequestTime: 123,
- retryDelay: null,
- },
- lens: {
- proposedNames: ['test.lens'],
- lastRequestTime: 123,
- retryDelay: null,
- },
-};
-
-const STATE_MOCK = {
- ...mockState,
+const storeMock = configureStore({
metamask: {
- ...mockState.metamask,
+ ...mockNetworkState({chainId: chainIdMock}),
useTokenDetection: true,
- tokensChainsCache: {},
+ tokenList: {
+ '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d': {
+ address: '0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d',
+ symbol: 'IUSD',
+ name: 'iZUMi Bond USD',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png',
+ },
+ '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': {
+ address: '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8',
+ symbol: 'USX',
+ name: 'dForce USD',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8.png',
+ },
+ },
names: {
[NameType.ETHEREUM_ADDRESS]: {
- [ADDRESS_MOCK]: {
- [VARIATION_MOCK]: {
- proposedNames: PROPOSED_NAMES_MOCK,
+ [addressNoSavedNameMock]: {
+ [chainIdMock]: {
+ proposedNames: {
+ ens: {
+ proposedNames: ['test.eth'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ etherscan: {
+ proposedNames: ['TestContract'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ token: {
+ proposedNames: ['Test Token'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ lens: {
+ proposedNames: ['test.lens'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ },
},
},
- [ADDRESS_NFT_MOCK]: {
- [VARIATION_MOCK]: {
- proposedNames: PROPOSED_NAMES_MOCK,
+ [addressSavedNameMock]: {
+ [chainIdMock]: {
+ proposedNames: {
+ ens: {
+ proposedNames: ['test.eth'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ etherscan: {
+ proposedNames: ['TestContract'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ token: {
+ proposedNames: ['Test Token'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ lens: {
+ proposedNames: ['test.lens'],
+ lastRequestTime: 123,
+ retryDelay: null,
+ },
+ },
+ name: 'Test Token',
+ sourceId: 'token',
},
},
- [ADDRESS_FIRST_PARTY_MOCK]: {
- [VARIATION_MOCK]: {
- proposedNames: PROPOSED_NAMES_MOCK,
+ [addressSavedTokenMock]: {
+ [chainIdMock]: {
+ proposedNames: {},
+ name: 'Saved Token Name',
+ sourceId: 'token',
},
},
},
},
- nameSources: {},
+ nameSources: {
+ ens: { label: 'Ethereum Name Service (ENS)' },
+ etherscan: { label: 'Etherscan (Verified Contract Name)' },
+ token: { label: 'Blockchain (Token Name)' },
+ lens: { label: 'Lens Protocol' },
+ },
},
-};
+});
/**
* Displays the saved name for a raw value such as an Ethereum address.
@@ -92,10 +125,6 @@ export default {
description: `The type of value.
Limited to the values in the \`NameType\` enum.`,
},
- variation: {
- control: 'text',
- description: `The variation of the value.
For example, the chain ID if the type is Ethereum address.`,
- },
disableEdit: {
control: 'boolean',
description: `Whether to prevent the modal from opening when the component is clicked.`,
@@ -105,141 +134,68 @@ export default {
},
},
args: {
- value: ADDRESS_MOCK,
+ value: addressNoSavedNameMock,
type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
disableEdit: false,
},
- render: ({ state, ...args }) => {
- const finalState = cloneDeep(STATE_MOCK);
- state?.(finalState);
-
- return (
-
-
-
- );
- },
+ decorators: [(story) => {story()}],
};
+// eslint-disable-next-line jsdoc/require-param
/**
* No name has been saved for the value and type.
*/
-export const NoSavedName = {
- name: 'No Saved Name',
- args: {
- value: ADDRESS_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- },
+export const DefaultStory = (args) => {
+ return ;
};
+DefaultStory.storyName = 'No Saved Name';
+
/**
* A name was previously saved for this value and type.
* The component will still display a modal when clicked to edit the name.
*/
-export const SavedNameStory = {
- name: 'Saved Name',
- args: {
- value: ADDRESS_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- state: (state) => {
- state.metamask.names[NameType.ETHEREUM_ADDRESS][ADDRESS_MOCK][
- VARIATION_MOCK
- ].name = NAME_MOCK;
- },
- },
+export const SavedNameStory = () => {
+ return ;
};
+SavedNameStory.storyName = 'Saved Name';
+
/**
* No name was previously saved for this recognized token.
* The component will still display a modal when clicked to edit the name.
*/
-export const DefaultTokenNameStory = {
- name: 'Default ERC-20 Token Name',
- args: {
- value: ADDRESS_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- state: (state) => {
- state.metamask.tokensChainsCache = {
- [VARIATION_MOCK]: {
- data: {
- [ADDRESS_MOCK]: {
- address: ADDRESS_MOCK,
- symbol: 'IUSD',
- name: 'iZUMi Bond USD',
- iconUrl:
- 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x0a3bb08b3a15a19b4de82f8acfc862606fb69a2d.png',
- },
- },
- },
- };
- },
- },
+export const UnsavedTokenNameStory = () => {
+ return (
+
+ );
};
-/**
- * No name was previously saved for this watched NFT.
- * The component will still display a modal when clicked to edit the name.
- */
-export const DefaultWatchedNFTNameStory = {
- name: 'Default Watched NFT Name',
- args: {
- value: ADDRESS_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- state: (state) => {
- state.metamask.allNftContracts = {
- '0x123': {
- [VARIATION_MOCK]: [
- {
- address: ADDRESS_MOCK,
- name: 'Everything I Own',
- },
- ],
- },
- };
- },
- },
-};
+UnsavedTokenNameStory.storyName = 'Unsaved Token Name';
/**
- * No name was previously saved for this recognized NFT.
+ * A name was previously saved for this recognized token.
* The component will still display a modal when clicked to edit the name.
*/
-export const DefaultNFTNameStory = {
- name: 'Default NFT Name',
- args: {
- value: ADDRESS_NFT_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- },
+export const SavedTokenNameStory = () => {
+ return (
+
+ );
};
-/**
- * No name was previously saved for this first-party contract.
- * The component will still display a modal when clicked to edit the name.
- */
-export const DefaultFirstPartyNameStory = {
- name: 'Default First-Party Name',
- args: {
- value: ADDRESS_FIRST_PARTY_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- },
-};
+SavedTokenNameStory.storyName = 'Saved Token Name';
/**
* Clicking the component will not display a modal to edit the name.
*/
-export const EditDisabledStory = {
- name: 'Edit Disabled',
- args: {
- value: ADDRESS_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- disableEdit: true,
- },
+export const EditDisabledStory = () => {
+ return (
+
+ );
};
+
+EditDisabledStory.storyName = 'Edit Disabled';
diff --git a/ui/components/app/name/name.test.tsx b/ui/components/app/name/name.test.tsx
index 33648e98e38c..061d39e670de 100644
--- a/ui/components/app/name/name.test.tsx
+++ b/ui/components/app/name/name.test.tsx
@@ -22,7 +22,6 @@ 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: {
@@ -45,11 +44,7 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -66,7 +61,6 @@ describe('Name', () => {
,
store,
);
@@ -81,11 +75,7 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -100,11 +90,7 @@ describe('Name', () => {
});
const { container } = renderWithProvider(
- ,
+ ,
store,
);
@@ -128,11 +114,7 @@ describe('Name', () => {
renderWithProvider(
-
+
,
store,
);
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx
index 2097d21faf07..5af2851c8885 100644
--- a/ui/components/app/name/name.tsx
+++ b/ui/components/app/name/name.tsx
@@ -38,12 +38,6 @@ 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 {
@@ -67,17 +61,15 @@ 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) {
@@ -108,12 +100,7 @@ const Name = memo(
return (
{!disableEdit && modalOpen && (
-
+
)}
{
const tooltipTitle = await waitFor(() =>
container.querySelector(
- '[data-original-title="This account is not set up for use with Goerli"]',
+ '[data-original-title="This account is not set up for use with goerli"]',
),
);
diff --git a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap
index 7dd3749a6e5f..d29236409dbc 100644
--- a/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap
+++ b/ui/components/app/snaps/snap-ui-address/__snapshots__/snap-ui-address.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`SnapUIAddress renders Bitcoin address 1`] = `
= ({
[caipIdentifier],
);
- const displayName = useDisplayName(parsed);
-
- const value =
- displayName ??
- shortenAddress(
- parsed.chain.namespace === 'eip155'
- ? toChecksumHexAddress(parsed.address)
- : parsed.address,
- );
+ // For EVM addresses, we make sure they are checksummed.
+ const transformedAddress =
+ parsed.chain.namespace === 'eip155'
+ ? toChecksumHexAddress(parsed.address)
+ : parsed.address;
+ const shortenedAddress = shortenAddress(transformedAddress);
return (
-
+
- {value}
+ {shortenedAddress}
);
};
diff --git a/ui/components/app/snaps/snap-ui-link/index.scss b/ui/components/app/snaps/snap-ui-link/index.scss
deleted file mode 100644
index 7d3f75f0e372..000000000000
--- a/ui/components/app/snaps/snap-ui-link/index.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.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 58a22008a52a..a1289543fd45 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,14 +51,7 @@ export const SnapUILink = ({ href, children }) => {
externalLink
size={ButtonLinkSize.Inherit}
display={Display.Inline}
- 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,
- }}
+ className="snap-ui-link"
>
{children}
diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss
index d32edf726479..7e18e72c917f 100644
--- a/ui/components/app/snaps/snap-ui-renderer/index.scss
+++ b/ui/components/app/snaps/snap-ui-renderer/index.scss
@@ -34,10 +34,6 @@
border-radius: 8px;
border-color: var(--color-border-muted);
- & .mm-icon {
- top: 0;
- }
-
.mm-text--overflow-wrap-anywhere {
overflow-wrap: normal;
}
@@ -52,6 +48,10 @@
&__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
deleted file mode 100644
index b88762c3bc19..000000000000
--- a/ui/components/app/toast-master/selectors.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-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
deleted file mode 100644
index 584f1cc25983..000000000000
--- a/ui/components/app/toast-master/toast-master.js
+++ /dev/null
@@ -1,299 +0,0 @@
-/* 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
deleted file mode 100644
index 8b29f20a240d..000000000000
--- a/ui/components/app/toast-master/toast-master.test.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-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
deleted file mode 100644
index d6544707f45d..000000000000
--- a/ui/components/app/toast-master/utils.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-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 226a2a9113c0..751b0f53e73a 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 { chainId, hash } = transaction;
+ const { hash } = transaction;
return (
@@ -332,7 +332,6 @@ 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/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap
index 4a9fc4d3cf7a..b29efce542e3 100644
--- a/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap
+++ b/ui/components/app/user-preferenced-currency-display/__snapshots__/user-preferenced-currency-display.test.js.snap
@@ -8,7 +8,6 @@ exports[`UserPreferencedCurrencyDisplay Component rendering should match snapsho
>
0
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
index 779309858a18..4db61d568f4a 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts
@@ -16,7 +16,6 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion<
showCurrencySuffix?: boolean;
shouldCheckShowNativeToken?: boolean;
isAggregatedFiatOverviewBalance?: boolean;
- privacyMode?: boolean;
}
>;
diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
index a466f7813672..613b731d0a16 100644
--- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
+++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js
@@ -28,7 +28,6 @@ export default function UserPreferencedCurrencyDisplay({
showNative,
showCurrencySuffix,
shouldCheckShowNativeToken,
- privacyMode = false,
...restProps
}) {
// NOTE: When displaying currencies, we need the actual account to detect whether we're in a
@@ -84,7 +83,6 @@ export default function UserPreferencedCurrencyDisplay({
numberOfDecimals={numberOfDecimals}
prefixComponent={prefixComponent}
suffix={showCurrencySuffix && !showEthLogo && currency}
- privacyMode={privacyMode}
/>
);
}
@@ -128,7 +126,6 @@ const UserPreferencedCurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
shouldCheckShowNativeToken: PropTypes.bool,
- privacyMode: PropTypes.bool,
};
UserPreferencedCurrencyDisplay.propTypes =
diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
index 8da096151908..95e0d92fa2b8 100644
--- a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
+++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
@@ -7,7 +7,6 @@ import {
getSelectedAccount,
getShouldHideZeroBalanceTokens,
getTokensMarketData,
- getPreferences,
} from '../../../selectors';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
import { AggregatedPercentageOverview } from './aggregated-percentage-overview';
@@ -23,7 +22,6 @@ jest.mock('../../../ducks/locale/locale', () => ({
jest.mock('../../../selectors', () => ({
getCurrentCurrency: jest.fn(),
getSelectedAccount: jest.fn(),
- getPreferences: jest.fn(),
getShouldHideZeroBalanceTokens: jest.fn(),
getTokensMarketData: jest.fn(),
}));
@@ -34,7 +32,6 @@ jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({
const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock;
const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock;
-const mockGetPreferences = getPreferences as jest.Mock;
const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock;
const mockGetShouldHideZeroBalanceTokens =
getShouldHideZeroBalanceTokens as jest.Mock;
@@ -162,7 +159,6 @@ describe('AggregatedPercentageOverview', () => {
beforeEach(() => {
mockGetIntlLocale.mockReturnValue('en-US');
mockGetCurrentCurrency.mockReturnValue('USD');
- mockGetPreferences.mockReturnValue({ privacyMode: false });
mockGetSelectedAccount.mockReturnValue(selectedAccountMock);
mockGetShouldHideZeroBalanceTokens.mockReturnValue(false);
mockGetTokensMarketData.mockReturnValue(marketDataMock);
diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
index 8c609610daa1..94555d3bc0cd 100644
--- a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
+++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
@@ -7,7 +7,6 @@ import {
getSelectedAccount,
getShouldHideZeroBalanceTokens,
getTokensMarketData,
- getPreferences,
} from '../../../selectors';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
@@ -20,7 +19,7 @@ import {
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
-import { Box, SensitiveText } from '../../component-library';
+import { Box, Text } from '../../component-library';
import { getCalculatedTokenAmount1dAgo } from '../../../helpers/utils/util';
// core already has this exported type but its not yet available in this version
@@ -35,7 +34,6 @@ export const AggregatedPercentageOverview = () => {
useSelector(getTokensMarketData);
const locale = useSelector(getIntlLocale);
const fiatCurrency = useSelector(getCurrentCurrency);
- const { privacyMode } = useSelector(getPreferences);
const selectedAccount = useSelector(getSelectedAccount);
const shouldHideZeroBalanceTokens = useSelector(
getShouldHideZeroBalanceTokens,
@@ -112,7 +110,7 @@ export const AggregatedPercentageOverview = () => {
let color = TextColor.textDefault;
- if (!privacyMode && isValidAmount(amountChange)) {
+ if (isValidAmount(amountChange)) {
if ((amountChange as number) === 0) {
color = TextColor.textDefault;
} else if ((amountChange as number) > 0) {
@@ -120,33 +118,26 @@ export const AggregatedPercentageOverview = () => {
} else {
color = TextColor.errorDefault;
}
- } else {
- color = TextColor.textAlternative;
}
-
return (
-
{formattedAmountChange}
-
-
+
{formattedPercentChange}
-
+
);
};
diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx
index 93d9e1061428..2de787ef23c0 100644
--- a/ui/components/app/wallet-overview/coin-overview.tsx
+++ b/ui/components/app/wallet-overview/coin-overview.tsx
@@ -28,7 +28,6 @@ import {
JustifyContent,
TextAlign,
TextVariant,
- IconColor,
} from '../../../helpers/constants/design-system';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import { getPortfolioUrl } from '../../../helpers/utils/portfolio';
@@ -62,10 +61,7 @@ import Spinner from '../../ui/spinner';
import { PercentageAndAmountChange } from '../../multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change';
import { getMultichainIsEvm } from '../../../selectors/multichain';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
-import {
- setAggregatedBalancePopoverShown,
- setPrivacyMode,
-} from '../../../store/actions';
+import { setAggregatedBalancePopoverShown } from '../../../store/actions';
import { useTheme } from '../../../hooks/useTheme';
import { getSpecificSettingsRoute } from '../../../helpers/utils/settings-search';
import { useI18nContext } from '../../../hooks/useI18nContext';
@@ -132,8 +128,7 @@ export const CoinOverview = ({
const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover);
const isTestnet = useSelector(getIsTestnet);
- const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } =
- useSelector(getPreferences);
+ const { showFiatInTestnets } = useSelector(getPreferences);
const selectedAccount = useSelector(getSelectedAccount);
const shouldHideZeroBalanceTokens = useSelector(
@@ -144,6 +139,8 @@ export const CoinOverview = ({
shouldHideZeroBalanceTokens,
);
+ const { showNativeTokenAsMainBalance } = useSelector(getPreferences);
+
const isEvm = useSelector(getMultichainIsEvm);
const isNotAggregatedFiatBalance =
showNativeTokenAsMainBalance || isTestnet || !isEvm;
@@ -166,10 +163,6 @@ export const CoinOverview = ({
dispatch(setAggregatedBalancePopoverShown());
};
- const handleSensitiveToggle = () => {
- dispatch(setPrivacyMode(!privacyMode));
- };
-
const [referenceElement, setReferenceElement] =
useState(null);
const setBoxRef = (ref: HTMLSpanElement | null) => {
@@ -260,39 +253,26 @@ export const CoinOverview = ({
ref={setBoxRef}
>
{balanceToDisplay ? (
- <>
-
-
- >
+
) : (
)}
diff --git a/ui/components/app/wallet-overview/index.scss b/ui/components/app/wallet-overview/index.scss
index 47dc40200e69..318c26501097 100644
--- a/ui/components/app/wallet-overview/index.scss
+++ b/ui/components/app/wallet-overview/index.scss
@@ -78,8 +78,7 @@
display: flex;
max-width: inherit;
justify-content: center;
- align-items: center;
- flex-wrap: nowrap;
+ flex-wrap: wrap;
}
&__primary-balance {
@@ -143,8 +142,7 @@
display: flex;
max-width: inherit;
justify-content: center;
- align-items: center;
- flex-wrap: nowrap;
+ flex-wrap: wrap;
}
&__primary-balance {
diff --git a/ui/components/component-library/icon/icon.types.ts b/ui/components/component-library/icon/icon.types.ts
index 3afd17ef983b..9c87851d0b65 100644
--- a/ui/components/component-library/icon/icon.types.ts
+++ b/ui/components/component-library/icon/icon.types.ts
@@ -44,7 +44,6 @@ export enum IconName {
Book = 'book',
Bookmark = 'bookmark',
Bridge = 'bridge',
- Collapse = 'collapse',
Calculator = 'calculator',
CardPos = 'card-pos',
CardToken = 'card-token',
diff --git a/ui/components/component-library/index.ts b/ui/components/component-library/index.ts
index 634af093a41b..861fb80bcf2c 100644
--- a/ui/components/component-library/index.ts
+++ b/ui/components/component-library/index.ts
@@ -69,8 +69,6 @@ export { TagUrl } from './tag-url';
export type { TagUrlProps } from './tag-url';
export { Text, ValidTag, TextDirection, InvisibleCharacter } from './text';
export type { TextProps } from './text';
-export { SensitiveText, SensitiveTextLength } from './sensitive-text';
-export type { SensitiveTextProps } from './sensitive-text';
export { Input, InputType } from './input';
export type { InputProps } from './input';
export { TextField, TextFieldType, TextFieldSize } from './text-field';
diff --git a/ui/components/component-library/sensitive-text/README.mdx b/ui/components/component-library/sensitive-text/README.mdx
deleted file mode 100644
index 9e950381e6f3..000000000000
--- a/ui/components/component-library/sensitive-text/README.mdx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Controls, Canvas } from '@storybook/blocks';
-
-import * as SensitiveTextStories from './sensitive-text.stories';
-
-# SensitiveText
-
-SensitiveText is a component that extends the Text component to handle sensitive information. It provides the ability to hide or show the text content, replacing it with dots when hidden.
-
-
-
-## Props
-
-The `SensitiveText` component extends the `Text` component. See the `Text` component for an extended list of props.
-
-
-
-### Children
-
-The text content to be displayed or hidden.
-
-
-
-```jsx
-import { SensitiveText } from '../../component-library';
-
-
- Sensitive Information
-
-```
-
-
-### IsHidden
-
-Use the `isHidden` prop to determine whether the text should be hidden or visible. When `isHidden` is `true`, the component will display dots instead of the actual text.
-
-
-
-```jsx
-import { SensitiveText } from '../../component-library';
-
-
- Sensitive Information
-
-```
-
-### Length
-
-Use the `length` prop to determine the length of the hidden text (number of dots). Can be a predefined `SensitiveTextLength` or a custom string number.
-
-The following predefined length options are available:
-
-- `SensitiveTextLength.Short`: `6`
-- `SensitiveTextLength.Medium`: `9`
-- `SensitiveTextLength.Long`: `12`
-- `SensitiveTextLength.ExtraLong`: `20`
-
-- The number of dots displayed is determined by the `length` prop.
-- If an invalid `length` is provided, the component will fall back to `SensitiveTextLength.Short` and log a warning.
-- Custom length values can be provided as strings, e.g. `15`.
-
-
-
-```jsx
-import { SensitiveText, SensitiveTextLength } from '../../component-library';
-
-
- Length "short" (6 characters)
-
-
- Length "medium" (9 characters)
-
-
- Length "long" (12 characters)
-
-
- Length "extra long" (20 characters)
-
-
- Length "15" (15 characters)
-
-```
diff --git a/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap b/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap
deleted file mode 100644
index 6844feb1783e..000000000000
--- a/ui/components/component-library/sensitive-text/__snapshots__/sensitive-text.test.tsx.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SensitiveText should render correctly 1`] = `
-
-
- Sensitive Information
-
-
-`;
diff --git a/ui/components/component-library/sensitive-text/index.ts b/ui/components/component-library/sensitive-text/index.ts
deleted file mode 100644
index ff89896fd03b..000000000000
--- a/ui/components/component-library/sensitive-text/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { SensitiveText } from './sensitive-text';
-export { SensitiveTextLength } from './sensitive-text.types';
-export type { SensitiveTextProps } from './sensitive-text.types';
diff --git a/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx b/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx
deleted file mode 100644
index 142def9118b5..000000000000
--- a/ui/components/component-library/sensitive-text/sensitive-text.stories.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-import React from 'react';
-import { SensitiveText } from '.';
-import { SensitiveTextLength } from './sensitive-text.types';
-import README from './README.mdx';
-import { Box } from '../box';
-import {
- Display,
- FlexDirection,
-} from '../../../helpers/constants/design-system';
-
-const meta: Meta = {
- title: 'Components/ComponentLibrary/SensitiveText',
- component: SensitiveText,
- parameters: {
- docs: {
- page: README,
- },
- },
- args: {
- children: 'Sensitive information',
- isHidden: false,
- length: SensitiveTextLength.Short,
- },
-} as Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const DefaultStory: Story = {};
-DefaultStory.storyName = 'Default';
-
-export const Children: Story = {
- args: {
- children: 'Sensitive information',
- },
- render: (args) => (
-
- ),
-};
-
-export const IsHidden: Story = {
- args: {
- isHidden: true,
- },
- render: (args) => (
-
- ),
-};
-
-export const Length: Story = {
- args: {
- isHidden: true,
- },
- render: (args) => (
-
-
- Length "short" (6 characters)
-
-
- Length "medium" (9 characters)
-
-
- Length "long" (12 characters)
-
-
- Length "extra long" (20 characters)
-
-
- Length "15" (15 characters)
-
-
- ),
-};
diff --git a/ui/components/component-library/sensitive-text/sensitive-text.test.tsx b/ui/components/component-library/sensitive-text/sensitive-text.test.tsx
deleted file mode 100644
index a4be911ea78d..000000000000
--- a/ui/components/component-library/sensitive-text/sensitive-text.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { SensitiveText } from './sensitive-text';
-import { SensitiveTextLength } from './sensitive-text.types';
-
-describe('SensitiveText', () => {
- const testProps = {
- isHidden: false,
- length: SensitiveTextLength.Short,
- children: 'Sensitive Information',
- };
-
- it('should render correctly', () => {
- const { container } = render();
- expect(container).toMatchSnapshot();
- });
-
- it('should display the text when isHidden is false', () => {
- render();
- expect(screen.getByText('Sensitive Information')).toBeInTheDocument();
- });
-
- it('should hide the text when isHidden is true', () => {
- render();
- expect(screen.queryByText('Sensitive Information')).not.toBeInTheDocument();
- expect(screen.getByText('••••••')).toBeInTheDocument();
- });
-
- it('should render the correct number of bullets for different lengths', () => {
- const lengths = [
- SensitiveTextLength.Short,
- SensitiveTextLength.Medium,
- SensitiveTextLength.Long,
- SensitiveTextLength.ExtraLong,
- ];
-
- lengths.forEach((length) => {
- render();
- expect(screen.getByText('•'.repeat(Number(length)))).toBeInTheDocument();
- });
- });
-
- it('should handle all predefined SensitiveTextLength values', () => {
- Object.entries(SensitiveTextLength).forEach(([_, value]) => {
- render();
- expect(screen.getByText('•'.repeat(Number(value)))).toBeInTheDocument();
- });
- });
-
- it('should handle custom length as a string', () => {
- render();
- expect(screen.getByText('•'.repeat(15))).toBeInTheDocument();
- });
-
- it('should fall back to Short length for invalid custom length', () => {
- render();
- expect(
- screen.getByText('•'.repeat(Number(SensitiveTextLength.Short))),
- ).toBeInTheDocument();
- });
-
- it('should log a warning for invalid custom length', () => {
- const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
- render();
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Invalid length provided: abc. Falling back to Short.',
- );
- consoleSpy.mockRestore();
- });
-
- it('should apply additional props to the Text component', () => {
- render();
- expect(screen.getByTestId('sensitive-text')).toBeInTheDocument();
- });
-
- it('should forward ref to the Text component', () => {
- const ref = React.createRef();
- render();
- expect(ref.current).toBeInstanceOf(HTMLParagraphElement);
- });
-});
diff --git a/ui/components/component-library/sensitive-text/sensitive-text.tsx b/ui/components/component-library/sensitive-text/sensitive-text.tsx
deleted file mode 100644
index ddabda784fe1..000000000000
--- a/ui/components/component-library/sensitive-text/sensitive-text.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { useMemo } from 'react';
-import { Text } from '../text';
-import {
- SensitiveTextProps,
- SensitiveTextLength,
-} from './sensitive-text.types';
-
-export const SensitiveText = React.forwardRef<
- HTMLParagraphElement,
- SensitiveTextProps
->((props, ref) => {
- const {
- isHidden = false,
- length = SensitiveTextLength.Short,
- children = '',
- ...restProps
- } = props;
-
- const getFallbackLength = useMemo(
- () => (len: string) => {
- const numLength = Number(len);
- return Number.isNaN(numLength) ? 0 : numLength;
- },
- [],
- );
-
- const isValidCustomLength = (value: string): boolean => {
- const num = Number(value);
- return !Number.isNaN(num) && num > 0;
- };
-
- let adjustedLength = length;
- if (!(length in SensitiveTextLength) && !isValidCustomLength(length)) {
- console.warn(`Invalid length provided: ${length}. Falling back to Short.`);
- adjustedLength = SensitiveTextLength.Short;
- }
-
- const fallback = useMemo(
- () => '•'.repeat(getFallbackLength(adjustedLength)),
- [length, getFallbackLength],
- );
-
- return (
-
- {isHidden ? fallback : children}
-
- );
-});
diff --git a/ui/components/component-library/sensitive-text/sensitive-text.types.ts b/ui/components/component-library/sensitive-text/sensitive-text.types.ts
deleted file mode 100644
index 1ea8270d377f..000000000000
--- a/ui/components/component-library/sensitive-text/sensitive-text.types.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type { TextProps } from '../text/text.types';
-
-/**
- * SensitiveText length options.
- */
-export const SensitiveTextLength = {
- Short: '6',
- Medium: '9',
- Long: '12',
- ExtraLong: '20',
-} as const;
-
-/**
- * Type for SensitiveTextLength values.
- */
-export type SensitiveTextLengthType =
- (typeof SensitiveTextLength)[keyof typeof SensitiveTextLength];
-/**
- * Type for custom length values.
- */
-export type CustomLength = string;
-
-export type SensitiveTextProps = Omit<
- TextProps,
- 'children'
-> & {
- /**
- * Boolean to determine whether the text should be hidden or visible.
- *
- * @default false
- */
- isHidden?: boolean;
- /**
- * Determines the length of the hidden text (number of asterisks).
- * Can be a predefined SensitiveTextLength or a custom string number.
- *
- * @default SensitiveTextLength.Short
- */
- length?: SensitiveTextLengthType | CustomLength;
- /**
- * The text content to be displayed or hidden.
- */
- children?: React.ReactNode;
-};
diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
index e76dabc7add7..06fe1336ca7e 100644
--- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
+++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx
@@ -4,7 +4,7 @@ import { ICustodianType } from '@metamask-institutional/types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { hideModal } from '../../../store/actions';
-import { getSelectedInternalAccount } from '../../../selectors/accounts';
+import { getSelectedInternalAccount } from '../../../selectors/selectors';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import {
Box,
diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx
index 7c1d0f60488f..10dc049b8678 100644
--- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx
+++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx
@@ -56,7 +56,6 @@ const InteractiveReplacementTokenNotification: React.FC<
interactiveReplacementToken &&
Boolean(Object.keys(interactiveReplacementToken).length);
- // @ts-expect-error keyring type is wrong maybe?
if (!/^Custody/u.test(keyring.type) || !hasInteractiveReplacementToken) {
setShowNotification(false);
return;
@@ -67,7 +66,6 @@ const InteractiveReplacementTokenNotification: React.FC<
)) as unknown as string;
const custodyAccountDetails = await dispatch(
mmiActions.getAllCustodianAccountsWithToken(
- // @ts-expect-error keyring type is wrong maybe?
keyring.type.split(' - ')[1],
token,
),
@@ -107,7 +105,6 @@ const InteractiveReplacementTokenNotification: React.FC<
interactiveReplacementToken?.oldRefreshToken,
isUnlocked,
dispatch,
- // @ts-expect-error keyring type is wrong maybe?
keyring.type,
interactiveReplacementToken,
mmiActions,
diff --git a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap
index e320bd1de0e3..51f6f2e905f9 100644
--- a/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap
+++ b/ui/components/multichain/account-list-item/__snapshots__/account-list-item.test.js.snap
@@ -242,7 +242,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam
>
$100,000.00
@@ -539,7 +538,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam
>
0.006
@@ -583,7 +581,6 @@ exports[`AccountListItem renders AccountListItem component and shows account nam
>
0.006
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 143c4d142a16..517639b1c86e 100644
--- a/ui/components/multichain/account-list-item/account-list-item.js
+++ b/ui/components/multichain/account-list-item/account-list-item.js
@@ -86,8 +86,6 @@ const AccountListItem = ({
isActive = false,
startAccessory,
onActionClick,
- shouldScrollToWhenSelected = true,
- privacyMode = false,
}) => {
const t = useI18nContext();
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false);
@@ -130,10 +128,10 @@ const AccountListItem = ({
// scroll the item into view
const itemRef = useRef(null);
useEffect(() => {
- if (selected && shouldScrollToWhenSelected) {
+ if (selected) {
itemRef.current?.scrollIntoView?.();
}
- }, [itemRef, selected, shouldScrollToWhenSelected]);
+ }, [itemRef, selected]);
const trackEvent = useContext(MetaMetricsContext);
const primaryTokenImage = useMultichainSelector(
@@ -314,7 +312,6 @@ const AccountListItem = ({
type={PRIMARY}
showFiat={showFiat}
data-testid="first-currency-display"
- privacyMode={privacyMode}
/>
@@ -362,7 +359,6 @@ const AccountListItem = ({
type={SECONDARY}
showNative
data-testid="second-currency-display"
- privacyMode={privacyMode}
/>
@@ -506,14 +502,6 @@ AccountListItem.propTypes = {
* Represents start accessory
*/
startAccessory: PropTypes.node,
- /**
- * Determines if list item should be scrolled to when selected
- */
- shouldScrollToWhenSelected: PropTypes.bool,
- /**
- * Determines if list balance should be obfuscated
- */
- privacyMode: PropTypes.bool,
};
AccountListItem.displayName = 'AccountListItem';
diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx
index cfb49d246ca6..19d313aedf54 100644
--- a/ui/components/multichain/account-list-menu/account-list-menu.tsx
+++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx
@@ -188,7 +188,6 @@ export const mergeAccounts = (
type AccountListMenuProps = {
onClose: () => void;
- privacyMode?: boolean;
showAccountCreation?: boolean;
accountListItemProps?: object;
allowedAccountTypes?: KeyringAccountType[];
@@ -196,7 +195,6 @@ type AccountListMenuProps = {
export const AccountListMenu = ({
onClose,
- privacyMode = false,
showAccountCreation = true,
accountListItemProps,
allowedAccountTypes = [
@@ -457,7 +455,6 @@ export const AccountListMenu = ({
{
trackEvent({
category: MetaMetricsEventCategory.Navigation,
@@ -646,7 +643,6 @@ export const AccountListMenu = ({
isHidden={Boolean(account.hidden)}
currentTabOrigin={currentTabOrigin}
isActive={Boolean(account.active)}
- privacyMode={privacyMode}
{...accountListItemProps}
/>
diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
index 247f7aeb5c78..d22597edd89f 100644
--- a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
+++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
@@ -616,7 +616,6 @@ exports[`App Header unlocked state matches snapshot: unlocked 1`] = `
>
1
diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap
index a0c808186082..9c0bd9c49482 100644
--- a/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap
+++ b/ui/components/multichain/asset-picker-amount/asset-balance/__snapshots__/asset-balance-text.test.tsx.snap
@@ -8,7 +8,6 @@ exports[`AssetBalanceText matches snapshot 1`] = `
>
prefix-fiat value
diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
index b4a4836db2d6..d53c8e7d8d8a 100644
--- a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
+++ b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap
@@ -358,7 +358,6 @@ exports[`Connect More Accounts Modal should render correctly 1`] = `
>
0
@@ -402,7 +401,6 @@ exports[`Connect More Accounts Modal should render correctly 1`] = `
>
0
diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx
index 34ec98e671b9..509a4aa60a2a 100644
--- a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx
+++ b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx
@@ -57,7 +57,7 @@ describe('FundingMethodModal', () => {
expect(queryByTestId('funding-method-modal')).toBeNull();
});
- it('should call openBuyCryptoInPdapp when the Token Marketplace item is clicked', () => {
+ it('should call openBuyCryptoInPdapp when the Buy Crypto item is clicked', () => {
const { getByText } = renderWithProvider(
{
store,
);
- fireEvent.click(getByText('Token marketplace'));
+ fireEvent.click(getByText('Buy crypto'));
expect(openBuyCryptoInPdapp).toHaveBeenCalled();
});
diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx
index baa0e234a32a..47d6ed22c2e8 100644
--- a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx
+++ b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx
@@ -115,8 +115,8 @@ export const FundingMethodModal: React.FC = ({
{
@@ -22,7 +21,6 @@ exports[`CurrencyDisplay Component should render text with a className 1`] = `
>
$123.45
@@ -38,7 +36,6 @@ exports[`CurrencyDisplay Component should render text with a prefix 1`] = `
>
-
$123.45
diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js
index 7e2569ffaee3..ca9322661d79 100644
--- a/ui/components/ui/currency-display/currency-display.component.js
+++ b/ui/components/ui/currency-display/currency-display.component.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
import { EtherDenomination } from '../../../../shared/constants/common';
-import { SensitiveText, Box } from '../../component-library';
+import { Text, Box } from '../../component-library';
import {
AlignItems,
Display,
@@ -33,7 +33,6 @@ export default function CurrencyDisplay({
textProps = {},
suffixProps = {},
isAggregatedFiatOverviewBalance = false,
- privacyMode = false,
...props
}) {
const [title, parts] = useCurrencyDisplay(value, {
@@ -69,33 +68,26 @@ export default function CurrencyDisplay({
{prefixComponent}
) : null}
-
{parts.prefix}
{parts.value}
-
+
{parts.suffix ? (
-
{parts.suffix}
-
+
) : null}
);
@@ -123,7 +115,6 @@ const CurrencyDisplayPropTypes = {
textProps: PropTypes.object,
suffixProps: PropTypes.object,
isAggregatedFiatOverviewBalance: PropTypes.bool,
- privacyMode: PropTypes.bool,
};
CurrencyDisplay.propTypes = CurrencyDisplayPropTypes;
diff --git a/ui/components/ui/definition-list/definition-list.js b/ui/components/ui/definition-list/definition-list.js
index 84d3f48135ab..84a23325b37a 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]) => (
) : (
) : (
@@ -297,5 +292,4 @@ SenderToRecipient.propTypes = {
onSenderClick: PropTypes.func,
warnUserOnAccountMismatch: PropTypes.bool,
recipientIsOwnedAccount: PropTypes.bool,
- chainId: PropTypes.string,
};
diff --git a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx
index 932d54210b84..7cf850c42c84 100644
--- a/ui/components/ui/token-currency-display/token-currency-display.stories.tsx
+++ b/ui/components/ui/token-currency-display/token-currency-display.stories.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import type { Meta, StoryObj } from '@storybook/react';
+import { Meta, Story } from '@storybook/react';
import TokenCurrencyDisplay from './token-currency-display.component';
+import { TokenCurrencyDisplayProps } from './token-currency-display.types';
-const meta: Meta
= {
+export default {
title: 'Components/UI/TokenCurrencyDisplay',
component: TokenCurrencyDisplay,
argTypes: {
@@ -11,15 +12,14 @@ const meta: Meta = {
token: { control: 'object' },
prefix: { control: 'text' },
},
- args: {
- className: '',
- transactionData: '0x123',
- token: { symbol: 'ETH' },
- prefix: '',
- },
-};
+} as Meta;
-export default meta;
-type Story = StoryObj;
+const Template: Story = (args) => ;
-export const Default: Story = {};
+export const Default = Template.bind({});
+Default.args = {
+ className: '',
+ transactionData: '0x123',
+ token: { symbol: 'ETH' },
+ prefix: '',
+};
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 2db9784dad8e..ae1782979866 100644
--- a/ui/components/ui/truncated-definition-list/truncated-definition-list.js
+++ b/ui/components/ui/truncated-definition-list/truncated-definition-list.js
@@ -5,6 +5,7 @@ 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({
@@ -12,6 +13,7 @@ export default function TruncatedDefinitionList({
tooltips,
warnings,
prefaceKeys,
+ title,
}) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const t = useI18nContext();
@@ -31,27 +33,55 @@ export default function TruncatedDefinitionList({
type="link"
onClick={() => setIsPopoverOpen(true)}
>
- {t('seeDetails')}
+ {t(process.env.CHAIN_PERMISSIONS ? 'seeDetails' : 'viewAllDetails')}
);
+ const renderPopover = () =>
+ isPopoverOpen && (
+ setIsPopoverOpen(false)}
+ footer={
+
+ }
+ >
+
+ {renderDefinitionList(true)}
+
+
+ );
+
const renderContent = () => {
- return isPopoverOpen ? (
- renderDefinitionList(true)
- ) : (
+ if (process.env.CHAIN_PERMISSIONS) {
+ return isPopoverOpen ? (
+ renderDefinitionList(true)
+ ) : (
+ <>
+ {renderDefinitionList(false)}
+ {renderButton()}
+ >
+ );
+ }
+ return (
<>
{renderDefinitionList(false)}
{renderButton()}
+ {renderPopover()}
>
);
};
return (
(
bridgeAction: BridgeUserAction | BridgeBackgroundAction,
- args?: T,
+ args?: T[],
) => {
return async (dispatch: MetaMaskReduxDispatch) => {
- await submitRequestToBackground(bridgeAction, [args]);
+ await submitRequestToBackground(bridgeAction, args);
await forceUpdateMetamaskState(dispatch);
};
};
@@ -53,29 +53,20 @@ export const setBridgeFeatureFlags = () => {
export const setFromChain = (chainId: Hex) => {
return async (dispatch: MetaMaskReduxDispatch) => {
dispatch(
- callBridgeControllerMethod(
- BridgeUserAction.SELECT_SRC_NETWORK,
+ callBridgeControllerMethod(BridgeUserAction.SELECT_SRC_NETWORK, [
chainId,
- ),
+ ]),
);
};
};
export const setToChain = (chainId: Hex) => {
return async (dispatch: MetaMaskReduxDispatch) => {
+ dispatch(setToChainId_(chainId));
dispatch(
- callBridgeControllerMethod(
- BridgeUserAction.SELECT_DEST_NETWORK,
+ callBridgeControllerMethod(BridgeUserAction.SELECT_DEST_NETWORK, [
chainId,
- ),
- );
- };
-};
-
-export const updateQuoteRequestParams = (params: Partial) => {
- return async (dispatch: MetaMaskReduxDispatch) => {
- await dispatch(
- callBridgeControllerMethod(BridgeUserAction.UPDATE_QUOTE_PARAMS, params),
+ ]),
);
};
};
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index 6b85565c6143..f4a566c233b5 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -1,6 +1,5 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
-import { zeroAddress } from 'ethereumjs-util';
import { createBridgeMockStore } from '../../../test/jest/mock-store';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { setBackgroundConnection } from '../../store/background-connection';
@@ -19,8 +18,7 @@ import {
setToToken,
setFromChain,
resetInputFields,
- setToChainId,
- updateQuoteRequestParams,
+ switchToAndFromTokens,
} from './actions';
const middleware = [thunk];
@@ -33,23 +31,9 @@ describe('Ducks - Bridge', () => {
store.clearActions();
});
- describe('setToChainId', () => {
- it('calls the "bridge/setToChainId" action', () => {
- const state = store.getState().bridge;
- const actionPayload = CHAIN_IDS.OPTIMISM;
-
- store.dispatch(setToChainId(actionPayload as never) as never);
-
- // Check redux state
- const actions = store.getActions();
- expect(actions[0].type).toStrictEqual('bridge/setToChainId');
- const newState = bridgeReducer(state, actions[0]);
- expect(newState.toChainId).toStrictEqual(actionPayload);
- });
- });
-
describe('setToChain', () => {
- it('calls the selectDestNetwork background action', () => {
+ it('calls the "bridge/setToChainId" action and the selectDestNetwork background action', () => {
+ const state = store.getState().bridge;
const actionPayload = CHAIN_IDS.OPTIMISM;
const mockSelectDestNetwork = jest.fn().mockReturnValue({});
@@ -59,6 +43,11 @@ describe('Ducks - Bridge', () => {
store.dispatch(setToChain(actionPayload as never) as never);
+ // Check redux state
+ const actions = store.getActions();
+ expect(actions[0].type).toStrictEqual('bridge/setToChainId');
+ const newState = bridgeReducer(state, actions[0]);
+ expect(newState.toChainId).toStrictEqual(actionPayload);
// Check background state
expect(mockSelectDestNetwork).toHaveBeenCalledTimes(1);
expect(mockSelectDestNetwork).toHaveBeenCalledWith(
@@ -72,7 +61,7 @@ describe('Ducks - Bridge', () => {
it('calls the "bridge/setFromToken" action', () => {
const state = store.getState().bridge;
const actionPayload = { symbol: 'SYMBOL', address: '0x13341432' };
- store.dispatch(setFromToken(actionPayload as never) as never);
+ store.dispatch(setFromToken(actionPayload));
const actions = store.getActions();
expect(actions[0].type).toStrictEqual('bridge/setFromToken');
const newState = bridgeReducer(state, actions[0]);
@@ -84,8 +73,7 @@ describe('Ducks - Bridge', () => {
it('calls the "bridge/setToToken" action', () => {
const state = store.getState().bridge;
const actionPayload = { symbol: 'SYMBOL', address: '0x13341431' };
-
- store.dispatch(setToToken(actionPayload as never) as never);
+ store.dispatch(setToToken(actionPayload));
const actions = store.getActions();
expect(actions[0].type).toStrictEqual('bridge/setToToken');
const newState = bridgeReducer(state, actions[0]);
@@ -97,8 +85,7 @@ describe('Ducks - Bridge', () => {
it('calls the "bridge/setFromTokenInputValue" action', () => {
const state = store.getState().bridge;
const actionPayload = '10';
-
- store.dispatch(setFromTokenInputValue(actionPayload as never) as never);
+ store.dispatch(setFromTokenInputValue(actionPayload));
const actions = store.getActions();
expect(actions[0].type).toStrictEqual('bridge/setFromTokenInputValue');
const newState = bridgeReducer(state, actions[0]);
@@ -150,30 +137,31 @@ describe('Ducks - Bridge', () => {
});
});
- describe('updateQuoteRequestParams', () => {
- it('dispatches quote params to the bridge controller', () => {
- const mockUpdateParams = jest.fn();
- setBackgroundConnection({
- [BridgeUserAction.UPDATE_QUOTE_PARAMS]: mockUpdateParams,
- } as never);
-
- store.dispatch(
- updateQuoteRequestParams({
- srcChainId: 1,
- srcTokenAddress: zeroAddress(),
- destTokenAddress: undefined,
- }) as never,
- );
-
- expect(mockUpdateParams).toHaveBeenCalledTimes(1);
- expect(mockUpdateParams).toHaveBeenCalledWith(
- {
- srcChainId: 1,
- srcTokenAddress: zeroAddress(),
- destTokenAddress: undefined,
- },
- expect.anything(),
+ describe('switchToAndFromTokens', () => {
+ it('switches to and from input values', async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const bridgeStore = configureMockStore(middleware)(
+ createBridgeMockStore(
+ {},
+ {
+ toChainId: CHAIN_IDS.MAINNET,
+ fromToken: { symbol: 'WETH', address: '0x13341432' },
+ toToken: { symbol: 'USDC', address: '0x13341431' },
+ fromTokenInputValue: '10',
+ },
+ ),
);
+ const state = bridgeStore.getState().bridge;
+ bridgeStore.dispatch(switchToAndFromTokens(CHAIN_IDS.POLYGON));
+ const actions = bridgeStore.getActions();
+ expect(actions[0].type).toStrictEqual('bridge/switchToAndFromTokens');
+ const newState = bridgeReducer(state, actions[0]);
+ expect(newState).toStrictEqual({
+ toChainId: CHAIN_IDS.POLYGON,
+ fromToken: { symbol: 'USDC', address: '0x13341431' },
+ toToken: { symbol: 'WETH', address: '0x13341432' },
+ fromTokenInputValue: null,
+ });
});
});
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index c75030c7591d..9ec744d9e953 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -39,6 +39,12 @@ const bridgeSlice = createSlice({
resetInputFields: () => ({
...initialState,
}),
+ switchToAndFromTokens: (state, { payload }) => ({
+ toChainId: payload,
+ fromToken: state.toToken,
+ toToken: state.fromToken,
+ fromTokenInputValue: null,
+ }),
},
});
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index 6be67515e6e4..cf27790aa943 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -30,7 +30,7 @@ describe('Bridge selectors', () => {
{ srcNetworkAllowlist: [CHAIN_IDS.ARBITRUM] },
{ toChainId: '0xe708' },
{},
- { ...mockNetworkState(FEATURED_RPCS[1]) },
+ { ...mockNetworkState(FEATURED_RPCS[0]) },
);
const result = getFromChain(state as never);
@@ -89,7 +89,7 @@ describe('Bridge selectors', () => {
);
const result = getAllBridgeableNetworks(state as never);
- expect(result).toHaveLength(8);
+ expect(result).toHaveLength(7);
expect(result[0]).toStrictEqual(
expect.objectContaining({ chainId: FEATURED_RPCS[0].chainId }),
);
@@ -190,19 +190,21 @@ describe('Bridge selectors', () => {
},
{},
{},
- mockNetworkState(...FEATURED_RPCS),
+ mockNetworkState(...FEATURED_RPCS, {
+ chainId: CHAIN_IDS.LINEA_MAINNET,
+ }),
);
const result = getToChains(state as never);
expect(result).toHaveLength(3);
expect(result[0]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.ARBITRUM }),
+ expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
);
expect(result[1]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
+ expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }),
);
expect(result[2]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }),
+ expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }),
);
});
@@ -295,9 +297,7 @@ describe('Bridge selectors', () => {
{
...mockNetworkState(
...Object.values(BUILT_IN_NETWORKS),
- ...FEATURED_RPCS.filter(
- (network) => network.chainId !== CHAIN_IDS.LINEA_MAINNET, // Linea mainnet is both a built in network, as well as featured RPC
- ),
+ ...FEATURED_RPCS,
),
useExternalServices: true,
},
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 568d62e7a2d4..8cd56928fc66 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -8,7 +8,7 @@ import {
getIsBridgeEnabled,
getSwapsDefaultToken,
SwapsEthToken,
-} from '../../selectors/selectors';
+} from '../../selectors';
import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge';
import {
BridgeControllerState,
@@ -110,7 +110,7 @@ export const getToTokens = (state: BridgeAppState) => {
export const getFromToken = (
state: BridgeAppState,
-): SwapsTokenObject | SwapsEthToken | null => {
+): SwapsTokenObject | SwapsEthToken => {
return state.bridge.fromToken?.address
? state.bridge.fromToken
: getSwapsDefaultToken(state);
diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js
index 9627608eb709..05cc6d46cb27 100644
--- a/ui/ducks/metamask/metamask.js
+++ b/ui/ducks/metamask/metamask.js
@@ -17,9 +17,9 @@ import {
checkNetworkAndAccountSupports1559,
getAddressBook,
getSelectedNetworkClientId,
+ getSelectedInternalAccount,
getNetworkConfigurationsByChainId,
-} from '../../selectors/selectors';
-import { getSelectedInternalAccount } from '../../selectors/accounts';
+} from '../../selectors';
import * as actionConstants from '../../store/actionConstants';
import { updateTransactionGasFees } from '../../store/actions';
import { setCustomGasLimit, setCustomGasPrice } from '../gas/gas.duck';
@@ -50,7 +50,6 @@ const initialState = {
smartTransactionsOptInStatus: false,
petnamesEnabled: true,
featureNotificationsEnabled: false,
- privacyMode: false,
showMultiRpcModal: false,
},
firstTimeFlowType: null,
diff --git a/ui/ducks/ramps/ramps.test.ts b/ui/ducks/ramps/ramps.test.ts
index 8bd6865295d8..3cd543a65219 100644
--- a/ui/ducks/ramps/ramps.test.ts
+++ b/ui/ducks/ramps/ramps.test.ts
@@ -205,7 +205,7 @@ describe('rampsSlice', () => {
});
it('should return true when Bitcoin is buyable and current chain is Bitcoin', () => {
- getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET);
+ getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN);
getMultichainIsBitcoinMock.mockReturnValue(true);
const mockBuyableChains = [
{ chainId: MultichainNetworks.BITCOIN, active: true },
@@ -219,7 +219,7 @@ describe('rampsSlice', () => {
});
it('should return false when Bitcoin is not buyable and current chain is Bitcoin', () => {
- getCurrentChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET);
+ getCurrentChainIdMock.mockReturnValue(MultichainNetworks.BITCOIN);
getMultichainIsBitcoinMock.mockReturnValue(true);
const mockBuyableChains = [
{ chainId: MultichainNetworks.BITCOIN, active: false },
diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js
index d23c0ce69381..8dd7336d7a62 100644
--- a/ui/ducks/swaps/swaps.js
+++ b/ui/ducks/swaps/swaps.js
@@ -858,7 +858,6 @@ export const fetchQuotesAndSetQuoteState = (
stx_enabled: smartTransactionsEnabled,
current_stx_enabled: currentSmartTransactionsEnabled,
stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state),
- gas_included: newSelectedQuote.isGasIncludedTrade,
anonymizedData: true,
},
});
diff --git a/ui/helpers/utils/notification.util.ts b/ui/helpers/utils/notification.util.ts
index afbba2b88172..489f1ca2f272 100644
--- a/ui/helpers/utils/notification.util.ts
+++ b/ui/helpers/utils/notification.util.ts
@@ -420,11 +420,9 @@ export const getNetworkFees = async (notification: OnChainRawNotification) => {
const rpcUrl = getRpcUrlByChainId(`0x${chainId}` as HexChainId);
const connection = {
url: rpcUrl,
- headers: process.env.STORYBOOK
- ? undefined
- : {
- 'Infura-Source': 'metamask/metamask',
- },
+ headers: {
+ 'Infura-Source': 'metamask/metamask',
+ },
};
const provider = new JsonRpcProvider(connection);
diff --git a/ui/helpers/utils/notification.utils.test.ts b/ui/helpers/utils/notification.utils.test.ts
index 2f4e66c26504..f82f8532cb58 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(Date.UTC(2024, 5, 7, 9, 40, 0))); // 2024-06-07T09:40:00Z
+ jest.setSystemTime(new Date('2024-06-07T09:40:00Z'));
});
afterAll(() => {
@@ -28,7 +28,7 @@ describe('formatMenuItemDate', () => {
// assert 1 hour ago
assertToday((testDate) => {
- testDate.setUTCHours(testDate.getUTCHours() - 1);
+ testDate.setHours(testDate.getHours() - 1);
return testDate;
});
});
@@ -42,14 +42,14 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 day ago
assertYesterday((testDate) => {
- testDate.setUTCDate(testDate.getUTCDate() - 1);
+ testDate.setDate(testDate.getDate() - 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.setUTCDate(testDate.getUTCDate() - 1);
- testDate.setUTCHours(testDate.getUTCHours() + 1);
+ testDate.setDate(testDate.getDate() - 1);
+ testDate.setHours(testDate.getHours() + 1);
});
});
@@ -62,18 +62,18 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 month ago
assertMonthsAgo((testDate) => {
- testDate.setUTCMonth(testDate.getUTCMonth() - 1);
+ testDate.setMonth(testDate.getMonth() - 1);
});
// assert 2 months ago
assertMonthsAgo((testDate) => {
- testDate.setUTCMonth(testDate.getUTCMonth() - 2);
+ testDate.setMonth(testDate.getMonth() - 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(Date.UTC(2024, 4, 20, 9, 40, 0)); // 2024-05-20T09:40:00Z
+ return new Date('2024-05-20T09:40:00Z');
});
});
@@ -86,18 +86,18 @@ describe('formatMenuItemDate', () => {
// assert exactly 1 year ago
assertYearsAgo((testDate) => {
- testDate.setUTCFullYear(testDate.getUTCFullYear() - 1);
+ testDate.setFullYear(testDate.getFullYear() - 1);
});
// assert 2 years ago
assertYearsAgo((testDate) => {
- testDate.setUTCFullYear(testDate.getUTCFullYear() - 2);
+ testDate.setFullYear(testDate.getFullYear() - 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(Date.UTC(2023, 10, 20, 9, 40, 0)); // 2023-11-20T09:40:00Z
+ return new Date('2023-11-20T09:40:00Z');
});
});
});
diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts
index fe7a21e2206f..a68aeb361bdd 100644
--- a/ui/hooks/bridge/useBridging.ts
+++ b/ui/hooks/bridge/useBridging.ts
@@ -43,7 +43,6 @@ const useBridging = () => {
const isMarketingEnabled = useSelector(getDataCollectionForMarketing);
const providerConfig = useSelector(getProviderConfig);
const keyring = useSelector(getCurrentKeyring);
- // @ts-expect-error keyring type is wrong maybe?
const usingHardwareWallet = isHardwareKeyring(keyring.type);
const isBridgeSupported = useSelector(getIsBridgeEnabled);
diff --git a/ui/hooks/metamask-notifications/useNotifications.ts b/ui/hooks/metamask-notifications/useNotifications.ts
index 9724253a8671..62367cdbe310 100644
--- a/ui/hooks/metamask-notifications/useNotifications.ts
+++ b/ui/hooks/metamask-notifications/useNotifications.ts
@@ -54,13 +54,8 @@ 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(previewToken ?? undefined),
- );
+ const data = await dispatch(fetchAndUpdateMetamaskNotifications());
setNotificationsData(data as unknown as Notification[]);
return data as unknown as Notification[];
} catch (e) {
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx
new file mode 100644
index 000000000000..951cec333ade
--- /dev/null
+++ b/ui/hooks/metamask-notifications/useProfileSyncing.test.tsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { renderHook, act } from '@testing-library/react-hooks';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { waitFor } from '@testing-library/react';
+import * as actions from '../../store/actions';
+import {
+ useEnableProfileSyncing,
+ useDisableProfileSyncing,
+ useAccountSyncingEffect,
+ useDeleteAccountSyncingDataFromUserStorage,
+} from './useProfileSyncing';
+
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+
+jest.mock('../../store/actions', () => ({
+ performSignIn: jest.fn(),
+ performSignOut: jest.fn(),
+ enableProfileSyncing: jest.fn(),
+ disableProfileSyncing: jest.fn(),
+ showLoadingIndication: jest.fn(),
+ hideLoadingIndication: jest.fn(),
+ syncInternalAccountsWithUserStorage: jest.fn(),
+ deleteAccountSyncingDataFromUserStorage: jest.fn(),
+}));
+
+type ArrangeMocksMetamaskStateOverrides = {
+ isSignedIn?: boolean;
+ isProfileSyncingEnabled?: boolean;
+ isUnlocked?: boolean;
+ useExternalServices?: boolean;
+ completedOnboarding?: boolean;
+};
+
+const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = {
+ isSignedIn: false,
+ isProfileSyncingEnabled: false,
+ isUnlocked: true,
+ useExternalServices: true,
+ completedOnboarding: true,
+};
+
+const arrangeMocks = (
+ metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides,
+) => {
+ const store = mockStore({
+ metamask: {
+ ...initialMetamaskState,
+ ...metamaskStateOverrides,
+ participateInMetaMetrics: false,
+ internalAccounts: {
+ accounts: {
+ '0x123': {
+ address: '0x123',
+ id: 'account1',
+ metadata: {},
+ options: {},
+ methods: [],
+ type: 'eip155:eoa',
+ },
+ },
+ },
+ },
+ });
+
+ store.dispatch = jest.fn().mockImplementation((action) => {
+ if (typeof action === 'function') {
+ return action(store.dispatch, store.getState);
+ }
+ return Promise.resolve();
+ });
+
+ jest.clearAllMocks();
+
+ return { store };
+};
+
+describe('useProfileSyncing', () => {
+ it('should enable profile syncing', async () => {
+ const { store } = arrangeMocks();
+
+ const { result } = renderHook(() => useEnableProfileSyncing(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ act(() => {
+ result.current.enableProfileSyncing();
+ });
+
+ expect(actions.enableProfileSyncing).toHaveBeenCalled();
+ });
+
+ it('should disable profile syncing', async () => {
+ const { store } = arrangeMocks();
+
+ const { result } = renderHook(() => useDisableProfileSyncing(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ act(() => {
+ result.current.disableProfileSyncing();
+ });
+
+ expect(actions.disableProfileSyncing).toHaveBeenCalled();
+ });
+
+ it('should dispatch account syncing when conditions are met', async () => {
+ const { store } = arrangeMocks({
+ isSignedIn: true,
+ isProfileSyncingEnabled: true,
+ });
+
+ renderHook(() => useAccountSyncingEffect(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ await waitFor(() => {
+ expect(actions.syncInternalAccountsWithUserStorage).toHaveBeenCalled();
+ });
+ });
+
+ it('should not dispatch account syncing when conditions are not met', async () => {
+ const { store } = arrangeMocks();
+
+ renderHook(() => useAccountSyncingEffect(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ await waitFor(() => {
+ expect(
+ actions.syncInternalAccountsWithUserStorage,
+ ).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should dispatch account sync data deletion', async () => {
+ const { store } = arrangeMocks();
+
+ const { result } = renderHook(
+ () => useDeleteAccountSyncingDataFromUserStorage(),
+ {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ },
+ );
+
+ act(() => {
+ result.current.dispatchDeleteAccountSyncingDataFromUserStorage();
+ });
+
+ expect(actions.deleteAccountSyncingDataFromUserStorage).toHaveBeenCalled();
+ });
+});
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing.ts
similarity index 53%
rename from ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts
rename to ui/hooks/metamask-notifications/useProfileSyncing.ts
index 5c073fdf6d94..67899aa73927 100644
--- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.ts
+++ b/ui/hooks/metamask-notifications/useProfileSyncing.ts
@@ -1,21 +1,35 @@
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
+import type { InternalAccount } from '@metamask/keyring-api';
import log from 'loglevel';
-import { useMetamaskNotificationsContext } from '../../../contexts/metamask-notifications/metamask-notifications';
import {
disableProfileSyncing as disableProfileSyncingAction,
enableProfileSyncing as enableProfileSyncingAction,
setIsProfileSyncingEnabled as setIsProfileSyncingEnabledAction,
hideLoadingIndication,
-} from '../../../store/actions';
+ syncInternalAccountsWithUserStorage,
+ deleteAccountSyncingDataFromUserStorage,
+} from '../../store/actions';
-import { selectIsSignedIn } from '../../../selectors/metamask-notifications/authentication';
-import { selectIsProfileSyncingEnabled } from '../../../selectors/metamask-notifications/profile-syncing';
-import { getUseExternalServices } from '../../../selectors';
+import { selectIsSignedIn } from '../../selectors/metamask-notifications/authentication';
+import { selectIsProfileSyncingEnabled } from '../../selectors/metamask-notifications/profile-syncing';
+import { getUseExternalServices } from '../../selectors';
import {
getIsUnlocked,
getCompletedOnboarding,
-} from '../../../ducks/metamask/metamask';
+} from '../../ducks/metamask/metamask';
+
+// Define KeyringType interface
+export type KeyringType = {
+ type: string;
+};
+
+// Define AccountType interface
+export type AccountType = InternalAccount & {
+ balance: string;
+ keyring: KeyringType;
+ label: string;
+};
/**
* Custom hook to enable profile syncing. This hook handles the process of signing in
@@ -60,7 +74,6 @@ export function useDisableProfileSyncing(): {
error: string | null;
} {
const dispatch = useDispatch();
- const { listNotifications } = useMetamaskNotificationsContext();
const [error, setError] = useState(null);
@@ -70,9 +83,6 @@ export function useDisableProfileSyncing(): {
try {
// disable profile syncing
await dispatch(disableProfileSyncingAction());
-
- // list notifications to update the counter
- await listNotifications();
} catch (e) {
const errorMessage =
e instanceof Error ? e.message : JSON.stringify(e ?? '');
@@ -114,29 +124,92 @@ export function useSetIsProfileSyncingEnabled(): {
}
/**
- * A utility used internally to decide if syncing features should be dispatched
- * Considers factors like basic functionality; unlocked; finished onboarding, and is logged in
+ * Custom hook to dispatch account syncing.
*
- * @returns a boolean if internally we can perform syncing features or not.
+ * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`,
+ * and error state.
*/
-export const useShouldDispatchProfileSyncing = () => {
+export const useAccountSyncing = () => {
+ const dispatch = useDispatch();
+
+ const [error, setError] = useState(null);
+
const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled);
- const basicFunctionality: boolean | undefined = useSelector(
- getUseExternalServices,
- );
- const isUnlocked: boolean | undefined = useSelector(getIsUnlocked);
+ const basicFunctionality = useSelector(getUseExternalServices);
+ const isUnlocked = useSelector(getIsUnlocked);
const isSignedIn = useSelector(selectIsSignedIn);
- const completedOnboarding: boolean | undefined = useSelector(
- getCompletedOnboarding,
- );
+ const completedOnboarding = useSelector(getCompletedOnboarding);
- const shouldDispatchProfileSyncing: boolean = Boolean(
- basicFunctionality &&
+ const shouldDispatchAccountSyncing = useMemo(
+ () =>
+ basicFunctionality &&
isProfileSyncingEnabled &&
isUnlocked &&
isSignedIn &&
completedOnboarding,
+ [
+ basicFunctionality,
+ isProfileSyncingEnabled,
+ isUnlocked,
+ isSignedIn,
+ completedOnboarding,
+ ],
);
- return shouldDispatchProfileSyncing;
+ const dispatchAccountSyncing = useCallback(() => {
+ setError(null);
+
+ try {
+ if (!shouldDispatchAccountSyncing) {
+ return;
+ }
+ dispatch(syncInternalAccountsWithUserStorage());
+ } catch (e) {
+ log.error(e);
+ setError(e instanceof Error ? e.message : 'An unexpected error occurred');
+ }
+ }, [dispatch, shouldDispatchAccountSyncing]);
+
+ return {
+ dispatchAccountSyncing,
+ shouldDispatchAccountSyncing,
+ error,
+ };
+};
+
+/**
+ * Custom hook to delete a user's account syncing data from user storage
+ */
+
+export const useDeleteAccountSyncingDataFromUserStorage = () => {
+ const dispatch = useDispatch();
+
+ const [error, setError] = useState(null);
+
+ const dispatchDeleteAccountSyncingDataFromUserStorage = useCallback(() => {
+ setError(null);
+
+ try {
+ dispatch(deleteAccountSyncingDataFromUserStorage());
+ } catch (e) {
+ log.error(e);
+ setError(e instanceof Error ? e.message : 'An unexpected error occurred');
+ }
+ }, [dispatch]);
+
+ return {
+ dispatchDeleteAccountSyncingDataFromUserStorage,
+ error,
+ };
+};
+
+/**
+ * Custom hook to apply account syncing effect.
+ */
+export const useAccountSyncingEffect = () => {
+ const { dispatchAccountSyncing } = useAccountSyncing();
+
+ useEffect(() => {
+ dispatchAccountSyncing();
+ }, [dispatchAccountSyncing]);
};
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx
deleted file mode 100644
index 604466b3a75c..000000000000
--- a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.test.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { waitFor } from '@testing-library/react';
-import { act } from '@testing-library/react-hooks';
-import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers';
-import * as actions from '../../../store/actions';
-import {
- useAccountSyncingEffect,
- useDeleteAccountSyncingDataFromUserStorage,
-} from './accountSyncing';
-import * as ProfileSyncModule from './profileSyncing';
-
-describe('useDeleteAccountSyncingDataFromUserStorage()', () => {
- it('should dispatch account sync data deletion', async () => {
- const mockDeleteAccountSyncAction = jest.spyOn(
- actions,
- 'deleteAccountSyncingDataFromUserStorage',
- );
-
- const { result } = renderHookWithProviderTyped(
- () => useDeleteAccountSyncingDataFromUserStorage(),
- {},
- );
-
- await act(async () => {
- await result.current.dispatchDeleteAccountData();
- });
-
- expect(mockDeleteAccountSyncAction).toHaveBeenCalled();
- });
-});
-
-describe('useAccountSyncingEffect', () => {
- const arrangeMocks = () => {
- const mockUseShouldProfileSync = jest.spyOn(
- ProfileSyncModule,
- 'useShouldDispatchProfileSyncing',
- );
- const mockSyncAccountsAction = jest.spyOn(
- actions,
- 'syncInternalAccountsWithUserStorage',
- );
- return {
- mockUseShouldProfileSync,
- mockSyncAccountsAction,
- };
- };
-
- const arrangeAndAct = (props: { profileSyncConditionsMet: boolean }) => {
- const mocks = arrangeMocks();
- mocks.mockUseShouldProfileSync.mockReturnValue(
- props.profileSyncConditionsMet,
- );
-
- renderHookWithProviderTyped(() => useAccountSyncingEffect(), {});
- return mocks;
- };
-
- it('should run effect if profile sync conditions are met', async () => {
- const mocks = arrangeAndAct({ profileSyncConditionsMet: true });
- await waitFor(() => {
- expect(mocks.mockSyncAccountsAction).toHaveBeenCalled();
- });
- });
-
- it('should not run effect if profile sync conditions are not met', async () => {
- const mocks = arrangeAndAct({ profileSyncConditionsMet: false });
- await waitFor(() => {
- expect(mocks.mockSyncAccountsAction).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts b/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts
deleted file mode 100644
index cef4dc80fa75..000000000000
--- a/ui/hooks/metamask-notifications/useProfileSyncing/accountSyncing.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import log from 'loglevel';
-import { useCallback, useEffect } from 'react';
-import { useDispatch } from 'react-redux';
-import {
- deleteAccountSyncingDataFromUserStorage,
- syncInternalAccountsWithUserStorage,
-} from '../../../store/actions';
-import { useShouldDispatchProfileSyncing } from './profileSyncing';
-
-/**
- * Custom hook to dispatch account syncing.
- *
- * @returns An object containing the `dispatchAccountSyncing` function, boolean `shouldDispatchAccountSyncing`,
- * and error state.
- */
-const useAccountSyncing = () => {
- const dispatch = useDispatch();
-
- const shouldDispatchAccountSyncing = useShouldDispatchProfileSyncing();
-
- const dispatchAccountSyncing = useCallback(() => {
- try {
- if (!shouldDispatchAccountSyncing) {
- return;
- }
- dispatch(syncInternalAccountsWithUserStorage());
- } catch (e) {
- log.error(e);
- }
- }, [dispatch, shouldDispatchAccountSyncing]);
-
- return {
- dispatchAccountSyncing,
- shouldDispatchAccountSyncing,
- };
-};
-
-/**
- * Custom hook to apply account syncing effect.
- */
-export const useAccountSyncingEffect = () => {
- const shouldSync = useShouldDispatchProfileSyncing();
- const { dispatchAccountSyncing } = useAccountSyncing();
-
- useEffect(() => {
- if (shouldSync) {
- dispatchAccountSyncing();
- }
- }, [shouldSync, dispatchAccountSyncing]);
-};
-
-/**
- * Custom hook to delete a user's account syncing data from user storage
- */
-export const useDeleteAccountSyncingDataFromUserStorage = () => {
- const dispatch = useDispatch();
- const dispatchDeleteAccountData = useCallback(async () => {
- try {
- await dispatch(deleteAccountSyncingDataFromUserStorage());
- } catch {
- // Do Nothing
- }
- }, []);
-
- return { dispatchDeleteAccountData };
-};
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/index.ts b/ui/hooks/metamask-notifications/useProfileSyncing/index.ts
deleted file mode 100644
index 9a6cda8468fb..000000000000
--- a/ui/hooks/metamask-notifications/useProfileSyncing/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export {
- useDisableProfileSyncing,
- useEnableProfileSyncing,
- useSetIsProfileSyncingEnabled,
-} from './profileSyncing';
-export {
- useAccountSyncingEffect,
- useDeleteAccountSyncingDataFromUserStorage,
-} from './accountSyncing';
diff --git a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx b/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx
deleted file mode 100644
index 99d3064085ea..000000000000
--- a/ui/hooks/metamask-notifications/useProfileSyncing/profileSyncing.test.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { act } from '@testing-library/react-hooks';
-import { renderHookWithProviderTyped } from '../../../../test/lib/render-helpers';
-import { MetamaskNotificationsProvider } from '../../../contexts/metamask-notifications';
-import * as actions from '../../../store/actions';
-import {
- useDisableProfileSyncing,
- useEnableProfileSyncing,
- useShouldDispatchProfileSyncing,
-} from './profileSyncing';
-
-type ArrangeMocksMetamaskStateOverrides = {
- isSignedIn?: boolean;
- isProfileSyncingEnabled?: boolean;
- isUnlocked?: boolean;
- useExternalServices?: boolean;
- completedOnboarding?: boolean;
-};
-
-const initialMetamaskState: ArrangeMocksMetamaskStateOverrides = {
- isSignedIn: false,
- isProfileSyncingEnabled: false,
- isUnlocked: true,
- useExternalServices: true,
- completedOnboarding: true,
-};
-
-const arrangeMockState = (
- metamaskStateOverrides?: ArrangeMocksMetamaskStateOverrides,
-) => {
- const state = {
- metamask: {
- ...initialMetamaskState,
- ...metamaskStateOverrides,
- },
- };
-
- return { state };
-};
-
-describe('useEnableProfileSyncing()', () => {
- it('should enable profile syncing', async () => {
- const mockEnableProfileSyncingAction = jest.spyOn(
- actions,
- 'enableProfileSyncing',
- );
-
- const { state } = arrangeMockState();
- const { result } = renderHookWithProviderTyped(
- () => useEnableProfileSyncing(),
- state,
- );
- await act(async () => {
- await result.current.enableProfileSyncing();
- });
-
- expect(mockEnableProfileSyncingAction).toHaveBeenCalled();
- });
-});
-
-describe('useDisableProfileSyncing()', () => {
- it('should disable profile syncing', async () => {
- const mockDisableProfileSyncingAction = jest.spyOn(
- actions,
- 'disableProfileSyncing',
- );
-
- const { state } = arrangeMockState();
-
- const { result } = renderHookWithProviderTyped(
- () => useDisableProfileSyncing(),
- state,
- undefined,
- MetamaskNotificationsProvider,
- );
-
- await act(async () => {
- await result.current.disableProfileSyncing();
- });
-
- expect(mockDisableProfileSyncingAction).toHaveBeenCalled();
- });
-});
-
-describe('useShouldDispatchProfileSyncing()', () => {
- const testCases = (() => {
- const properties = [
- 'isSignedIn',
- 'isProfileSyncingEnabled',
- 'isUnlocked',
- 'useExternalServices',
- 'completedOnboarding',
- ] as const;
- const baseState = {
- isSignedIn: true,
- isProfileSyncingEnabled: true,
- isUnlocked: true,
- useExternalServices: true,
- completedOnboarding: true,
- };
-
- const failureStateCases: {
- state: ArrangeMocksMetamaskStateOverrides;
- failingField: string;
- }[] = [];
-
- // Generate test cases by toggling each property
- properties.forEach((property) => {
- const state = { ...baseState, [property]: false };
- failureStateCases.push({ state, failingField: property });
- });
-
- const successTestCase = { state: baseState };
-
- return { successTestCase, failureStateCases };
- })();
-
- it('should return true if all conditions are met', () => {
- const { state } = arrangeMockState(testCases.successTestCase.state);
- const hook = renderHookWithProviderTyped(
- () => useShouldDispatchProfileSyncing(),
- state,
- );
- expect(hook.result.current).toBe(true);
- });
-
- testCases.failureStateCases.forEach(({ state, failingField }) => {
- it(`should return false if not all conditions are met [${failingField} = false]`, () => {
- const { state: newState } = arrangeMockState(state);
- const hook = renderHookWithProviderTyped(
- () => useShouldDispatchProfileSyncing(),
- newState,
- );
- expect(hook.result.current).toBe(false);
- });
- });
-});
diff --git a/ui/hooks/snaps/useDisplayName.ts b/ui/hooks/snaps/useDisplayName.ts
deleted file mode 100644
index 6a6d3d7e6b51..000000000000
--- a/ui/hooks/snaps/useDisplayName.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { NamespaceId } from '@metamask/snaps-utils';
-import { CaipChainId, KnownCaipNamespace } from '@metamask/utils';
-import { useSelector } from 'react-redux';
-import {
- getMemoizedAccountName,
- getAddressBookEntryByNetwork,
- AddressBookMetaMaskState,
- AccountsMetaMaskState,
-} from '../../selectors/snaps';
-import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
-import { decimalToHex } from '../../../shared/modules/conversion.utils';
-
-export type UseDisplayNameParams = {
- chain: {
- namespace: NamespaceId;
- reference: string;
- };
- chainId: CaipChainId;
- address: string;
-};
-
-/**
- * Get the display name for an address.
- * This will look for an account name in the state, and if not found, it will look for an address book entry.
- *
- * @param params - The parsed CAIP-10 ID.
- * @returns The display name for the address.
- */
-export const useDisplayName = (
- params: UseDisplayNameParams,
-): string | undefined => {
- const {
- address,
- chain: { namespace, reference },
- } = params;
-
- const isEip155 = namespace === KnownCaipNamespace.Eip155;
-
- const parsedAddress = isEip155 ? toChecksumHexAddress(address) : address;
-
- const accountName = useSelector((state: AccountsMetaMaskState) =>
- getMemoizedAccountName(state, parsedAddress),
- );
-
- const addressBookEntry = useSelector((state: AddressBookMetaMaskState) =>
- getAddressBookEntryByNetwork(
- state,
- parsedAddress,
- `0x${decimalToHex(isEip155 ? reference : `0`)}`,
- ),
- );
-
- return accountName || (isEip155 && addressBookEntry?.name) || undefined;
-};
diff --git a/ui/hooks/useCurrencyRatePolling.ts b/ui/hooks/useCurrencyRatePolling.ts
index e7ad21adedf5..fb14b1c94797 100644
--- a/ui/hooks/useCurrencyRatePolling.ts
+++ b/ui/hooks/useCurrencyRatePolling.ts
@@ -1,30 +1,24 @@
import { useSelector } from 'react-redux';
import {
- getNetworkConfigurationsByChainId,
+ getSelectedNetworkClientId,
getUseCurrencyRateCheck,
} from '../selectors';
import {
- currencyRateStartPolling,
+ currencyRateStartPollingByNetworkClientId,
currencyRateStopPollingByPollingToken,
} from '../store/actions';
import { getCompletedOnboarding } from '../ducks/metamask/metamask';
import usePolling from './usePolling';
-const useCurrencyRatePolling = () => {
+const useCurrencyRatePolling = (networkClientId?: string) => {
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
const completedOnboarding = useSelector(getCompletedOnboarding);
- const networkConfigurations = useSelector(getNetworkConfigurationsByChainId);
-
- const nativeCurrencies = [
- ...new Set(
- Object.values(networkConfigurations).map((n) => n.nativeCurrency),
- ),
- ];
+ const selectedNetworkClientId = useSelector(getSelectedNetworkClientId);
usePolling({
- startPolling: currencyRateStartPolling,
+ startPollingByNetworkClientId: currencyRateStartPollingByNetworkClientId,
stopPollingByPollingToken: currencyRateStopPollingByPollingToken,
- input: nativeCurrencies,
+ networkClientId: networkClientId ?? selectedNetworkClientId,
enabled: useCurrencyRateCheck && completedOnboarding,
});
};
diff --git a/ui/hooks/useDisplayName.test.ts b/ui/hooks/useDisplayName.test.ts
index 5c36b0a97ed2..1d6fb22b5e69 100644
--- a/ui/hooks/useDisplayName.test.ts
+++ b/ui/hooks/useDisplayName.test.ts
@@ -1,533 +1,218 @@
-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 { 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 { useDisplayName } from './useDisplayName';
-import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
import { useNames } from './useName';
+import { useFirstPartyContractNames } from './useFirstPartyContractName';
+import { useNftCollectionsMetadata } from './useNftCollectionsMetadata';
-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;
+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,
+};
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);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let state: any;
+ beforeEach(() => {
+ jest.resetAllMocks();
- function mockPetname(name: string) {
- useNamesMock.mockReturnValue([
+ useNamesMock.mockReturnValue([NO_PETNAME_FOUND_RETURN_VALUE]);
+ useFirstPartyContractNamesMock.mockReturnValue([
+ NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE,
+ ]);
+ getRemoteTokensMock.mockReturnValue([
{
- name,
- sourceId: null,
- proposedNames: {},
- origin: null,
+ name: NO_CONTRACT_NAME_FOUND_RETURN_VALUE,
},
]);
- }
-
- function mockERC20Token(
- value: string,
- variation: string,
- name: string,
- symbol: string,
- image: string,
- ) {
- state.metamask.tokensChainsCache = {
- [variation]: {
- data: {
- [value]: {
- name,
- symbol,
- iconUrl: image,
- },
- },
- },
- };
- }
+ getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
+ NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE,
+ );
+ useNftCollectionsMetadataMock.mockReturnValue({});
+ });
- 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 },
- },
+ it('handles no name found', () => {
+ const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+ expect(result.current).toEqual({
+ name: null,
+ hasPetname: false,
});
- }
-
- function mockFirstPartyContractName(
- value: string,
- variation: string,
- name: string,
- ) {
- FIRST_PARTY_CONTRACT_NAMES[name as EXPERIENCES_TYPE] = {
- [variation as Hex]: value as Hex,
- };
- }
-
- beforeEach(() => {
- jest.resetAllMocks();
-
- useNftCollectionsMetadataMock.mockReturnValue({});
+ });
- useNamesMock.mockReturnValue([
+ it('prioritizes a petname over all else', () => {
+ useNamesMock.mockReturnValue([PETNAME_FOUND_RETURN_VALUE]);
+ useFirstPartyContractNamesMock.mockReturnValue([
+ FIRST_PARTY_CONTRACT_NAME_MOCK,
+ ]);
+ getRemoteTokensMock.mockReturnValue([
{
- name: null,
- sourceId: null,
- proposedNames: {},
- origin: null,
+ name: CONTRACT_NAME_MOCK,
},
]);
-
- state = cloneDeep(mockState);
-
- delete FIRST_PARTY_CONTRACT_NAMES[
- FIRST_PARTY_CONTRACT_NAME_MOCK as EXPERIENCES_TYPE
- ];
- });
-
- it('returns no name if no defaults found', () => {
- const { result } = renderHookWithProvider(
- () =>
- useDisplayName({
- value: VALUE_MOCK,
- type: NameType.ETHEREUM_ADDRESS,
- variation: VARIATION_MOCK,
- }),
- mockState,
+ getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
+ WATCHED_NFT_FOUND_RETURN_VALUE,
);
- expect(result.current).toStrictEqual({
- contractDisplayName: undefined,
- hasPetname: false,
- image: undefined,
- name: null,
- });
- });
+ const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
- 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,
- });
+ expect(result.current).toEqual({
+ name: NAME_MOCK,
+ hasPetname: true,
+ contractDisplayName: CONTRACT_NAME_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,
- });
+ 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 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,
- });
- });
+ const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
- 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,
- });
+ expect(result.current).toEqual({
+ name: FIRST_PARTY_CONTRACT_NAME_MOCK,
+ hasPetname: false,
});
});
- 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,
- });
- });
+ it('prioritizes a contract name over a watched NFT name', () => {
+ getRemoteTokensMock.mockReturnValue([
+ {
+ name: CONTRACT_NAME_MOCK,
+ },
+ ]);
+ getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
+ WATCHED_NFT_FOUND_RETURN_VALUE,
+ );
- 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,
- });
+ const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+
+ expect(result.current).toEqual({
+ name: CONTRACT_NAME_MOCK,
+ hasPetname: false,
+ contractDisplayName: CONTRACT_NAME_MOCK,
});
});
- 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,
- });
- });
+ 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 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,
- });
+ const { result } = renderHook(() => useDisplayName(VALUE_MOCK, TYPE_MOCK));
+
+ expect(result.current).toEqual({
+ name: WATCHED_NFT_NAME_MOCK,
+ hasPetname: false,
});
});
- 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,
- });
- });
+ it('returns nft collection name from metadata if no other name is found', () => {
+ const IMAGE_MOCK = 'url';
- 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,
- });
+ useNftCollectionsMetadataMock.mockReturnValue({
+ [VALUE_MOCK.toLowerCase()]: {
+ name: CONTRACT_NAME_MOCK,
+ image: IMAGE_MOCK,
+ isSpam: false,
+ },
});
- 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,
- });
+ const { result } = renderHook(() =>
+ useDisplayName(VALUE_MOCK, TYPE_MOCK, false),
+ );
+
+ expect(result.current).toEqual({
+ name: CONTRACT_NAME_MOCK,
+ hasPetname: false,
+ contractDisplayName: undefined,
+ image: IMAGE_MOCK,
});
});
- 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,
- });
- });
+ it('does not return nft collection name if collection is marked as spam', () => {
+ const IMAGE_MOCK = 'url';
- 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,
- });
+ useNftCollectionsMetadataMock.mockReturnValue({
+ [VALUE_MOCK.toLowerCase()]: {
+ name: CONTRACT_NAME_MOCK,
+ image: IMAGE_MOCK,
+ isSpam: true,
+ },
});
- 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,
- });
- });
+ const { result } = renderHook(() =>
+ useDisplayName(VALUE_MOCK, TYPE_MOCK, false),
+ );
- 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,
- });
- });
+ expect(result.current).toEqual(
+ expect.objectContaining({
+ name: null,
+ image: undefined,
+ }),
+ );
});
});
diff --git a/ui/hooks/useDisplayName.ts b/ui/hooks/useDisplayName.ts
index 7b7429c7a0d4..64a878d2e357 100644
--- a/ui/hooks/useDisplayName.ts
+++ b/ui/hooks/useDisplayName.ts
@@ -1,20 +1,16 @@
+import { useMemo } from 'react';
import { NameType } from '@metamask/name-controller';
import { useSelector } from 'react-redux';
-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 { getRemoteTokens } from '../selectors';
+import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft';
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 = {
@@ -27,145 +23,79 @@ export type UseDisplayNameResponse = {
export function useDisplayNames(
requests: UseDisplayNameRequest[],
): UseDisplayNameResponse[] {
- const nameEntries = useNames(requests);
- const firstPartyContractNames = useFirstPartyContractNames(requests);
- const erc20Tokens = useERC20Tokens(requests);
- const watchedNFTNames = useWatchedNFTNames(requests);
- const nfts = useNFTs(requests);
+ 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),
+ );
- return requests.map((_request, index) => {
+ const watchedNftNames = useSelector(getNftContractsByAddressOnCurrentChain);
+
+ return requests.map(({ value, preferContractSymbol }, index) => {
const nameEntry = nameEntries[index];
const firstPartyContractName = firstPartyContractNames[index];
- const erc20Token = erc20Tokens[index];
- const watchedNftName = watchedNFTNames[index];
- const nft = nfts[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 name =
nameEntry?.name ||
firstPartyContractName ||
- nft?.name ||
- erc20Token?.name ||
+ nftCollectionName ||
+ contractDisplayName ||
watchedNftName ||
null;
- const image = nft?.image || erc20Token?.image;
-
const hasPetname = Boolean(nameEntry?.name);
return {
name,
hasPetname,
- contractDisplayName: erc20Token?.name,
- image,
+ contractDisplayName,
+ image: nftCollectionImage,
};
});
}
+/**
+ * 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(
- request: UseDisplayNameRequest,
+ value: string,
+ type: NameType,
+ preferContractSymbol: boolean = false,
): UseDisplayNameResponse {
- 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
- );
- });
- });
+ return useDisplayNames([{ preferContractSymbol, type, value }])[0];
}
diff --git a/ui/hooks/useFirstPartyContractName.test.ts b/ui/hooks/useFirstPartyContractName.test.ts
new file mode 100644
index 000000000000..14d0cd429e6f
--- /dev/null
+++ b/ui/hooks/useFirstPartyContractName.test.ts
@@ -0,0 +1,78 @@
+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
new file mode 100644
index 000000000000..47468b472955
--- /dev/null
+++ b/ui/hooks/useFirstPartyContractName.ts
@@ -0,0 +1,45 @@
+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/useGasFeeEstimates.js b/ui/hooks/useGasFeeEstimates.js
index abbaf0db0bb9..5ad37925054b 100644
--- a/ui/hooks/useGasFeeEstimates.js
+++ b/ui/hooks/useGasFeeEstimates.js
@@ -74,10 +74,9 @@ export function useGasFeeEstimates(_networkClientId) {
}, [networkClientId]);
usePolling({
- startPolling: (input) =>
- gasFeeStartPollingByNetworkClientId(input.networkClientId),
+ startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId,
stopPollingByPollingToken: gasFeeStopPollingByPollingToken,
- input: { networkClientId },
+ networkClientId,
});
return {
diff --git a/ui/hooks/useGasFeeEstimates.test.js b/ui/hooks/useGasFeeEstimates.test.js
index dd63e10581d0..0187ac793bbe 100644
--- a/ui/hooks/useGasFeeEstimates.test.js
+++ b/ui/hooks/useGasFeeEstimates.test.js
@@ -8,6 +8,7 @@ import {
getIsNetworkBusyByChainId,
} from '../ducks/metamask/metamask';
import {
+ gasFeeStartPollingByNetworkClientId,
gasFeeStopPollingByPollingToken,
getNetworkConfigurationByNetworkClientId,
} from '../store/actions';
@@ -114,9 +115,9 @@ describe('useGasFeeEstimates', () => {
renderHook(() => useGasFeeEstimates());
});
expect(usePolling).toHaveBeenCalledWith({
- startPolling: expect.any(Function),
+ startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId,
stopPollingByPollingToken: gasFeeStopPollingByPollingToken,
- input: { networkClientId: 'selectedNetworkClientId' },
+ networkClientId: 'selectedNetworkClientId',
});
});
@@ -126,9 +127,9 @@ describe('useGasFeeEstimates', () => {
renderHook(() => useGasFeeEstimates('networkClientId1'));
});
expect(usePolling).toHaveBeenCalledWith({
- startPolling: expect.any(Function),
+ startPollingByNetworkClientId: gasFeeStartPollingByNetworkClientId,
stopPollingByPollingToken: gasFeeStopPollingByPollingToken,
- input: { networkClientId: 'networkClientId1' },
+ networkClientId: 'networkClientId1',
});
});
diff --git a/ui/hooks/useMultichainSelector.ts b/ui/hooks/useMultichainSelector.ts
index 9bd979df7e7e..326ac79bf9cd 100644
--- a/ui/hooks/useMultichainSelector.ts
+++ b/ui/hooks/useMultichainSelector.ts
@@ -11,7 +11,6 @@ export function useMultichainSelector<
) {
return useSelector((state: TState) => {
// We either pass an account or fallback to the currently selected one
- // @ts-expect-error state types don't match
return selector(state, account || getSelectedInternalAccount(state));
});
}
diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts
index f746c4bb6267..76bd5dc593ad 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 { getNames } from '../selectors';
+import { getCurrentChainId, getNames } from '../selectors';
import { useName } from './useName';
jest.mock('react-redux', () => ({
@@ -19,14 +19,13 @@ jest.mock('../selectors', () => ({
getNames: jest.fn(),
}));
-const VARIATION_MOCK = '0x1';
-const VARIATION_2_MOCK = '0x2';
+const CHAIN_ID_MOCK = '0x1';
+const CHAIN_ID_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'],
@@ -36,6 +35,7 @@ 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,12 +43,14 @@ 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, VARIATION_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
expect(nameEntry).toStrictEqual({
name: null,
@@ -62,7 +64,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [VARIATION_2_MOCK]: {
+ [CHAIN_ID_2_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -72,7 +74,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
expect(nameEntry).toStrictEqual({
name: null,
@@ -86,7 +88,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [VARIATION_MOCK]: {
+ [CHAIN_ID_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -96,7 +98,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -110,7 +112,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [VARIATION_2_MOCK]: {
+ [CHAIN_ID_2_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -120,7 +122,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -145,7 +147,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -159,7 +161,7 @@ describe('useName', () => {
getNamesMock.mockReturnValue({
[TYPE_MOCK]: {
[VALUE_MOCK]: {
- [VARIATION_2_MOCK]: {
+ [CHAIN_ID_2_MOCK]: {
name: null,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: null,
@@ -175,7 +177,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, VARIATION_2_MOCK);
+ const nameEntry = useName(VALUE_MOCK, TYPE_MOCK, CHAIN_ID_2_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
@@ -186,11 +188,37 @@ 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]: {
- [VARIATION_MOCK]: {
+ [CHAIN_ID_MOCK]: {
name: NAME_MOCK,
proposedNames: PROPOSED_NAMES_MOCK,
sourceId: SOURCE_ID_MOCK,
@@ -200,7 +228,7 @@ describe('useName', () => {
},
});
- const nameEntry = useName('0xAbC123', TYPE_MOCK, VARIATION_MOCK);
+ const nameEntry = useName('0xAbC123', TYPE_MOCK);
expect(nameEntry).toStrictEqual({
name: NAME_MOCK,
diff --git a/ui/hooks/useName.ts b/ui/hooks/useName.ts
index dd587e81abf6..3af9b0457f79 100644
--- a/ui/hooks/useName.ts
+++ b/ui/hooks/useName.ts
@@ -5,29 +5,32 @@ import {
} from '@metamask/name-controller';
import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
-import { getNames } from '../selectors';
+import { getCurrentChainId, 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[variation];
+ const variationEntry = variationsToNameEntries[variationKey];
const fallbackEntry = variationsToNameEntries[FALLBACK_VARIATION];
const entry =
@@ -60,3 +63,13 @@ 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 e1e2b6745ad1..4897e449e6ad 100644
--- a/ui/hooks/useNftCollectionsMetadata.test.ts
+++ b/ui/hooks/useNftCollectionsMetadata.test.ts
@@ -1,5 +1,6 @@
import { renderHook } from '@testing-library/react-hooks';
import { TokenStandard } from '../../shared/constants/transaction';
+import { getCurrentChainId } from '../selectors';
import {
getNFTContractInfo,
getTokenStandardAndDetails,
@@ -41,6 +42,7 @@ const ERC_721_COLLECTION_2_MOCK = {
};
describe('useNftCollectionsMetadata', () => {
+ const mockGetCurrentChainId = jest.mocked(getCurrentChainId);
const mockGetNFTContractInfo = jest.mocked(getNFTContractInfo);
const mockGetTokenStandardAndDetails = jest.mocked(
getTokenStandardAndDetails,
@@ -48,6 +50,7 @@ describe('useNftCollectionsMetadata', () => {
beforeEach(() => {
jest.resetAllMocks();
+ mockGetCurrentChainId.mockReturnValue(CHAIN_ID_MOCK);
mockGetNFTContractInfo.mockResolvedValue({
collections: [ERC_721_COLLECTION_1_MOCK, ERC_721_COLLECTION_2_MOCK],
});
@@ -64,12 +67,10 @@ describe('useNftCollectionsMetadata', () => {
const { result, waitForNextUpdate } = renderHook(() =>
useNftCollectionsMetadata([
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: ERC_721_ADDRESS_1,
+ value: ERC_721_ADDRESS_1,
},
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: ERC_721_ADDRESS_2,
+ value: ERC_721_ADDRESS_2,
},
]),
);
@@ -78,10 +79,8 @@ describe('useNftCollectionsMetadata', () => {
expect(mockGetNFTContractInfo).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual({
- [CHAIN_ID_MOCK]: {
- [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK,
- [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK,
- },
+ [ERC_721_ADDRESS_1.toLowerCase()]: ERC_721_COLLECTION_1_MOCK,
+ [ERC_721_ADDRESS_2.toLowerCase()]: ERC_721_COLLECTION_2_MOCK,
});
});
@@ -100,8 +99,7 @@ describe('useNftCollectionsMetadata', () => {
renderHook(() =>
useNftCollectionsMetadata([
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: '0xERC20Address',
+ value: '0xERC20Address',
},
]),
);
@@ -116,8 +114,7 @@ describe('useNftCollectionsMetadata', () => {
renderHook(() =>
useNftCollectionsMetadata([
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: '0xERC20Address',
+ value: '0xERC20Address',
},
]),
);
@@ -129,12 +126,10 @@ describe('useNftCollectionsMetadata', () => {
const { waitForNextUpdate, rerender } = renderHook(() =>
useNftCollectionsMetadata([
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: ERC_721_ADDRESS_1,
+ value: ERC_721_ADDRESS_1,
},
{
- chainId: CHAIN_ID_MOCK,
- contractAddress: ERC_721_ADDRESS_2,
+ value: ERC_721_ADDRESS_2,
},
]),
);
diff --git a/ui/hooks/useNftCollectionsMetadata.ts b/ui/hooks/useNftCollectionsMetadata.ts
index e71216e254c9..641e0fb25dcd 100644
--- a/ui/hooks/useNftCollectionsMetadata.ts
+++ b/ui/hooks/useNftCollectionsMetadata.ts
@@ -1,5 +1,9 @@
+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,
@@ -7,62 +11,28 @@ import {
import { useAsyncResult } from './useAsyncResult';
export type UseNftCollectionsMetadataRequest = {
- chainId: string;
- contractAddress: string;
+ value: string;
+ chainId?: string;
+};
+
+type CollectionsData = {
+ [key: string]: Collection;
};
// For now, we only support ERC721 tokens
const SUPPORTED_NFT_TOKEN_STANDARDS = [TokenStandard.ERC721];
-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[],
+async function fetchCollections(
+ memoisedContracts: string[],
chainId: string,
-) {
+): Promise {
const contractStandardsResponses = await Promise.all(
- contractAddresses.map((contractAddress) =>
+ memoisedContracts.map((contractAddress) =>
getTokenStandardAndDetails(contractAddress, chainId),
),
);
- const supportedNFTContracts = contractAddresses.filter(
+ const supportedNFTContracts = memoisedContracts.filter(
(_contractAddress, index) =>
SUPPORTED_NFT_TOKEN_STANDARDS.includes(
contractStandardsResponses[index].standard as TokenStandard,
@@ -78,16 +48,37 @@ async function fetchCollectionsForChain(
chainId,
);
- const collectionsData = collectionsResult.collections.reduce<
- Record
- >((acc, collection, index) => {
- acc[supportedNFTContracts[index]] = {
- name: collection?.name,
- image: collection?.image,
- isSpam: collection?.isSpam,
- };
- return acc;
- }, {});
+ const collectionsData: CollectionsData = collectionsResult.collections.reduce(
+ (acc: CollectionsData, 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/hooks/usePolling.test.js b/ui/hooks/usePolling.test.js
index a556bb86be54..9250257d3cbc 100644
--- a/ui/hooks/usePolling.test.js
+++ b/ui/hooks/usePolling.test.js
@@ -4,12 +4,13 @@ import usePolling from './usePolling';
describe('usePolling', () => {
// eslint-disable-next-line jest/no-done-callback
- it('calls startPolling and calls back with polling token when component instantiating the hook mounts', (done) => {
+ it('calls startPollingByNetworkClientId and callback option args with polling token when component instantiating the hook mounts', (done) => {
const mockStart = jest.fn().mockImplementation(() => {
return Promise.resolve('pollingToken');
});
const mockStop = jest.fn();
const networkClientId = 'mainnet';
+ const options = {};
const mockState = {
metamask: {},
};
@@ -17,16 +18,17 @@ describe('usePolling', () => {
renderHookWithProvider(() => {
usePolling({
callback: (pollingToken) => {
- expect(mockStart).toHaveBeenCalledWith({ networkClientId });
+ expect(mockStart).toHaveBeenCalledWith(networkClientId, options);
expect(pollingToken).toBeDefined();
done();
return (_pollingToken) => {
// noop
};
},
- startPolling: mockStart,
+ startPollingByNetworkClientId: mockStart,
stopPollingByPollingToken: mockStop,
- input: { networkClientId },
+ networkClientId,
+ options,
});
}, mockState);
});
@@ -37,6 +39,7 @@ describe('usePolling', () => {
});
const mockStop = jest.fn();
const networkClientId = 'mainnet';
+ const options = {};
const mockState = {
metamask: {},
};
@@ -51,9 +54,10 @@ describe('usePolling', () => {
done();
};
},
- startPolling: mockStart,
+ startPollingByNetworkClientId: mockStart,
stopPollingByPollingToken: mockStop,
- input: { networkClientId },
+ networkClientId,
+ options,
}),
mockState,
);
diff --git a/ui/hooks/usePolling.ts b/ui/hooks/usePolling.ts
index 613e70cf17b5..1a9d6b1f576e 100644
--- a/ui/hooks/usePolling.ts
+++ b/ui/hooks/usePolling.ts
@@ -1,16 +1,22 @@
import { useEffect, useRef } from 'react';
-type UsePollingOptions = {
+type UsePollingOptions = {
callback?: (pollingToken: string) => (pollingToken: string) => void;
- startPolling: (input: PollingInput) => Promise;
+ startPollingByNetworkClientId: (
+ networkClientId: string,
+ // TODO: Replace `any` with type
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ options: any,
+ ) => Promise;
stopPollingByPollingToken: (pollingToken: string) => void;
- input: PollingInput;
+ networkClientId: string;
+ // TODO: Replace `any` with type
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ options?: any;
enabled?: boolean;
};
-const usePolling = (
- usePollingOptions: UsePollingOptions,
-) => {
+const usePolling = (usePollingOptions: UsePollingOptions) => {
const pollTokenRef = useRef(null);
const cleanupRef = useRef void)>(null);
let isMounted = false;
@@ -32,7 +38,10 @@ const usePolling = (
// Start polling when the component mounts
usePollingOptions
- .startPolling(usePollingOptions.input)
+ .startPollingByNetworkClientId(
+ usePollingOptions.networkClientId,
+ usePollingOptions.options,
+ )
.then((pollToken) => {
pollTokenRef.current = pollToken;
cleanupRef.current = usePollingOptions.callback?.(pollToken) || null;
@@ -47,7 +56,12 @@ const usePolling = (
cleanup();
};
}, [
- usePollingOptions.input && JSON.stringify(usePollingOptions.input),
+ usePollingOptions.networkClientId,
+ usePollingOptions.options &&
+ JSON.stringify(
+ usePollingOptions.options,
+ Object.keys(usePollingOptions.options).sort(),
+ ),
usePollingOptions.enabled,
]);
};
diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
index 79400367de13..95828e3e250e 100644
--- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
+++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
@@ -268,17 +268,17 @@ exports[`AssetPage should render a native asset 1`] = `
- Tips for using a wallet
+ Start your journey with ETH
- Adding tokens unlocks more ways to use web3.
+ Get started with web3 by adding some ETH to your wallet.
- Tips for using a wallet
+ Start your journey with ETH
- Adding tokens unlocks more ways to use web3.
+ Get started with web3 by adding some ETH to your wallet.
- Tips for using a wallet
+ Start your journey with ETH
- Adding tokens unlocks more ways to use web3.
+ Get started with web3 by adding some ETH to your wallet.
{
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
describe('fetchBridgeFeatureFlags', () => {
it('should fetch bridge feature flags successfully', async () => {
const mockResponse = {
- 'extension-config': {
- refreshRate: 3,
- maxRefreshCount: 1,
- },
'extension-support': true,
'src-network-allowlist': [1, 10, 59144, 120],
'dest-network-allowlist': [1, 137, 59144, 11111],
@@ -43,10 +28,6 @@ describe('Bridge utils', () => {
});
expect(result).toStrictEqual({
- extensionConfig: {
- maxRefreshCount: 1,
- refreshRate: 3,
- },
extensionSupport: true,
srcNetworkAllowlist: [
CHAIN_IDS.MAINNET,
@@ -65,10 +46,8 @@ describe('Bridge utils', () => {
it('should use fallback bridge feature flags if response is unexpected', async () => {
const mockResponse = {
- 'extension-support': 25,
- 'src-network-allowlist': ['a', 'b', 1],
- a: 'b',
- 'dest-network-allowlist': [1, 137, 59144, 11111],
+ flag1: true,
+ flag2: false,
};
(fetchWithCache as jest.Mock).mockResolvedValue(mockResponse);
@@ -86,10 +65,6 @@ describe('Bridge utils', () => {
});
expect(result).toStrictEqual({
- extensionConfig: {
- maxRefreshCount: 5,
- refreshRate: 30000,
- },
extensionSupport: false,
srcNetworkAllowlist: [],
destNetworkAllowlist: [],
@@ -166,113 +141,4 @@ describe('Bridge utils', () => {
await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError);
});
});
-
- describe('fetchBridgeQuotes', () => {
- it('should fetch bridge quotes successfully, no approvals', async () => {
- (fetchWithCache as jest.Mock).mockResolvedValue(
- mockBridgeQuotesNativeErc20,
- );
-
- const result = await fetchBridgeQuotes({
- walletAddress: '0x123',
- srcChainId: 1,
- destChainId: 10,
- srcTokenAddress: zeroAddress(),
- destTokenAddress: zeroAddress(),
- srcTokenAmount: '20000',
- slippage: 0.5,
- });
-
- expect(fetchWithCache).toHaveBeenCalledWith({
- url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false',
- fetchOptions: {
- method: 'GET',
- headers: { 'X-Client-Id': 'extension' },
- },
- cacheOptions: { cacheRefreshTime: 0 },
- functionName: 'fetchBridgeQuotes',
- });
-
- expect(result).toStrictEqual(mockBridgeQuotesNativeErc20);
- });
-
- it('should fetch bridge quotes successfully, with approvals', async () => {
- (fetchWithCache as jest.Mock).mockResolvedValue([
- ...mockBridgeQuotesErc20Erc20,
- { ...mockBridgeQuotesErc20Erc20[0], approval: null },
- { ...mockBridgeQuotesErc20Erc20[0], trade: null },
- ]);
-
- const result = await fetchBridgeQuotes({
- walletAddress: '0x123',
- srcChainId: 1,
- destChainId: 10,
- srcTokenAddress: zeroAddress(),
- destTokenAddress: zeroAddress(),
- srcTokenAmount: '20000',
- slippage: 0.5,
- });
-
- expect(fetchWithCache).toHaveBeenCalledWith({
- url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false',
- fetchOptions: {
- method: 'GET',
- headers: { 'X-Client-Id': 'extension' },
- },
- cacheOptions: { cacheRefreshTime: 0 },
- functionName: 'fetchBridgeQuotes',
- });
-
- expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20);
- });
-
- it('should filter out malformed bridge quotes', async () => {
- (fetchWithCache as jest.Mock).mockResolvedValue([
- ...mockBridgeQuotesErc20Erc20,
- ...mockBridgeQuotesErc20Erc20.map(
- ({ quote, ...restOfQuote }) => restOfQuote,
- ),
- {
- ...mockBridgeQuotesErc20Erc20[0],
- quote: {
- srcAsset: {
- ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset,
- decimals: undefined,
- },
- },
- },
- {
- ...mockBridgeQuotesErc20Erc20[1],
- quote: {
- srcAsset: {
- ...mockBridgeQuotesErc20Erc20[1].quote.destAsset,
- address: undefined,
- },
- },
- },
- ]);
-
- const result = await fetchBridgeQuotes({
- walletAddress: '0x123',
- srcChainId: 1,
- destChainId: 10,
- srcTokenAddress: zeroAddress(),
- destTokenAddress: zeroAddress(),
- srcTokenAmount: '20000',
- slippage: 0.5,
- });
-
- expect(fetchWithCache).toHaveBeenCalledWith({
- url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false',
- fetchOptions: {
- method: 'GET',
- headers: { 'X-Client-Id': 'extension' },
- },
- cacheOptions: { cacheRefreshTime: 0 },
- functionName: 'fetchBridgeQuotes',
- });
-
- expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20);
- });
- });
});
diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts
index f154b7e62b19..915a933e7c02 100644
--- a/ui/pages/bridge/bridge.util.ts
+++ b/ui/pages/bridge/bridge.util.ts
@@ -11,6 +11,7 @@ import {
} from '../../../shared/constants/bridge';
import { MINUTE } from '../../../shared/constants/time';
import fetchWithCache from '../../../shared/lib/fetch-with-cache';
+import { validateData } from '../../../shared/lib/swaps-utils';
import {
decimalToHex,
hexToDecimal,
@@ -19,37 +20,43 @@ import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
SwapsTokenObject,
} from '../../../shared/constants/swaps';
+import { TOKEN_VALIDATORS } from '../swaps/swaps.util';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
} from '../../../shared/modules/swaps.utils';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants';
-import {
- BridgeAsset,
- BridgeFlag,
- FeatureFlagResponse,
- FeeData,
- FeeType,
- Quote,
- QuoteRequest,
- QuoteResponse,
- TxData,
-} from './types';
-import {
- FEATURE_FLAG_VALIDATORS,
- QUOTE_VALIDATORS,
- TX_DATA_VALIDATORS,
- TOKEN_VALIDATORS,
- validateResponse,
- QUOTE_RESPONSE_VALIDATORS,
- FEE_DATA_VALIDATORS,
-} from './utils/validators';
const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID };
const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE;
+// Types copied from Metabridge API
+enum BridgeFlag {
+ EXTENSION_SUPPORT = 'extension-support',
+ NETWORK_SRC_ALLOWLIST = 'src-network-allowlist',
+ NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist',
+}
+
+export type FeatureFlagResponse = {
+ [BridgeFlag.EXTENSION_SUPPORT]: boolean;
+ [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[];
+ [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[];
+};
+// End of copied types
+
+type Validator
= {
+ property: keyof ExpectedResponse | string;
+ type: string;
+ validator: (value: DataToValidate) => boolean;
+};
+
+const validateResponse = (
+ validators: Validator[],
+ data: unknown,
+ urlUsed: string,
+): data is ExpectedResponse => {
+ return validateData(validators, data, urlUsed);
+};
+
export async function fetchBridgeFeatureFlags(): Promise {
const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`;
const rawFeatureFlags = await fetchWithCache({
@@ -60,15 +67,35 @@ export async function fetchBridgeFeatureFlags(): Promise {
});
if (
- validateResponse(
- FEATURE_FLAG_VALIDATORS,
+ validateResponse(
+ [
+ {
+ property: BridgeFlag.EXTENSION_SUPPORT,
+ type: 'boolean',
+ validator: (v) => typeof v === 'boolean',
+ },
+ {
+ property: BridgeFlag.NETWORK_SRC_ALLOWLIST,
+ type: 'object',
+ validator: (v): v is number[] =>
+ Object.values(v as { [s: string]: unknown }).every(
+ (i) => typeof i === 'number',
+ ),
+ },
+ {
+ property: BridgeFlag.NETWORK_DEST_ALLOWLIST,
+ type: 'object',
+ validator: (v): v is number[] =>
+ Object.values(v as { [s: string]: unknown }).every(
+ (i) => typeof i === 'number',
+ ),
+ },
+ ],
rawFeatureFlags,
url,
)
) {
return {
- [BridgeFeatureFlagsKey.EXTENSION_CONFIG]:
- rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG],
[BridgeFeatureFlagsKey.EXTENSION_SUPPORT]:
rawFeatureFlags[BridgeFlag.EXTENSION_SUPPORT],
[BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: rawFeatureFlags[
@@ -81,10 +108,6 @@ export async function fetchBridgeFeatureFlags(): Promise {
}
return {
- [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: {
- refreshRate: REFRESH_INTERVAL_MS,
- maxRefreshCount: 5,
- },
// TODO set default to true once bridging is live
[BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false,
// TODO set default to ALLOWED_BRIDGE_CHAIN_IDS once bridging is live
@@ -119,9 +142,13 @@ export async function fetchBridgeTokens(
transformedTokens[nativeToken.address] = nativeToken;
}
- tokens.forEach((token: unknown) => {
+ tokens.forEach((token: SwapsTokenObject) => {
if (
- validateResponse(TOKEN_VALIDATORS, token, url) &&
+ validateResponse(
+ TOKEN_VALIDATORS,
+ token,
+ url,
+ ) &&
!(
isSwapsDefaultTokenSymbol(token.symbol, chainId) ||
isSwapsDefaultTokenAddress(token.address, chainId)
@@ -132,51 +159,3 @@ export async function fetchBridgeTokens(
});
return transformedTokens;
}
-
-// Returns a list of bridge tx quotes
-export async function fetchBridgeQuotes(
- request: QuoteRequest,
-): Promise {
- const queryParams = new URLSearchParams({
- walletAddress: request.walletAddress,
- srcChainId: request.srcChainId.toString(),
- destChainId: request.destChainId.toString(),
- srcTokenAddress: request.srcTokenAddress,
- destTokenAddress: request.destTokenAddress,
- srcTokenAmount: request.srcTokenAmount,
- slippage: request.slippage.toString(),
- insufficientBal: request.insufficientBal ? 'true' : 'false',
- resetApproval: request.resetApproval ? 'true' : 'false',
- });
- const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`;
- const quotes = await fetchWithCache({
- url,
- fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER },
- cacheOptions: { cacheRefreshTime: 0 },
- functionName: 'fetchBridgeQuotes',
- });
-
- const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => {
- const { quote, approval, trade } = quoteResponse;
- return (
- validateResponse(
- QUOTE_RESPONSE_VALIDATORS,
- quoteResponse,
- url,
- ) &&
- validateResponse(QUOTE_VALIDATORS, quote, url) &&
- validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) &&
- validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) &&
- validateResponse(TX_DATA_VALIDATORS, trade, url) &&
- validateResponse(
- FEE_DATA_VALIDATORS,
- quote.feeData[FeeType.METABRIDGE],
- url,
- ) &&
- (approval
- ? validateResponse(TX_DATA_VALIDATORS, approval, url)
- : true)
- );
- });
- return filteredQuotes;
-}
diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
index 4284c1893d7c..b406cafe0941 100644
--- a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
+++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
@@ -107,7 +107,6 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] =
>
$0.00
@@ -192,7 +191,6 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] =
>
$0.00
@@ -318,7 +316,6 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
>
$0.00
@@ -447,7 +444,6 @@ exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
>
$0.00
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index b0907f83dab7..2fdb11289c5b 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -1,15 +1,13 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classnames from 'classnames';
-import { debounce } from 'lodash';
import {
setFromChain,
setFromToken,
setFromTokenInputValue,
setToChain,
- setToChainId,
setToToken,
- updateQuoteRequestParams,
+ switchToAndFromTokens,
} from '../../../ducks/bridge/actions';
import {
getFromAmount,
@@ -30,14 +28,11 @@ import {
ButtonIcon,
IconName,
} from '../../../components/component-library';
-import { BlockSize } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { TokenBucketPriority } from '../../../../shared/constants/swaps';
import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering';
import { setActiveNetwork } from '../../../store/actions';
-import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
-import { QuoteRequest } from '../types';
-import { calcTokenValue } from '../../../../shared/lib/swaps-utils';
+import { BlockSize } from '../../../helpers/constants/design-system';
import { BridgeInputGroup } from './bridge-input-group';
const PrepareBridgePage = () => {
@@ -76,36 +71,6 @@ const PrepareBridgePage = () => {
const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
- const quoteParams = useMemo(
- () => ({
- srcTokenAddress: fromToken?.address,
- destTokenAddress: toToken?.address || undefined,
- srcTokenAmount:
- fromAmount && fromAmount !== '' && fromToken?.decimals
- ? calcTokenValue(fromAmount, fromToken.decimals).toString()
- : undefined,
- srcChainId: fromChain?.chainId
- ? Number(hexToDecimal(fromChain.chainId))
- : undefined,
- destChainId: toChain?.chainId
- ? Number(hexToDecimal(toChain.chainId))
- : undefined,
- }),
- [fromToken, toToken, fromChain?.chainId, toChain?.chainId, fromAmount],
- );
-
- const debouncedUpdateQuoteRequestInController = useCallback(
- debounce(
- (p: Partial) => dispatch(updateQuoteRequestParams(p)),
- 300,
- ),
- [],
- );
-
- useEffect(() => {
- debouncedUpdateQuoteRequestInController(quoteParams);
- }, Object.values(quoteParams));
-
return (
@@ -116,10 +81,7 @@ const PrepareBridgePage = () => {
onAmountChange={(e) => {
dispatch(setFromTokenInputValue(e));
}}
- onAssetChange={(token) => {
- dispatch(setFromToken(token));
- dispatch(setFromTokenInputValue(null));
- }}
+ onAssetChange={(token) => dispatch(setFromToken(token))}
networkProps={{
network: fromChain,
networks: fromChains,
@@ -132,8 +94,6 @@ const PrepareBridgePage = () => {
),
);
dispatch(setFromChain(networkConfig.chainId));
- dispatch(setFromToken(null));
- dispatch(setFromTokenInputValue(null));
},
}}
customTokenListGenerator={
@@ -161,18 +121,12 @@ const PrepareBridgePage = () => {
onClick={() => {
setRotateSwitchTokens(!rotateSwitchTokens);
const toChainClientId =
- toChain?.defaultRpcEndpointIndex !== undefined &&
- toChain?.rpcEndpoints
- ? toChain.rpcEndpoints[toChain.defaultRpcEndpointIndex]
+ toChain?.defaultRpcEndpointIndex && toChain?.rpcEndpoints
+ ? toChain.rpcEndpoints?.[toChain.defaultRpcEndpointIndex]
.networkClientId
: undefined;
toChainClientId && dispatch(setActiveNetwork(toChainClientId));
- toChain && dispatch(setFromChain(toChain.chainId));
- dispatch(setFromToken(toToken));
- dispatch(setFromTokenInputValue(null));
- fromChain?.chainId && dispatch(setToChain(fromChain.chainId));
- fromChain?.chainId && dispatch(setToChainId(fromChain.chainId));
- dispatch(setToToken(fromToken));
+ dispatch(switchToAndFromTokens({ fromChain }));
}}
/>
@@ -186,7 +140,6 @@ const PrepareBridgePage = () => {
network: toChain,
networks: toChains,
onNetworkChange: (networkConfig) => {
- dispatch(setToChainId(networkConfig.chainId));
dispatch(setToChain(networkConfig.chainId));
},
}}
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
deleted file mode 100644
index 5d001e7ef7fc..000000000000
--- a/ui/pages/bridge/types.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-// Types copied from Metabridge API
-export enum BridgeFlag {
- EXTENSION_CONFIG = 'extension-config',
- EXTENSION_SUPPORT = 'extension-support',
- NETWORK_SRC_ALLOWLIST = 'src-network-allowlist',
- NETWORK_DEST_ALLOWLIST = 'dest-network-allowlist',
-}
-
-export type FeatureFlagResponse = {
- [BridgeFlag.EXTENSION_CONFIG]: {
- refreshRate: number;
- maxRefreshCount: number;
- };
- [BridgeFlag.EXTENSION_SUPPORT]: boolean;
- [BridgeFlag.NETWORK_SRC_ALLOWLIST]: number[];
- [BridgeFlag.NETWORK_DEST_ALLOWLIST]: number[];
-};
-
-export type BridgeAsset = {
- chainId: ChainId;
- address: string;
- symbol: string;
- name: string;
- decimals: number;
- icon?: string;
-};
-
-export type QuoteRequest = {
- walletAddress: string;
- destWalletAddress?: string;
- srcChainId: ChainId;
- destChainId: ChainId;
- srcTokenAddress: string;
- destTokenAddress: string;
- srcTokenAmount: string;
- slippage: number;
- aggIds?: string[];
- bridgeIds?: string[];
- insufficientBal?: boolean;
- resetApproval?: boolean;
- refuel?: boolean;
-};
-
-type Protocol = {
- name: string;
- displayName?: string;
- icon?: string;
-};
-
-enum ActionTypes {
- BRIDGE = 'bridge',
- SWAP = 'swap',
- REFUEL = 'refuel',
-}
-
-type Step = {
- action: ActionTypes;
- srcChainId: ChainId;
- destChainId?: ChainId;
- srcAsset: BridgeAsset;
- destAsset: BridgeAsset;
- srcAmount: string;
- destAmount: string;
- protocol: Protocol;
-};
-
-type RefuelData = Step;
-
-export type Quote = {
- requestId: string;
- srcChainId: ChainId;
- srcAsset: BridgeAsset;
- srcTokenAmount: string;
- destChainId: ChainId;
- destAsset: BridgeAsset;
- destTokenAmount: string;
- feeData: Record
&
- Partial>;
- bridgeId: string;
- bridges: string[];
- steps: Step[];
- refuel?: RefuelData;
-};
-
-export type QuoteResponse = {
- quote: Quote;
- approval: TxData | null;
- trade: TxData;
- estimatedProcessingTimeInSeconds: number;
-};
-
-enum ChainId {
- ETH = 1,
- OPTIMISM = 10,
- BSC = 56,
- POLYGON = 137,
- ZKSYNC = 324,
- BASE = 8453,
- ARBITRUM = 42161,
- AVALANCHE = 43114,
- LINEA = 59144,
-}
-
-export enum FeeType {
- METABRIDGE = 'metabridge',
- REFUEL = 'refuel',
-}
-export type FeeData = {
- amount: string;
- asset: BridgeAsset;
-};
-export type TxData = {
- chainId: ChainId;
- to: string;
- from: string;
- value: string;
- data: string;
- gasLimit: number | null;
-};
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
deleted file mode 100644
index 0b83205580b4..000000000000
--- a/ui/pages/bridge/utils/quote.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { QuoteRequest } from '../types';
-
-export const isValidQuoteRequest = (
- partialRequest: Partial,
- requireAmount = true,
-): partialRequest is QuoteRequest => {
- const STRING_FIELDS = ['srcTokenAddress', 'destTokenAddress'];
- if (requireAmount) {
- STRING_FIELDS.push('srcTokenAmount');
- }
- const NUMBER_FIELDS = ['srcChainId', 'destChainId', 'slippage'];
-
- return (
- STRING_FIELDS.every(
- (field) =>
- field in partialRequest &&
- typeof partialRequest[field as keyof typeof partialRequest] ===
- 'string' &&
- partialRequest[field as keyof typeof partialRequest] !== undefined &&
- partialRequest[field as keyof typeof partialRequest] !== '' &&
- partialRequest[field as keyof typeof partialRequest] !== null,
- ) &&
- NUMBER_FIELDS.every(
- (field) =>
- field in partialRequest &&
- typeof partialRequest[field as keyof typeof partialRequest] ===
- 'number' &&
- partialRequest[field as keyof typeof partialRequest] !== undefined &&
- !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) &&
- partialRequest[field as keyof typeof partialRequest] !== null,
- )
- );
-};
diff --git a/ui/pages/bridge/utils/validators.ts b/ui/pages/bridge/utils/validators.ts
deleted file mode 100644
index 01c716522968..000000000000
--- a/ui/pages/bridge/utils/validators.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { isStrictHexString } from '@metamask/utils';
-import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils';
-import {
- truthyDigitString,
- validateData,
-} from '../../../../shared/lib/swaps-utils';
-import { BridgeFlag, FeatureFlagResponse } from '../types';
-
-type Validator = {
- property: keyof ExpectedResponse | string;
- type: string;
- validator?: (value: unknown) => boolean;
-};
-
-export const validateResponse = (
- validators: Validator[],
- data: unknown,
- urlUsed: string,
-): data is ExpectedResponse => {
- return validateData(validators, data, urlUsed);
-};
-
-export const isValidNumber = (v: unknown): v is number => typeof v === 'number';
-const isValidObject = (v: unknown): v is object =>
- typeof v === 'object' && v !== null;
-const isValidString = (v: unknown): v is string =>
- typeof v === 'string' && v.length > 0;
-const isValidHexAddress = (v: unknown) =>
- isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false });
-
-export const FEATURE_FLAG_VALIDATORS = [
- {
- property: BridgeFlag.EXTENSION_CONFIG,
- type: 'object',
- validator: (
- v: unknown,
- ): v is Pick =>
- isValidObject(v) &&
- 'refreshRate' in v &&
- isValidNumber(v.refreshRate) &&
- 'maxRefreshCount' in v &&
- isValidNumber(v.maxRefreshCount),
- },
- { property: BridgeFlag.EXTENSION_SUPPORT, type: 'boolean' },
- {
- property: BridgeFlag.NETWORK_SRC_ALLOWLIST,
- type: 'object',
- validator: (v: unknown): v is number[] =>
- isValidObject(v) && Object.values(v).every(isValidNumber),
- },
- {
- property: BridgeFlag.NETWORK_DEST_ALLOWLIST,
- type: 'object',
- validator: (v: unknown): v is number[] =>
- isValidObject(v) && Object.values(v).every(isValidNumber),
- },
-];
-
-export const TOKEN_VALIDATORS = [
- { property: 'decimals', type: 'number' },
- { property: 'address', type: 'string', validator: isValidHexAddress },
- {
- property: 'symbol',
- type: 'string',
- validator: (v: unknown) => isValidString(v) && v.length <= 12,
- },
-];
-
-export const QUOTE_RESPONSE_VALIDATORS = [
- { property: 'quote', type: 'object', validator: isValidObject },
- { property: 'estimatedProcessingTimeInSeconds', type: 'number' },
- {
- property: 'approval',
- type: 'object|undefined',
- validator: (v: unknown) => v === undefined || isValidObject(v),
- },
- { property: 'trade', type: 'object', validator: isValidObject },
-];
-
-export const QUOTE_VALIDATORS = [
- { property: 'requestId', type: 'string' },
- { property: 'srcTokenAmount', type: 'string' },
- { property: 'destTokenAmount', type: 'string' },
- { property: 'bridgeId', type: 'string' },
- { property: 'bridges', type: 'object', validator: isValidObject },
- { property: 'srcChainId', type: 'number' },
- { property: 'destChainId', type: 'number' },
- { property: 'srcAsset', type: 'object', validator: isValidObject },
- { property: 'destAsset', type: 'object', validator: isValidObject },
- { property: 'feeData', type: 'object', validator: isValidObject },
-];
-
-export const FEE_DATA_VALIDATORS = [
- { property: 'amount', type: 'string', validator: truthyDigitString },
- { property: 'asset', type: 'object', validator: isValidObject },
-];
-
-export const TX_DATA_VALIDATORS = [
- { property: 'chainId', type: 'number' },
- { property: 'value', type: 'string', validator: isStrictHexString },
- { property: 'gasLimit', type: 'number' },
- { property: 'to', type: 'string', validator: isValidHexAddress },
- { property: 'from', type: 'string', validator: isValidHexAddress },
- { property: 'data', type: 'string', validator: isStrictHexString },
-];
diff --git a/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap b/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap
index d5e060e31d72..cb4715881e27 100644
--- a/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap
+++ b/ui/pages/confirm-decrypt-message/__snapshots__/confirm-decrypt-message.component.test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ConfirmDecryptMessage Component matches snapshot 1`] = `
+exports[`ConfirmDecryptMessage Component should match snapshot when preference is ETH currency 1`] = `
-
-
-
-
-
-
- Balance:
-
-
- 966.987986 ETH
-
-
-
-
-
-
- T
-
-
- test would like to read this message to complete your action
-
-
-
-
-
-
- {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"}}
-
-
-
-
-
-
-
- Decrypt message
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`ConfirmDecryptMessage Component matches snapshot if no unapproved decrypt messages 1`] = `
`;
-
-exports[`ConfirmDecryptMessage Component shows error on decrypt inline error 1`] = `
-
-
-
-
-
-
-
- Account:
-
-
-
+
+
+
+
+
+
+
+
+
@@ -304,7 +189,7 @@ exports[`ConfirmDecryptMessage Component shows error on decrypt inline error 1`]
- 966.987986 ETH
+ 966.987986 ABC
@@ -325,40 +210,35 @@ exports[`ConfirmDecryptMessage Component shows error on decrypt inline error 1`]
+ {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}
+
+
+
+
-
- This message cannot be decrypted due to error: Decrypt inline error
-
-
-
+
-
-
- Decrypt message
-
+ Decrypt message
-
+
+
+
+
@@ -494,7 +451,7 @@ exports[`ConfirmDecryptMessage Component shows the correct message data 1`] = `
- 966.987986 ETH
+ 1520956.064158 DEF
@@ -515,66 +472,35 @@ exports[`ConfirmDecryptMessage Component shows the correct message data 1`] = `
-
- raw message
-
-
-
-
-
-
-
- Decrypt message
-
-
-
+ {"domain":{"chainId":97,"name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}
+
+
+