mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Move "Import NFTs" to Modal (#19806)
* moved import nft to modal * fixed modal state * updated port-nft-popup * updated onChange for import nft modal * updated tests * updated tests * updated tests * added story and updated spec file * updated spec file * updated spec file * updated spec file for import-nft * added focus to form field * added autofocus to tokenId
This commit is contained in:
parent
7bdd76a4ad
commit
5bc0ba7f3a
@ -1162,9 +1162,6 @@
|
|||||||
"ui/hooks/useUserPreferencedCurrency.test.js",
|
"ui/hooks/useUserPreferencedCurrency.test.js",
|
||||||
"ui/index.js",
|
"ui/index.js",
|
||||||
"ui/index.test.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/asset.js",
|
||||||
"ui/pages/asset/components/asset-breadcrumb.js",
|
"ui/pages/asset/components/asset-breadcrumb.js",
|
||||||
"ui/pages/asset/components/asset-navigation.js",
|
"ui/pages/asset/components/asset-navigation.js",
|
||||||
|
@ -38,9 +38,11 @@ describe('Import ERC1155 NFT', function () {
|
|||||||
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
||||||
|
|
||||||
// Enter a valid NFT that belongs to user and check success message appears
|
// Enter a valid NFT that belongs to user and check success message appears
|
||||||
await driver.fill('[data-testid="address"]', contractAddress);
|
await driver.fill('#address', contractAddress);
|
||||||
await driver.fill('[data-testid="token-id"]', '1');
|
await driver.fill('#token-id', '1');
|
||||||
await driver.clickElement({ text: 'Add', tag: 'button' });
|
await driver.clickElement(
|
||||||
|
'[data-testid="import-nfts-modal-import-button"]',
|
||||||
|
);
|
||||||
|
|
||||||
const newNftNotification = await driver.findVisibleElement({
|
const newNftNotification = await driver.findVisibleElement({
|
||||||
text: 'NFT was successfully added!',
|
text: 'NFT was successfully added!',
|
||||||
@ -86,14 +88,15 @@ describe('Import ERC1155 NFT', function () {
|
|||||||
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
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
|
// 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('#address', contractAddress);
|
||||||
await driver.fill('[data-testid="token-id"]', '4');
|
await driver.fill('#token-id', '4');
|
||||||
await driver.clickElement({ text: 'Add', tag: 'button' });
|
await driver.clickElement(
|
||||||
|
'[data-testid="import-nfts-modal-import-button"]',
|
||||||
|
);
|
||||||
// Check error message appears
|
// Check error message appears
|
||||||
const invalidNftNotification = await driver.findElement({
|
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.',
|
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);
|
assert.equal(await invalidNftNotification.isDisplayed(), true);
|
||||||
},
|
},
|
||||||
|
@ -38,9 +38,11 @@ describe('Import NFT', function () {
|
|||||||
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
||||||
|
|
||||||
// Enter a valid NFT that belongs to user and check success message appears
|
// Enter a valid NFT that belongs to user and check success message appears
|
||||||
await driver.fill('[data-testid="address"]', contractAddress);
|
await driver.fill('#address', contractAddress);
|
||||||
await driver.fill('[data-testid="token-id"]', '1');
|
await driver.fill('#token-id', '1');
|
||||||
await driver.clickElement({ text: 'Add', tag: 'button' });
|
await driver.clickElement(
|
||||||
|
'[data-testid="import-nfts-modal-import-button"]',
|
||||||
|
);
|
||||||
|
|
||||||
const newNftNotification = await driver.findElement({
|
const newNftNotification = await driver.findElement({
|
||||||
text: 'NFT was successfully added!',
|
text: 'NFT was successfully added!',
|
||||||
@ -85,14 +87,16 @@ describe('Import NFT', function () {
|
|||||||
await driver.clickElement({ text: 'Import NFT', tag: 'button' });
|
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
|
// 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('#address', contractAddress);
|
||||||
await driver.fill('[data-testid="token-id"]', '2');
|
await driver.fill('#token-id', '2');
|
||||||
await driver.clickElement({ text: 'Add', tag: 'button' });
|
await driver.clickElement(
|
||||||
|
'[data-testid="import-nfts-modal-import-button"]',
|
||||||
|
);
|
||||||
|
|
||||||
// Check error message appears
|
// Check error message appears
|
||||||
const invalidNftNotification = await driver.findElement({
|
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.',
|
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);
|
assert.equal(await invalidNftNotification.isDisplayed(), true);
|
||||||
},
|
},
|
||||||
|
@ -7,12 +7,9 @@ import Typography from '../../../ui/typography';
|
|||||||
import { TypographyVariant } from '../../../../helpers/constants/design-system';
|
import { TypographyVariant } from '../../../../helpers/constants/design-system';
|
||||||
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
|
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
|
||||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
import {
|
import { ASSET_ROUTE } from '../../../../helpers/constants/routes';
|
||||||
ADD_NFT_ROUTE,
|
|
||||||
ASSET_ROUTE,
|
|
||||||
} from '../../../../helpers/constants/routes';
|
|
||||||
import { getNfts } from '../../../../ducks/metamask/metamask';
|
import { getNfts } from '../../../../ducks/metamask/metamask';
|
||||||
import { ignoreTokens } from '../../../../store/actions';
|
import { ignoreTokens, showImportNftsModal } from '../../../../store/actions';
|
||||||
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
|
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
|
||||||
|
|
||||||
const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
|
const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
|
||||||
@ -39,10 +36,7 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
|
|||||||
pathname: `${ASSET_ROUTE}/${tokenAddress}/${tokenId}`,
|
pathname: `${ASSET_ROUTE}/${tokenAddress}/${tokenId}`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
history.push({
|
dispatch(showImportNftsModal());
|
||||||
pathname: ADD_NFT_ROUTE,
|
|
||||||
state: { tokenAddress },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
hideModal();
|
hideModal();
|
||||||
}}
|
}}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
.nfts-detection-notice {
|
.nfts-detection-notice {
|
||||||
margin: 16px 16px 0 16px;
|
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.75rem 0.75rem 1rem 0.75rem !important;
|
padding: 0.75rem 0.75rem 1rem 0.75rem !important;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import NftsItems from '../nfts-items';
|
import NftsItems from '../nfts-items';
|
||||||
@ -19,13 +18,14 @@ import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
|
|||||||
import {
|
import {
|
||||||
checkAndUpdateAllNftsOwnershipStatus,
|
checkAndUpdateAllNftsOwnershipStatus,
|
||||||
detectNfts,
|
detectNfts,
|
||||||
|
showImportNftsModal,
|
||||||
} from '../../../store/actions';
|
} from '../../../store/actions';
|
||||||
import { useNftsCollections } from '../../../hooks/useNftsCollections';
|
import { useNftsCollections } from '../../../hooks/useNftsCollections';
|
||||||
import { Box, ButtonLink, IconName, Text } from '../../component-library';
|
import { Box, ButtonLink, IconName, Text } from '../../component-library';
|
||||||
import NftsDetectionNotice from '../nfts-detection-notice';
|
import NftsDetectionNotice from '../nfts-detection-notice';
|
||||||
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
||||||
|
|
||||||
export default function NftsTab({ onAddNFT }) {
|
export default function NftsTab() {
|
||||||
const useNftDetection = useSelector(getUseNftDetection);
|
const useNftDetection = useSelector(getUseNftDetection);
|
||||||
const isMainnet = useSelector(getIsMainnet);
|
const isMainnet = useSelector(getIsMainnet);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -60,7 +60,11 @@ export default function NftsTab({ onAddNFT }) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isMainnet && !useNftDetection ? <NftsDetectionNotice /> : null}{' '}
|
{isMainnet && !useNftDetection ? (
|
||||||
|
<Box padding={4}>
|
||||||
|
<NftsDetectionNotice />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
<Box
|
<Box
|
||||||
padding={12}
|
padding={12}
|
||||||
display={Display.Flex}
|
display={Display.Flex}
|
||||||
@ -90,7 +94,6 @@ export default function NftsTab({ onAddNFT }) {
|
|||||||
</Text>
|
</Text>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
size={Size.MD}
|
size={Size.MD}
|
||||||
data-testid="import-nft-button"
|
|
||||||
href={ZENDESK_URLS.NFT_TOKENS}
|
href={ZENDESK_URLS.NFT_TOKENS}
|
||||||
externalLink
|
externalLink
|
||||||
>
|
>
|
||||||
@ -113,7 +116,9 @@ export default function NftsTab({ onAddNFT }) {
|
|||||||
size={Size.MD}
|
size={Size.MD}
|
||||||
data-testid="import-nft-button"
|
data-testid="import-nft-button"
|
||||||
startIconName={IconName.Add}
|
startIconName={IconName.Add}
|
||||||
onClick={onAddNFT}
|
onClick={() => {
|
||||||
|
dispatch(showImportNftsModal());
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('importNFT')}
|
{t('importNFT')}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
@ -149,7 +154,3 @@ export default function NftsTab({ onAddNFT }) {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NftsTab.propTypes = {
|
|
||||||
onAddNFT: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
@ -150,7 +150,6 @@ const render = ({
|
|||||||
selectedAddress,
|
selectedAddress,
|
||||||
chainId = '0x1',
|
chainId = '0x1',
|
||||||
useNftDetection,
|
useNftDetection,
|
||||||
onAddNFT = jest.fn(),
|
|
||||||
}) => {
|
}) => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
metamask: {
|
metamask: {
|
||||||
@ -170,7 +169,7 @@ const render = ({
|
|||||||
nftsDropdownState,
|
nftsDropdownState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return renderWithProvider(<NftsTab onAddNFT={onAddNFT} />, store);
|
return renderWithProvider(<NftsTab />, store);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('NFT Items', () => {
|
describe('NFT Items', () => {
|
||||||
@ -295,16 +294,5 @@ describe('NFT Items', () => {
|
|||||||
expect(historyPushMock).toHaveBeenCalledTimes(1);
|
expect(historyPushMock).toHaveBeenCalledTimes(1);
|
||||||
expect(historyPushMock).toHaveBeenCalledWith(EXPERIMENTAL_ROUTE);
|
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
286
ui/components/multichain/import-nfts-modal/import-nfts-modal.js
Normal file
286
ui/components/multichain/import-nfts-modal/import-nfts-modal.js
Normal file
@ -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 (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="import-nfts-modal"
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader
|
||||||
|
onClose={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('importNFT')}
|
||||||
|
</ModalHeader>
|
||||||
|
<Box>
|
||||||
|
{isMainnet && !useNftDetection ? (
|
||||||
|
<Box marginTop={6}>
|
||||||
|
<NftsDetectionNotice />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
{nftAddFailed && (
|
||||||
|
<Box marginTop={6}>
|
||||||
|
<BannerAlert
|
||||||
|
severity={Severity.Danger}
|
||||||
|
onClose={() => setNftAddFailed(false)}
|
||||||
|
closeButtonProps={{ 'data-testid': 'add-nft-error-close' }}
|
||||||
|
>
|
||||||
|
{t('nftAddFailedMessage')}
|
||||||
|
</BannerAlert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
flexDirection={FlexDirection.Column}
|
||||||
|
gap={6}
|
||||||
|
marginTop={6}
|
||||||
|
marginBottom={6}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
alignItems={AlignItems.flexEnd}
|
||||||
|
>
|
||||||
|
<Box display={Display.Flex} alignItems={AlignItems.center}>
|
||||||
|
<Label htmlFor="address">{t('address')}</Label>
|
||||||
|
<Tooltip
|
||||||
|
title={t('importNFTAddressToolTip')}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={IconName.Info}
|
||||||
|
size={IconSize.Sm}
|
||||||
|
marginLeft={1}
|
||||||
|
color={IconColor.iconAlternative}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<FormTextField
|
||||||
|
autoFocus
|
||||||
|
dataTestId="address"
|
||||||
|
id="address"
|
||||||
|
placeholder="0x..."
|
||||||
|
value={nftAddress}
|
||||||
|
onChange={(e) => {
|
||||||
|
validateAndSetAddress(e.target.value);
|
||||||
|
setNftAddFailed(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
alignItems={AlignItems.flexEnd}
|
||||||
|
>
|
||||||
|
<Box display={Display.Flex} alignItems={AlignItems.center}>
|
||||||
|
<Label htmlFor="token-id">{t('tokenId')}</Label>
|
||||||
|
<Tooltip
|
||||||
|
title={t('importNFTTokenIdToolTip')}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={IconName.Info}
|
||||||
|
size={IconSize.Sm}
|
||||||
|
marginLeft={1}
|
||||||
|
color={IconColor.iconAlternative}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<FormTextField
|
||||||
|
autoFocus
|
||||||
|
dataTestId="token-id"
|
||||||
|
id="token-id"
|
||||||
|
placeholder={t('nftTokenIdPlaceholder')}
|
||||||
|
value={tokenId}
|
||||||
|
onChange={(e) => {
|
||||||
|
validateAndSetTokenId(e.target.value);
|
||||||
|
setNftAddFailed(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
display={Display.Flex}
|
||||||
|
flexDirection={FlexDirection.Row}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
gap={4}
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={4}
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
size={Size.LG}
|
||||||
|
onClick={() => onClose()}
|
||||||
|
block
|
||||||
|
className="import-nfts-modal__cancel-button"
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</ButtonSecondary>
|
||||||
|
<ButtonPrimary
|
||||||
|
size={Size.LG}
|
||||||
|
onClick={() => handleAddNft()}
|
||||||
|
disabled={disabled}
|
||||||
|
block
|
||||||
|
data-testid="import-nfts-modal-import-button"
|
||||||
|
>
|
||||||
|
{t('import')}
|
||||||
|
</ButtonPrimary>
|
||||||
|
</Box>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportNftsModal.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
@ -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) => <ImportNftsModal {...args} />;
|
||||||
|
DefaultStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={createStore()}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
@ -3,16 +3,16 @@ import { fireEvent, waitFor } from '@testing-library/react';
|
|||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { renderWithProvider } from '../../../test/jest/rendering';
|
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||||
import mockState from '../../../test/data/mock-state.json';
|
import mockState from '../../../../test/data/mock-state.json';
|
||||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
|
||||||
import {
|
import {
|
||||||
addNftVerifyOwnership,
|
addNftVerifyOwnership,
|
||||||
ignoreTokens,
|
ignoreTokens,
|
||||||
setNewNftAddedMessage,
|
setNewNftAddedMessage,
|
||||||
updateNftDropDownState,
|
updateNftDropDownState,
|
||||||
} from '../../store/actions';
|
} from '../../../store/actions';
|
||||||
import AddNft from '.';
|
import { ImportNftsModal } from '.';
|
||||||
|
|
||||||
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9';
|
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9';
|
||||||
const INVALID_ADDRESS = 'aoinsafasdfa';
|
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
|
addNftVerifyOwnership: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(jest.fn().mockResolvedValue()),
|
.mockReturnValue(jest.fn().mockResolvedValue()),
|
||||||
@ -47,56 +47,72 @@ jest.mock('../../store/actions.ts', () => ({
|
|||||||
.mockReturnValue(jest.fn().mockResolvedValue()),
|
.mockReturnValue(jest.fn().mockResolvedValue()),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AddNft', () => {
|
describe('ImportNftsModal', () => {
|
||||||
const store = configureMockStore([thunk])(mockState);
|
const store = configureMockStore([thunk])(mockState);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable the "Add" button when valid entries are input into both Address and TokenId fields', () => {
|
it('should enable the "Import" button when valid entries are input into both Address and TokenId fields', () => {
|
||||||
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
|
const { getByText, getByPlaceholderText } = renderWithProvider(
|
||||||
expect(getByText('Add')).not.toBeEnabled();
|
<ImportNftsModal />,
|
||||||
fireEvent.change(getByTestId('address'), {
|
store,
|
||||||
|
);
|
||||||
|
expect(getByText('Import')).not.toBeEnabled();
|
||||||
|
const addressInput = getByPlaceholderText('0x...');
|
||||||
|
const tokenIdInput = getByPlaceholderText('Enter the token id');
|
||||||
|
fireEvent.change(addressInput, {
|
||||||
target: { value: VALID_ADDRESS },
|
target: { value: VALID_ADDRESS },
|
||||||
});
|
});
|
||||||
fireEvent.change(getByTestId('token-id'), {
|
fireEvent.change(tokenIdInput, {
|
||||||
target: { value: VALID_TOKENID },
|
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', () => {
|
it('should not enable the "Import" button when an invalid entry is input into one or both Address and TokenId fields', () => {
|
||||||
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
|
const { getByText, getByPlaceholderText } = renderWithProvider(
|
||||||
expect(getByText('Add')).not.toBeEnabled();
|
<ImportNftsModal />,
|
||||||
fireEvent.change(getByTestId('address'), {
|
store,
|
||||||
|
);
|
||||||
|
expect(getByText('Import')).not.toBeEnabled();
|
||||||
|
const addressInput = getByPlaceholderText('0x...');
|
||||||
|
const tokenIdInput = getByPlaceholderText('Enter the token id');
|
||||||
|
fireEvent.change(addressInput, {
|
||||||
target: { value: INVALID_ADDRESS },
|
target: { value: INVALID_ADDRESS },
|
||||||
});
|
});
|
||||||
fireEvent.change(getByTestId('token-id'), {
|
fireEvent.change(tokenIdInput, {
|
||||||
target: { value: VALID_TOKENID },
|
target: { value: VALID_TOKENID },
|
||||||
});
|
});
|
||||||
expect(getByText('Add')).not.toBeEnabled();
|
expect(getByText('Import')).not.toBeEnabled();
|
||||||
fireEvent.change(getByTestId('address'), {
|
fireEvent.change(addressInput, {
|
||||||
target: { value: VALID_ADDRESS },
|
target: { value: VALID_ADDRESS },
|
||||||
});
|
});
|
||||||
expect(getByText('Add')).toBeEnabled();
|
expect(getByText('Import')).toBeEnabled();
|
||||||
fireEvent.change(getByTestId('token-id'), {
|
fireEvent.change(tokenIdInput, {
|
||||||
target: { value: INVALID_TOKENID },
|
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 () => {
|
it('should call addNftVerifyOwnership, updateNftDropDownState, setNewNftAddedMessage, and ignoreTokens action with correct values (tokenId should not be in scientific notation)', async () => {
|
||||||
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
|
const onClose = jest.fn();
|
||||||
fireEvent.change(getByTestId('address'), {
|
const { getByPlaceholderText, getByText } = renderWithProvider(
|
||||||
|
<ImportNftsModal onClose={onClose} />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
const addressInput = getByPlaceholderText('0x...');
|
||||||
|
const tokenIdInput = getByPlaceholderText('Enter the token id');
|
||||||
|
fireEvent.change(addressInput, {
|
||||||
target: { value: VALID_ADDRESS },
|
target: { value: VALID_ADDRESS },
|
||||||
});
|
});
|
||||||
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
|
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
|
||||||
fireEvent.change(getByTestId('token-id'), {
|
fireEvent.change(tokenIdInput, {
|
||||||
target: { value: LARGE_TOKEN_ID },
|
target: { value: LARGE_TOKEN_ID },
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(getByText('Add'));
|
fireEvent.click(getByText('Import'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(addNftVerifyOwnership).toHaveBeenCalledWith(
|
expect(addNftVerifyOwnership).toHaveBeenCalledWith(
|
||||||
@ -127,16 +143,21 @@ describe('AddNft', () => {
|
|||||||
jest.fn().mockRejectedValue(new Error('error')),
|
jest.fn().mockRejectedValue(new Error('error')),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
|
const { getByTestId, getByText, getByPlaceholderText } = renderWithProvider(
|
||||||
fireEvent.change(getByTestId('address'), {
|
<ImportNftsModal />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
const addressInput = getByPlaceholderText('0x...');
|
||||||
|
const tokenIdInput = getByPlaceholderText('Enter the token id');
|
||||||
|
fireEvent.change(addressInput, {
|
||||||
target: { value: VALID_ADDRESS },
|
target: { value: VALID_ADDRESS },
|
||||||
});
|
});
|
||||||
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
|
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
|
||||||
fireEvent.change(getByTestId('token-id'), {
|
fireEvent.change(tokenIdInput, {
|
||||||
target: { value: LARGE_TOKEN_ID },
|
target: { value: LARGE_TOKEN_ID },
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(getByText('Add'));
|
fireEvent.click(getByText('Import'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(setNewNftAddedMessage).toHaveBeenCalledWith('error');
|
expect(setNewNftAddedMessage).toHaveBeenCalledWith('error');
|
||||||
@ -148,19 +169,23 @@ describe('AddNft', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should route to default route when cancel button is clicked', () => {
|
it('should route to default route when cancel button is clicked', () => {
|
||||||
const { queryByTestId } = renderWithProvider(<AddNft />, store);
|
const onClose = jest.fn();
|
||||||
|
const { getByText } = renderWithProvider(
|
||||||
|
<ImportNftsModal onClose={onClose} />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
const cancelButton = queryByTestId('page-container-footer-cancel');
|
const cancelButton = getByText('Cancel');
|
||||||
fireEvent.click(cancelButton);
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
|
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should route to default route when close button is clicked', () => {
|
it('should route to default route when close button is clicked', () => {
|
||||||
const { queryByLabelText } = renderWithProvider(<AddNft />, store);
|
const onClose = jest.fn();
|
||||||
|
renderWithProvider(<ImportNftsModal onClose={onClose} />, store);
|
||||||
|
|
||||||
const closeButton = queryByLabelText('close');
|
fireEvent.click(document.querySelector('button[aria-label="Close"]'));
|
||||||
fireEvent.click(closeButton);
|
|
||||||
|
|
||||||
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
|
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
|
||||||
});
|
});
|
1
ui/components/multichain/import-nfts-modal/index.js
Normal file
1
ui/components/multichain/import-nfts-modal/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ImportNftsModal } from './import-nfts-modal';
|
@ -15,3 +15,4 @@ export { ProductTour } from './product-tour-popover';
|
|||||||
export { AccountDetails } from './account-details';
|
export { AccountDetails } from './account-details';
|
||||||
export { CreateAccount } from './create-account';
|
export { CreateAccount } from './create-account';
|
||||||
export { ImportAccount } from './import-account';
|
export { ImportAccount } from './import-account';
|
||||||
|
export { ImportNftsModal } from './import-nfts-modal';
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
* unintended overrides.
|
* unintended overrides.
|
||||||
**/
|
**/
|
||||||
@import 'address-copy-button/index';
|
@import 'address-copy-button/index';
|
||||||
|
@import 'import-nfts-modal/index';
|
||||||
@import 'account-list-item/index';
|
@import 'account-list-item/index';
|
||||||
@import 'account-list-item-menu/index';
|
@import 'account-list-item-menu/index';
|
||||||
@import 'account-list-menu/index';
|
@import 'account-list-menu/index';
|
||||||
|
@ -26,6 +26,7 @@ interface AppState {
|
|||||||
values?: { address?: string | null };
|
values?: { address?: string | null };
|
||||||
} | null;
|
} | null;
|
||||||
networkDropdownOpen: boolean;
|
networkDropdownOpen: boolean;
|
||||||
|
importNftsModalOpen: boolean;
|
||||||
accountDetail: {
|
accountDetail: {
|
||||||
subview?: string;
|
subview?: string;
|
||||||
accountExport?: string;
|
accountExport?: string;
|
||||||
@ -94,6 +95,7 @@ const initialState: AppState = {
|
|||||||
alertMessage: null,
|
alertMessage: null,
|
||||||
qrCodeData: null,
|
qrCodeData: null,
|
||||||
networkDropdownOpen: false,
|
networkDropdownOpen: false,
|
||||||
|
importNftsModalOpen: false,
|
||||||
accountDetail: {
|
accountDetail: {
|
||||||
privateKey: '',
|
privateKey: '',
|
||||||
},
|
},
|
||||||
@ -163,6 +165,17 @@ export default function reduceApp(
|
|||||||
networkDropdownOpen: false,
|
networkDropdownOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case actionConstants.IMPORT_NFTS_MODAL_OPEN:
|
||||||
|
return {
|
||||||
|
...appState,
|
||||||
|
importNftsModalOpen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case actionConstants.IMPORT_NFTS_MODAL_CLOSE:
|
||||||
|
return {
|
||||||
|
...appState,
|
||||||
|
importNftsModalOpen: false,
|
||||||
|
};
|
||||||
// alert methods
|
// alert methods
|
||||||
case actionConstants.ALERT_OPEN:
|
case actionConstants.ALERT_OPEN:
|
||||||
return {
|
return {
|
||||||
|
@ -66,7 +66,6 @@ const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status';
|
|||||||
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
|
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
|
||||||
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
|
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
|
||||||
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
|
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
|
||||||
const ADD_NFT_ROUTE = '/add-nft';
|
|
||||||
|
|
||||||
const ONBOARDING_ROUTE = '/onboarding';
|
const ONBOARDING_ROUTE = '/onboarding';
|
||||||
const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase';
|
const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase';
|
||||||
@ -273,7 +272,6 @@ export {
|
|||||||
SWAPS_ERROR_ROUTE,
|
SWAPS_ERROR_ROUTE,
|
||||||
SWAPS_MAINTENANCE_ROUTE,
|
SWAPS_MAINTENANCE_ROUTE,
|
||||||
SMART_TRANSACTION_STATUS_ROUTE,
|
SMART_TRANSACTION_STATUS_ROUTE,
|
||||||
ADD_NFT_ROUTE,
|
|
||||||
ONBOARDING_ROUTE,
|
ONBOARDING_ROUTE,
|
||||||
ONBOARDING_HELP_US_IMPROVE_ROUTE,
|
ONBOARDING_HELP_US_IMPROVE_ROUTE,
|
||||||
ONBOARDING_CREATE_PASSWORD_ROUTE,
|
ONBOARDING_CREATE_PASSWORD_ROUTE,
|
||||||
|
@ -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 (
|
|
||||||
<PageContainer
|
|
||||||
title={t('importNFT')}
|
|
||||||
onSubmit={() => {
|
|
||||||
handleAddNft();
|
|
||||||
}}
|
|
||||||
submitText={t('add')}
|
|
||||||
onCancel={() => {
|
|
||||||
history.push(DEFAULT_ROUTE);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
history.push(DEFAULT_ROUTE);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
contentComponent={
|
|
||||||
<Box>
|
|
||||||
{isMainnet && !useNftDetection ? <NftsDetectionNotice /> : null}
|
|
||||||
{nftAddFailed && (
|
|
||||||
<Box marginLeft={4} marginRight={4}>
|
|
||||||
<ActionableMessage
|
|
||||||
type="danger"
|
|
||||||
useIcon
|
|
||||||
iconFillColor="var(--color-error-default)"
|
|
||||||
message={
|
|
||||||
<Box display={DISPLAY.INLINE_FLEX}>
|
|
||||||
<Typography
|
|
||||||
variant={TypographyVariant.H7}
|
|
||||||
fontWeight={FONT_WEIGHT.NORMAL}
|
|
||||||
marginTop={0}
|
|
||||||
>
|
|
||||||
{t('nftAddFailedMessage')}
|
|
||||||
</Typography>
|
|
||||||
<ButtonIcon
|
|
||||||
className="add-nft__close"
|
|
||||||
iconName={IconName.Close}
|
|
||||||
size={ButtonIconSize.Sm}
|
|
||||||
ariaLabel={t('close')}
|
|
||||||
data-testid="add-nft-error-close"
|
|
||||||
onClick={() => setNftAddFailed(false)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box margin={4}>
|
|
||||||
<FormField
|
|
||||||
dataTestId="address"
|
|
||||||
titleText={t('address')}
|
|
||||||
placeholder="0x..."
|
|
||||||
value={nftAddress}
|
|
||||||
onChange={(val) => {
|
|
||||||
validateAndSetAddress(val);
|
|
||||||
setNftAddFailed(false);
|
|
||||||
}}
|
|
||||||
tooltipText={t('importNFTAddressToolTip')}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
dataTestId="token-id"
|
|
||||||
titleText={t('tokenId')}
|
|
||||||
placeholder={t('nftTokenIdPlaceholder')}
|
|
||||||
value={tokenId}
|
|
||||||
onChange={(val) => {
|
|
||||||
validateAndSetTokenId(val);
|
|
||||||
setNftAddFailed(false);
|
|
||||||
}}
|
|
||||||
tooltipText={t('importNFTTokenIdToolTip')}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './add-nft';
|
|
@ -62,7 +62,6 @@ import {
|
|||||||
BUILD_QUOTE_ROUTE,
|
BUILD_QUOTE_ROUTE,
|
||||||
VIEW_QUOTE_ROUTE,
|
VIEW_QUOTE_ROUTE,
|
||||||
CONFIRMATION_V_NEXT_ROUTE,
|
CONFIRMATION_V_NEXT_ROUTE,
|
||||||
ADD_NFT_ROUTE,
|
|
||||||
ONBOARDING_SECURE_YOUR_WALLET_ROUTE,
|
ONBOARDING_SECURE_YOUR_WALLET_ROUTE,
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
CONFIRM_INSTITUTIONAL_FEATURE_CONNECT,
|
CONFIRM_INSTITUTIONAL_FEATURE_CONNECT,
|
||||||
@ -879,11 +878,7 @@ export default class Home extends PureComponent {
|
|||||||
name={this.context.t('nfts')}
|
name={this.context.t('nfts')}
|
||||||
tabKey="nfts"
|
tabKey="nfts"
|
||||||
>
|
>
|
||||||
<NftsTab
|
<NftsTab />
|
||||||
onAddNFT={() => {
|
|
||||||
history.push(ADD_NFT_ROUTE);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from '../../helpers/utils/util';
|
} from '../../helpers/utils/util';
|
||||||
import { tokenInfoGetter } from '../../helpers/utils/token-util';
|
import { tokenInfoGetter } from '../../helpers/utils/token-util';
|
||||||
import {
|
import {
|
||||||
ADD_NFT_ROUTE,
|
|
||||||
CONFIRM_IMPORT_TOKEN_ROUTE,
|
CONFIRM_IMPORT_TOKEN_ROUTE,
|
||||||
SECURITY_ROUTE,
|
SECURITY_ROUTE,
|
||||||
} from '../../helpers/constants/routes';
|
} from '../../helpers/constants/routes';
|
||||||
@ -60,6 +59,11 @@ class ImportToken extends Component {
|
|||||||
*/
|
*/
|
||||||
clearPendingTokens: PropTypes.func,
|
clearPendingTokens: PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the list of pending tokens. Called when closing the modal.
|
||||||
|
*/
|
||||||
|
showImportNftsModal: PropTypes.func,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of already added tokens.
|
* The list of already added tokens.
|
||||||
*/
|
*/
|
||||||
@ -314,21 +318,15 @@ class ImportToken extends Component {
|
|||||||
nftAddressError: this.context.t('nftAddressError', [
|
nftAddressError: this.context.t('nftAddressError', [
|
||||||
<a
|
<a
|
||||||
className="import-token__nft-address-error-link"
|
className="import-token__nft-address-error-link"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
this.props.history.push({
|
this.props.showImportNftsModal();
|
||||||
pathname: ADD_NFT_ROUTE,
|
}}
|
||||||
state: {
|
|
||||||
addressEnteredOnImportTokensPage: this.state.customAddress,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
key="nftAddressError"
|
key="nftAddressError"
|
||||||
>
|
>
|
||||||
{this.context.t('importNFTPage')}
|
{this.context.t('importNFTPage')}
|
||||||
</a>,
|
</a>,
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case isMainnetToken && !isMainnetNetwork:
|
case isMainnetToken && !isMainnetNetwork:
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
setPendingTokens,
|
setPendingTokens,
|
||||||
clearPendingTokens,
|
clearPendingTokens,
|
||||||
getTokenStandardAndDetails,
|
getTokenStandardAndDetails,
|
||||||
|
showImportNftsModal,
|
||||||
} from '../../store/actions';
|
} from '../../store/actions';
|
||||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||||
import { getProviderConfig } from '../../ducks/metamask/metamask';
|
import { getProviderConfig } from '../../ducks/metamask/metamask';
|
||||||
@ -58,6 +59,7 @@ const mapDispatchToProps = (dispatch) => {
|
|||||||
return {
|
return {
|
||||||
setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)),
|
setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)),
|
||||||
clearPendingTokens: () => dispatch(clearPendingTokens()),
|
clearPendingTokens: () => dispatch(clearPendingTokens()),
|
||||||
|
showImportNftsModal: () => dispatch(showImportNftsModal()),
|
||||||
getTokenStandardAndDetails: (address, selectedAddress) =>
|
getTokenStandardAndDetails: (address, selectedAddress) =>
|
||||||
getTokenStandardAndDetails(address, selectedAddress, null),
|
getTokenStandardAndDetails(address, selectedAddress, null),
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
/** Please import your files in alphabetical order **/
|
/** Please import your files in alphabetical order **/
|
||||||
@import 'add-nft/index';
|
|
||||||
@import 'keyring-snaps/index';
|
@import 'keyring-snaps/index';
|
||||||
@import 'import-token/index';
|
@import 'import-token/index';
|
||||||
@import 'asset/asset';
|
@import 'asset/asset';
|
||||||
|
@ -19,7 +19,6 @@ import PermissionsConnect from '../permissions-connect';
|
|||||||
import RestoreVaultPage from '../keychains/restore-vault';
|
import RestoreVaultPage from '../keychains/restore-vault';
|
||||||
import RevealSeedConfirmation from '../keychains/reveal-seed';
|
import RevealSeedConfirmation from '../keychains/reveal-seed';
|
||||||
import ImportTokenPage from '../import-token';
|
import ImportTokenPage from '../import-token';
|
||||||
import AddNftPage from '../add-nft';
|
|
||||||
import ConfirmImportTokenPage from '../confirm-import-token';
|
import ConfirmImportTokenPage from '../confirm-import-token';
|
||||||
import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
|
import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
|
||||||
import CreateAccountPage from '../create-account/create-account.component';
|
import CreateAccountPage from '../create-account/create-account.component';
|
||||||
@ -33,6 +32,7 @@ import {
|
|||||||
AccountListMenu,
|
AccountListMenu,
|
||||||
NetworkListMenu,
|
NetworkListMenu,
|
||||||
AccountDetails,
|
AccountDetails,
|
||||||
|
ImportNftsModal,
|
||||||
} from '../../components/multichain';
|
} from '../../components/multichain';
|
||||||
import UnlockPage from '../unlock-page';
|
import UnlockPage from '../unlock-page';
|
||||||
import Alerts from '../../components/app/alerts';
|
import Alerts from '../../components/app/alerts';
|
||||||
@ -80,7 +80,6 @@ import {
|
|||||||
CONFIRMATION_V_NEXT_ROUTE,
|
CONFIRMATION_V_NEXT_ROUTE,
|
||||||
CONFIRM_IMPORT_TOKEN_ROUTE,
|
CONFIRM_IMPORT_TOKEN_ROUTE,
|
||||||
ONBOARDING_ROUTE,
|
ONBOARDING_ROUTE,
|
||||||
ADD_NFT_ROUTE,
|
|
||||||
ONBOARDING_UNLOCK_ROUTE,
|
ONBOARDING_UNLOCK_ROUTE,
|
||||||
TOKEN_DETAILS,
|
TOKEN_DETAILS,
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
||||||
@ -162,6 +161,8 @@ export default class Routes extends Component {
|
|||||||
isNetworkMenuOpen: PropTypes.bool,
|
isNetworkMenuOpen: PropTypes.bool,
|
||||||
toggleNetworkMenu: PropTypes.func,
|
toggleNetworkMenu: PropTypes.func,
|
||||||
accountDetailsAddress: PropTypes.string,
|
accountDetailsAddress: PropTypes.string,
|
||||||
|
isImportNftsModalOpen: PropTypes.bool.isRequired,
|
||||||
|
hideImportNftsModal: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -284,7 +285,6 @@ export default class Routes extends Component {
|
|||||||
component={ImportTokenPage}
|
component={ImportTokenPage}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Authenticated path={ADD_NFT_ROUTE} component={AddNftPage} exact />
|
|
||||||
<Authenticated
|
<Authenticated
|
||||||
path={CONFIRM_IMPORT_TOKEN_ROUTE}
|
path={CONFIRM_IMPORT_TOKEN_ROUTE}
|
||||||
component={ConfirmImportTokenPage}
|
component={ConfirmImportTokenPage}
|
||||||
@ -525,6 +525,8 @@ export default class Routes extends Component {
|
|||||||
toggleNetworkMenu,
|
toggleNetworkMenu,
|
||||||
accountDetailsAddress,
|
accountDetailsAddress,
|
||||||
location,
|
location,
|
||||||
|
isImportNftsModalOpen,
|
||||||
|
hideImportNftsModal,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const loadMessage =
|
const loadMessage =
|
||||||
loadingMessage || isNetworkLoading
|
loadingMessage || isNetworkLoading
|
||||||
@ -583,6 +585,9 @@ export default class Routes extends Component {
|
|||||||
{accountDetailsAddress ? (
|
{accountDetailsAddress ? (
|
||||||
<AccountDetails address={accountDetailsAddress} />
|
<AccountDetails address={accountDetailsAddress} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{isImportNftsModalOpen ? (
|
||||||
|
<ImportNftsModal onClose={() => hideImportNftsModal()} />
|
||||||
|
) : null}
|
||||||
<Box className="main-container-wrapper">
|
<Box className="main-container-wrapper">
|
||||||
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
|
{isLoading ? <Loading loadingMessage={loadMessage} /> : null}
|
||||||
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
|
{!isLoading && isNetworkLoading ? <LoadingNetwork /> : null}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
lockMetamask,
|
lockMetamask,
|
||||||
|
hideImportNftsModal,
|
||||||
setCurrentCurrency,
|
setCurrentCurrency,
|
||||||
setLastActiveTime,
|
setLastActiveTime,
|
||||||
setMouseUserState,
|
setMouseUserState,
|
||||||
@ -63,6 +64,7 @@ function mapStateToProps(state) {
|
|||||||
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
||||||
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
|
isNetworkMenuOpen: state.metamask.isNetworkMenuOpen,
|
||||||
accountDetailsAddress: state.appState.accountDetailsAddress,
|
accountDetailsAddress: state.appState.accountDetailsAddress,
|
||||||
|
isImportNftsModalOpen: state.appState.importNftsModalOpen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +79,7 @@ function mapDispatchToProps(dispatch) {
|
|||||||
prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()),
|
prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()),
|
||||||
toggleAccountMenu: () => dispatch(toggleAccountMenu()),
|
toggleAccountMenu: () => dispatch(toggleAccountMenu()),
|
||||||
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
|
toggleNetworkMenu: () => dispatch(toggleNetworkMenu()),
|
||||||
|
hideImportNftsModal: () => dispatch(hideImportNftsModal()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ export const QR_CODE_DETECTED = 'UI_QR_CODE_DETECTED';
|
|||||||
// network dropdown open
|
// network dropdown open
|
||||||
export const NETWORK_DROPDOWN_OPEN = 'UI_NETWORK_DROPDOWN_OPEN';
|
export const NETWORK_DROPDOWN_OPEN = 'UI_NETWORK_DROPDOWN_OPEN';
|
||||||
export const NETWORK_DROPDOWN_CLOSE = 'UI_NETWORK_DROPDOWN_CLOSE';
|
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
|
// remote state
|
||||||
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
|
export const UPDATE_METAMASK_STATE = 'UPDATE_METAMASK_STATE';
|
||||||
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
|
export const SELECTED_ADDRESS_CHANGED = 'SELECTED_ADDRESS_CHANGED';
|
||||||
|
@ -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<
|
export function closeCurrentNotificationWindow(): ThunkAction<
|
||||||
void,
|
void,
|
||||||
MetaMaskReduxState,
|
MetaMaskReduxState,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user