1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Add error that redirects users to Import NFT page when they attempt to add an NFT on the Import Token page (#13271)

* Add error that redirects users to Import NFT page when they attempt to add an NFT on the Import Token page
This commit is contained in:
Alex Donesky 2022-01-19 08:38:33 -06:00 committed by GitHub
parent 9a3c917a48
commit f7849a0b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 261 additions and 132 deletions

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Προσθήκη σημειώματος" "message": "Προσθήκη σημειώματος"
}, },
"addNFT": {
"message": "Προσθήκη NFT"
},
"addNetwork": { "addNetwork": {
"message": "Προσθήκη Δικτύου" "message": "Προσθήκη Δικτύου"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Add memo" "message": "Add memo"
}, },
"addNFT": {
"message": "Add NFT"
},
"addNetwork": { "addNetwork": {
"message": "Add Network" "message": "Add Network"
}, },
@ -458,6 +455,10 @@
"close": { "close": {
"message": "Close" "message": "Close"
}, },
"collectibleAddressError": {
"message": "This token is an NFT. Add on the $1",
"description": "$1 is a clickable link with text defined by the 'importNFTPage' key"
},
"confirm": { "confirm": {
"message": "Confirm" "message": "Confirm"
}, },
@ -1402,6 +1403,12 @@
"importMyWallet": { "importMyWallet": {
"message": "Import My Wallet" "message": "Import My Wallet"
}, },
"importNFT": {
"message": "Import NFT"
},
"importNFTPage": {
"message": "Import NFT page"
},
"importNFTs": { "importNFTs": {
"message": "Import NFTs" "message": "Import NFTs"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Ajouter un mémo" "message": "Ajouter un mémo"
}, },
"addNFT": {
"message": "Ajouter un NFT"
},
"addNetwork": { "addNetwork": {
"message": "Ajouter un réseau" "message": "Ajouter un réseau"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "मेमो जोड़ें" "message": "मेमो जोड़ें"
}, },
"addNFT": {
"message": "NFT जोड़ें"
},
"addNetwork": { "addNetwork": {
"message": "नेटवर्क जोड़ें" "message": "नेटवर्क जोड़ें"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Tambahkan memo" "message": "Tambahkan memo"
}, },
"addNFT": {
"message": "Tambahkan NFT"
},
"addNetwork": { "addNetwork": {
"message": "Tambahkan Jaringan" "message": "Tambahkan Jaringan"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "メモを追加" "message": "メモを追加"
}, },
"addNFT": {
"message": "NFTを追加"
},
"addNetwork": { "addNetwork": {
"message": "ネットワークの追加" "message": "ネットワークの追加"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "메모 추가" "message": "메모 추가"
}, },
"addNFT": {
"message": "NFT 추가"
},
"addNetwork": { "addNetwork": {
"message": "네트워크 추가" "message": "네트워크 추가"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Добавить примечание" "message": "Добавить примечание"
}, },
"addNFT": {
"message": "Добавить NFT"
},
"addNetwork": { "addNetwork": {
"message": "Добавить сеть" "message": "Добавить сеть"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Magdagdag ng memo" "message": "Magdagdag ng memo"
}, },
"addNFT": {
"message": "Magdagdag ng NFT"
},
"addNetwork": { "addNetwork": {
"message": "Magdagdag ng Network" "message": "Magdagdag ng Network"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Not ekleyin" "message": "Not ekleyin"
}, },
"addNFT": {
"message": "NFT ekleyin"
},
"addNetwork": { "addNetwork": {
"message": "Ağ ekleyin" "message": "Ağ ekleyin"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "Thêm bản ghi nhớ" "message": "Thêm bản ghi nhớ"
}, },
"addNFT": {
"message": "Thêm NFT"
},
"addNetwork": { "addNetwork": {
"message": "Thêm mạng" "message": "Thêm mạng"
}, },

View File

@ -140,9 +140,6 @@
"addMemo": { "addMemo": {
"message": "添加备忘录" "message": "添加备忘录"
}, },
"addNFT": {
"message": "添加NFT"
},
"addNetwork": { "addNetwork": {
"message": "添加网络" "message": "添加网络"
}, },

View File

@ -220,22 +220,22 @@ export default class MetamaskController extends EventEmitter {
onNetworkStateChange: this.networkController.store.subscribe.bind( onNetworkStateChange: this.networkController.store.subscribe.bind(
this.networkController.store, this.networkController.store,
), ),
getAssetName: this.assetsContractController.getAssetName.bind( getERC721AssetName: this.assetsContractController.getERC721AssetName.bind(
this.assetsContractController, this.assetsContractController,
), ),
getAssetSymbol: this.assetsContractController.getAssetSymbol.bind( getERC721AssetSymbol: this.assetsContractController.getERC721AssetSymbol.bind(
this.assetsContractController, this.assetsContractController,
), ),
getCollectibleTokenURI: this.assetsContractController.getCollectibleTokenURI.bind( getERC721TokenURI: this.assetsContractController.getERC721TokenURI.bind(
this.assetsContractController, this.assetsContractController,
), ),
getOwnerOf: this.assetsContractController.getOwnerOf.bind( getERC721OwnerOf: this.assetsContractController.getERC721OwnerOf.bind(
this.assetsContractController, this.assetsContractController,
), ),
balanceOfERC1155Collectible: this.assetsContractController.balanceOfERC1155Collectible.bind( getERC1155BalanceOf: this.assetsContractController.getERC1155BalanceOf.bind(
this.assetsContractController, this.assetsContractController,
), ),
uriERC1155Collectible: this.assetsContractController.uriERC1155Collectible.bind( getERC1155TokenURI: this.assetsContractController.getERC1155TokenURI.bind(
this.assetsContractController, this.assetsContractController,
), ),
}, },
@ -1040,6 +1040,7 @@ export default class MetamaskController extends EventEmitter {
appStateController, appStateController,
collectiblesController, collectiblesController,
collectibleDetectionController, collectibleDetectionController,
assetsContractController,
currencyRateController, currencyRateController,
detectTokensController, detectTokensController,
ensController, ensController,
@ -1192,6 +1193,11 @@ export default class MetamaskController extends EventEmitter {
preferencesController, preferencesController,
), ),
// AssetsContractController
getTokenStandardAndDetails: assetsContractController.getTokenStandardAndDetails.bind(
assetsContractController,
),
// CollectiblesController // CollectiblesController
addCollectible: collectiblesController.addCollectible.bind( addCollectible: collectiblesController.addCollectible.bind(
collectiblesController, collectiblesController,

View File

@ -107,7 +107,7 @@
"@keystonehq/metamask-airgapped-keyring": "0.2.1", "@keystonehq/metamask-airgapped-keyring": "0.2.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.31.0", "@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^24.0.0", "@metamask/controllers": "^25.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-ledger-bridge-keyring": "^0.10.0",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",

View File

@ -28,6 +28,7 @@
right: 16px; right: 16px;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
background-color: transparent;
&::after { &::after {
content: '\00D7'; content: '\00D7';

View File

@ -47,9 +47,10 @@ export default class PageContainerHeader extends Component {
return ( return (
onClose && ( onClose && (
<div <button
className="page-container__header-close" className="page-container__header-close"
onClick={() => onClose()} onClick={() => onClose()}
aria-label="close"
/> />
) )
); );

View File

@ -246,7 +246,7 @@ TextField.propTypes = {
/** /**
* Show error message * Show error message
*/ */
error: PropTypes.string, error: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
/** /**
* Add custom CSS class * Add custom CSS class
*/ */

View File

@ -17,8 +17,13 @@ export default function AddCollectible() {
const t = useI18nContext(); const t = useI18nContext();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
const addressEnteredOnImportTokensPage =
history?.location?.state?.addressEnteredOnImportTokensPage;
const [address, setAddress] = useState(
addressEnteredOnImportTokensPage ?? '',
);
const [address, setAddress] = useState('');
const [tokenId, setTokenId] = useState(''); const [tokenId, setTokenId] = useState('');
const [disabled, setDisabled] = useState(true); const [disabled, setDisabled] = useState(true);
@ -47,7 +52,7 @@ export default function AddCollectible() {
return ( return (
<PageContainer <PageContainer
title={t('addNFT')} title={t('importNFT')}
onSubmit={() => { onSubmit={() => {
handleAddCollectible(); handleAddCollectible();
}} }}

View File

@ -8,6 +8,7 @@ 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_COLLECTIBLE_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE, CONFIRM_IMPORT_TOKEN_ROUTE,
EXPERIMENTAL_ROUTE, EXPERIMENTAL_ROUTE,
} from '../../helpers/constants/routes'; } from '../../helpers/constants/routes';
@ -46,6 +47,8 @@ class ImportToken extends Component {
rpcPrefs: PropTypes.object, rpcPrefs: PropTypes.object,
tokenList: PropTypes.object, tokenList: PropTypes.object,
useTokenDetection: PropTypes.bool, useTokenDetection: PropTypes.bool,
getTokenStandardAndDetails: PropTypes.func,
selectedAddress: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -62,6 +65,7 @@ class ImportToken extends Component {
customAddressError: null, customAddressError: null,
customSymbolError: null, customSymbolError: null,
customDecimalsError: null, customDecimalsError: null,
collectibleAddressError: null,
forceEditSymbol: false, forceEditSymbol: false,
symbolAutoFilled: false, symbolAutoFilled: false,
decimalAutoFilled: false, decimalAutoFilled: false,
@ -126,13 +130,15 @@ class ImportToken extends Component {
customAddressError, customAddressError,
customSymbolError, customSymbolError,
customDecimalsError, customDecimalsError,
collectibleAddressError,
} = this.state; } = this.state;
return ( return (
tokenSelectorError || tokenSelectorError ||
customAddressError || customAddressError ||
customSymbolError || customSymbolError ||
customDecimalsError customDecimalsError ||
collectibleAddressError
); );
} }
@ -186,11 +192,12 @@ class ImportToken extends Component {
this.handleCustomDecimalsChange(decimals); this.handleCustomDecimalsChange(decimals);
} }
handleCustomAddressChange(value) { async handleCustomAddressChange(value) {
const customAddress = value.trim(); const customAddress = value.trim();
this.setState({ this.setState({
customAddress, customAddress,
customAddressError: null, customAddressError: null,
collectibleAddressError: null,
tokenSelectorError: null, tokenSelectorError: null,
symbolAutoFilled: false, symbolAutoFilled: false,
decimalAutoFilled: false, decimalAutoFilled: false,
@ -208,8 +215,23 @@ class ImportToken extends Component {
const isMainnetNetwork = this.props.chainId === '0x1'; const isMainnetNetwork = this.props.chainId === '0x1';
let standard;
if (addressIsValid) {
try {
({ standard } = await this.props.getTokenStandardAndDetails(
standardAddress,
this.props.selectedAddress,
));
} catch (error) {
// ignore
}
}
const addressIsEmpty =
customAddress.length === 0 || customAddress === emptyAddr;
switch (true) { switch (true) {
case !addressIsValid: case !addressIsValid && !addressIsEmpty:
this.setState({ this.setState({
customAddressError: this.context.t('invalidAddress'), customAddressError: this.context.t('invalidAddress'),
customSymbol: '', customSymbol: '',
@ -218,6 +240,28 @@ class ImportToken extends Component {
customDecimalsError: null, customDecimalsError: null,
}); });
break;
case process.env.COLLECTIBLES_V1 &&
(standard === 'ERC1155' || standard === 'ERC721'):
this.setState({
collectibleAddressError: this.context.t('collectibleAddressError', [
<a
className="import-token__collectible-address-error-link"
onClick={() =>
this.props.history.push({
pathname: ADD_COLLECTIBLE_ROUTE,
state: {
addressEnteredOnImportTokensPage: this.state.customAddress,
},
})
}
key="collectibleAddressError"
>
{this.context.t('importNFTPage')}
</a>,
]),
});
break; break;
case isMainnetToken && !isMainnetNetwork: case isMainnetToken && !isMainnetNetwork:
this.setState({ this.setState({
@ -242,7 +286,7 @@ class ImportToken extends Component {
break; break;
default: default:
if (customAddress !== emptyAddr) { if (!addressIsEmpty) {
this.attemptToAutoFillTokenParams(customAddress); this.attemptToAutoFillTokenParams(customAddress);
} }
} }
@ -290,6 +334,7 @@ class ImportToken extends Component {
symbolAutoFilled, symbolAutoFilled,
decimalAutoFilled, decimalAutoFilled,
mainnetTokenWarning, mainnetTokenWarning,
collectibleAddressError,
} = this.state; } = this.state;
const { chainId, rpcPrefs } = this.props; const { chainId, rpcPrefs } = this.props;
@ -330,7 +375,9 @@ class ImportToken extends Component {
type="text" type="text"
value={customAddress} value={customAddress}
onChange={(e) => this.handleCustomAddressChange(e.target.value)} onChange={(e) => this.handleCustomAddressChange(e.target.value)}
error={customAddressError || mainnetTokenWarning} error={
customAddressError || mainnetTokenWarning || collectibleAddressError
}
fullWidth fullWidth
autoFocus autoFocus
margin="normal" margin="normal"

View File

@ -1,6 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { setPendingTokens, clearPendingTokens } from '../../store/actions'; import {
setPendingTokens,
clearPendingTokens,
getTokenStandardAndDetails,
} from '../../store/actions';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
getRpcPrefsForCurrentProvider, getRpcPrefsForCurrentProvider,
@ -17,6 +21,7 @@ const mapStateToProps = (state) => {
provider: { chainId }, provider: { chainId },
useTokenDetection, useTokenDetection,
tokenList, tokenList,
selectedAddress,
}, },
} = state; } = state;
const showSearchTabCustomNetwork = const showSearchTabCustomNetwork =
@ -33,13 +38,15 @@ const mapStateToProps = (state) => {
rpcPrefs: getRpcPrefsForCurrentProvider(state), rpcPrefs: getRpcPrefsForCurrentProvider(state),
tokenList, tokenList,
useTokenDetection, useTokenDetection,
selectedAddress,
}; };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)), setPendingTokens: (tokens) => dispatch(setPendingTokens(tokens)),
clearPendingTokens: () => dispatch(clearPendingTokens()), clearPendingTokens: () => dispatch(clearPendingTokens()),
getTokenStandardAndDetails: (address, selectedAddress) =>
getTokenStandardAndDetails(address, selectedAddress, null),
}; };
}; };

View File

@ -1,110 +1,177 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react';
import sinon from 'sinon'; import { renderWithProvider } from '../../../test/lib/render-helpers';
import configureMockStore from 'redux-mock-store'; import configureStore from '../../store/store';
import { mountWithRouter } from '../../../test/lib/render-helpers'; import {
setPendingTokens,
clearPendingTokens,
getTokenStandardAndDetails,
} from '../../store/actions';
import ImportToken from './import-token.container'; import ImportToken from './import-token.container';
jest.mock('../../store/actions', () => ({
getTokenStandardAndDetails: jest
.fn()
.mockImplementation(() => Promise.resolve({ standard: 'ERC20' })),
setPendingTokens: jest
.fn()
.mockImplementation(() => ({ type: 'SET_PENDING_TOKENS' })),
clearPendingTokens: jest
.fn()
.mockImplementation(() => ({ type: 'CLEAR_PENDING_TOKENS' })),
}));
describe('Import Token', () => { describe('Import Token', () => {
let wrapper; const historyStub = jest.fn();
const state = {
metamask: {
tokens: [],
},
};
const store = configureMockStore()(state);
const props = { const props = {
history: { history: {
push: sinon.stub().callsFake(() => undefined), push: historyStub,
}, },
setPendingTokens: sinon.spy(),
clearPendingTokens: sinon.spy(),
tokens: [],
identities: {},
mostRecentOverviewPage: '/',
showSearchTab: true, showSearchTab: true,
tokenList: {}, tokenList: {},
}; };
const render = () => {
const baseStore = {
metamask: {
tokens: [],
provider: { chainId: '0x1' },
frequentRpcListDetail: [],
identities: {},
selectedAddress: '0x1231231',
},
history: {
mostRecentOverviewPage: '/',
},
};
const store = configureStore(baseStore);
return renderWithProvider(<ImportToken {...props} />, store);
};
describe('Import Token', () => { describe('Import Token', () => {
beforeAll(() => { it('add Custom Token button is disabled when no fields are populated', () => {
wrapper = mountWithRouter( const { getByText } = render();
<Provider store={store}> const customTokenButton = getByText('Custom Token');
<ImportToken.WrappedComponent {...props} /> fireEvent.click(customTokenButton);
</Provider>, const submit = getByText('Add Custom Token');
store,
);
wrapper.find({ name: 'customToken' }).simulate('click'); expect(submit).toBeDisabled();
});
afterEach(() => {
props.history.push.reset();
});
it('next button is disabled when no fields are populated', () => {
const nextButton = wrapper.find(
'.button.btn-primary.page-container__footer-button',
);
expect(nextButton.props().disabled).toStrictEqual(true);
}); });
it('edits token address', () => { it('edits token address', () => {
const { getByText } = render();
const customTokenButton = getByText('Custom Token');
fireEvent.click(customTokenButton);
const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4'; const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4';
const event = { target: { value: tokenAddress } }; const event = { target: { value: tokenAddress } };
const customAddress = wrapper.find('input#custom-address'); fireEvent.change(document.getElementById('custom-address'), event);
customAddress.simulate('change', event); expect(document.getElementById('custom-address').value).toStrictEqual(
expect( tokenAddress,
wrapper.find('ImportToken').instance().state.customAddress, );
).toStrictEqual(tokenAddress);
}); });
it('edits token symbol', () => { it('edits token symbol', () => {
const { getByText } = render();
const customTokenButton = getByText('Custom Token');
fireEvent.click(customTokenButton);
const tokenSymbol = 'META'; const tokenSymbol = 'META';
const event = { target: { value: tokenSymbol } }; const event = { target: { value: tokenSymbol } };
const customAddress = wrapper.find('#custom-symbol'); fireEvent.change(document.getElementById('custom-symbol'), event);
customAddress.last().simulate('change', event);
expect( expect(document.getElementById('custom-symbol').value).toStrictEqual(
wrapper.find('ImportToken').instance().state.customSymbol, tokenSymbol,
).toStrictEqual(tokenSymbol); );
}); });
it('edits token decimal precision', () => { it('edits token decimal precision', () => {
const { getByText } = render();
const customTokenButton = getByText('Custom Token');
fireEvent.click(customTokenButton);
const tokenPrecision = '2'; const tokenPrecision = '2';
const event = { target: { value: tokenPrecision } }; const event = { target: { value: tokenPrecision } };
const customAddress = wrapper.find('#custom-decimals'); fireEvent.change(document.getElementById('custom-decimals'), event);
customAddress.last().simulate('change', event);
expect( expect(document.getElementById('custom-decimals').value).toStrictEqual(
wrapper.find('ImportToken').instance().state.customDecimals, tokenPrecision,
).toStrictEqual(Number(tokenPrecision));
});
it('next', () => {
const nextButton = wrapper.find(
'.button.btn-primary.page-container__footer-button',
);
nextButton.simulate('click');
expect(props.setPendingTokens.calledOnce).toStrictEqual(true);
expect(props.history.push.calledOnce).toStrictEqual(true);
expect(props.history.push.getCall(0).args[0]).toStrictEqual(
'/confirm-import-token',
); );
}); });
it('cancels', () => { it('adds custom tokens successfully', async () => {
const cancelButton = wrapper.find('.page-container__header-close'); const { getByText } = render();
cancelButton.simulate('click'); const customTokenButton = getByText('Custom Token');
fireEvent.click(customTokenButton);
expect(props.clearPendingTokens.calledOnce).toStrictEqual(true); const submit = getByText('Add Custom Token');
expect(props.history.push.getCall(0).args[0]).toStrictEqual('/'); expect(submit).toBeDisabled();
const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4';
fireEvent.change(document.getElementById('custom-address'), {
target: { value: tokenAddress },
});
expect(submit).not.toBeDisabled();
const tokenSymbol = 'META';
fireEvent.change(document.getElementById('custom-symbol'), {
target: { value: tokenSymbol },
});
const tokenPrecision = '2';
await fireEvent.change(document.getElementById('custom-decimals'), {
target: { value: tokenPrecision },
});
expect(submit).not.toBeDisabled();
fireEvent.click(submit);
expect(setPendingTokens).toHaveBeenCalledWith({
customToken: {
address: tokenAddress,
decimals: Number(tokenPrecision),
symbol: tokenSymbol,
},
selectedTokens: {},
tokenAddressList: [],
});
expect(historyStub).toHaveBeenCalledWith('/confirm-import-token');
});
it('cancels out of import token flow', () => {
const { getByRole } = render();
const closeButton = getByRole('button', { name: 'close' });
fireEvent.click(closeButton);
expect(clearPendingTokens).toHaveBeenCalled();
expect(historyStub).toHaveBeenCalledWith('/');
});
it('sets and error when a token is an NFT', async () => {
process.env.COLLECTIBLES_V1 = true;
getTokenStandardAndDetails.mockImplementation(() =>
Promise.resolve({ standard: 'ERC721' }),
);
const { getByText } = render();
const customTokenButton = getByText('Custom Token');
fireEvent.click(customTokenButton);
const submit = getByText('Add Custom Token');
expect(submit).toBeDisabled();
const tokenAddress = '0x617b3f8050a0BD94b6b1da02B4384eE5B4DF13F4';
await fireEvent.change(document.getElementById('custom-address'), {
target: { value: tokenAddress },
});
expect(submit).toBeDisabled();
// The last part of this error message won't be found by getByText because it is wrapped as a link.
const errorMessage = getByText('This token is an NFT. Add on the');
expect(errorMessage).toBeInTheDocument();
}); });
}); });
}); });

View File

@ -6,12 +6,12 @@
&__custom-token-form { &__custom-token-form {
padding: 8px 16px 16px; padding: 8px 16px 16px;
input[type="number"]::-webkit-inner-spin-button { input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
display: none; display: none;
} }
input[type="number"]:hover::-webkit-inner-spin-button { input[type='number']:hover::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
display: none; display: none;
} }
@ -59,4 +59,13 @@
margin-bottom: 16px; margin-bottom: 16px;
margin-top: 0; margin-top: 0;
} }
&__collectible-address-error-link {
color: var(--primary-blue);
cursor: pointer;
&:hover {
color: var(--Blue-400);
}
}
} }

View File

@ -1418,6 +1418,18 @@ export async function checkAndUpdateSingleCollectibleOwnershipStatus(
); );
} }
export async function getTokenStandardAndDetails(
address,
userAddress,
tokenId,
) {
return await promisifiedBackground.getTokenStandardAndDetails(
address,
userAddress,
tokenId,
);
}
export function removeToken(address) { export function removeToken(address) {
return async (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());

View File

@ -2652,10 +2652,10 @@
web3 "^0.20.7" web3 "^0.20.7"
web3-provider-engine "^16.0.3" web3-provider-engine "^16.0.3"
"@metamask/controllers@^24.0.0": "@metamask/controllers@^25.0.0":
version "24.0.0" version "25.0.0"
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-24.0.0.tgz#380d4e10bd9b903afc38b041ea7e64b30ae34dab" resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-25.0.0.tgz#764ba46a169f197253e35ca5273720c91eae7662"
integrity sha512-gOTpvxQTNTrXOUpGOiRKcqHUgnjSiTgJcwNLxenZWqPIixz7eI2qHnjO7ZaGbV/baK7SOa4rRGokvsCNV0mafA== integrity sha512-BfTRaK87Iha+EXlEsu330Jp9Wtx0gks2vA+q3Lu/AKGbcz2J0cokECNwN7uEe4GQLPLsfCIAkUIAlP3pRiCvjg==
dependencies: dependencies:
"@ethereumjs/common" "^2.3.1" "@ethereumjs/common" "^2.3.1"
"@ethereumjs/tx" "^3.2.1" "@ethereumjs/tx" "^3.2.1"