mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Add modal with directions to re-add token as NFT (#13291)
* Add modal pop for NFTs previously added as tokens - with directions to re-add as NFTs
This commit is contained in:
parent
5b92dc4cf0
commit
f087e501a1
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "Ιστορικό"
|
"message": "Ιστορικό"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "Αναγνωριστικό"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Εισαγωγή",
|
"message": "Εισαγωγή",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -596,6 +596,9 @@
|
|||||||
"contractInteraction": {
|
"contractInteraction": {
|
||||||
"message": "Contract Interaction"
|
"message": "Contract Interaction"
|
||||||
},
|
},
|
||||||
|
"convertTokenToNFTDescription": {
|
||||||
|
"message": "We've detected that this asset is an NFT. Metamask now has full native support for NFTs. Would you like to remove it from your token list and add it as an NFT?"
|
||||||
|
},
|
||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "Copied!"
|
"message": "Copied!"
|
||||||
},
|
},
|
||||||
@ -1367,9 +1370,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "History"
|
"message": "History"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Import",
|
"message": "Import",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
@ -1406,9 +1406,15 @@
|
|||||||
"importNFT": {
|
"importNFT": {
|
||||||
"message": "Import NFT"
|
"message": "Import NFT"
|
||||||
},
|
},
|
||||||
|
"importNFTAddressToolTip": {
|
||||||
|
"message": "On OpenSea, for example, on the NFT's page under Details, there is a blue hyperlinked value labeled 'Contract Address'. If you click on this, it will take you to the contract's address on Etherscan; at the top-left of that page, there should be an icon labeled 'Contract', and to the right, a long string of letters and numbers. This is the address of the contract that created your NFT. Click on the 'copy' icon to the right of the address, and you'll have it on your clipboard."
|
||||||
|
},
|
||||||
"importNFTPage": {
|
"importNFTPage": {
|
||||||
"message": "Import NFT page"
|
"message": "Import NFT page"
|
||||||
},
|
},
|
||||||
|
"importNFTTokenIdToolTip": {
|
||||||
|
"message": "A collectible's ID is a unique identifier since no two NFTs are alike. Again, on OpenSea this number is under 'Details'. Make a note of it, or copy it onto your clipboard."
|
||||||
|
},
|
||||||
"importNFTs": {
|
"importNFTs": {
|
||||||
"message": "Import NFTs"
|
"message": "Import NFTs"
|
||||||
},
|
},
|
||||||
@ -1462,6 +1468,9 @@
|
|||||||
"invalidAddressRecipientNotEthNetwork": {
|
"invalidAddressRecipientNotEthNetwork": {
|
||||||
"message": "Not ETH network, set to lowercase"
|
"message": "Not ETH network, set to lowercase"
|
||||||
},
|
},
|
||||||
|
"invalidAssetType": {
|
||||||
|
"message": "This asset is an NFT and needs to be re-added on the Import NFTs page found under the NFTs tab"
|
||||||
|
},
|
||||||
"invalidBlockExplorerURL": {
|
"invalidBlockExplorerURL": {
|
||||||
"message": "Invalid Block Explorer URL"
|
"message": "Invalid Block Explorer URL"
|
||||||
},
|
},
|
||||||
@ -1914,7 +1923,7 @@
|
|||||||
"description": "The next nonce according to MetaMask's internal logic"
|
"description": "The next nonce according to MetaMask's internal logic"
|
||||||
},
|
},
|
||||||
"nftTokenIdPlaceholder": {
|
"nftTokenIdPlaceholder": {
|
||||||
"message": "Enter the collectible ID"
|
"message": "Enter the Token ID"
|
||||||
},
|
},
|
||||||
"nfts": {
|
"nfts": {
|
||||||
"message": "NFTs"
|
"message": "NFTs"
|
||||||
@ -3531,6 +3540,9 @@
|
|||||||
"message": "$1 of $2 pending",
|
"message": "$1 of $2 pending",
|
||||||
"description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total"
|
"description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total"
|
||||||
},
|
},
|
||||||
|
"yes": {
|
||||||
|
"message": "Yes"
|
||||||
|
},
|
||||||
"yesLetsTry": {
|
"yesLetsTry": {
|
||||||
"message": "Yes, let's try"
|
"message": "Yes, let's try"
|
||||||
},
|
},
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "Historique"
|
"message": "Historique"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Importer",
|
"message": "Importer",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "इतिहास"
|
"message": "इतिहास"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "आयात करें",
|
"message": "आयात करें",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "Riwayat"
|
"message": "Riwayat"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Impor",
|
"message": "Impor",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "履歴"
|
"message": "履歴"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "インポート",
|
"message": "インポート",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "기록"
|
"message": "기록"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "가져오기",
|
"message": "가져오기",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "История"
|
"message": "История"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "Ид."
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Импорт",
|
"message": "Импорт",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "History"
|
"message": "History"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Mag-import",
|
"message": "Mag-import",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "Geçmiş"
|
"message": "Geçmiş"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "Kimlik"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Al",
|
"message": "Al",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "Lịch sử"
|
"message": "Lịch sử"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Nhập",
|
"message": "Nhập",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -1306,9 +1306,6 @@
|
|||||||
"history": {
|
"history": {
|
||||||
"message": "历史记录"
|
"message": "历史记录"
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"message": "ID"
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"message": "导入",
|
"message": "导入",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
|
@ -13,6 +13,7 @@ import { useMetricEvent } from '../../../hooks/useMetricEvent';
|
|||||||
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
|
import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
|
||||||
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
||||||
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
||||||
|
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
|
||||||
|
|
||||||
const AssetListItem = ({
|
const AssetListItem = ({
|
||||||
className,
|
className,
|
||||||
@ -65,21 +66,26 @@ const AssetListItem = ({
|
|||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
className="asset-list-item__send-token-button"
|
className="asset-list-item__send-token-button"
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
sendTokenEvent();
|
sendTokenEvent();
|
||||||
dispatch(
|
try {
|
||||||
updateSendAsset({
|
await dispatch(
|
||||||
type: ASSET_TYPES.TOKEN,
|
updateSendAsset({
|
||||||
details: {
|
type: ASSET_TYPES.TOKEN,
|
||||||
address: tokenAddress,
|
details: {
|
||||||
decimals: tokenDecimals,
|
address: tokenAddress,
|
||||||
symbol: tokenSymbol,
|
decimals: tokenDecimals,
|
||||||
},
|
symbol: tokenSymbol,
|
||||||
}),
|
},
|
||||||
).then(() => {
|
}),
|
||||||
|
);
|
||||||
history.push(SEND_ROUTE);
|
history.push(SEND_ROUTE);
|
||||||
});
|
} catch (err) {
|
||||||
|
if (!err.message.includes(INVALID_ASSET_TYPE)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('sendSpecifiedTokens', [tokenSymbol])}
|
{t('sendSpecifiedTokens', [tokenSymbol])}
|
||||||
|
@ -20,7 +20,7 @@ export default function CollectiblesDetectionNotice() {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box marginBottom={8} className="collectibles-detection-notice">
|
<Box marginBottom={4} className="collectibles-detection-notice">
|
||||||
<Dialog type="message" className="collectibles-detection-notice__message">
|
<Dialog type="message" className="collectibles-detection-notice__message">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCollectiblesDetectionNoticeDismissed()}
|
onClick={() => setCollectiblesDetectionNoticeDismissed()}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
a.collectibles-detection-notice__message__link {
|
a.collectibles-detection-notice__message__link {
|
||||||
@include H6;
|
@include H6;
|
||||||
|
|
||||||
width: 60%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Modal from '../../modal';
|
||||||
|
import Typography from '../../../ui/typography';
|
||||||
|
import { TYPOGRAPHY } from '../../../../helpers/constants/design-system';
|
||||||
|
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import { ADD_COLLECTIBLE_ROUTE } from '../../../../helpers/constants/routes';
|
||||||
|
|
||||||
|
const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const t = useI18nContext();
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onSubmit={() => {
|
||||||
|
history.push({
|
||||||
|
pathname: ADD_COLLECTIBLE_ROUTE,
|
||||||
|
state: { tokenAddress },
|
||||||
|
});
|
||||||
|
hideModal();
|
||||||
|
}}
|
||||||
|
submitText={t('yes')}
|
||||||
|
onCancel={() => hideModal()}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
>
|
||||||
|
<div className="convert-token-to-nft-modal">
|
||||||
|
<Typography
|
||||||
|
variant={TYPOGRAPHY.H6}
|
||||||
|
boxProps={{
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('convertTokenToNFTDescription')}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ConvertTokenToNFTModal.propTypes = {
|
||||||
|
hideModal: PropTypes.func.isRequired,
|
||||||
|
tokenAddress: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withModalProps(ConvertTokenToNFTModal);
|
@ -0,0 +1,4 @@
|
|||||||
|
.convert-token-to-nft-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
@import 'qr-scanner/index';
|
@import 'qr-scanner/index';
|
||||||
@import 'transaction-confirmed/index';
|
@import 'transaction-confirmed/index';
|
||||||
@import 'customize-nonce/index';
|
@import 'customize-nonce/index';
|
||||||
|
@import 'convert-token-to-nft-modal/index';
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
z-index: 1050;
|
z-index: 1050;
|
||||||
|
@ -30,6 +30,7 @@ import AddToAddressBookModal from './add-to-addressbook-modal';
|
|||||||
import EditApprovalPermission from './edit-approval-permission';
|
import EditApprovalPermission from './edit-approval-permission';
|
||||||
import NewAccountModal from './new-account-modal';
|
import NewAccountModal from './new-account-modal';
|
||||||
import CustomizeNonceModal from './customize-nonce';
|
import CustomizeNonceModal from './customize-nonce';
|
||||||
|
import ConvertTokenToNftModal from './convert-token-to-nft-modal/convert-token-to-nft-modal';
|
||||||
|
|
||||||
const modalContainerBaseStyle = {
|
const modalContainerBaseStyle = {
|
||||||
transform: 'translate3d(-50%, 0, 0px)',
|
transform: 'translate3d(-50%, 0, 0px)',
|
||||||
@ -237,6 +238,19 @@ const MODALS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
CONVERT_TOKEN_TO_NFT: {
|
||||||
|
contents: <ConvertTokenToNftModal />,
|
||||||
|
mobileModalStyle: {
|
||||||
|
...modalContainerMobileStyle,
|
||||||
|
},
|
||||||
|
laptopModalStyle: {
|
||||||
|
...modalContainerLaptopStyle,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
CONFIRM_DELETE_NETWORK: {
|
CONFIRM_DELETE_NETWORK: {
|
||||||
contents: <ConfirmDeleteNetwork />,
|
contents: <ConfirmDeleteNetwork />,
|
||||||
mobileModalStyle: {
|
mobileModalStyle: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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';
|
||||||
@ -29,6 +29,8 @@ import SwapIcon from '../../ui/icon/swap-icon.component';
|
|||||||
import SendIcon from '../../ui/icon/overview-send-icon.component';
|
import SendIcon from '../../ui/icon/overview-send-icon.component';
|
||||||
|
|
||||||
import IconButton from '../../ui/icon-button';
|
import IconButton from '../../ui/icon-button';
|
||||||
|
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
|
||||||
|
import { showModal } from '../../../store/actions';
|
||||||
import WalletOverview from './wallet-overview';
|
import WalletOverview from './wallet-overview';
|
||||||
|
|
||||||
const TokenOverview = ({ className, token }) => {
|
const TokenOverview = ({ className, token }) => {
|
||||||
@ -59,6 +61,17 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token.isERC721) {
|
||||||
|
dispatch(
|
||||||
|
showModal({
|
||||||
|
name: 'CONVERT_TOKEN_TO_NFT',
|
||||||
|
tokenAddress: token.address,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [token.isERC721, token.address, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletOverview
|
<WalletOverview
|
||||||
balance={
|
balance={
|
||||||
@ -81,16 +94,21 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="token-overview__button"
|
className="token-overview__button"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
sendTokenEvent();
|
sendTokenEvent();
|
||||||
dispatch(
|
try {
|
||||||
updateSendAsset({
|
await dispatch(
|
||||||
type: ASSET_TYPES.TOKEN,
|
updateSendAsset({
|
||||||
details: token,
|
type: ASSET_TYPES.TOKEN,
|
||||||
}),
|
details: token,
|
||||||
).then(() => {
|
}),
|
||||||
|
);
|
||||||
history.push(SEND_ROUTE);
|
history.push(SEND_ROUTE);
|
||||||
});
|
} catch (err) {
|
||||||
|
if (!err.message.includes(INVALID_ASSET_TYPE)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
Icon={SendIcon}
|
Icon={SendIcon}
|
||||||
label={t('send')}
|
label={t('send')}
|
||||||
|
74
ui/components/app/wallet-overview/token-overview.test.js
Normal file
74
ui/components/app/wallet-overview/token-overview.test.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||||
|
import TokenOverview from './token-overview';
|
||||||
|
|
||||||
|
describe('TokenOverview', () => {
|
||||||
|
const mockStore = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
|
},
|
||||||
|
identities: {
|
||||||
|
'0x1': {
|
||||||
|
address: '0x1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedAddress: '0x1',
|
||||||
|
keyrings: [
|
||||||
|
{
|
||||||
|
type: 'HD Key Tree',
|
||||||
|
accounts: ['0x1', '0x2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Ledger Hardware',
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
contractExchangeRates: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = configureMockStore([thunk])(mockStore);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.clearActions();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TokenOverview', () => {
|
||||||
|
it('should not show a modal when token passed in props is not an ERC721', () => {
|
||||||
|
const token = {
|
||||||
|
name: 'test',
|
||||||
|
isERC721: false,
|
||||||
|
address: '0x01',
|
||||||
|
symbol: 'test',
|
||||||
|
};
|
||||||
|
renderWithProvider(<TokenOverview token={token} />, store);
|
||||||
|
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show ConvertTokenToNFT modal when token passed in props is an ERC721', () => {
|
||||||
|
const token = {
|
||||||
|
name: 'test',
|
||||||
|
isERC721: true,
|
||||||
|
address: '0x01',
|
||||||
|
symbol: 'test',
|
||||||
|
};
|
||||||
|
renderWithProvider(<TokenOverview token={token} />, store);
|
||||||
|
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions).toHaveLength(1);
|
||||||
|
expect(actions[0].type).toBe('UI_MODAL_OPEN');
|
||||||
|
expect(actions[0].payload).toStrictEqual({
|
||||||
|
name: 'CONVERT_TOKEN_TO_NFT',
|
||||||
|
tokenAddress: '0x01',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -30,6 +30,7 @@ export default function FormField({
|
|||||||
password,
|
password,
|
||||||
allowDecimals,
|
allowDecimals,
|
||||||
disabled,
|
disabled,
|
||||||
|
placeholder,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -85,6 +86,7 @@ export default function FormField({
|
|||||||
allowDecimals={allowDecimals}
|
allowDecimals={allowDecimals}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
dataTestId={dataTestId}
|
dataTestId={dataTestId}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@ -97,6 +99,7 @@ export default function FormField({
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
@ -170,6 +173,10 @@ FormField.propTypes = {
|
|||||||
* Check if the form disabled
|
* Check if the form disabled
|
||||||
*/
|
*/
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Set the placeholder text for the input field
|
||||||
|
*/
|
||||||
|
placeholder: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
FormField.defaultProps = {
|
FormField.defaultProps = {
|
||||||
|
@ -30,6 +30,7 @@ export default {
|
|||||||
password: { control: 'boolean' },
|
password: { control: 'boolean' },
|
||||||
allowDecimals: { control: 'boolean' },
|
allowDecimals: { control: 'boolean' },
|
||||||
disabled: { control: 'boolean' },
|
disabled: { control: 'boolean' },
|
||||||
|
placeholder: { control: 'text' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ export default function NumericInput({
|
|||||||
allowDecimals = true,
|
allowDecimals = true,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
dataTestId,
|
dataTestId,
|
||||||
|
placeholder,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -40,6 +41,7 @@ export default function NumericInput({
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
{detailText && (
|
{detailText && (
|
||||||
<Typography color={COLORS.UI4} variant={TYPOGRAPHY.H7} tag="span">
|
<Typography color={COLORS.UI4} variant={TYPOGRAPHY.H7} tag="span">
|
||||||
@ -59,4 +61,5 @@ NumericInput.propTypes = {
|
|||||||
allowDecimals: PropTypes.bool,
|
allowDecimals: PropTypes.bool,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
dataTestId: PropTypes.string,
|
dataTestId: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -53,11 +53,12 @@ import {
|
|||||||
hideLoadingIndication,
|
hideLoadingIndication,
|
||||||
showConfTxPage,
|
showConfTxPage,
|
||||||
showLoadingIndication,
|
showLoadingIndication,
|
||||||
updateTokenType,
|
|
||||||
updateTransaction,
|
updateTransaction,
|
||||||
addPollingTokenToAppState,
|
addPollingTokenToAppState,
|
||||||
removePollingTokenFromAppState,
|
removePollingTokenFromAppState,
|
||||||
isCollectibleOwner,
|
isCollectibleOwner,
|
||||||
|
getTokenStandardAndDetails,
|
||||||
|
showModal,
|
||||||
} from '../../store/actions';
|
} from '../../store/actions';
|
||||||
import { setCustomGasLimit } from '../gas/gas.duck';
|
import { setCustomGasLimit } from '../gas/gas.duck';
|
||||||
import {
|
import {
|
||||||
@ -91,9 +92,16 @@ import {
|
|||||||
isValidHexAddress,
|
isValidHexAddress,
|
||||||
} from '../../../shared/modules/hexstring-utils';
|
} from '../../../shared/modules/hexstring-utils';
|
||||||
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
|
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
|
||||||
import { ERC20, ETH, GWEI } from '../../helpers/constants/common';
|
import {
|
||||||
|
ERC20,
|
||||||
|
ERC721,
|
||||||
|
ERC1155,
|
||||||
|
ETH,
|
||||||
|
GWEI,
|
||||||
|
} from '../../helpers/constants/common';
|
||||||
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
|
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
|
||||||
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
|
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
|
||||||
|
import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys';
|
||||||
// typedefs
|
// typedefs
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
|
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
|
||||||
@ -607,6 +615,8 @@ export const initialState = {
|
|||||||
// In the case of tokens, the address, decimals and symbol of the token
|
// In the case of tokens, the address, decimals and symbol of the token
|
||||||
// will be included in details
|
// will be included in details
|
||||||
details: null,
|
details: null,
|
||||||
|
// error to display when there is an issue with the asset
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
draftTransaction: {
|
draftTransaction: {
|
||||||
// The metamask internal id of the transaction. Only populated in the EDIT
|
// The metamask internal id of the transaction. Only populated in the EDIT
|
||||||
@ -736,6 +746,7 @@ const slice = createSlice({
|
|||||||
state.amount.value = action.payload.amount;
|
state.amount.value = action.payload.amount;
|
||||||
state.gas.error = null;
|
state.gas.error = null;
|
||||||
state.amount.error = null;
|
state.amount.error = null;
|
||||||
|
state.asset.error = null;
|
||||||
state.recipient.address = action.payload.address;
|
state.recipient.address = action.payload.address;
|
||||||
state.recipient.nickname = action.payload.nickname;
|
state.recipient.nickname = action.payload.nickname;
|
||||||
state.draftTransaction.id = action.payload.id;
|
state.draftTransaction.id = action.payload.id;
|
||||||
@ -887,6 +898,7 @@ const slice = createSlice({
|
|||||||
updateAsset: (state, action) => {
|
updateAsset: (state, action) => {
|
||||||
state.asset.type = action.payload.type;
|
state.asset.type = action.payload.type;
|
||||||
state.asset.balance = action.payload.balance;
|
state.asset.balance = action.payload.balance;
|
||||||
|
state.asset.error = action.payload.error;
|
||||||
if (
|
if (
|
||||||
state.asset.type === ASSET_TYPES.TOKEN ||
|
state.asset.type === ASSET_TYPES.TOKEN ||
|
||||||
state.asset.type === ASSET_TYPES.COLLECTIBLE
|
state.asset.type === ASSET_TYPES.COLLECTIBLE
|
||||||
@ -1146,7 +1158,7 @@ const slice = createSlice({
|
|||||||
},
|
},
|
||||||
validateSendState: (state) => {
|
validateSendState: (state) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
// 1 + 2. State is invalid when either gas or amount fields have errors
|
// 1 + 2. State is invalid when either gas or amount or asset fields have errors
|
||||||
// 3. State is invalid if asset type is a token and the token details
|
// 3. State is invalid if asset type is a token and the token details
|
||||||
// are unknown.
|
// are unknown.
|
||||||
// 4. State is invalid if no recipient has been added
|
// 4. State is invalid if no recipient has been added
|
||||||
@ -1156,6 +1168,7 @@ const slice = createSlice({
|
|||||||
// 8. State is invalid if the selected asset is a ERC721
|
// 8. State is invalid if the selected asset is a ERC721
|
||||||
case Boolean(state.amount.error):
|
case Boolean(state.amount.error):
|
||||||
case Boolean(state.gas.error):
|
case Boolean(state.gas.error):
|
||||||
|
case Boolean(state.asset.error):
|
||||||
case state.asset.type === ASSET_TYPES.TOKEN &&
|
case state.asset.type === ASSET_TYPES.TOKEN &&
|
||||||
state.asset.details === null:
|
state.asset.details === null:
|
||||||
case state.stage === SEND_STAGES.ADD_RECIPIENT:
|
case state.stage === SEND_STAGES.ADD_RECIPIENT:
|
||||||
@ -1166,10 +1179,6 @@ const slice = createSlice({
|
|||||||
):
|
):
|
||||||
state.status = SEND_STATUSES.INVALID;
|
state.status = SEND_STATUSES.INVALID;
|
||||||
break;
|
break;
|
||||||
case state.asset.type === ASSET_TYPES.TOKEN &&
|
|
||||||
state.asset.details.isERC721 === true:
|
|
||||||
state.status = SEND_STATUSES.INVALID;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
state.status = SEND_STATUSES.VALID;
|
state.status = SEND_STATUSES.VALID;
|
||||||
// Recompute the draftTransaction object
|
// Recompute the draftTransaction object
|
||||||
@ -1419,31 +1428,43 @@ export function updateSendAmount(amount) {
|
|||||||
export function updateSendAsset({ type, details }) {
|
export function updateSendAsset({ type, details }) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
let { balance } = state.send.asset;
|
let { balance, error } = state.send.asset;
|
||||||
|
const userAddress = state.send.account.address ?? getSelectedAddress(state);
|
||||||
if (type === ASSET_TYPES.TOKEN) {
|
if (type === ASSET_TYPES.TOKEN) {
|
||||||
// if changing to a token, get the balance from the network. The asset
|
|
||||||
// overview page and asset list on the wallet overview page contain
|
|
||||||
// send buttons that call this method before initialization occurs.
|
|
||||||
// When this happens we don't yet have an account.address so default to
|
|
||||||
// the currently active account. In addition its possible for the balance
|
|
||||||
// check to take a decent amount of time, so we display a loading
|
|
||||||
// indication so that that immediate feedback is displayed to the user.
|
|
||||||
await dispatch(showLoadingIndication());
|
|
||||||
balance = await getERC20Balance(
|
|
||||||
details,
|
|
||||||
state.send.account.address ?? getSelectedAddress(state),
|
|
||||||
);
|
|
||||||
// TODO remove along with migration of isERC721 tokens and stripping away this designation
|
|
||||||
if (details) {
|
if (details) {
|
||||||
if (details.isERC721 === undefined) {
|
|
||||||
const updatedAssetDetails = await updateTokenType(details.address);
|
|
||||||
details.isERC721 = updatedAssetDetails.isERC721;
|
|
||||||
}
|
|
||||||
if (details.standard === undefined) {
|
if (details.standard === undefined) {
|
||||||
details.standard = ERC20;
|
await dispatch(showLoadingIndication());
|
||||||
|
const { standard } = await getTokenStandardAndDetails(
|
||||||
|
details.address,
|
||||||
|
userAddress,
|
||||||
|
);
|
||||||
|
if (standard === ERC721 || standard === ERC1155) {
|
||||||
|
await dispatch(hideLoadingIndication());
|
||||||
|
dispatch(
|
||||||
|
showModal({
|
||||||
|
name: 'CONVERT_TOKEN_TO_NFT',
|
||||||
|
tokenAddress: details.address,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
error = INVALID_ASSET_TYPE;
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
details.standard = standard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if changing to a token, get the balance from the network. The asset
|
||||||
|
// overview page and asset list on the wallet overview page contain
|
||||||
|
// send buttons that call this method before initialization occurs.
|
||||||
|
// When this happens we don't yet have an account.address so default to
|
||||||
|
// the currently active account. In addition its possible for the balance
|
||||||
|
// check to take a decent amount of time, so we display a loading
|
||||||
|
// indication so that that immediate feedback is displayed to the user.
|
||||||
|
if (details.standard === ERC20) {
|
||||||
|
error = null;
|
||||||
|
balance = await getERC20Balance(details, userAddress);
|
||||||
|
}
|
||||||
|
await dispatch(hideLoadingIndication());
|
||||||
}
|
}
|
||||||
await dispatch(hideLoadingIndication());
|
|
||||||
} else if (type === ASSET_TYPES.COLLECTIBLE) {
|
} else if (type === ASSET_TYPES.COLLECTIBLE) {
|
||||||
let isCurrentOwner = true;
|
let isCurrentOwner = true;
|
||||||
try {
|
try {
|
||||||
@ -1452,16 +1473,17 @@ export function updateSendAsset({ type, details }) {
|
|||||||
details.address,
|
details.address,
|
||||||
details.tokenId,
|
details.tokenId,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
if (error.message.includes('Unable to verify ownership.')) {
|
if (err.message.includes('Unable to verify ownership.')) {
|
||||||
// this would indicate that either our attempts to verify ownership failed because of network issues,
|
// this would indicate that either our attempts to verify ownership failed because of network issues,
|
||||||
// or, somehow a token has been added to collectibles state with an incorrect chainId.
|
// or, somehow a token has been added to collectibles state with an incorrect chainId.
|
||||||
} else {
|
} else {
|
||||||
// Any other error is unexpected and should be surfaced.
|
// Any other error is unexpected and should be surfaced.
|
||||||
dispatch(displayWarning(error.message));
|
dispatch(displayWarning(err.message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isCurrentOwner) {
|
if (isCurrentOwner) {
|
||||||
|
error = null;
|
||||||
balance = '0x1';
|
balance = '0x1';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -1469,12 +1491,13 @@ export function updateSendAsset({ type, details }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
error = null;
|
||||||
// if changing to native currency, get it from the account key in send
|
// if changing to native currency, get it from the account key in send
|
||||||
// state which is kept in sync when accounts change.
|
// state which is kept in sync when accounts change.
|
||||||
balance = state.send.account.balance;
|
balance = state.send.account.balance;
|
||||||
}
|
}
|
||||||
// update the asset in state which will re-run amount and gas validation
|
// update the asset in state which will re-run amount and gas validation
|
||||||
await dispatch(actions.updateAsset({ type, details, balance }));
|
await dispatch(actions.updateAsset({ type, details, balance, error }));
|
||||||
await dispatch(computeEstimatedGasLimit());
|
await dispatch(computeEstimatedGasLimit());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1870,7 +1893,6 @@ export function getGasInputMode(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Asset Selectors
|
// Asset Selectors
|
||||||
|
|
||||||
export function getSendAsset(state) {
|
export function getSendAsset(state) {
|
||||||
return state[name].asset;
|
return state[name].asset;
|
||||||
}
|
}
|
||||||
@ -1886,6 +1908,10 @@ export function getIsAssetSendable(state) {
|
|||||||
return state[name].asset.details.isERC721 === false;
|
return state[name].asset.details.isERC721 === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAssetError(state) {
|
||||||
|
return state[name].asset.error;
|
||||||
|
}
|
||||||
|
|
||||||
// Amount Selectors
|
// Amount Selectors
|
||||||
export function getSendAmount(state) {
|
export function getSendAmount(state) {
|
||||||
return state[name].amount.value;
|
return state[name].amount.value;
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
TRANSACTION_ENVELOPE_TYPES,
|
TRANSACTION_ENVELOPE_TYPES,
|
||||||
TRANSACTION_TYPES,
|
TRANSACTION_TYPES,
|
||||||
} from '../../../shared/constants/transaction';
|
} from '../../../shared/constants/transaction';
|
||||||
|
import * as Actions from '../../store/actions';
|
||||||
import sendReducer, {
|
import sendReducer, {
|
||||||
initialState,
|
initialState,
|
||||||
initializeSendState,
|
initializeSendState,
|
||||||
@ -67,17 +68,6 @@ import sendReducer, {
|
|||||||
|
|
||||||
const mockStore = createMockStore([thunk]);
|
const mockStore = createMockStore([thunk]);
|
||||||
|
|
||||||
jest.mock('../../store/actions', () => {
|
|
||||||
const actual = jest.requireActual('../../store/actions');
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
estimateGas: jest.fn(() => Promise.resolve('0x0')),
|
|
||||||
getGasFeeEstimatesAndStartPolling: jest.fn(() => Promise.resolve()),
|
|
||||||
updateTokenType: jest.fn(() => Promise.resolve({ isERC721: false })),
|
|
||||||
isCollectibleOwner: jest.fn(() => Promise.resolve(true)),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('./send', () => {
|
jest.mock('./send', () => {
|
||||||
const actual = jest.requireActual('./send');
|
const actual = jest.requireActual('./send');
|
||||||
return {
|
return {
|
||||||
@ -88,6 +78,25 @@ jest.mock('./send', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Send Slice', () => {
|
describe('Send Slice', () => {
|
||||||
|
let getTokenStandardAndDetailsStub;
|
||||||
|
beforeEach(() => {
|
||||||
|
getTokenStandardAndDetailsStub = jest
|
||||||
|
.spyOn(Actions, 'getTokenStandardAndDetails')
|
||||||
|
.mockImplementation(() => Promise.resolve({ standard: 'ERC20' }));
|
||||||
|
jest
|
||||||
|
.spyOn(Actions, 'estimateGas')
|
||||||
|
.mockImplementation(() => Promise.resolve('0x0'));
|
||||||
|
jest
|
||||||
|
.spyOn(Actions, 'getGasFeeEstimatesAndStartPolling')
|
||||||
|
.mockImplementation(() => Promise.resolve());
|
||||||
|
jest
|
||||||
|
.spyOn(Actions, 'updateTokenType')
|
||||||
|
.mockImplementation(() => Promise.resolve({ isERC721: false }));
|
||||||
|
jest
|
||||||
|
.spyOn(Actions, 'isCollectibleOwner')
|
||||||
|
.mockImplementation(() => Promise.resolve(true));
|
||||||
|
});
|
||||||
|
|
||||||
describe('Reducers', () => {
|
describe('Reducers', () => {
|
||||||
describe('updateSendAmount', () => {
|
describe('updateSendAmount', () => {
|
||||||
it('should', async () => {
|
it('should', async () => {
|
||||||
@ -1457,6 +1466,7 @@ describe('Send Slice', () => {
|
|||||||
expect(actionResult[0].payload).toStrictEqual({
|
expect(actionResult[0].payload).toStrictEqual({
|
||||||
...newSendAsset,
|
...newSendAsset,
|
||||||
balance: '',
|
balance: '',
|
||||||
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(actionResult[1].type).toStrictEqual(
|
expect(actionResult[1].type).toStrictEqual(
|
||||||
@ -1499,6 +1509,7 @@ describe('Send Slice', () => {
|
|||||||
expect(actionResult[2].payload).toStrictEqual({
|
expect(actionResult[2].payload).toStrictEqual({
|
||||||
...newSendAsset,
|
...newSendAsset,
|
||||||
balance: '0x0',
|
balance: '0x0',
|
||||||
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(actionResult[3].type).toStrictEqual(
|
expect(actionResult[3].type).toStrictEqual(
|
||||||
@ -1511,6 +1522,37 @@ describe('Send Slice', () => {
|
|||||||
'send/computeEstimatedGasLimit/fulfilled',
|
'send/computeEstimatedGasLimit/fulfilled',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show ConvertTokenToNFT modal and throw "invalidAssetType" error when token passed in props is an ERC721 or ERC1155', async () => {
|
||||||
|
getTokenStandardAndDetailsStub.mockImplementation(() =>
|
||||||
|
Promise.resolve({ standard: 'ERC1155' }),
|
||||||
|
);
|
||||||
|
const store = mockStore(defaultSendAssetState);
|
||||||
|
|
||||||
|
const newSendAsset = {
|
||||||
|
type: ASSET_TYPES.TOKEN,
|
||||||
|
details: {
|
||||||
|
address: 'tokenAddress',
|
||||||
|
symbol: 'tokenSymbol',
|
||||||
|
decimals: 'tokenDecimals',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(() =>
|
||||||
|
store.dispatch(updateSendAsset(newSendAsset)),
|
||||||
|
).rejects.toThrow('invalidAssetType');
|
||||||
|
const actionResult = store.getActions();
|
||||||
|
expect(actionResult).toHaveLength(3);
|
||||||
|
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION');
|
||||||
|
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION');
|
||||||
|
expect(actionResult[2]).toStrictEqual({
|
||||||
|
payload: {
|
||||||
|
name: 'CONVERT_TOKEN_TO_NFT',
|
||||||
|
tokenAddress: 'tokenAddress',
|
||||||
|
},
|
||||||
|
type: 'UI_MODAL_OPEN',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateRecipientUserInput', () => {
|
describe('updateRecipientUserInput', () => {
|
||||||
@ -2231,6 +2273,7 @@ describe('Send Slice', () => {
|
|||||||
expect(actionResult[0].payload).toStrictEqual({
|
expect(actionResult[0].payload).toStrictEqual({
|
||||||
balance: '0x1',
|
balance: '0x1',
|
||||||
type: ASSET_TYPES.COLLECTIBLE,
|
type: ASSET_TYPES.COLLECTIBLE,
|
||||||
|
error: null,
|
||||||
details: {
|
details: {
|
||||||
address: '0xTokenAddress',
|
address: '0xTokenAddress',
|
||||||
description: 'A test NFT dispensed from faucet.paradigm.xyz.',
|
description: 'A test NFT dispensed from faucet.paradigm.xyz.',
|
||||||
@ -2361,11 +2404,11 @@ describe('Send Slice', () => {
|
|||||||
expect(actionResult[2].payload).toStrictEqual({
|
expect(actionResult[2].payload).toStrictEqual({
|
||||||
balance: '0x0',
|
balance: '0x0',
|
||||||
type: ASSET_TYPES.TOKEN,
|
type: ASSET_TYPES.TOKEN,
|
||||||
|
error: null,
|
||||||
details: {
|
details: {
|
||||||
address: '0xTokenAddress',
|
address: '0xTokenAddress',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
symbol: 'SYMB',
|
symbol: 'SYMB',
|
||||||
isERC721: false,
|
|
||||||
standard: 'ERC20',
|
standard: 'ERC20',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ export const SECONDARY = 'SECONDARY';
|
|||||||
|
|
||||||
export const ERC20 = 'ERC20';
|
export const ERC20 = 'ERC20';
|
||||||
export const ERC721 = 'ERC721';
|
export const ERC721 = 'ERC721';
|
||||||
|
export const ERC1155 = 'ERC1155';
|
||||||
|
|
||||||
export const GAS_ESTIMATE_TYPES = {
|
export const GAS_ESTIMATE_TYPES = {
|
||||||
SLOW: 'SLOW',
|
SLOW: 'SLOW',
|
||||||
|
@ -7,3 +7,4 @@ export const GAS_PRICE_FETCH_FAILURE_ERROR_KEY = 'gasPriceFetchFailed';
|
|||||||
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
|
export const GAS_PRICE_EXCESSIVE_ERROR_KEY = 'gasPriceExcessive';
|
||||||
export const UNSENDABLE_ASSET_ERROR_KEY = 'unsendableAsset';
|
export const UNSENDABLE_ASSET_ERROR_KEY = 'unsendableAsset';
|
||||||
export const INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY = 'insufficientFundsForGas';
|
export const INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY = 'insufficientFundsForGas';
|
||||||
|
export const INVALID_ASSET_TYPE = 'invalidAssetType';
|
||||||
|
@ -17,6 +17,8 @@ import {
|
|||||||
getShouldShowFiat,
|
getShouldShowFiat,
|
||||||
getPreferences,
|
getPreferences,
|
||||||
txDataSelector,
|
txDataSelector,
|
||||||
|
getCurrentKeyring,
|
||||||
|
getTokenExchangeRates,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { ETH } from '../../helpers/constants/common';
|
import { ETH } from '../../helpers/constants/common';
|
||||||
|
|
||||||
@ -140,6 +142,12 @@ export const generateUseSelectorRouter = ({
|
|||||||
if (selector === getEIP1559V2Enabled) {
|
if (selector === getEIP1559V2Enabled) {
|
||||||
return eip1559V2Enabled;
|
return eip1559V2Enabled;
|
||||||
}
|
}
|
||||||
|
if (selector === getCurrentKeyring) {
|
||||||
|
return { type: '' };
|
||||||
|
}
|
||||||
|
if (selector === getTokenExchangeRates) {
|
||||||
|
return { '0x1': '1' };
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,41 +1,60 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { util } from '@metamask/controllers';
|
import { util } from '@metamask/controllers';
|
||||||
import { useI18nContext } from '../../hooks/useI18nContext';
|
import { useI18nContext } from '../../hooks/useI18nContext';
|
||||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
||||||
|
|
||||||
import Box from '../../components/ui/box';
|
import Box from '../../components/ui/box';
|
||||||
import TextField from '../../components/ui/text-field';
|
|
||||||
import PageContainer from '../../components/ui/page-container';
|
import PageContainer from '../../components/ui/page-container';
|
||||||
import {
|
import {
|
||||||
addCollectibleVerifyOwnership,
|
addCollectibleVerifyOwnership,
|
||||||
|
removeToken,
|
||||||
setNewCollectibleAddedMessage,
|
setNewCollectibleAddedMessage,
|
||||||
} from '../../store/actions';
|
} from '../../store/actions';
|
||||||
|
import FormField from '../../components/ui/form-field';
|
||||||
|
import { getIsMainnet, getUseCollectibleDetection } from '../../selectors';
|
||||||
|
import { getCollectiblesDetectionNoticeDismissed } from '../../ducks/metamask/metamask';
|
||||||
|
import CollectiblesDetectionNotice from '../../components/app/collectibles-detection-notice';
|
||||||
|
|
||||||
export default function AddCollectible() {
|
export default function AddCollectible() {
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const useCollectibleDetection = useSelector(getUseCollectibleDetection);
|
||||||
|
const isMainnet = useSelector(getIsMainnet);
|
||||||
|
const collectibleDetectionNoticeDismissed = useSelector(
|
||||||
|
getCollectiblesDetectionNoticeDismissed,
|
||||||
|
);
|
||||||
const addressEnteredOnImportTokensPage =
|
const addressEnteredOnImportTokensPage =
|
||||||
history?.location?.state?.addressEnteredOnImportTokensPage;
|
history?.location?.state?.addressEnteredOnImportTokensPage;
|
||||||
|
const contractAddressToConvertFromTokenToCollectible =
|
||||||
|
history?.location?.state?.tokenAddress;
|
||||||
|
|
||||||
const [address, setAddress] = useState(
|
const [address, setAddress] = useState(
|
||||||
addressEnteredOnImportTokensPage ?? '',
|
addressEnteredOnImportTokensPage ??
|
||||||
|
contractAddressToConvertFromTokenToCollectible ??
|
||||||
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
const [tokenId, setTokenId] = useState('');
|
const [tokenId, setTokenId] = useState('');
|
||||||
const [disabled, setDisabled] = useState(true);
|
const [disabled, setDisabled] = useState(true);
|
||||||
|
|
||||||
const handleAddCollectible = async () => {
|
const handleAddCollectible = async () => {
|
||||||
try {
|
try {
|
||||||
await dispatch(addCollectibleVerifyOwnership(address, tokenId));
|
await dispatch(
|
||||||
|
addCollectibleVerifyOwnership(address, tokenId.toString()),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { message } = error;
|
const { message } = error;
|
||||||
dispatch(setNewCollectibleAddedMessage(message));
|
dispatch(setNewCollectibleAddedMessage(message));
|
||||||
history.push(DEFAULT_ROUTE);
|
history.push(DEFAULT_ROUTE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (contractAddressToConvertFromTokenToCollectible) {
|
||||||
|
await dispatch(
|
||||||
|
removeToken(contractAddressToConvertFromTokenToCollectible),
|
||||||
|
);
|
||||||
|
}
|
||||||
dispatch(setNewCollectibleAddedMessage('success'));
|
dispatch(setNewCollectibleAddedMessage('success'));
|
||||||
history.push(DEFAULT_ROUTE);
|
history.push(DEFAULT_ROUTE);
|
||||||
};
|
};
|
||||||
@ -66,29 +85,33 @@ export default function AddCollectible() {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
contentComponent={
|
contentComponent={
|
||||||
<Box padding={4}>
|
<Box padding={4}>
|
||||||
|
{isMainnet &&
|
||||||
|
!useCollectibleDetection &&
|
||||||
|
!collectibleDetectionNoticeDismissed ? (
|
||||||
|
<CollectiblesDetectionNotice />
|
||||||
|
) : null}
|
||||||
<Box>
|
<Box>
|
||||||
<TextField
|
<FormField
|
||||||
id="address"
|
id="address"
|
||||||
label={t('address')}
|
titleText={t('address')}
|
||||||
placeholder="0x..."
|
placeholder="0x..."
|
||||||
type="text"
|
|
||||||
value={address}
|
value={address}
|
||||||
onChange={(e) => validateAndSetAddress(e.target.value)}
|
onChange={(val) => validateAndSetAddress(val)}
|
||||||
fullWidth
|
tooltipText={t('importNFTAddressToolTip')}
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="normal"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<TextField
|
<FormField
|
||||||
id="token-id"
|
id="token-id"
|
||||||
label={t('id')}
|
titleText={t('tokenId')}
|
||||||
placeholder={t('nftTokenIdPlaceholder')}
|
placeholder={t('nftTokenIdPlaceholder')}
|
||||||
type="number"
|
|
||||||
value={tokenId}
|
value={tokenId}
|
||||||
onChange={(e) => validateAndSetTokenId(e.target.value)}
|
onChange={(val) => {
|
||||||
fullWidth
|
validateAndSetTokenId(val);
|
||||||
margin="normal"
|
}}
|
||||||
|
tooltipText={t('importNFTTokenIdToolTip')}
|
||||||
|
numeric
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -21,7 +21,7 @@ const Asset = () => {
|
|||||||
|
|
||||||
const collectible = collectibles.find(
|
const collectible = collectibles.find(
|
||||||
({ address, tokenId }) =>
|
({ address, tokenId }) =>
|
||||||
isEqualCaseInsensitive(address, asset) && id === tokenId,
|
isEqualCaseInsensitive(address, asset) && id === tokenId.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -216,7 +216,7 @@ class ImportToken extends Component {
|
|||||||
const isMainnetNetwork = this.props.chainId === '0x1';
|
const isMainnetNetwork = this.props.chainId === '0x1';
|
||||||
|
|
||||||
let standard;
|
let standard;
|
||||||
if (addressIsValid) {
|
if (addressIsValid && process.env.COLLECTIBLES_V1) {
|
||||||
try {
|
try {
|
||||||
({ standard } = await this.props.getTokenStandardAndDetails(
|
({ standard } = await this.props.getTokenStandardAndDetails(
|
||||||
standardAddress,
|
standardAddress,
|
||||||
|
@ -37,6 +37,7 @@ export default class SendContent extends Component {
|
|||||||
getIsBalanceInsufficient: PropTypes.bool,
|
getIsBalanceInsufficient: PropTypes.bool,
|
||||||
asset: PropTypes.object,
|
asset: PropTypes.object,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
|
assetError: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -49,6 +50,7 @@ export default class SendContent extends Component {
|
|||||||
networkOrAccountNotSupports1559,
|
networkOrAccountNotSupports1559,
|
||||||
getIsBalanceInsufficient,
|
getIsBalanceInsufficient,
|
||||||
asset,
|
asset,
|
||||||
|
assetError,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let gasError;
|
let gasError;
|
||||||
@ -67,6 +69,7 @@ export default class SendContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<PageContainerContent>
|
<PageContainerContent>
|
||||||
<div className="send-v2__form">
|
<div className="send-v2__form">
|
||||||
|
{assetError ? this.renderError(assetError) : null}
|
||||||
{gasError ? this.renderError(gasError) : null}
|
{gasError ? this.renderError(gasError) : null}
|
||||||
{isEthGasPrice
|
{isEthGasPrice
|
||||||
? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
|
? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
getIsBalanceInsufficient,
|
getIsBalanceInsufficient,
|
||||||
getSendTo,
|
getSendTo,
|
||||||
getSendAsset,
|
getSendAsset,
|
||||||
|
getAssetError,
|
||||||
} from '../../../ducks/send';
|
} from '../../../ducks/send';
|
||||||
|
|
||||||
import SendContent from './send-content.component';
|
import SendContent from './send-content.component';
|
||||||
@ -32,6 +33,7 @@ function mapStateToProps(state) {
|
|||||||
),
|
),
|
||||||
getIsBalanceInsufficient: getIsBalanceInsufficient(state),
|
getIsBalanceInsufficient: getIsBalanceInsufficient(state),
|
||||||
asset: getSendAsset(state),
|
asset: getSendAsset(state),
|
||||||
|
assetError: getAssetError(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user