diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json
index 08d1a48e1..0ba074760 100644
--- a/development/ts-migration-dashboard/files-to-convert.json
+++ b/development/ts-migration-dashboard/files-to-convert.json
@@ -1162,9 +1162,6 @@
"ui/hooks/useUserPreferencedCurrency.test.js",
"ui/index.js",
"ui/index.test.js",
- "ui/pages/add-nft/add-nft.js",
- "ui/pages/add-nft/add-nft.test.js",
- "ui/pages/add-nft/index.js",
"ui/pages/asset/asset.js",
"ui/pages/asset/components/asset-breadcrumb.js",
"ui/pages/asset/components/asset-navigation.js",
diff --git a/test/e2e/nft/import-erc1155.spec.js b/test/e2e/nft/import-erc1155.spec.js
index e27155e3c..516b82aba 100644
--- a/test/e2e/nft/import-erc1155.spec.js
+++ b/test/e2e/nft/import-erc1155.spec.js
@@ -38,9 +38,11 @@ describe('Import ERC1155 NFT', function () {
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
// Enter a valid NFT that belongs to user and check success message appears
- await driver.fill('[data-testid="address"]', contractAddress);
- await driver.fill('[data-testid="token-id"]', '1');
- await driver.clickElement({ text: 'Add', tag: 'button' });
+ await driver.fill('#address', contractAddress);
+ await driver.fill('#token-id', '1');
+ await driver.clickElement(
+ '[data-testid="import-nfts-modal-import-button"]',
+ );
const newNftNotification = await driver.findVisibleElement({
text: 'NFT was successfully added!',
@@ -86,14 +88,15 @@ describe('Import ERC1155 NFT', function () {
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
// Enter an NFT that not belongs to user with a valid address and an invalid token id
- await driver.fill('[data-testid="address"]', contractAddress);
- await driver.fill('[data-testid="token-id"]', '4');
- await driver.clickElement({ text: 'Add', tag: 'button' });
-
+ await driver.fill('#address', contractAddress);
+ await driver.fill('#token-id', '4');
+ await driver.clickElement(
+ '[data-testid="import-nfts-modal-import-button"]',
+ );
// Check error message appears
const invalidNftNotification = await driver.findElement({
text: 'NFT can’t be added as the ownership details do not match. Make sure you have entered correct information.',
- tag: 'h6',
+ tag: 'p',
});
assert.equal(await invalidNftNotification.isDisplayed(), true);
},
diff --git a/test/e2e/nft/import-nft.spec.js b/test/e2e/nft/import-nft.spec.js
index 373edc70a..c0fc22876 100644
--- a/test/e2e/nft/import-nft.spec.js
+++ b/test/e2e/nft/import-nft.spec.js
@@ -38,9 +38,11 @@ describe('Import NFT', function () {
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
// Enter a valid NFT that belongs to user and check success message appears
- await driver.fill('[data-testid="address"]', contractAddress);
- await driver.fill('[data-testid="token-id"]', '1');
- await driver.clickElement({ text: 'Add', tag: 'button' });
+ await driver.fill('#address', contractAddress);
+ await driver.fill('#token-id', '1');
+ await driver.clickElement(
+ '[data-testid="import-nfts-modal-import-button"]',
+ );
const newNftNotification = await driver.findElement({
text: 'NFT was successfully added!',
@@ -85,14 +87,16 @@ describe('Import NFT', function () {
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
// Enter an NFT that not belongs to user with a valid address and an invalid token id
- await driver.fill('[data-testid="address"]', contractAddress);
- await driver.fill('[data-testid="token-id"]', '2');
- await driver.clickElement({ text: 'Add', tag: 'button' });
+ await driver.fill('#address', contractAddress);
+ await driver.fill('#token-id', '2');
+ await driver.clickElement(
+ '[data-testid="import-nfts-modal-import-button"]',
+ );
// Check error message appears
const invalidNftNotification = await driver.findElement({
text: 'NFT can’t be added as the ownership details do not match. Make sure you have entered correct information.',
- tag: 'h6',
+ tag: 'p',
});
assert.equal(await invalidNftNotification.isDisplayed(), true);
},
diff --git a/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js
index c818c80f1..9311153fc 100644
--- a/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js
+++ b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js
@@ -7,12 +7,9 @@ import Typography from '../../../ui/typography';
import { TypographyVariant } from '../../../../helpers/constants/design-system';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../../hooks/useI18nContext';
-import {
- ADD_NFT_ROUTE,
- ASSET_ROUTE,
-} from '../../../../helpers/constants/routes';
+import { ASSET_ROUTE } from '../../../../helpers/constants/routes';
import { getNfts } from '../../../../ducks/metamask/metamask';
-import { ignoreTokens } from '../../../../store/actions';
+import { ignoreTokens, showImportNftsModal } from '../../../../store/actions';
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
@@ -39,10 +36,7 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
pathname: `${ASSET_ROUTE}/${tokenAddress}/${tokenId}`,
});
} else {
- history.push({
- pathname: ADD_NFT_ROUTE,
- state: { tokenAddress },
- });
+ dispatch(showImportNftsModal());
}
hideModal();
}}
diff --git a/ui/components/app/nfts-detection-notice/index.scss b/ui/components/app/nfts-detection-notice/index.scss
index 95d13a24c..5c792262e 100644
--- a/ui/components/app/nfts-detection-notice/index.scss
+++ b/ui/components/app/nfts-detection-notice/index.scss
@@ -1,6 +1,4 @@
.nfts-detection-notice {
- margin: 16px 16px 0 16px;
-
&__message {
position: relative;
padding: 0.75rem 0.75rem 1rem 0.75rem !important;
diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js
index 69f9b3e11..2e1918c0d 100644
--- a/ui/components/app/nfts-tab/nfts-tab.js
+++ b/ui/components/app/nfts-tab/nfts-tab.js
@@ -1,5 +1,4 @@
import React from 'react';
-import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import NftsItems from '../nfts-items';
@@ -19,13 +18,14 @@ import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
import {
checkAndUpdateAllNftsOwnershipStatus,
detectNfts,
+ showImportNftsModal,
} from '../../../store/actions';
import { useNftsCollections } from '../../../hooks/useNftsCollections';
import { Box, ButtonLink, IconName, Text } from '../../component-library';
import NftsDetectionNotice from '../nfts-detection-notice';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
-export default function NftsTab({ onAddNFT }) {
+export default function NftsTab() {
const useNftDetection = useSelector(getUseNftDetection);
const isMainnet = useSelector(getIsMainnet);
const history = useHistory();
@@ -60,7 +60,11 @@ export default function NftsTab({ onAddNFT }) {
/>
) : (
<>
- {isMainnet && !useNftDetection ? : null}{' '}
+ {isMainnet && !useNftDetection ? (
+
+
+
+ ) : null}
@@ -113,7 +116,9 @@ export default function NftsTab({ onAddNFT }) {
size={Size.MD}
data-testid="import-nft-button"
startIconName={IconName.Add}
- onClick={onAddNFT}
+ onClick={() => {
+ dispatch(showImportNftsModal());
+ }}
>
{t('importNFT')}
@@ -149,7 +154,3 @@ export default function NftsTab({ onAddNFT }) {
);
}
-
-NftsTab.propTypes = {
- onAddNFT: PropTypes.func.isRequired,
-};
diff --git a/ui/components/app/nfts-tab/nfts-tab.test.js b/ui/components/app/nfts-tab/nfts-tab.test.js
index 2cc2048d1..1bdd19e8a 100644
--- a/ui/components/app/nfts-tab/nfts-tab.test.js
+++ b/ui/components/app/nfts-tab/nfts-tab.test.js
@@ -150,7 +150,6 @@ const render = ({
selectedAddress,
chainId = '0x1',
useNftDetection,
- onAddNFT = jest.fn(),
}) => {
const store = configureStore({
metamask: {
@@ -170,7 +169,7 @@ const render = ({
nftsDropdownState,
},
});
- return renderWithProvider(, store);
+ return renderWithProvider(, store);
};
describe('NFT Items', () => {
@@ -295,16 +294,5 @@ describe('NFT Items', () => {
expect(historyPushMock).toHaveBeenCalledTimes(1);
expect(historyPushMock).toHaveBeenCalledWith(EXPERIMENTAL_ROUTE);
});
- it('should render a link "Import NFTs" when some NFTs are present, which, when clicked calls the passed in onAddNFT method', () => {
- const onAddNFTStub = jest.fn();
- render({
- selectedAddress: ACCOUNT_1,
- nfts: NFTS,
- onAddNFT: onAddNFTStub,
- });
- expect(onAddNFTStub).toHaveBeenCalledTimes(0);
- fireEvent.click(screen.queryByText('Import NFT'));
- expect(onAddNFTStub).toHaveBeenCalledTimes(1);
- });
});
});
diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js
new file mode 100644
index 000000000..20e14d83f
--- /dev/null
+++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.js
@@ -0,0 +1,286 @@
+import React, { useContext, useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { isValidHexAddress } from '@metamask/controller-utils';
+import PropTypes from 'prop-types';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
+import {
+ AlignItems,
+ Display,
+ FlexDirection,
+ IconColor,
+ JustifyContent,
+ Severity,
+ Size,
+} from '../../../helpers/constants/design-system';
+
+import {
+ addNftVerifyOwnership,
+ getTokenStandardAndDetails,
+ ignoreTokens,
+ setNewNftAddedMessage,
+ updateNftDropDownState,
+} from '../../../store/actions';
+import {
+ getCurrentChainId,
+ getIsMainnet,
+ getSelectedAddress,
+ getUseNftDetection,
+} from '../../../selectors';
+import { getNftsDropdownState } from '../../../ducks/metamask/metamask';
+import NftsDetectionNotice from '../../app/nfts-detection-notice';
+import { MetaMetricsContext } from '../../../contexts/metametrics';
+import { AssetType } from '../../../../shared/constants/transaction';
+import {
+ MetaMetricsEventName,
+ MetaMetricsTokenEventSource,
+} from '../../../../shared/constants/metametrics';
+import {
+ IconName,
+ ModalContent,
+ ModalOverlay,
+ ModalHeader,
+ Modal,
+ ButtonPrimary,
+ ButtonSecondary,
+ Box,
+ FormTextField,
+ Label,
+ Icon,
+ IconSize,
+ BannerAlert,
+} from '../../component-library';
+import Tooltip from '../../ui/tooltip';
+
+export const ImportNftsModal = ({ onClose }) => {
+ const t = useI18nContext();
+ const history = useHistory();
+ const dispatch = useDispatch();
+ const useNftDetection = useSelector(getUseNftDetection);
+ const isMainnet = useSelector(getIsMainnet);
+ const nftsDropdownState = useSelector(getNftsDropdownState);
+ const selectedAddress = useSelector(getSelectedAddress);
+ const chainId = useSelector(getCurrentChainId);
+ const addressEnteredOnImportTokensPage =
+ history?.location?.state?.addressEnteredOnImportTokensPage;
+ const contractAddressToConvertFromTokenToNft =
+ history?.location?.state?.tokenAddress;
+
+ const [nftAddress, setNftAddress] = useState(
+ addressEnteredOnImportTokensPage ??
+ contractAddressToConvertFromTokenToNft ??
+ '',
+ );
+ const [tokenId, setTokenId] = useState('');
+ const [disabled, setDisabled] = useState(true);
+ const [nftAddFailed, setNftAddFailed] = useState(false);
+ const trackEvent = useContext(MetaMetricsContext);
+
+ const handleAddNft = async () => {
+ try {
+ await dispatch(addNftVerifyOwnership(nftAddress, tokenId));
+ const newNftDropdownState = {
+ ...nftsDropdownState,
+ [selectedAddress]: {
+ ...nftsDropdownState?.[selectedAddress],
+ [chainId]: {
+ ...nftsDropdownState?.[selectedAddress]?.[chainId],
+ [nftAddress]: true,
+ },
+ },
+ };
+
+ dispatch(updateNftDropDownState(newNftDropdownState));
+ } catch (error) {
+ const { message } = error;
+ dispatch(setNewNftAddedMessage(message));
+ setNftAddFailed(true);
+ return;
+ }
+ if (contractAddressToConvertFromTokenToNft) {
+ await dispatch(
+ ignoreTokens({
+ tokensToIgnore: contractAddressToConvertFromTokenToNft,
+ dontShowLoadingIndicator: true,
+ }),
+ );
+ }
+ dispatch(setNewNftAddedMessage('success'));
+
+ const tokenDetails = await getTokenStandardAndDetails(
+ nftAddress,
+ null,
+ tokenId.toString(),
+ );
+
+ trackEvent({
+ event: MetaMetricsEventName.TokenAdded,
+ category: 'Wallet',
+ sensitiveProperties: {
+ token_contract_address: nftAddress,
+ token_symbol: tokenDetails?.symbol,
+ tokenId: tokenId.toString(),
+ asset_type: AssetType.NFT,
+ token_standard: tokenDetails?.standard,
+ source_connection_method: MetaMetricsTokenEventSource.Custom,
+ },
+ });
+
+ history.push(DEFAULT_ROUTE);
+ onClose();
+ };
+
+ const validateAndSetAddress = (val) => {
+ setDisabled(!isValidHexAddress(val) || !tokenId);
+ setNftAddress(val);
+ };
+
+ const validateAndSetTokenId = (val) => {
+ setDisabled(!isValidHexAddress(nftAddress) || !val || isNaN(Number(val)));
+ setTokenId(val);
+ };
+
+ return (
+ {
+ onClose();
+ }}
+ className="import-nfts-modal"
+ >
+
+
+ {
+ onClose();
+ }}
+ >
+ {t('importNFT')}
+
+
+ {isMainnet && !useNftDetection ? (
+
+
+
+ ) : null}
+ {nftAddFailed && (
+
+ setNftAddFailed(false)}
+ closeButtonProps={{ 'data-testid': 'add-nft-error-close' }}
+ >
+ {t('nftAddFailedMessage')}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {
+ validateAndSetAddress(e.target.value);
+ setNftAddFailed(false);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {
+ validateAndSetTokenId(e.target.value);
+ setNftAddFailed(false);
+ }}
+ />
+
+
+
+
+ onClose()}
+ block
+ className="import-nfts-modal__cancel-button"
+ >
+ {t('cancel')}
+
+ handleAddNft()}
+ disabled={disabled}
+ block
+ data-testid="import-nfts-modal-import-button"
+ >
+ {t('import')}
+
+
+
+
+ );
+};
+
+ImportNftsModal.propTypes = {
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/ui/components/multichain/import-nfts-modal/import-nfts-modal.stories.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.stories.js
new file mode 100644
index 000000000..f6e6139ae
--- /dev/null
+++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.stories.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from '../../../store/store';
+import testData from '../../../../.storybook/test-data';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { ImportNftsModal } from '.';
+
+const createStore = (chainId = CHAIN_IDS.MAINNET, useTokenDetection = true) => {
+ return configureStore({
+ ...testData,
+ metamask: {
+ ...testData.metamask,
+ useTokenDetection,
+ providerConfig: { chainId },
+ },
+ });
+};
+
+export default {
+ title: 'Components/Multichain/ImportNftsModal',
+ component: ImportNftsModal,
+ argTypes: {
+ onClose: {
+ action: 'onClose',
+ },
+ },
+};
+
+export const DefaultStory = (args) => ;
+DefaultStory.decorators = [
+ (Story) => (
+
+
+
+ ),
+];
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/pages/add-nft/add-nft.test.js b/ui/components/multichain/import-nfts-modal/import-nfts-modal.test.js
similarity index 56%
rename from ui/pages/add-nft/add-nft.test.js
rename to ui/components/multichain/import-nfts-modal/import-nfts-modal.test.js
index cb76d1a16..e251fc7d7 100644
--- a/ui/pages/add-nft/add-nft.test.js
+++ b/ui/components/multichain/import-nfts-modal/import-nfts-modal.test.js
@@ -3,16 +3,16 @@ import { fireEvent, waitFor } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { useHistory } from 'react-router-dom';
-import { renderWithProvider } from '../../../test/jest/rendering';
-import mockState from '../../../test/data/mock-state.json';
-import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
+import { renderWithProvider } from '../../../../test/jest/rendering';
+import mockState from '../../../../test/data/mock-state.json';
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import {
addNftVerifyOwnership,
ignoreTokens,
setNewNftAddedMessage,
updateNftDropDownState,
-} from '../../store/actions';
-import AddNft from '.';
+} from '../../../store/actions';
+import { ImportNftsModal } from '.';
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9';
const INVALID_ADDRESS = 'aoinsafasdfa';
@@ -33,7 +33,7 @@ jest.mock('react-router-dom', () => ({
),
}));
-jest.mock('../../store/actions.ts', () => ({
+jest.mock('../../../store/actions.ts', () => ({
addNftVerifyOwnership: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
@@ -47,56 +47,72 @@ jest.mock('../../store/actions.ts', () => ({
.mockReturnValue(jest.fn().mockResolvedValue()),
}));
-describe('AddNft', () => {
+describe('ImportNftsModal', () => {
const store = configureMockStore([thunk])(mockState);
beforeEach(() => {
jest.restoreAllMocks();
});
- it('should enable the "Add" button when valid entries are input into both Address and TokenId fields', () => {
- const { getByTestId, getByText } = renderWithProvider(, store);
- expect(getByText('Add')).not.toBeEnabled();
- fireEvent.change(getByTestId('address'), {
+ it('should enable the "Import" button when valid entries are input into both Address and TokenId fields', () => {
+ const { getByText, getByPlaceholderText } = renderWithProvider(
+ ,
+ store,
+ );
+ expect(getByText('Import')).not.toBeEnabled();
+ const addressInput = getByPlaceholderText('0x...');
+ const tokenIdInput = getByPlaceholderText('Enter the token id');
+ fireEvent.change(addressInput, {
target: { value: VALID_ADDRESS },
});
- fireEvent.change(getByTestId('token-id'), {
+ fireEvent.change(tokenIdInput, {
target: { value: VALID_TOKENID },
});
- expect(getByText('Add')).toBeEnabled();
+ expect(getByText('Import')).toBeEnabled();
});
- it('should not enable the "Add" button when an invalid entry is input into one or both Address and TokenId fields', () => {
- const { getByTestId, getByText } = renderWithProvider(, store);
- expect(getByText('Add')).not.toBeEnabled();
- fireEvent.change(getByTestId('address'), {
+ it('should not enable the "Import" button when an invalid entry is input into one or both Address and TokenId fields', () => {
+ const { getByText, getByPlaceholderText } = renderWithProvider(
+ ,
+ store,
+ );
+ expect(getByText('Import')).not.toBeEnabled();
+ const addressInput = getByPlaceholderText('0x...');
+ const tokenIdInput = getByPlaceholderText('Enter the token id');
+ fireEvent.change(addressInput, {
target: { value: INVALID_ADDRESS },
});
- fireEvent.change(getByTestId('token-id'), {
+ fireEvent.change(tokenIdInput, {
target: { value: VALID_TOKENID },
});
- expect(getByText('Add')).not.toBeEnabled();
- fireEvent.change(getByTestId('address'), {
+ expect(getByText('Import')).not.toBeEnabled();
+ fireEvent.change(addressInput, {
target: { value: VALID_ADDRESS },
});
- expect(getByText('Add')).toBeEnabled();
- fireEvent.change(getByTestId('token-id'), {
+ expect(getByText('Import')).toBeEnabled();
+ fireEvent.change(tokenIdInput, {
target: { value: INVALID_TOKENID },
});
- expect(getByText('Add')).not.toBeEnabled();
+ expect(getByText('Import')).not.toBeEnabled();
});
it('should call addNftVerifyOwnership, updateNftDropDownState, setNewNftAddedMessage, and ignoreTokens action with correct values (tokenId should not be in scientific notation)', async () => {
- const { getByTestId, getByText } = renderWithProvider(, store);
- fireEvent.change(getByTestId('address'), {
+ const onClose = jest.fn();
+ const { getByPlaceholderText, getByText } = renderWithProvider(
+ ,
+ store,
+ );
+ const addressInput = getByPlaceholderText('0x...');
+ const tokenIdInput = getByPlaceholderText('Enter the token id');
+ fireEvent.change(addressInput, {
target: { value: VALID_ADDRESS },
});
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
- fireEvent.change(getByTestId('token-id'), {
+ fireEvent.change(tokenIdInput, {
target: { value: LARGE_TOKEN_ID },
});
- fireEvent.click(getByText('Add'));
+ fireEvent.click(getByText('Import'));
await waitFor(() => {
expect(addNftVerifyOwnership).toHaveBeenCalledWith(
@@ -127,16 +143,21 @@ describe('AddNft', () => {
jest.fn().mockRejectedValue(new Error('error')),
);
- const { getByTestId, getByText } = renderWithProvider(, store);
- fireEvent.change(getByTestId('address'), {
+ const { getByTestId, getByText, getByPlaceholderText } = renderWithProvider(
+ ,
+ store,
+ );
+ const addressInput = getByPlaceholderText('0x...');
+ const tokenIdInput = getByPlaceholderText('Enter the token id');
+ fireEvent.change(addressInput, {
target: { value: VALID_ADDRESS },
});
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
- fireEvent.change(getByTestId('token-id'), {
+ fireEvent.change(tokenIdInput, {
target: { value: LARGE_TOKEN_ID },
});
- fireEvent.click(getByText('Add'));
+ fireEvent.click(getByText('Import'));
await waitFor(() => {
expect(setNewNftAddedMessage).toHaveBeenCalledWith('error');
@@ -148,19 +169,23 @@ describe('AddNft', () => {
});
it('should route to default route when cancel button is clicked', () => {
- const { queryByTestId } = renderWithProvider(, store);
+ const onClose = jest.fn();
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
- const cancelButton = queryByTestId('page-container-footer-cancel');
+ const cancelButton = getByText('Cancel');
fireEvent.click(cancelButton);
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it('should route to default route when close button is clicked', () => {
- const { queryByLabelText } = renderWithProvider(, store);
+ const onClose = jest.fn();
+ renderWithProvider(, store);
- const closeButton = queryByLabelText('close');
- fireEvent.click(closeButton);
+ fireEvent.click(document.querySelector('button[aria-label="Close"]'));
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
diff --git a/ui/components/multichain/import-nfts-modal/index.js b/ui/components/multichain/import-nfts-modal/index.js
new file mode 100644
index 000000000..659f65ac0
--- /dev/null
+++ b/ui/components/multichain/import-nfts-modal/index.js
@@ -0,0 +1 @@
+export { ImportNftsModal } from './import-nfts-modal';
diff --git a/ui/pages/add-nft/index.scss b/ui/components/multichain/import-nfts-modal/index.scss
similarity index 100%
rename from ui/pages/add-nft/index.scss
rename to ui/components/multichain/import-nfts-modal/index.scss
diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js
index fa535a66e..0f6edcf25 100644
--- a/ui/components/multichain/index.js
+++ b/ui/components/multichain/index.js
@@ -15,3 +15,4 @@ export { ProductTour } from './product-tour-popover';
export { AccountDetails } from './account-details';
export { CreateAccount } from './create-account';
export { ImportAccount } from './import-account';
+export { ImportNftsModal } from './import-nfts-modal';
diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss
index a93926b22..00cea05fa 100644
--- a/ui/components/multichain/multichain-components.scss
+++ b/ui/components/multichain/multichain-components.scss
@@ -5,6 +5,7 @@
* unintended overrides.
**/
@import 'address-copy-button/index';
+@import 'import-nfts-modal/index';
@import 'account-list-item/index';
@import 'account-list-item-menu/index';
@import 'account-list-menu/index';
diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts
index e6ea054db..e2b3a151e 100644
--- a/ui/ducks/app/app.ts
+++ b/ui/ducks/app/app.ts
@@ -26,6 +26,7 @@ interface AppState {
values?: { address?: string | null };
} | null;
networkDropdownOpen: boolean;
+ importNftsModalOpen: boolean;
accountDetail: {
subview?: string;
accountExport?: string;
@@ -94,6 +95,7 @@ const initialState: AppState = {
alertMessage: null,
qrCodeData: null,
networkDropdownOpen: false,
+ importNftsModalOpen: false,
accountDetail: {
privateKey: '',
},
@@ -163,6 +165,17 @@ export default function reduceApp(
networkDropdownOpen: false,
};
+ case actionConstants.IMPORT_NFTS_MODAL_OPEN:
+ return {
+ ...appState,
+ importNftsModalOpen: true,
+ };
+
+ case actionConstants.IMPORT_NFTS_MODAL_CLOSE:
+ return {
+ ...appState,
+ importNftsModalOpen: false,
+ };
// alert methods
case actionConstants.ALERT_OPEN:
return {
diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts
index 3e2e4e1b1..53b864cb8 100644
--- a/ui/helpers/constants/routes.ts
+++ b/ui/helpers/constants/routes.ts
@@ -66,7 +66,6 @@ const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status';
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
-const ADD_NFT_ROUTE = '/add-nft';
const ONBOARDING_ROUTE = '/onboarding';
const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase';
@@ -273,7 +272,6 @@ export {
SWAPS_ERROR_ROUTE,
SWAPS_MAINTENANCE_ROUTE,
SMART_TRANSACTION_STATUS_ROUTE,
- ADD_NFT_ROUTE,
ONBOARDING_ROUTE,
ONBOARDING_HELP_US_IMPROVE_ROUTE,
ONBOARDING_CREATE_PASSWORD_ROUTE,
diff --git a/ui/pages/add-nft/add-nft.js b/ui/pages/add-nft/add-nft.js
deleted file mode 100644
index 9772c9c25..000000000
--- a/ui/pages/add-nft/add-nft.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, { useContext, useState } from 'react';
-import { useHistory } from 'react-router-dom';
-import { useDispatch, useSelector } from 'react-redux';
-import { isValidHexAddress } from '@metamask/controller-utils';
-import { useI18nContext } from '../../hooks/useI18nContext';
-import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
-import {
- DISPLAY,
- FONT_WEIGHT,
- TypographyVariant,
-} from '../../helpers/constants/design-system';
-
-import Box from '../../components/ui/box';
-import Typography from '../../components/ui/typography';
-import ActionableMessage from '../../components/ui/actionable-message';
-import PageContainer from '../../components/ui/page-container';
-import {
- addNftVerifyOwnership,
- getTokenStandardAndDetails,
- ignoreTokens,
- setNewNftAddedMessage,
- updateNftDropDownState,
-} from '../../store/actions';
-import FormField from '../../components/ui/form-field';
-import {
- getCurrentChainId,
- getIsMainnet,
- getSelectedAddress,
- getUseNftDetection,
-} from '../../selectors';
-import { getNftsDropdownState } from '../../ducks/metamask/metamask';
-import NftsDetectionNotice from '../../components/app/nfts-detection-notice';
-import { MetaMetricsContext } from '../../contexts/metametrics';
-import { AssetType } from '../../../shared/constants/transaction';
-import {
- MetaMetricsEventName,
- MetaMetricsTokenEventSource,
-} from '../../../shared/constants/metametrics';
-import {
- ButtonIcon,
- IconName,
- ButtonIconSize,
-} from '../../components/component-library';
-
-export default function AddNft() {
- const t = useI18nContext();
- const history = useHistory();
- const dispatch = useDispatch();
- const useNftDetection = useSelector(getUseNftDetection);
- const isMainnet = useSelector(getIsMainnet);
- const nftsDropdownState = useSelector(getNftsDropdownState);
- const selectedAddress = useSelector(getSelectedAddress);
- const chainId = useSelector(getCurrentChainId);
- const addressEnteredOnImportTokensPage =
- history?.location?.state?.addressEnteredOnImportTokensPage;
- const contractAddressToConvertFromTokenToNft =
- history?.location?.state?.tokenAddress;
-
- const [nftAddress, setNftAddress] = useState(
- addressEnteredOnImportTokensPage ??
- contractAddressToConvertFromTokenToNft ??
- '',
- );
- const [tokenId, setTokenId] = useState('');
- const [disabled, setDisabled] = useState(true);
- const [nftAddFailed, setNftAddFailed] = useState(false);
- const trackEvent = useContext(MetaMetricsContext);
-
- const handleAddNft = async () => {
- try {
- await dispatch(addNftVerifyOwnership(nftAddress, tokenId));
- const newNftDropdownState = {
- ...nftsDropdownState,
- [selectedAddress]: {
- ...nftsDropdownState?.[selectedAddress],
- [chainId]: {
- ...nftsDropdownState?.[selectedAddress]?.[chainId],
- [nftAddress]: true,
- },
- },
- };
-
- dispatch(updateNftDropDownState(newNftDropdownState));
- } catch (error) {
- const { message } = error;
- dispatch(setNewNftAddedMessage(message));
- setNftAddFailed(true);
- return;
- }
- if (contractAddressToConvertFromTokenToNft) {
- await dispatch(
- ignoreTokens({
- tokensToIgnore: contractAddressToConvertFromTokenToNft,
- dontShowLoadingIndicator: true,
- }),
- );
- }
- dispatch(setNewNftAddedMessage('success'));
-
- const tokenDetails = await getTokenStandardAndDetails(
- nftAddress,
- null,
- tokenId.toString(),
- );
-
- trackEvent({
- event: MetaMetricsEventName.TokenAdded,
- category: 'Wallet',
- sensitiveProperties: {
- token_contract_address: nftAddress,
- token_symbol: tokenDetails?.symbol,
- tokenId: tokenId.toString(),
- asset_type: AssetType.NFT,
- token_standard: tokenDetails?.standard,
- source_connection_method: MetaMetricsTokenEventSource.Custom,
- },
- });
-
- history.push(DEFAULT_ROUTE);
- };
-
- const validateAndSetAddress = (val) => {
- setDisabled(!isValidHexAddress(val) || !tokenId);
- setNftAddress(val);
- };
-
- const validateAndSetTokenId = (val) => {
- setDisabled(!isValidHexAddress(nftAddress) || !val || isNaN(Number(val)));
- setTokenId(val);
- };
-
- return (
- {
- handleAddNft();
- }}
- submitText={t('add')}
- onCancel={() => {
- history.push(DEFAULT_ROUTE);
- }}
- onClose={() => {
- history.push(DEFAULT_ROUTE);
- }}
- disabled={disabled}
- contentComponent={
-
- {isMainnet && !useNftDetection ? : null}
- {nftAddFailed && (
-
-
-
- {t('nftAddFailedMessage')}
-
- setNftAddFailed(false)}
- />
-
- }
- />
-
- )}
-
- {
- validateAndSetAddress(val);
- setNftAddFailed(false);
- }}
- tooltipText={t('importNFTAddressToolTip')}
- autoFocus
- />
- {
- validateAndSetTokenId(val);
- setNftAddFailed(false);
- }}
- tooltipText={t('importNFTTokenIdToolTip')}
- />
-
-
- }
- />
- );
-}
diff --git a/ui/pages/add-nft/index.js b/ui/pages/add-nft/index.js
deleted file mode 100644
index 603e8378b..000000000
--- a/ui/pages/add-nft/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './add-nft';
diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js
index 602ce34cd..6bd76ea55 100644
--- a/ui/pages/home/home.component.js
+++ b/ui/pages/home/home.component.js
@@ -62,7 +62,6 @@ import {
BUILD_QUOTE_ROUTE,
VIEW_QUOTE_ROUTE,
CONFIRMATION_V_NEXT_ROUTE,
- ADD_NFT_ROUTE,
ONBOARDING_SECURE_YOUR_WALLET_ROUTE,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
CONFIRM_INSTITUTIONAL_FEATURE_CONNECT,
@@ -879,11 +878,7 @@ export default class Home extends PureComponent {
name={this.context.t('nfts')}
tabKey="nfts"
>
- {
- history.push(ADD_NFT_ROUTE);
- }}
- />
+
///: END:ONLY_INCLUDE_IN
}
diff --git a/ui/pages/import-token/import-token.component.js b/ui/pages/import-token/import-token.component.js
index ffda0ca12..20a00d1aa 100644
--- a/ui/pages/import-token/import-token.component.js
+++ b/ui/pages/import-token/import-token.component.js
@@ -8,7 +8,6 @@ import {
} from '../../helpers/utils/util';
import { tokenInfoGetter } from '../../helpers/utils/token-util';
import {
- ADD_NFT_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE,
SECURITY_ROUTE,
} from '../../helpers/constants/routes';
@@ -60,6 +59,11 @@ class ImportToken extends Component {
*/
clearPendingTokens: PropTypes.func,
+ /**
+ * Clear the list of pending tokens. Called when closing the modal.
+ */
+ showImportNftsModal: PropTypes.func,
+
/**
* The list of already added tokens.
*/
@@ -314,21 +318,15 @@ class ImportToken extends Component {
nftAddressError: this.context.t('nftAddressError', [
- this.props.history.push({
- pathname: ADD_NFT_ROUTE,
- state: {
- addressEnteredOnImportTokensPage: this.state.customAddress,
- },
- })
- }
+ onClick={() => {
+ this.props.showImportNftsModal();
+ }}
key="nftAddressError"
>
{this.context.t('importNFTPage')}
,
]),
});
-
break;
case isMainnetToken && !isMainnetNetwork:
this.setState({
diff --git a/ui/pages/import-token/import-token.container.js b/ui/pages/import-token/import-token.container.js
index 566783bc5..9d238d646 100644
--- a/ui/pages/import-token/import-token.container.js
+++ b/ui/pages/import-token/import-token.container.js
@@ -4,6 +4,7 @@ import {
setPendingTokens,
clearPendingTokens,
getTokenStandardAndDetails,
+ showImportNftsModal,
} from '../../store/actions';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getProviderConfig } from '../../ducks/metamask/metamask';
@@ -58,6 +59,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)),
clearPendingTokens: () => dispatch(clearPendingTokens()),
+ showImportNftsModal: () => dispatch(showImportNftsModal()),
getTokenStandardAndDetails: (address, selectedAddress) =>
getTokenStandardAndDetails(address, selectedAddress, null),
};
diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss
index 313f1f538..b582d5fa5 100644
--- a/ui/pages/pages.scss
+++ b/ui/pages/pages.scss
@@ -1,5 +1,4 @@
/** Please import your files in alphabetical order **/
-@import 'add-nft/index';
@import 'keyring-snaps/index';
@import 'import-token/index';
@import 'asset/asset';
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js
index e9386e606..681f05a51 100644
--- a/ui/pages/routes/routes.component.js
+++ b/ui/pages/routes/routes.component.js
@@ -19,7 +19,6 @@ import PermissionsConnect from '../permissions-connect';
import RestoreVaultPage from '../keychains/restore-vault';
import RevealSeedConfirmation from '../keychains/reveal-seed';
import ImportTokenPage from '../import-token';
-import AddNftPage from '../add-nft';
import ConfirmImportTokenPage from '../confirm-import-token';
import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
import CreateAccountPage from '../create-account/create-account.component';
@@ -33,6 +32,7 @@ import {
AccountListMenu,
NetworkListMenu,
AccountDetails,
+ ImportNftsModal,
} from '../../components/multichain';
import UnlockPage from '../unlock-page';
import Alerts from '../../components/app/alerts';
@@ -80,7 +80,6 @@ import {
CONFIRMATION_V_NEXT_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE,
ONBOARDING_ROUTE,
- ADD_NFT_ROUTE,
ONBOARDING_UNLOCK_ROUTE,
TOKEN_DETAILS,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
@@ -162,6 +161,8 @@ export default class Routes extends Component {
isNetworkMenuOpen: PropTypes.bool,
toggleNetworkMenu: PropTypes.func,
accountDetailsAddress: PropTypes.string,
+ isImportNftsModalOpen: PropTypes.bool.isRequired,
+ hideImportNftsModal: PropTypes.func.isRequired,
};
static contextTypes = {
@@ -284,7 +285,6 @@ export default class Routes extends Component {
component={ImportTokenPage}
exact
/>
-
) : null}
+ {isImportNftsModalOpen ? (
+ hideImportNftsModal()} />
+ ) : null}
{isLoading ? : null}
{!isLoading && isNetworkLoading ? : null}
diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js
index ba1c33777..5f31829d1 100644
--- a/ui/pages/routes/routes.container.js
+++ b/ui/pages/routes/routes.container.js
@@ -15,6 +15,7 @@ import {
} from '../../selectors';
import {
lockMetamask,
+ hideImportNftsModal,
setCurrentCurrency,
setLastActiveTime,
setMouseUserState,
@@ -63,6 +64,7 @@ function mapStateToProps(state) {
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
accountDetailsAddress: state.appState.accountDetailsAddress,
+ isImportNftsModalOpen: state.appState.importNftsModalOpen,
};
}
@@ -77,6 +79,7 @@ function mapDispatchToProps(dispatch) {
prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()),
toggleAccountMenu: () => dispatch(toggleAccountMenu()),
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
+ hideImportNftsModal: () => dispatch(hideImportNftsModal()),
};
}
diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts
index 73d488a81..951b6d710 100644
--- a/ui/store/actionConstants.ts
+++ b/ui/store/actionConstants.ts
@@ -9,6 +9,8 @@ export const QR_CODE_DETECTED = 'UI_QR_CODE_DETECTED';
// network dropdown open
export const NETWORK_DROPDOWN_OPEN = 'UI_NETWORK_DROPDOWN_OPEN';
export const NETWORK_DROPDOWN_CLOSE = 'UI_NETWORK_DROPDOWN_CLOSE';
+export const IMPORT_NFTS_MODAL_OPEN = 'UI_IMPORT_NFTS_MODAL_OPEN';
+export const IMPORT_NFTS_MODAL_CLOSE = 'UI_IMPORT_NFTS_MODAL_CLOSE';
// remote state
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index 8708c0fd4..46ef92d12 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -2402,6 +2402,18 @@ export function hideModal(): Action {
};
}
+export function showImportNftsModal(): Action {
+ return {
+ type: actionConstants.IMPORT_NFTS_MODAL_OPEN,
+ };
+}
+
+export function hideImportNftsModal(): Action {
+ return {
+ type: actionConstants.IMPORT_NFTS_MODAL_CLOSE,
+ };
+}
+
export function closeCurrentNotificationWindow(): ThunkAction<
void,
MetaMaskReduxState,