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

NFT: Replaced all the instances of collectibles with NFTs (#17741)

* replaced all the instances of collectibles with nfts

* updated actions

* updated e2e seeder

* updated confirm Approve test

* updated test dapp change

* updated test dapp change

* nit fix

* nit fix

* updated casing and snapshots

* updated casinG

* added migrations

* updated ,igration

* updated 078.test

* updated tests for 078 migration

* updated migration

* updated 078 index.js
This commit is contained in:
Nidhi Kumari 2023-02-17 00:53:29 +05:30 committed by GitHub
parent cf49761d71
commit 33cc8d587a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 841 additions and 719 deletions

View File

@ -2109,7 +2109,7 @@
"message": "Το “$1” προστέθηκε με επιτυχία!"
},
"newNftAddedMessage": {
"message": "Το Collectible προστέθηκε με επιτυχία!"
"message": "Το Nft προστέθηκε με επιτυχία!"
},
"newPassword": {
"message": "Νέος Κωδικός Πρόσβασης (ελάχιστο 8 χαρακτήρες)"

View File

@ -1570,7 +1570,7 @@
"message": "page Importer des NFT"
},
"importNFTTokenIdToolTip": {
"message": "LID dun collectible est un identifiant unique puisquil ny a pas deux NFT identiques. Encore une fois, sur OpenSea, ce numéro se trouve dans la section « Détails ». Prenez-en note ou copiez-le dans votre presse-papiers."
"message": "LID dun NFT est un identifiant unique puisquil ny a pas deux NFT identiques. Encore une fois, sur OpenSea, ce numéro se trouve dans la section « Détails ». Prenez-en note ou copiez-le dans votre presse-papiers."
},
"importNFTs": {
"message": "Importer des NFT"
@ -2109,7 +2109,7 @@
"message": "« $1» a été ajouté avec succès!"
},
"newNftAddedMessage": {
"message": "Le collectible a été ajouté avec succès!"
"message": "Le NFT a été ajouté avec succès!"
},
"newPassword": {
"message": "Nouveau mot de passe (min 8 caractères)"
@ -4157,7 +4157,7 @@
"description": "Shown when the protocol is unknown by the extension. $1 is the protocol code."
},
"unsendableAsset": {
"message": "Lenvoi de jetons collectibles (ERC-721) nest pas pris en charge actuellement",
"message": "Lenvoi de jetons NFTs (ERC-721) nest pas pris en charge actuellement",
"description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending"
},
"unverifiedContractAddressMessage": {

View File

@ -1570,7 +1570,7 @@
"message": "I-import ang pahina ng NFT"
},
"importNFTTokenIdToolTip": {
"message": "Ang ID ng collectible ay isang natatanging pagkakakilanlan dahil walang dalawang NFT ang magkatulad. Muli, sa OpenSea ang numerong ito ay nasa ilalim ng 'Mga Detalye'. Itala ito, o kopyahin ito sa iyong clipboard."
"message": "Ang ID ng NFT ay isang natatanging pagkakakilanlan dahil walang dalawang NFT ang magkatulad. Muli, sa OpenSea ang numerong ito ay nasa ilalim ng 'Mga Detalye'. Itala ito, o kopyahin ito sa iyong clipboard."
},
"importNFTs": {
"message": "I-import ang mga NFT"
@ -2109,7 +2109,7 @@
"message": "Ang “$1” matagumpay na naidagdag!"
},
"newNftAddedMessage": {
"message": "Ang collectible ay tagumpay na naidagdag!"
"message": "Ang NFT ay tagumpay na naidagdag!"
},
"newPassword": {
"message": "Bagong password (min na 8 char)"
@ -4157,7 +4157,7 @@
"description": "Shown when the protocol is unknown by the extension. $1 is the protocol code."
},
"unsendableAsset": {
"message": "Ang pagpapadala ng collectible (ERC-721) token ay kasalukuyang hindi magagamit",
"message": "Ang pagpapadala ng NFT (ERC-721) token ay kasalukuyang hindi magagamit",
"description": "This is an error message we show the user if they attempt to send an NFT asset type, for which currently don't support sending"
},
"unverifiedContractAddressMessage": {

View File

@ -984,7 +984,7 @@
"description": "The next nonce according to MetaMask's internal logic"
},
"nftTokenIdPlaceholder": {
"message": "Enter the collectible ID"
"message": "Enter the NFT ID"
},
"noAccountsFound": {
"message": "指定的搜尋條件找不到帳戶"

View File

@ -38,14 +38,14 @@ export default class AppStateController extends EventEmitter {
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(),
outdatedBrowserWarningLastShown: new Date().getTime(),
collectiblesDetectionNoticeDismissed: false,
nftsDetectionNoticeDismissed: false,
showTestnetMessageInDropdown: true,
showPortfolioTooltip: true,
showBetaHeader: isBeta(),
trezorModel: null,
...initState,
qrHardware: {},
collectiblesDropdownState: {},
nftsDropdownState: {},
usedNetworks: {
'0x1': true,
'0x5': true,
@ -330,13 +330,13 @@ export default class AppStateController extends EventEmitter {
}
/**
* A setter for the `collectiblesDropdownState` property
* A setter for the `nftsDropdownState` property
*
* @param collectiblesDropdownState
* @param nftsDropdownState
*/
updateCollectibleDropDownState(collectiblesDropdownState) {
updateNftDropDownState(nftsDropdownState) {
this.store.updateState({
collectiblesDropdownState,
nftsDropdownState,
});
}

View File

@ -31,7 +31,7 @@ function getMockController() {
return mcState;
}
const jsonData = `{"preferences":{"frequentRpcListDetail":[{"chainId":"0x539","nickname":"Localhost 8545","rpcPrefs":{},"rpcUrl":"http://localhost:8545","ticker":"ETH"},{"chainId":"0x38","nickname":"Binance Smart Chain Mainnet","rpcPrefs":{"blockExplorerUrl":"https://bscscan.com"},"rpcUrl":"https://bsc-dataseed1.binance.org","ticker":"BNB"},{"chainId":"0x61","nickname":"Binance Smart Chain Testnet","rpcPrefs":{"blockExplorerUrl":"https://testnet.bscscan.com"},"rpcUrl":"https://data-seed-prebsc-1-s1.binance.org:8545","ticker":"tBNB"},{"chainId":"0x89","nickname":"Polygon Mainnet","rpcPrefs":{"blockExplorerUrl":"https://polygonscan.com"},"rpcUrl":"https://polygon-rpc.com","ticker":"MATIC"}],"useBlockie":false,"useNonceField":false,"usePhishDetect":true,"dismissSeedBackUpReminder":false,"useTokenDetection":false,"useCollectibleDetection":false,"openSeaEnabled":false,"advancedGasFee":null,"featureFlags":{"sendHexData":true,"showIncomingTransactions":true},"knownMethodData":{},"currentLocale":"en","forgottenPassword":false,"preferences":{"hideZeroBalanceTokens":false,"showFiatInTestnets":false,"showTestNetworks":true,"useNativeCurrencyAsPrimaryCurrency":true},"ipfsGateway":"dweb.link","infuraBlocked":false,"ledgerTransportType":"webhid","theme":"light","customNetworkListEnabled":false,"textDirection":"auto"},"addressBook":{"addressBook":{"0x61":{"0x42EB768f2244C8811C63729A21A3569731535f06":{"address":"0x42EB768f2244C8811C63729A21A3569731535f06","chainId":"0x61","isEns":false,"memo":"","name":""}}}}}`;
const jsonData = `{"preferences":{"frequentRpcListDetail":[{"chainId":"0x539","nickname":"Localhost 8545","rpcPrefs":{},"rpcUrl":"http://localhost:8545","ticker":"ETH"},{"chainId":"0x38","nickname":"Binance Smart Chain Mainnet","rpcPrefs":{"blockExplorerUrl":"https://bscscan.com"},"rpcUrl":"https://bsc-dataseed1.binance.org","ticker":"BNB"},{"chainId":"0x61","nickname":"Binance Smart Chain Testnet","rpcPrefs":{"blockExplorerUrl":"https://testnet.bscscan.com"},"rpcUrl":"https://data-seed-prebsc-1-s1.binance.org:8545","ticker":"tBNB"},{"chainId":"0x89","nickname":"Polygon Mainnet","rpcPrefs":{"blockExplorerUrl":"https://polygonscan.com"},"rpcUrl":"https://polygon-rpc.com","ticker":"MATIC"}],"useBlockie":false,"useNonceField":false,"usePhishDetect":true,"dismissSeedBackUpReminder":false,"useTokenDetection":false,"useNftDetection":false,"openSeaEnabled":false,"advancedGasFee":null,"featureFlags":{"sendHexData":true,"showIncomingTransactions":true},"knownMethodData":{},"currentLocale":"en","forgottenPassword":false,"preferences":{"hideZeroBalanceTokens":false,"showFiatInTestnets":false,"showTestNetworks":true,"useNativeCurrencyAsPrimaryCurrency":true},"ipfsGateway":"dweb.link","infuraBlocked":false,"ledgerTransportType":"webhid","theme":"light","customNetworkListEnabled":false,"textDirection":"auto"},"addressBook":{"addressBook":{"0x61":{"0x42EB768f2244C8811C63729A21A3569731535f06":{"address":"0x42EB768f2244C8811C63729A21A3569731535f06","chainId":"0x61","isEns":false,"memo":"","name":""}}}}}`;
describe('BackupController', function () {
const getBackupController = () => {

View File

@ -758,7 +758,7 @@ export default class MetaMetricsController {
}
/**
* Returns an array of all of the collectibles/NFTs the user
* Returns an array of all of the NFTs the user
* possesses across all networks and accounts.
*
* @param {object} allNfts
@ -771,7 +771,7 @@ export default class MetaMetricsController {
});
/**
* Returns the number of unique collectible/NFT addresses the user
* Returns the number of unique NFT addresses the user
* possesses across all networks and accounts.
*
* @param {object} allNfts

View File

@ -151,7 +151,7 @@ export default class PreferencesController {
/**
* Setter for the `useNftDetection` property
*
* @param {boolean} useNftDetection - Whether or not the user prefers to autodetect collectibles.
* @param {boolean} useNftDetection - Whether or not the user prefers to autodetect NFTs.
*/
setUseNftDetection(useNftDetection) {
this.store.updateState({ useNftDetection });
@ -169,7 +169,7 @@ export default class PreferencesController {
/**
* Setter for the `openSeaEnabled` property
*
* @param {boolean} openSeaEnabled - Whether or not the user prefers to use the OpenSea API for collectibles data.
* @param {boolean} openSeaEnabled - Whether or not the user prefers to use the OpenSea API for NFTs data.
*/
setOpenSeaEnabled(openSeaEnabled) {
this.store.updateState({

View File

@ -960,8 +960,8 @@ export default class MetamaskController extends EventEmitter {
const { txReceipt } = txMeta;
// if this is a transferFrom method generated from within the app it may be a collectible transfer transaction
// in which case we will want to check and update ownership status of the transferred collectible.
// if this is a transferFrom method generated from within the app it may be an NFT transfer transaction
// in which case we will want to check and update ownership status of the transferred NFT.
if (
txMeta.type === TransactionType.tokenMethodTransferFrom &&
txMeta.txParams !== undefined
@ -982,19 +982,17 @@ export default class MetamaskController extends EventEmitter {
const { allNfts } = this.nftController.state;
const chainIdAsDecimal = hexToDecimal(chainId);
// check if its a known collectible
const knownCollectible = allNfts?.[userAddress]?.[
chainIdAsDecimal
]?.find(
// check if its a known NFT
const knownNft = allNfts?.[userAddress]?.[chainIdAsDecimal]?.find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, contractAddress) &&
tokenId === transactionDataTokenId,
);
// if it is we check and update ownership status.
if (knownCollectible) {
if (knownNft) {
this.nftController.checkAndUpdateSingleNftOwnershipStatus(
knownCollectible,
knownNft,
false,
{ userAddress, chainId: chainIdAsDecimal },
);
@ -1904,10 +1902,8 @@ export default class MetamaskController extends EventEmitter {
appStateController.setShowPortfolioTooltip.bind(appStateController),
setShowBetaHeader:
appStateController.setShowBetaHeader.bind(appStateController),
updateCollectibleDropDownState:
appStateController.updateCollectibleDropDownState.bind(
appStateController,
),
updateNftDropDownState:
appStateController.updateNftDropDownState.bind(appStateController),
setFirstTimeUsedNetwork:
appStateController.setFirstTimeUsedNetwork.bind(appStateController),
// EnsController
@ -2144,7 +2140,7 @@ export default class MetamaskController extends EventEmitter {
detectTokensController,
),
// DetectCollectibleController
// DetectNftController
detectNfts: process.env.NFTS_V1
? nftDetectionController.detectNfts.bind(nftDetectionController)
: null,

View File

@ -0,0 +1,31 @@
import { cloneDeep } from 'lodash';
const version = 78;
/**
* Remove collectiblesDropdownState and collectiblesDetectionNoticeDismissed:.
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
if (
state?.AppStateController?.collectiblesDetectionNoticeDismissed !==
undefined
) {
delete state.AppStateController.collectiblesDetectionNoticeDismissed;
}
if (state?.metamask?.collectiblesDropdownState !== undefined) {
delete state.metamask.collectiblesDropdownState;
}
return state;
}

View File

@ -0,0 +1,173 @@
import migration78 from './078';
describe('migration #78', () => {
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 77,
},
};
const newStorage = await migration78.migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version: 78,
});
});
it('should remove the "collectiblesDetectionNoticeDismissed"', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {
AppStateController: {
collectiblesDetectionNoticeDismissed: false,
bar: 'baz',
},
},
};
const newStorage = await migration78.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 78,
},
data: {
AppStateController: {
bar: 'baz',
},
},
});
});
it('should remove the "collectiblesDropdownState"', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {
metamask: {
isInitialized: true,
isUnlocked: true,
isAccountMenuOpen: false,
identities: {
'0x00000': {
address: '0x00000',
lastSelected: 1675966229118,
name: 'Account 1',
},
'0x00001': {
address: '0x00001',
name: 'Account 2',
},
},
collectiblesDropdownState: {},
qrHardware: {},
},
},
};
const newStorage = await migration78.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 78,
},
data: {
metamask: {
isInitialized: true,
isUnlocked: true,
isAccountMenuOpen: false,
identities: {
'0x00000': {
address: '0x00000',
lastSelected: 1675966229118,
name: 'Account 1',
},
'0x00001': {
address: '0x00001',
name: 'Account 2',
},
},
qrHardware: {},
},
},
});
});
it('should make no changes if "collectiblesDetectionNoticeDismissed" never existed', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {
AppStateController: {
bar: 'baz',
},
},
};
const newStorage = await migration78.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 78,
},
data: {
AppStateController: {
bar: 'baz',
},
},
});
});
it('should make no changes if "collectiblesDropdownState" never existed', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {
metamask: {
isInitialized: true,
isUnlocked: true,
isAccountMenuOpen: false,
identities: {
'0x00000': {
address: '0x00000',
lastSelected: 1675966229118,
name: 'Account 1',
},
'0x00001': {
address: '0x00001',
name: 'Account 2',
},
},
qrHardware: {},
},
},
};
const newStorage = await migration78.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 78,
},
data: {
metamask: {
isInitialized: true,
isUnlocked: true,
isAccountMenuOpen: false,
identities: {
'0x00000': {
address: '0x00000',
lastSelected: 1675966229118,
name: 'Account 1',
},
'0x00001': {
address: '0x00001',
name: 'Account 2',
},
},
qrHardware: {},
},
},
});
});
});

View File

@ -81,6 +81,7 @@ import m074 from './074';
import m075 from './075';
import m076 from './076';
import m077 from './077';
import m078 from './078';
const migrations = [
m002,
@ -159,6 +160,7 @@ const migrations = [
m075,
m076,
m077,
m078,
];
export default migrations;

View File

@ -1149,7 +1149,7 @@
"ui/hooks/useApproveTransaction.js",
"ui/hooks/useAssetDetails.js",
"ui/hooks/useAssetDetails.test.js",
"ui/hooks/useCollectiblesCollections.js",
"ui/hooks/useNftsCollections.js",
"ui/hooks/useCopyToClipboard.js",
"ui/hooks/useCurrencyDisplay.js",
"ui/hooks/useCurrencyDisplay.test.js",

View File

@ -261,7 +261,7 @@
"maxBaseFee": "75",
"priorityFee": "2"
},
"collectiblesDropdownState": {
"nftsDropdownState": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"0x5": {
"0x495f947276749Ce646f68AC8c248420045cb7b5e": false

View File

@ -115,7 +115,7 @@ function defaultFixture() {
},
AppStateController: {
browserEnvironment: {},
collectiblesDropdownState: {},
nftsDropdownState: {},
connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: null,
fullScreenGasPollTokens: [],
@ -281,7 +281,7 @@ function onboardingFixture() {
data: {
AppStateController: {
browserEnvironment: {},
collectiblesDropdownState: {},
nftsDropdownState: {},
connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: null,
fullScreenGasPollTokens: [],
@ -411,7 +411,7 @@ class FixtureBuilder {
return this;
}
withCollectiblesController(data) {
withNftsController(data) {
merge(
this.fixture.data.NftController
? this.fixture.data.NftController

View File

@ -4,7 +4,7 @@ const { SMART_CONTRACTS } = require('../seeder/smart-contracts');
const FixtureBuilder = require('../fixture-builder');
describe('Import NFT', function () {
const smartContract = SMART_CONTRACTS.COLLECTIBLES;
const smartContract = SMART_CONTRACTS.NFTS;
const ganacheOptions = {
accounts: [
{
@ -54,7 +54,7 @@ describe('Import NFT', function () {
text: 'TestDappCollectibles',
});
const importedNftImage = await driver.findElement(
'.collectibles-items__item-image',
'.nfts-items__item-image',
);
assert.equal(await importedNft.isDisplayed(), true);
assert.equal(await importedNftImage.isDisplayed(), true);

View File

@ -4,7 +4,7 @@ const { SMART_CONTRACTS } = require('../seeder/smart-contracts');
const FixtureBuilder = require('../fixture-builder');
describe('Send NFT', function () {
const smartContract = SMART_CONTRACTS.COLLECTIBLES;
const smartContract = SMART_CONTRACTS.NFTS;
const ganacheOptions = {
accounts: [
{
@ -40,7 +40,7 @@ describe('Send NFT', function () {
await driver.clickElement({ text: 'Add', tag: 'button' });
// Fill the send NFT form and confirm the transaction
await driver.clickElement('.collectibles-items__item-image');
await driver.clickElement('.nfts-items__item-image');
await driver.clickElement({ text: 'Send', tag: 'button' });
await driver.fill(
'input[placeholder="Search, public address (0x), or ENS"]',
@ -49,7 +49,7 @@ describe('Send NFT', function () {
await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.clickElement({ text: 'Confirm', tag: 'button' });
// When transaction complete, check the send nft item is displayed in activity tab
// When transaction complete, check the send NFT is displayed in activity tab
await driver.wait(async () => {
const confirmedTxes = await driver.findElements(
'.transaction-list__completed-transactions .transaction-list-item',

View File

@ -44,7 +44,7 @@ class GanacheSeeder {
await contract.deployTransaction.wait();
if (contractName === SMART_CONTRACTS.COLLECTIBLES) {
if (contractName === SMART_CONTRACTS.NFTS) {
const transaction = await contract.mintCollectibles(1, {
from: fromAddress,
});

View File

@ -20,7 +20,7 @@ const hstFactory = {
abi: hstAbi,
};
const collectiblesFactory = {
const nftsFactory = {
bytecode: collectiblesBytecode,
abi: collectiblesAbi,
};
@ -42,7 +42,7 @@ const multisigFactory = {
const SMART_CONTRACTS = {
HST: 'hst',
COLLECTIBLES: 'collectibles',
NFTS: 'nfts',
PIGGYBANK: 'piggybank',
FAILING: 'failing',
MULTISIG: 'multisig',
@ -50,7 +50,7 @@ const SMART_CONTRACTS = {
const contractConfiguration = {
[SMART_CONTRACTS.HST]: hstFactory,
[SMART_CONTRACTS.COLLECTIBLES]: collectiblesFactory,
[SMART_CONTRACTS.NFTS]: nftsFactory,
[SMART_CONTRACTS.PIGGYBANK]: piggybankFactory,
[SMART_CONTRACTS.FAILING]: failingContract,
[SMART_CONTRACTS.MULTISIG]: multisigFactory,

View File

@ -3,8 +3,8 @@ const { convertToHexValue, withFixtures } = require('../helpers');
const { SMART_CONTRACTS } = require('../seeder/smart-contracts');
const FixtureBuilder = require('../fixture-builder');
describe('Collectibles', function () {
const smartContract = SMART_CONTRACTS.COLLECTIBLES;
describe('NFTs', function () {
const smartContract = SMART_CONTRACTS.NFTS;
const ganacheOptions = {
accounts: [
{

View File

@ -350,7 +350,7 @@ ContractDetailsModal.propTypes = {
*/
siteImage: PropTypes.string,
/**
* The token id of the collectible
* The token id of the NFT
*/
tokenId: PropTypes.string,
/**

View File

@ -8,10 +8,10 @@ import { TypographyVariant } from '../../../../helpers/constants/design-system';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
ADD_COLLECTIBLE_ROUTE,
ADD_NFT_ROUTE,
ASSET_ROUTE,
} from '../../../../helpers/constants/routes';
import { getCollectibles } from '../../../../ducks/metamask/metamask';
import { getNfts } from '../../../../ducks/metamask/metamask';
import { ignoreTokens } from '../../../../store/actions';
import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils';
@ -19,7 +19,7 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
const history = useHistory();
const t = useI18nContext();
const dispatch = useDispatch();
const allNfts = useSelector(getCollectibles);
const allNfts = useSelector(getNfts);
const tokenAddedAsNFT = allNfts.find(({ address }) =>
isEqualCaseInsensitive(address, tokenAddress),
);
@ -40,7 +40,7 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => {
});
} else {
history.push({
pathname: ADD_COLLECTIBLE_ROUTE,
pathname: ADD_NFT_ROUTE,
state: { tokenAddress },
});
}

View File

@ -1,16 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collectible Default Image should match snapshot with all provided props 1`] = `
exports[`NFT Default Image should match snapshot with all provided props 1`] = `
<div>
<button
class="collectible-default collectible-default--clickable"
data-testid="collectible-default-image"
class="nft-default nft-default--clickable"
data-testid="nft-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography nft-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
Collectible Name
NFT Name
<br />
#
@ -20,17 +20,17 @@ exports[`Collectible Default Image should match snapshot with all provided props
</div>
`;
exports[`Collectible Default Image should match snapshot with missing image click handler 1`] = `
exports[`NFT Default Image should match snapshot with missing image click handler 1`] = `
<div>
<div
class="collectible-default"
data-testid="collectible-default-image"
class="nft-default"
data-testid="nft-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography nft-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
Collectible Name
NFT Name
<br />
#
@ -40,15 +40,15 @@ exports[`Collectible Default Image should match snapshot with missing image clic
</div>
`;
exports[`Collectible Default Image should render with no props 1`] = `
exports[`NFT Default Image should render with no props 1`] = `
<div>
<div
class="collectible-default"
data-testid="collectible-default-image"
class="nft-default"
data-testid="nft-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography nft-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
[unknownCollection]

View File

@ -1,4 +1,4 @@
.collectible-default {
.nft-default {
background-color: var(--color-background-alternative);
padding-top: 100%; // retains 1:1 aspect ratio
position: relative;

View File

@ -5,43 +5,36 @@ import Typography from '../../ui/typography';
import { TypographyVariant } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
export default function CollectibleDefaultImage({
name,
tokenId,
handleImageClick,
}) {
export default function NftDefaultImage({ name, tokenId, handleImageClick }) {
const t = useI18nContext();
const Tag = handleImageClick ? 'button' : 'div';
return (
<Tag
tabIndex={0}
data-testid="collectible-default-image"
className={classnames('collectible-default', {
'collectible-default--clickable': handleImageClick,
data-testid="nft-default-image"
className={classnames('nft-default', {
'nft-default--clickable': handleImageClick,
})}
onClick={handleImageClick}
>
<Typography
variant={TypographyVariant.H6}
className="collectible-default__text"
>
<Typography variant={TypographyVariant.H6} className="nft-default__text">
{name ?? t('unknownCollection')} <br /> #{tokenId}
</Typography>
</Tag>
);
}
CollectibleDefaultImage.propTypes = {
NftDefaultImage.propTypes = {
/**
* The name of the collectible collection if not supplied will default to "Unnamed collection"
* The name of the NFT collection if not supplied will default to "Unnamed collection"
*/
name: PropTypes.string,
/**
* The token id of the collectible
* The token id of the nft
*/
tokenId: PropTypes.string,
/**
* The click handler for the collectible default image
* The click handler for the NFT default image
*/
handleImageClick: PropTypes.func,
};

View File

@ -1,8 +1,8 @@
import React from 'react';
import CollectibleDefaultImage from '.';
import NftDefaultImage from '.';
export default {
title: 'Components/App/CollectibleDefaultImage',
title: 'Components/App/NftDefaultImage',
argTypes: {
name: {
@ -24,7 +24,7 @@ export default {
export const DefaultStory = (args) => (
<div style={{ width: 200, height: 200 }}>
<CollectibleDefaultImage {...args} />
<NftDefaultImage {...args} />
</div>
);
@ -32,11 +32,11 @@ DefaultStory.storyName = 'Default';
export const HandleImageClick = (args) => (
<div style={{ width: 200, height: 200 }}>
<CollectibleDefaultImage {...args} />
<NftDefaultImage {...args} />
</div>
);
HandleImageClick.args = {
// eslint-disable-next-line no-alert
handleImageClick: () => window.alert('CollectibleDefaultImage clicked!'),
handleImageClick: () => window.alert('NftDefaultImage clicked!'),
};

View File

@ -1,69 +1,61 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import CollectibleDefaultImage from '.';
import NftDefaultImage from '.';
describe('Collectible Default Image', () => {
describe('NFT Default Image', () => {
it('should render with no props', () => {
const { container } = renderWithProvider(<CollectibleDefaultImage />);
const { container } = renderWithProvider(<NftDefaultImage />);
expect(container).toMatchSnapshot();
});
it('should match snapshot with all provided props', () => {
const props = {
name: 'Collectible Name',
name: 'NFT Name',
tokenId: '123',
handleImageClick: jest.fn(),
};
const { container } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const { container } = renderWithProvider(<NftDefaultImage {...props} />);
expect(container).toMatchSnapshot();
});
it('should match snapshot with missing image click handler', () => {
const props = {
name: 'Collectible Name',
name: 'NFT Name',
tokenId: '123',
};
const { container } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const { container } = renderWithProvider(<NftDefaultImage {...props} />);
expect(container).toMatchSnapshot();
});
it('should render collectible name', () => {
it('should render NFT name', () => {
const props = {
name: 'Collectible Name',
name: 'NFT Name',
};
const { queryByText } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const { queryByText } = renderWithProvider(<NftDefaultImage {...props} />);
const collectibleElement = queryByText(`${props.name} #`);
const nftElement = queryByText(`${props.name} #`);
expect(collectibleElement).toBeInTheDocument();
expect(nftElement).toBeInTheDocument();
});
it('should render collectible name and tokenId', () => {
it('should render NFT name and tokenId', () => {
const props = {
name: 'Collectible Name',
name: 'NFT Name',
tokenId: '123',
};
const { queryByText } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const { queryByText } = renderWithProvider(<NftDefaultImage {...props} />);
const collectibleElement = queryByText(`${props.name} #${props.tokenId}`);
const nftElement = queryByText(`${props.name} #${props.tokenId}`);
expect(collectibleElement).toBeInTheDocument();
expect(nftElement).toBeInTheDocument();
});
it('should handle image click', () => {
@ -72,11 +64,11 @@ describe('Collectible Default Image', () => {
};
const { queryByTestId } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
<NftDefaultImage {...props} />,
);
const collectibleImageElement = queryByTestId('collectible-default-image');
fireEvent.click(collectibleImageElement);
const nftImageElement = queryByTestId('nft-default-image');
fireEvent.click(nftImageElement);
expect(props.handleImageClick).toHaveBeenCalled();
});

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collectible Details should match minimal props and state snapshot 1`] = `
exports[`NFT Details should match minimal props and state snapshot 1`] = `
<div>
<div
class="asset-navigation"
@ -26,8 +26,8 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
<div>
<button
aria-label="NFT Options"
class="box mm-button-icon mm-button-icon--size-lg collectible-options__button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent box--rounded-lg"
data-testid="collectible-options__button"
class="box mm-button-icon mm-button-icon--size-lg nft-options__button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent box--rounded-lg"
data-testid="nft-options__button"
>
<div
class="box mm-icon mm-icon--size-lg box--flex-direction-row box--color-inherit"
@ -37,22 +37,22 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
</div>
</div>
<div
class="box collectible-details box--flex-direction-row"
class="box nft-details box--flex-direction-row"
>
<div
class="collectible-details__top-section"
class="nft-details__top-section"
>
<div
class="box collectible-details__card box--flex-direction-row box--justify-content-center box--background-color-background-default box--rounded-md box--border-style-solid box--border-color-border-muted box--border-width-1 box--display-flex"
class="box nft-details__card box--flex-direction-row box--justify-content-center box--background-color-background-default box--rounded-md box--border-style-solid box--border-color-border-muted box--border-width-1 box--display-flex"
>
<img
alt="MUNK #1 1"
class="collectible-details__image"
class="nft-details__image"
src="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
/>
</div>
<div
class="box collectible-details__info box--flex-direction-column box--justify-content-space-between box--display-flex"
class="box nft-details__info box--flex-direction-column box--justify-content-space-between box--display-flex"
>
<div>
<h4
@ -71,8 +71,8 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
class="box box--display-flex box--flex-direction-row box--width-1/2"
>
<button
class="button btn--rounded btn-primary collectible-details__send-button"
data-testid="collectible-send-button"
class="button btn--rounded btn-primary nft-details__send-button"
data-testid="nft-send-button"
role="button"
tabindex="0"
>
@ -88,12 +88,12 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
class="box box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography collectible-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography nft-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Source
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography collectible-details__image-source typography--h6 typography--weight-normal typography--style-normal typography--color-primary-default"
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography nft-details__image-source typography--h6 typography--weight-normal typography--style-normal typography--color-primary-default"
>
<a
href="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
@ -109,12 +109,12 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
class="box box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography collectible-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography nft-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Contract address
</h6>
<div
class="box collectible-details__contract-wrapper box--display-flex box--flex-direction-row"
class="box nft-details__contract-wrapper box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative typography--overflowwrap-break-word"
@ -122,7 +122,7 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
0xDc7...6414
</h6>
<div
class="collectible-details__tooltip-wrapper"
class="nft-details__tooltip-wrapper"
>
<div
aria-describedby="tippy-tooltip-1"
@ -134,8 +134,8 @@ exports[`Collectible Details should match minimal props and state snapshot 1`] =
>
<button
aria-label="copy"
class="box mm-button-icon mm-button-icon--size-lg collectible-details__contract-copy-button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-alternative box--background-color-transparent box--rounded-lg"
data-testid="collectible-address-copy"
class="box mm-button-icon mm-button-icon--size-lg nft-details__contract-copy-button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-alternative box--background-color-transparent box--rounded-lg"
data-testid="nft-address-copy"
>
<div
class="box mm-icon mm-icon--size-lg box--flex-direction-row box--color-inherit"

View File

@ -3,7 +3,7 @@ $link-title-width: 160px;
$spacer-break-large: 24px;
$spacer-break-small: 16px;
.collectible-details {
.nft-details {
padding: 0 $spacer-break-small;
@include screen-sm-min {

View File

@ -23,24 +23,24 @@ import {
getAssetImageURL,
shortenAddress,
} from '../../../helpers/utils/util';
import { getCollectibleImageAlt } from '../../../helpers/utils/nfts';
import { getNftImageAlt } from '../../../helpers/utils/nfts';
import {
getCurrentChainId,
getIpfsGateway,
getSelectedIdentity,
} from '../../../selectors';
import AssetNavigation from '../../../pages/asset/components/asset-navigation';
import { getCollectibleContracts } from '../../../ducks/metamask/metamask';
import { getNftContracts } from '../../../ducks/metamask/metamask';
import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
import {
checkAndUpdateSingleNftOwnershipStatus,
removeAndIgnoreNft,
setRemoveCollectibleMessage,
setRemoveNftMessage,
} from '../../../store/actions';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import CollectibleOptions from '../nft-options/nft-options';
import NftOptions from '../nft-options/nft-options';
import Button from '../../ui/button';
import { startNewDraftTransaction } from '../../../ducks/send';
import InfoTooltip from '../../ui/info-tooltip';
@ -51,11 +51,11 @@ import {
AssetType,
TokenStandard,
} from '../../../../shared/constants/transaction';
import CollectibleDefaultImage from '../nft-default-image';
import NftDefaultImage from '../nft-default-image';
import { ButtonIcon, ICON_NAMES } from '../../component-library';
import Tooltip from '../../ui/tooltip';
export default function CollectibleDetails({ collectible }) {
export default function NftDetails({ nft }) {
const {
image,
imageOriginal,
@ -67,28 +67,24 @@ export default function CollectibleDetails({ collectible }) {
isCurrentlyOwned,
lastSale,
imageThumbnail,
} = collectible;
} = nft;
const t = useI18nContext();
const history = useHistory();
const dispatch = useDispatch();
const ipfsGateway = useSelector(getIpfsGateway);
const collectibleContracts = useSelector(getCollectibleContracts);
const nftContracts = useSelector(getNftContracts);
const currentNetwork = useSelector(getCurrentChainId);
const [addressCopied, handleAddressCopy] = useCopyToClipboard();
const collectibleContractName = collectibleContracts.find(
({ address: contractAddress }) =>
isEqualCaseInsensitive(contractAddress, address),
const nftContractName = nftContracts.find(({ address: contractAddress }) =>
isEqualCaseInsensitive(contractAddress, address),
)?.name;
const selectedAccountName = useSelector(
(state) => getSelectedIdentity(state).name,
);
const collectibleImageAlt = getCollectibleImageAlt(collectible);
const collectibleImageURL = getAssetImageURL(
imageOriginal ?? image,
ipfsGateway,
);
const isDataURI = collectibleImageURL.startsWith('data:');
const nftImageAlt = getNftImageAlt(nft);
const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway);
const isDataURI = nftImageURL.startsWith('data:');
const formattedTimestamp = formatDate(
new Date(lastSale?.event_timestamp).getTime(),
'M/d/y',
@ -96,16 +92,16 @@ export default function CollectibleDetails({ collectible }) {
const onRemove = () => {
dispatch(removeAndIgnoreNft(address, tokenId));
dispatch(setRemoveCollectibleMessage('success'));
dispatch(setRemoveNftMessage('success'));
history.push(DEFAULT_ROUTE);
};
const prevCollectible = usePrevious(collectible);
const prevNft = usePrevious(nft);
useEffect(() => {
if (!isEqual(prevCollectible, collectible)) {
checkAndUpdateSingleNftOwnershipStatus(collectible);
if (!isEqual(prevNft, nft)) {
checkAndUpdateSingleNftOwnershipStatus(nft);
}
}, [collectible, prevCollectible]);
}, [nft, prevNft]);
const getOpenSeaLink = () => {
switch (currentNetwork) {
@ -129,7 +125,7 @@ export default function CollectibleDetails({ collectible }) {
await dispatch(
startNewDraftTransaction({
type: AssetType.NFT,
details: collectible,
details: nft,
}),
);
history.push(SEND_ROUTE);
@ -149,8 +145,8 @@ export default function CollectibleDetails({ collectible }) {
type="primary"
onClick={onSend}
disabled={sendDisabled}
className="collectible-details__send-button"
data-testid="collectible-send-button"
className="nft-details__send-button"
data-testid="nft-send-button"
>
{t('send')}
</Button>
@ -165,10 +161,10 @@ export default function CollectibleDetails({ collectible }) {
<>
<AssetNavigation
accountName={selectedAccountName}
assetName={collectibleContractName}
assetName={nftContractName}
onBack={() => history.push(DEFAULT_ROUTE)}
optionsButton={
<CollectibleOptions
<NftOptions
onViewOnOpensea={
openSeaLink
? () => global.platform.openTab({ url: openSeaLink })
@ -178,26 +174,26 @@ export default function CollectibleDetails({ collectible }) {
/>
}
/>
<Box className="collectible-details">
<div className="collectible-details__top-section">
<Box className="nft-details">
<div className="nft-details__top-section">
<Card
padding={0}
justifyContent={JustifyContent.center}
className="collectible-details__card"
className="nft-details__card"
>
{image ? (
<img
className="collectible-details__image"
src={collectibleImageURL}
alt={collectibleImageAlt}
className="nft-details__image"
src={nftImageURL}
alt={nftImageAlt}
/>
) : (
<CollectibleDefaultImage name={name} tokenId={tokenId} />
<NftDefaultImage name={name} tokenId={tokenId} />
)}
</Card>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
className="collectible-details__info"
className="nft-details__info"
justifyContent={JustifyContent.spaceBetween}
>
<div>
@ -224,7 +220,7 @@ export default function CollectibleDetails({ collectible }) {
color={TextColor.textDefault}
variant={TypographyVariant.H6}
fontWeight={FONT_WEIGHT.BOLD}
className="collectible-details__description"
className="nft-details__description"
boxProps={{ margin: 0, marginBottom: 2 }}
>
{t('description')}
@ -255,14 +251,14 @@ export default function CollectibleDetails({ collectible }) {
marginBottom: 4,
marginRight: 2,
}}
className="collectible-details__link-title"
className="nft-details__link-title"
>
{t('lastSold')}
</Typography>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
className="collectible-details__contract-wrapper"
className="nft-details__contract-wrapper"
>
<Typography
color={TextColor.textAlternative}
@ -284,14 +280,14 @@ export default function CollectibleDetails({ collectible }) {
marginBottom: 4,
marginRight: 2,
}}
className="collectible-details__link-title"
className="nft-details__link-title"
>
{t('lastPriceSold')}
</Typography>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
className="collectible-details__contract-wrapper"
className="nft-details__contract-wrapper"
>
<Typography
color={TextColor.textAlternative}
@ -315,7 +311,7 @@ export default function CollectibleDetails({ collectible }) {
marginBottom: 4,
marginRight: 2,
}}
className="collectible-details__link-title"
className="nft-details__link-title"
>
{t('source')}
</Typography>
@ -325,21 +321,21 @@ export default function CollectibleDetails({ collectible }) {
margin: 0,
marginBottom: 4,
}}
className="collectible-details__image-source"
className="nft-details__image-source"
color={
isDataURI ? TextColor.textDefault : TextColor.primaryDefault
}
>
{isDataURI ? (
<>{collectibleImageURL}</>
<>{nftImageURL}</>
) : (
<a
target="_blank"
rel="noopener noreferrer"
href={collectibleImageURL}
title={collectibleImageURL}
href={nftImageURL}
title={nftImageURL}
>
{collectibleImageURL}
{nftImageURL}
</a>
)}
</Typography>
@ -355,7 +351,7 @@ export default function CollectibleDetails({ collectible }) {
marginBottom: 4,
marginRight: 2,
}}
className="collectible-details__link-title"
className="nft-details__link-title"
>
{t('link')}
</Typography>
@ -365,7 +361,7 @@ export default function CollectibleDetails({ collectible }) {
margin: 0,
marginBottom: 4,
}}
className="collectible-details__image-source"
className="nft-details__image-source"
color={
isDataURI ? TextColor.textDefault : TextColor.primaryDefault
}
@ -373,8 +369,8 @@ export default function CollectibleDetails({ collectible }) {
<a
target="_blank"
rel="noopener noreferrer"
href={collectibleImageURL}
title={collectibleImageURL}
href={nftImageURL}
title={nftImageURL}
>
{imageThumbnail}
</a>
@ -391,14 +387,14 @@ export default function CollectibleDetails({ collectible }) {
marginBottom: 4,
marginRight: 2,
}}
className="collectible-details__link-title"
className="nft-details__link-title"
>
{t('contractAddress')}
</Typography>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
className="collectible-details__contract-wrapper"
className="nft-details__contract-wrapper"
>
<Typography
color={TextColor.textAlternative}
@ -409,7 +405,7 @@ export default function CollectibleDetails({ collectible }) {
{shortenAddress(address)}
</Typography>
<Tooltip
wrapperClassName="collectible-details__tooltip-wrapper"
wrapperClassName="nft-details__tooltip-wrapper"
position="bottom"
title={
addressCopied ? t('copiedExclamation') : t('copyToClipboard')
@ -418,8 +414,8 @@ export default function CollectibleDetails({ collectible }) {
<ButtonIcon
ariaLabel="copy"
color={IconColor.iconAlternative}
className="collectible-details__contract-copy-button"
data-testid="collectible-address-copy"
className="nft-details__contract-copy-button"
data-testid="nft-address-copy"
onClick={() => {
handleAddressCopy(address);
}}
@ -443,8 +439,8 @@ export default function CollectibleDetails({ collectible }) {
);
}
CollectibleDetails.propTypes = {
collectible: PropTypes.shape({
NftDetails.propTypes = {
nft: PropTypes.shape({
address: PropTypes.string.isRequired,
tokenId: PropTypes.string.isRequired,
isCurrentlyOwned: PropTypes.bool,

View File

@ -1,7 +1,7 @@
import React from 'react';
import CollectibleDetails from './nft-details';
import NftDetails from './nft-details';
const collectible = {
const nft = {
name: 'Catnip Spicywright',
tokenId: '1124157',
address: '0x06012c8cf97bead5deae237070f9587f8e7a266d',
@ -16,31 +16,31 @@ const collectible = {
};
export default {
title: 'Components/App/CollectiblesDetail',
title: 'Components/App/NftsDetail',
argTypes: {
collectible: {
nft: {
control: 'object',
},
},
args: {
collectible,
nft,
},
};
export const DefaultStory = (args) => {
return <CollectibleDetails {...args} />;
return <NftDetails {...args} />;
};
DefaultStory.storyName = 'Default';
export const NoImage = (args) => {
return <CollectibleDetails {...args} />;
return <NftDetails {...args} />;
};
NoImage.args = {
collectible: {
...collectible,
nft: {
...nft,
image: undefined,
},
};

View File

@ -10,9 +10,9 @@ import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
import { AssetType } from '../../../../shared/constants/transaction';
import {
removeAndIgnoreNft,
setRemoveCollectibleMessage,
setRemoveNftMessage,
} from '../../../store/actions';
import CollectibleDetails from './nft-details';
import NftDetails from './nft-details';
jest.mock('copy-to-clipboard');
@ -36,17 +36,17 @@ jest.mock('../../../store/actions.ts', () => ({
...jest.requireActual('../../../store/actions.ts'),
checkAndUpdateSingleNftOwnershipStatus: jest.fn().mockReturnValue(jest.fn()),
removeAndIgnoreNft: jest.fn().mockReturnValue(jest.fn()),
setRemoveCollectibleMessage: jest.fn().mockReturnValue(jest.fn()),
setRemoveNftMessage: jest.fn().mockReturnValue(jest.fn()),
}));
describe('Collectible Details', () => {
describe('NFT Details', () => {
const mockStore = configureMockStore([thunk])(mockState);
const collectibles =
const nfts =
mockState.metamask.allNftContracts[mockState.metamask.selectedAddress][5];
const props = {
collectible: collectibles[5],
nft: nfts[5],
};
beforeEach(() => {
@ -55,7 +55,7 @@ describe('Collectible Details', () => {
it('should match minimal props and state snapshot', () => {
const { container } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
@ -64,7 +64,7 @@ describe('Collectible Details', () => {
it(`should route to '/' route when the back button is clicked`, () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
@ -75,51 +75,51 @@ describe('Collectible Details', () => {
expect(mockHistoryPush).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it(`should call removeAndIgnoreNft with proper collectible details and route to '/' when removing collectible`, () => {
it(`should call removeAndIgnoreNFT with proper nft details and route to '/' when removing nft`, () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const removeCollectibleButton = queryByTestId('collectible-item-remove');
fireEvent.click(removeCollectibleButton);
const removeNftButton = queryByTestId('nft-item-remove');
fireEvent.click(removeNftButton);
expect(removeAndIgnoreNft).toHaveBeenCalledWith(
collectibles[5].address,
collectibles[5].tokenId,
nfts[5].address,
nfts[5].tokenId,
);
expect(setRemoveCollectibleMessage).toHaveBeenCalledWith('success');
expect(setRemoveNftMessage).toHaveBeenCalledWith('success');
expect(mockHistoryPush).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it('should copy collectible address', async () => {
it('should copy nft address', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
const copyAddressButton = queryByTestId('collectible-address-copy');
const copyAddressButton = queryByTestId('nft-address-copy');
fireEvent.click(copyAddressButton);
expect(copyToClipboard).toHaveBeenCalledWith(collectibles[5].address);
expect(copyToClipboard).toHaveBeenCalledWith(nfts[5].address);
});
it('should navigate to draft transaction send route with ERC721 data', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
const collectibleSendButton = queryByTestId('collectible-send-button');
fireEvent.click(collectibleSendButton);
const nftSendButton = queryByTestId('nft-send-button');
fireEvent.click(nftSendButton);
await waitFor(() => {
expect(startNewDraftTransaction).toHaveBeenCalledWith({
type: AssetType.NFT,
details: collectibles[5],
details: nfts[5],
});
expect(mockHistoryPush).toHaveBeenCalledWith(SEND_ROUTE);
@ -127,18 +127,18 @@ describe('Collectible Details', () => {
});
it('should not render send button if isCurrentlyOwned is false', () => {
const sixthCollectibleProps = {
collectible: collectibles[6],
const sixthNftProps = {
nft: nfts[6],
};
collectibles[6].isCurrentlyOwned = false;
nfts[6].isCurrentlyOwned = false;
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...sixthCollectibleProps} />,
<NftDetails {...sixthNftProps} />,
mockStore,
);
const collectibleSendButton = queryByTestId('collectible-send-button');
expect(collectibleSendButton).not.toBeInTheDocument();
const nftSendButton = queryByTestId('nft-send-button');
expect(nftSendButton).not.toBeInTheDocument();
});
describe(`Alternative Networks' OpenSea Links`, () => {
@ -146,24 +146,24 @@ describe('Collectible Details', () => {
global.platform = { openTab: jest.fn() };
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
it('should open tab to mainnet opensea url with collectible info', async () => {
it('should open tab to mainnet opensea url with nft info', async () => {
global.platform = { openTab: jest.fn() };
const mainnetState = {
@ -178,24 +178,24 @@ describe('Collectible Details', () => {
const mainnetMockStore = configureMockStore([thunk])(mainnetState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
mainnetMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
url: `https://opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
it('should open tab to polygon opensea url with collectible info', async () => {
it('should open tab to polygon opensea url with nft info', async () => {
const polygonState = {
...mockState,
metamask: {
@ -208,24 +208,24 @@ describe('Collectible Details', () => {
const polygonMockStore = configureMockStore([thunk])(polygonState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
polygonMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://opensea.io/assets/matic/${collectibles[5].address}/${collectibles[5].tokenId}`,
url: `https://opensea.io/assets/matic/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
it('should open tab to sepolia opensea url with collectible info', async () => {
it('should open tab to sepolia opensea url with nft info', async () => {
const sepoliaState = {
...mockState,
metamask: {
@ -238,19 +238,19 @@ describe('Collectible Details', () => {
const sepoliaMockStore = configureMockStore([thunk])(sepoliaState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
sepoliaMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
url: `https://testnets.opensea.io/assets/${nfts[5].address}/${nfts[5].tokenId}`,
});
});
});
@ -270,14 +270,14 @@ describe('Collectible Details', () => {
);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
<NftDetails {...props} />,
randomNetworkMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
await waitFor(() => {
expect(openOpenSea).not.toBeInTheDocument();
});

View File

@ -1,4 +1,4 @@
.collectible-options {
.nft-options {
&__button {
padding: 2px 0 2px 8px;
}

View File

@ -6,34 +6,34 @@ import { Menu, MenuItem } from '../../ui/menu';
import { ButtonIcon, ICON_NAMES } from '../../component-library';
import { Color } from '../../../helpers/constants/design-system';
const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
const NftOptions = ({ onRemove, onViewOnOpensea }) => {
const t = useContext(I18nContext);
const [collectibleOptionsOpen, setCollectibleOptionsOpen] = useState(false);
const [nftOptionsOpen, setNftOptionsOpen] = useState(false);
const ref = useRef(false);
return (
<div ref={ref}>
<ButtonIcon
iconName={ICON_NAMES.MORE_VERTICAL}
className="collectible-options__button"
data-testid="collectible-options__button"
onClick={() => setCollectibleOptionsOpen(true)}
className="nft-options__button"
data-testid="nft-options__button"
onClick={() => setNftOptionsOpen(true)}
color={Color.textDefault}
ariaLabel={t('nftOptions')}
/>
{collectibleOptionsOpen ? (
{nftOptionsOpen ? (
<Menu
data-testid="close-collectible-options-menu"
data-testid="close-nft-options-menu"
anchorElement={ref.current}
onHide={() => setCollectibleOptionsOpen(false)}
onHide={() => setNftOptionsOpen(false)}
>
{onViewOnOpensea ? (
<MenuItem
iconName={ICON_NAMES.EXPORT}
data-testid="collectible-options__view-on-opensea"
data-testid="nft-options__view-on-opensea"
onClick={() => {
setCollectibleOptionsOpen(false);
setNftOptionsOpen(false);
onViewOnOpensea();
}}
>
@ -42,9 +42,9 @@ const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
) : null}
<MenuItem
iconName={ICON_NAMES.TRASH}
data-testid="collectible-item-remove"
data-testid="nft-item-remove"
onClick={() => {
setCollectibleOptionsOpen(false);
setNftOptionsOpen(false);
onRemove();
}}
>
@ -56,9 +56,9 @@ const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
);
};
CollectibleOptions.propTypes = {
NftOptions.propTypes = {
onRemove: PropTypes.func.isRequired,
onViewOnOpensea: PropTypes.func,
};
export default CollectibleOptions;
export default NftOptions;

View File

@ -1,42 +1,36 @@
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import CollectibleOptions from './nft-options';
import NftOptions from './nft-options';
describe('Collectible Options Component', () => {
describe('NFT Options Component', () => {
const props = {
onRemove: jest.fn(),
onViewOnOpensea: jest.fn(),
};
it('should expand collectible options menu`', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
it('should expand NFT options menu`', async () => {
const { queryByTestId } = renderWithProvider(<NftOptions {...props} />);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
expect(queryByTestId('collectible-item-remove')).not.toBeInTheDocument();
expect(queryByTestId('nft-item-remove')).not.toBeInTheDocument();
fireEvent.click(openOptionMenuButton);
await waitFor(() => {
expect(queryByTestId('collectible-item-remove')).toBeInTheDocument();
expect(queryByTestId('nft-item-remove')).toBeInTheDocument();
});
});
it('should expand and close menu options when clicked`', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const { queryByTestId } = renderWithProvider(<NftOptions {...props} />);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const closeOptionMenuButton = queryByTestId(
'close-collectible-options-menu',
);
const closeOptionMenuButton = queryByTestId('close-nft-options-menu');
fireEvent.click(closeOptionMenuButton);
@ -44,37 +38,33 @@ describe('Collectible Options Component', () => {
});
it('should click onRemove handler and close option menu', () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const { queryByTestId } = renderWithProvider(<NftOptions {...props} />);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const openOptionMenuButton = queryByTestId('nft-options__button');
fireEvent.click(openOptionMenuButton);
const removeCollectibleButton = queryByTestId('collectible-item-remove');
const removeNftButton = queryByTestId('nft-item-remove');
fireEvent.click(removeCollectibleButton);
fireEvent.click(removeNftButton);
expect(props.onRemove).toHaveBeenCalled();
expect(removeCollectibleButton).not.toBeInTheDocument();
expect(removeNftButton).not.toBeInTheDocument();
});
it('should click onViewOnOpensea handler and close option menu', () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const { queryByTestId } = renderWithProvider(<NftOptions {...props} />);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const removeCollectibleButton = queryByTestId('collectible-item-remove');
const openOptionMenuButton = queryByTestId('nft-options__button');
const removeNftButton = queryByTestId('nft-item-remove');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
const openOpenSea = queryByTestId('nft-options__view-on-opensea');
fireEvent.click(openOpenSea);
expect(props.onViewOnOpensea).toHaveBeenCalled();
expect(removeCollectibleButton).not.toBeInTheDocument();
expect(removeNftButton).not.toBeInTheDocument();
});
});

View File

@ -1,4 +1,4 @@
.collectibles-detection-notice {
.nfts-detection-notice {
margin: 16px 16px 0 16px;
&__message {
@ -13,7 +13,7 @@
right: 8px;
}
a.collectibles-detection-notice__message__link {
a.nfts-detection-notice__message__link {
@include H7;
width: 100%;

View File

@ -14,13 +14,13 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
import Button from '../../ui/button';
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
export default function CollectiblesDetectionNotice() {
export default function NftsDetectionNotice() {
const t = useI18nContext();
const history = useHistory();
return (
<Box className="collectibles-detection-notice">
<Dialog type="message" className="collectibles-detection-notice__message">
<Box className="nfts-detection-notice">
<Dialog type="message" className="nfts-detection-notice__message">
<Box display={DISPLAY.FLEX}>
<Box paddingTop={1}>
<i
@ -54,7 +54,7 @@ export default function CollectiblesDetectionNotice() {
e.preventDefault();
history.push(`${EXPERIMENTAL_ROUTE}#autodetect-nfts`);
}}
className="collectibles-detection-notice__message__link"
className="nfts-detection-notice__message__link"
>
{t('selectNFTPrivacyPreference')}
</Button>

View File

@ -1,4 +1,4 @@
.collectibles-items {
.nfts-items {
&__collection {
margin-bottom: 24px;

View File

@ -25,13 +25,13 @@ import {
} from '../../../selectors';
import { ASSET_ROUTE } from '../../../helpers/constants/routes';
import { getAssetImageURL } from '../../../helpers/utils/util';
import { getCollectibleImageAlt } from '../../../helpers/utils/nfts';
import { updateCollectibleDropDownState } from '../../../store/actions';
import { getNftImageAlt } from '../../../helpers/utils/nfts';
import { updateNftDropDownState } from '../../../store/actions';
import { usePrevious } from '../../../hooks/usePrevious';
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
import { getNftsDropdownState } from '../../../ducks/metamask/metamask';
import { useI18nContext } from '../../../hooks/useI18nContext';
import CollectibleDefaultImage from '../nft-default-image';
import { Icon, ICON_NAMES } from '../../component-library';
import NftDefaultImage from '../nft-default-image';
const width =
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
@ -40,13 +40,13 @@ const width =
const PREVIOUSLY_OWNED_KEY = 'previouslyOwned';
export default function CollectiblesItems({
export default function NftsItems({
collections = {},
previouslyOwnedCollection = {},
}) {
const dispatch = useDispatch();
const collectionsKeys = Object.keys(collections);
const collectiblesDropdownState = useSelector(getCollectiblesDropdownState);
const nftsDropdownState = useSelector(getNftsDropdownState);
const previousCollectionKeys = usePrevious(collectionsKeys);
const selectedAddress = useSelector(getSelectedAddress);
const chainId = useSelector(getCurrentChainId);
@ -57,29 +57,29 @@ export default function CollectiblesItems({
chainId !== undefined &&
selectedAddress !== undefined &&
!isEqual(previousCollectionKeys, collectionsKeys) &&
(collectiblesDropdownState?.[selectedAddress]?.[chainId] === undefined ||
Object.keys(collectiblesDropdownState?.[selectedAddress]?.[chainId])
.length === 0)
(nftsDropdownState?.[selectedAddress]?.[chainId] === undefined ||
Object.keys(nftsDropdownState?.[selectedAddress]?.[chainId]).length ===
0)
) {
const initState = {};
collectionsKeys.forEach((key) => {
initState[key] = true;
});
const newCollectibleDropdownState = {
...collectiblesDropdownState,
const newNftDropdownState = {
...nftsDropdownState,
[selectedAddress]: {
...collectiblesDropdownState?.[selectedAddress],
...nftsDropdownState?.[selectedAddress],
[chainId]: initState,
},
};
dispatch(updateCollectibleDropDownState(newCollectibleDropdownState));
dispatch(updateNftDropDownState(newNftDropdownState));
}
}, [
collectionsKeys,
previousCollectionKeys,
collectiblesDropdownState,
nftsDropdownState,
selectedAddress,
chainId,
dispatch,
@ -94,51 +94,44 @@ export default function CollectiblesItems({
<img
alt={collectionName}
src={getAssetImageURL(collectionImage, ipfsGateway)}
className="collectibles-items__collection-image"
className="nfts-items__collection-image"
/>
);
}
return (
<div className="collectibles-items__collection-image-alt">
<div className="nfts-items__collection-image-alt">
{collectionName?.[0]?.toUpperCase() ?? null}
</div>
);
};
const updateCollectibleDropDownStateKey = (key, isExpanded) => {
const currentAccountCollectibleDropdownState =
collectiblesDropdownState[selectedAddress][chainId];
const updateNftDropDownStateKey = (key, isExpanded) => {
const currentAccountNftDropdownState =
nftsDropdownState[selectedAddress][chainId];
const newCurrentAccountState = {
...currentAccountCollectibleDropdownState,
...currentAccountNftDropdownState,
[key]: !isExpanded,
};
collectiblesDropdownState[selectedAddress][chainId] =
newCurrentAccountState;
nftsDropdownState[selectedAddress][chainId] = newCurrentAccountState;
dispatch(updateCollectibleDropDownState(collectiblesDropdownState));
dispatch(updateNftDropDownState(nftsDropdownState));
};
const renderCollection = ({
collectibles,
collectionName,
collectionImage,
key,
}) => {
if (!collectibles.length) {
const renderCollection = ({ nfts, collectionName, collectionImage, key }) => {
if (!nfts.length) {
return null;
}
const isExpanded =
collectiblesDropdownState[selectedAddress]?.[chainId]?.[key];
const isExpanded = nftsDropdownState[selectedAddress]?.[chainId]?.[key];
return (
<div className="collectibles-items__collection" key={`collection-${key}`}>
<div className="nfts-items__collection" key={`collection-${key}`}>
<button
className="collectibles-items__collection-wrapper"
className="nfts-items__collection-wrapper"
data-testid="collection-expander-button"
onClick={() => {
updateCollectibleDropDownStateKey(key, isExpanded);
updateNftDropDownStateKey(key, isExpanded);
}}
>
<Box
@ -146,11 +139,11 @@ export default function CollectiblesItems({
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
className="collectibles-items__collection-accordion-title"
className="nfts-items__collection-accordion-title"
>
<Box
alignItems={AlignItems.center}
className="collectibles-items__collection-header"
className="nfts-items__collection-header"
>
{renderCollectionImage(collectionImage, collectionName)}
<Typography
@ -158,9 +151,7 @@ export default function CollectiblesItems({
variant={TypographyVariant.H5}
margin={2}
>
{`${collectionName ?? t('unknownCollection')} (${
collectibles.length
})`}
{`${collectionName ?? t('unknownCollection')} (${nfts.length})`}
</Typography>
</Box>
<Box alignItems={AlignItems.flexEnd}>
@ -176,43 +167,42 @@ export default function CollectiblesItems({
{isExpanded ? (
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
{collectibles.map((collectible, i) => {
const { image, address, tokenId, backgroundColor, name } =
collectible;
const collectibleImage = getAssetImageURL(image, ipfsGateway);
const collectibleImageAlt = getCollectibleImageAlt(collectible);
{nfts.map((nft, i) => {
const { image, address, tokenId, backgroundColor, name } = nft;
const nftImage = getAssetImageURL(image, ipfsGateway);
const nftImageAlt = getNftImageAlt(nft);
const handleImageClick = () =>
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`);
return (
<Box
data-testid="collectible-wrapper"
data-testid="nft-wrapper"
width={width}
key={`collectible-${i}`}
className="collectibles-items__item-wrapper"
key={`nft-${i}`}
className="nfts-items__item-wrapper"
>
<Card
padding={0}
justifyContent={JustifyContent.center}
className="collectibles-items__item-wrapper__card"
className="nfts-items__item-wrapper__card"
>
{collectibleImage ? (
{nftImage ? (
<button
className="collectibles-items__item"
className="nfts-items__item"
style={{
backgroundColor,
}}
onClick={handleImageClick}
>
<img
className="collectibles-items__item-image"
data-testid="collectible-image"
src={collectibleImage}
alt={collectibleImageAlt}
className="nfts-items__item-image"
data-testid="nft-image"
src={nftImage}
alt={nftImageAlt}
/>
</button>
) : (
<CollectibleDefaultImage
<NftDefaultImage
name={name}
tokenId={tokenId}
handleImageClick={handleImageClick}
@ -229,7 +219,7 @@ export default function CollectiblesItems({
};
return (
<div className="collectibles-items">
<div className="nfts-items">
<Box
paddingTop={6}
paddingBottom={6}
@ -239,11 +229,10 @@ export default function CollectiblesItems({
>
<>
{collectionsKeys.map((key) => {
const { collectibles, collectionName, collectionImage } =
collections[key];
const { nfts, collectionName, collectionImage } = collections[key];
return renderCollection({
collectibles,
nfts,
collectionName,
collectionImage,
key,
@ -251,9 +240,9 @@ export default function CollectiblesItems({
});
})}
{renderCollection({
collectibles: previouslyOwnedCollection.collectibles,
nfts: previouslyOwnedCollection.nfts,
collectionName: previouslyOwnedCollection.collectionName,
collectionImage: previouslyOwnedCollection.collectibles[0]?.image,
collectionImage: previouslyOwnedCollection.nfts[0]?.image,
isPreviouslyOwnedCollection: true,
key: PREVIOUSLY_OWNED_KEY,
})}
@ -263,9 +252,9 @@ export default function CollectiblesItems({
);
}
CollectiblesItems.propTypes = {
NftsItems.propTypes = {
previouslyOwnedCollection: PropTypes.shape({
collectibles: PropTypes.arrayOf(
nfts: PropTypes.arrayOf(
PropTypes.shape({
address: PropTypes.string.isRequired,
tokenId: PropTypes.string.isRequired,
@ -286,7 +275,7 @@ CollectiblesItems.propTypes = {
collectionImage: PropTypes.string,
}),
collections: PropTypes.shape({
collectibles: PropTypes.arrayOf(
nfts: PropTypes.arrayOf(
PropTypes.shape({
address: PropTypes.string.isRequired,
tokenId: PropTypes.string.isRequired,

View File

@ -4,8 +4,8 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { updateCollectibleDropDownState } from '../../../store/actions';
import CollectiblesItems from '.';
import { updateNftDropDownState } from '../../../store/actions';
import NftsItems from '.';
const mockHistoryPush = jest.fn();
@ -19,30 +19,30 @@ jest.mock('react-router-dom', () => ({
jest.mock('../../../store/actions.ts', () => ({
...jest.requireActual('../../../store/actions.ts'),
updateCollectibleDropDownState: jest.fn().mockReturnValue(jest.fn()),
updateNftDropDownState: jest.fn().mockReturnValue(jest.fn()),
}));
describe('Collectibles Item Component', () => {
const collectibles =
describe('NFTs Item Component', () => {
const nfts =
mockState.metamask.allNftContracts[mockState.metamask.selectedAddress][5];
const props = {
collections: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
collectibles,
nfts,
collectionImage: '',
collectionName: 'Collectible Collection',
collectionName: 'NFT Collection',
},
},
previouslyOwnedCollection: {
collectibles: [],
nfts: [],
},
};
const mockStore = configureMockStore([thunk])(mockState);
it('should expand collectible collection showing individual collectibles', async () => {
it('should expand NFT collection showing individual NFTs', async () => {
const { queryByTestId, queryAllByTestId, rerender } = renderWithProvider(
<CollectiblesItems {...props} />,
<NftsItems {...props} />,
mockStore,
);
@ -50,11 +50,11 @@ describe('Collectibles Item Component', () => {
'collection-expander-button',
);
expect(queryAllByTestId('collectible-wrapper')).toHaveLength(0);
expect(queryAllByTestId('nft-wrapper')).toHaveLength(0);
fireEvent.click(collectionExpanderButton);
expect(updateCollectibleDropDownState).toHaveBeenCalledWith({
expect(updateNftDropDownState).toHaveBeenCalledWith({
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'0x5': {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': true,
@ -63,24 +63,24 @@ describe('Collectibles Item Component', () => {
},
});
rerender(<CollectiblesItems {...props} />, mockStore);
rerender(<NftsItems {...props} />, mockStore);
expect(queryAllByTestId('collectible-wrapper')).toHaveLength(8);
expect(queryAllByTestId('nft-wrapper')).toHaveLength(8);
});
it('should collectible click image', () => {
it('should NFT click image', () => {
const { queryAllByTestId } = renderWithProvider(
<CollectiblesItems {...props} />,
<NftsItems {...props} />,
mockStore,
);
const collectibleImages = queryAllByTestId('collectible-image');
const nftImages = queryAllByTestId('nft-image');
fireEvent.click(collectibleImages[0]);
fireEvent.click(nftImages[0]);
const firstCollectible = collectibles[0];
const collectibleRoute = `/asset/${firstCollectible.address}/${firstCollectible.tokenId}`;
const firstNft = nfts[0];
const nftRoute = `/asset/${firstNft.address}/${firstNft.tokenId}`;
expect(mockHistoryPush).toHaveBeenCalledWith(collectibleRoute);
expect(mockHistoryPush).toHaveBeenCalledWith(nftRoute);
});
});

View File

@ -1,4 +1,4 @@
.collectibles-tab {
.nfts-tab {
&__link {
a {
padding: 4px;

View File

@ -5,8 +5,8 @@ import { useHistory } from 'react-router-dom';
import Box from '../../ui/box';
import Button from '../../ui/button';
import Typography from '../../ui/typography/typography';
import CollectiblesDetectionNotice from '../nfts-detection-notice';
import CollectiblesItems from '../nfts-items';
import NftsDetectionNotice from '../nfts-detection-notice';
import NftsItems from '../nfts-items';
import {
TypographyVariant,
TEXT_ALIGN,
@ -23,18 +23,18 @@ import {
checkAndUpdateAllNftsOwnershipStatus,
detectNfts,
} from '../../../store/actions';
import { useCollectiblesCollections } from '../../../hooks/useNftsCollections';
import { useNftsCollections } from '../../../hooks/useNftsCollections';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
export default function CollectiblesTab({ onAddNFT }) {
export default function NftsTab({ onAddNFT }) {
const useNftDetection = useSelector(getUseNftDetection);
const isMainnet = useSelector(getIsMainnet);
const history = useHistory();
const t = useI18nContext();
const dispatch = useDispatch();
const { collectiblesLoading, collections, previouslyOwnedCollection } =
useCollectiblesCollections();
const { nftsLoading, collections, previouslyOwnedCollection } =
useNftsCollections();
const onEnableAutoDetect = () => {
history.push(EXPERIMENTAL_ROUTE);
@ -47,23 +47,21 @@ export default function CollectiblesTab({ onAddNFT }) {
checkAndUpdateAllNftsOwnershipStatus();
};
if (collectiblesLoading) {
return <div className="collectibles-tab__loading">{t('loadingNFTs')}</div>;
if (nftsLoading) {
return <div className="nfts-tab__loading">{t('loadingNFTs')}</div>;
}
return (
<Box className="collectibles-tab">
<Box className="nfts-tab">
{Object.keys(collections).length > 0 ||
previouslyOwnedCollection.collectibles.length > 0 ? (
<CollectiblesItems
previouslyOwnedCollection.nfts.length > 0 ? (
<NftsItems
collections={collections}
previouslyOwnedCollection={previouslyOwnedCollection}
/>
) : (
<>
{isMainnet && !useNftDetection ? (
<CollectiblesDetectionNotice />
) : null}
{isMainnet && !useNftDetection ? <NftsDetectionNotice /> : null}
<Box padding={12}>
<Box justifyContent={JustifyContent.center}>
<img src="./images/no-nfts.svg" />
@ -73,7 +71,7 @@ export default function CollectiblesTab({ onAddNFT }) {
marginBottom={12}
justifyContent={JustifyContent.center}
flexDirection={FLEX_DIRECTION.COLUMN}
className="collectibles-tab__link"
className="nfts-tab__link"
>
<Typography
color={TextColor.textMuted}
@ -114,7 +112,7 @@ export default function CollectiblesTab({ onAddNFT }) {
{!isMainnet && Object.keys(collections).length < 1 ? null : (
<>
<Box
className="collectibles-tab__link"
className="nfts-tab__link"
justifyContent={JustifyContent.flexEnd}
>
{isMainnet && !useNftDetection ? (
@ -138,7 +136,7 @@ export default function CollectiblesTab({ onAddNFT }) {
)}
<Box
justifyContent={JustifyContent.flexStart}
className="collectibles-tab__link"
className="nfts-tab__link"
>
<Button type="link" onClick={onAddNFT}>
{t('importNFTs')}
@ -150,6 +148,6 @@ export default function CollectiblesTab({ onAddNFT }) {
);
}
CollectiblesTab.propTypes = {
NftsTab.propTypes = {
onAddNFT: PropTypes.func.isRequired,
};

View File

@ -6,9 +6,9 @@ import { renderWithProvider } from '../../../../test/jest/rendering';
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
import { setBackgroundConnection } from '../../../../test/jest';
import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
import CollectiblesTab from '.';
import NftsTab from '.';
const COLLECTIBLES = [
const NFTS = [
{
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
tokenId:
@ -123,7 +123,7 @@ const COLLECTIBLES = [
},
];
const COLLECTIBLES_CONTRACTS = [
const NFTS_CONTRACTS = [
{
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
name: 'PUNKS',
@ -137,7 +137,7 @@ const COLLECTIBLES_CONTRACTS = [
},
];
const collectiblesDropdownState = {
const nftsDropdownState = {
'0x495f947276749ce646f68ac8c248420045cb7b5e': true,
'0xdc7382eb0bc9c352a4cba23c909bda01e0206414': true,
};
@ -146,8 +146,8 @@ const ACCOUNT_1 = '0x123';
const ACCOUNT_2 = '0x456';
const render = ({
collectibleContracts = [],
collectibles = [],
nftContracts = [],
nfts = [],
selectedAddress,
chainId = '0x1',
useNftDetection,
@ -158,34 +158,34 @@ const render = ({
metamask: {
allNfts: {
[ACCOUNT_1]: {
[chainIdAsDecimal]: collectibles,
[chainIdAsDecimal]: nfts,
},
},
allNftContracts: {
[ACCOUNT_1]: {
[chainIdAsDecimal]: collectibleContracts,
[chainIdAsDecimal]: nftContracts,
},
},
provider: { chainId },
selectedAddress,
useNftDetection,
collectiblesDropdownState,
nftsDropdownState,
},
});
return renderWithProvider(<CollectiblesTab onAddNFT={onAddNFT} />, store);
return renderWithProvider(<NftsTab onAddNFT={onAddNFT} />, store);
};
describe('Collectible Items', () => {
const detectCollectiblesStub = jest.fn();
describe('NFT Items', () => {
const detectNftsStub = jest.fn();
const getStateStub = jest.fn();
const checkAndUpdateAllCollectiblesOwnershipStatusStub = jest.fn();
const updateCollectibleDropDownStateStub = jest.fn();
const checkAndUpdateAllNftsOwnershipStatusStub = jest.fn();
const updateNftDropDownStateStub = jest.fn();
setBackgroundConnection({
detectNfts: detectCollectiblesStub,
detectNfts: detectNftsStub,
getState: getStateStub,
checkAndUpdateAllNftsOwnershipStatus:
checkAndUpdateAllCollectiblesOwnershipStatusStub,
updateCollectibleDropDownState: updateCollectibleDropDownStateStub,
checkAndUpdateAllNftsOwnershipStatusStub,
updateNftDropDownState: updateNftDropDownStateStub,
});
const historyPushMock = jest.fn();
@ -198,25 +198,25 @@ describe('Collectible Items', () => {
jest.clearAllMocks();
});
describe('Collectibles Detection Notice', () => {
it('should render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles', () => {
describe('NFTs Detection Notice', () => {
it('should render the NFTs Detection Notice when currently selected network is Mainnet and currently selected account has no nfts', () => {
render({
selectedAddress: ACCOUNT_2,
collectibles: COLLECTIBLES,
nfts: NFTS,
});
expect(screen.queryByText('New! NFT detection')).toBeInTheDocument();
});
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has collectibles', () => {
it('should not render the NFTs Detection Notice when currently selected network is Mainnet and currently selected account has NFTs', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
});
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
});
it('should take user to the experimental settings tab in settings when user clicks "Turn on NFT detection in Settings"', () => {
render({
selectedAddress: ACCOUNT_2,
collectibles: COLLECTIBLES,
nfts: NFTS,
});
fireEvent.click(screen.queryByText('Turn on NFT detection in Settings'));
expect(historyPushMock).toHaveBeenCalledTimes(1);
@ -224,92 +224,84 @@ describe('Collectible Items', () => {
`${EXPERIMENTAL_ROUTE}#autodetect-nfts`,
);
});
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles but use collectible autodetection preference is set to true', () => {
it('should not render the NFTs Detection Notice when currently selected network is Mainnet and currently selected account has no NFTs but use NFT autodetection preference is set to true', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
useNftDetection: true,
});
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
});
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles but user has dismissed the notice before', () => {
it('should not render the NFTs Detection Notice when currently selected network is Mainnet and currently selected account has no NFTs but user has dismissed the notice before', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
});
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
});
});
describe('Collections', () => {
it('should render the name of the collections and number of collectibles in each collection if current account/chainId combination has collectibles', () => {
it('should render the name of the collections and number of NFTs in each collection if current account/chainId combination has NFTs', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
collectibleContracts: COLLECTIBLES_CONTRACTS,
nfts: NFTS,
nftContracts: NFTS_CONTRACTS,
});
expect(screen.queryByText('PUNKS (5)')).toBeInTheDocument();
expect(screen.queryByText('Munks (3)')).toBeInTheDocument();
});
it('should not render collections if current account/chainId combination has collectibles', () => {
it('should not render collections if current account/chainId combination has NFTs', () => {
render({
selectedAddress: ACCOUNT_2,
collectibles: COLLECTIBLES,
collectibleContracts: COLLECTIBLES_CONTRACTS,
nfts: NFTS,
nftContracts: NFTS_CONTRACTS,
});
expect(screen.queryByText('PUNKS (5)')).not.toBeInTheDocument();
expect(screen.queryByText('Munks (3)')).not.toBeInTheDocument();
});
});
describe('Collectibles options', () => {
it('should render a link "Refresh list" when some collectibles are present on mainnet and collectible auto-detection preference is set to true, which, when clicked calls methods DetectCollectibles and checkAndUpdateCollectiblesOwnershipStatus', () => {
describe('NFTs options', () => {
it('should render a link "Refresh list" when some NFTs are present on mainnet and NFT auto-detection preference is set to true, which, when clicked calls methods DetectNFTs and checkAndUpdateNftsOwnershipStatus', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
useNftDetection: true,
});
expect(detectCollectiblesStub).not.toHaveBeenCalled();
expect(
checkAndUpdateAllCollectiblesOwnershipStatusStub,
).not.toHaveBeenCalled();
expect(detectNftsStub).not.toHaveBeenCalled();
expect(checkAndUpdateAllNftsOwnershipStatusStub).not.toHaveBeenCalled();
fireEvent.click(screen.queryByText('Refresh list'));
expect(detectCollectiblesStub).toHaveBeenCalled();
expect(
checkAndUpdateAllCollectiblesOwnershipStatusStub,
).toHaveBeenCalled();
expect(detectNftsStub).toHaveBeenCalled();
expect(checkAndUpdateAllNftsOwnershipStatusStub).toHaveBeenCalled();
});
it('should render a link "Refresh list" when some collectibles are present on a non-mainnet chain, which, when clicked calls a method checkAndUpdateCollectiblesOwnershipStatus', () => {
it('should render a link "Refresh list" when some NFTs are present on a non-mainnet chain, which, when clicked calls a method checkAndUpdateNftsOwnershipStatus', () => {
render({
chainId: '0x5',
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
useNftDetection: true,
});
expect(
checkAndUpdateAllCollectiblesOwnershipStatusStub,
).not.toHaveBeenCalled();
expect(checkAndUpdateAllNftsOwnershipStatusStub).not.toHaveBeenCalled();
fireEvent.click(screen.queryByText('Refresh list'));
expect(
checkAndUpdateAllCollectiblesOwnershipStatusStub,
).toHaveBeenCalled();
expect(checkAndUpdateAllNftsOwnershipStatusStub).toHaveBeenCalled();
});
it('should render a link "Enable autodetect" when some collectibles are present and collectible auto-detection preference is set to false, which, when clicked sends user to the experimental tab of settings', () => {
it('should render a link "Enable autodetect" when some NFTs are present and NFT auto-detection preference is set to false, which, when clicked sends user to the experimental tab of settings', () => {
render({
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
nfts: NFTS,
});
expect(historyPushMock).toHaveBeenCalledTimes(0);
fireEvent.click(screen.queryByText('Enable autodetect'));
expect(historyPushMock).toHaveBeenCalledTimes(1);
expect(historyPushMock).toHaveBeenCalledWith(EXPERIMENTAL_ROUTE);
});
it('should render a link "Import NFTs" when some collectibles are present, which, when clicked calls the passed in onAddNFT method', () => {
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,
collectibles: COLLECTIBLES,
nfts: NFTS,
onAddNFT: onAddNFTStub,
});
expect(onAddNFTStub).toHaveBeenCalledTimes(0);

View File

@ -5,10 +5,10 @@ import Box from '../box';
import { Color, TEXT_ALIGN } from '../../../helpers/constants/design-system';
import Identicon from '../identicon';
import { getTokenList } from '../../../selectors';
import { useCollectiblesCollections } from '../../../hooks/useNftsCollections';
import { useNftsCollections } from '../../../hooks/useNftsCollections';
export default function NftCollectionImage({ assetName, tokenAddress }) {
const { collections } = useCollectiblesCollections();
const { collections } = useNftsCollections();
const tokenList = useSelector(getTokenList);
const nftTokenListImage = tokenList[tokenAddress.toLowerCase()]?.iconUrl;

View File

@ -350,13 +350,13 @@ export default function reduceApp(
newTokensImported: action.payload,
};
case actionConstants.SET_NEW_COLLECTIBLE_ADDED_MESSAGE:
case actionConstants.SET_NEW_NFT_ADDED_MESSAGE:
return {
...appState,
newNftAddedMessage: action.payload,
};
case actionConstants.SET_REMOVE_COLLECTIBLE_MESSAGE:
case actionConstants.SET_REMOVE_NFT_MESSAGE:
return {
...appState,
removeNftMessage: action.payload,

View File

@ -227,11 +227,11 @@ export const getPendingTokens = (state) => state.metamask.pendingTokens;
export const getTokens = (state) => state.metamask.tokens;
export function getCollectiblesDropdownState(state) {
return state.metamask.collectiblesDropdownState;
export function getNftsDropdownState(state) {
return state.metamask.nftsDropdownState;
}
export const getCollectibles = (state) => {
export const getNfts = (state) => {
const {
metamask: {
allNfts,
@ -245,7 +245,7 @@ export const getCollectibles = (state) => {
return allNfts?.[selectedAddress]?.[chainIdAsDecimal] ?? [];
};
export const getCollectibleContracts = (state) => {
export const getNftContracts = (state) => {
const {
metamask: {
allNftContracts,

View File

@ -54,7 +54,7 @@ describe('Send Slice Helpers', () => {
});
});
it('should generate a txParams for a collectible transfer', () => {
it('should generate a txParams for an NFT transfer', () => {
const txParams = generateTransactionParams(
getInitialSendStateWithExistingTxState({
fromAccount: {

View File

@ -2120,7 +2120,7 @@ export function updateSendAsset(
} catch (err) {
if (err.message.includes('Unable to verify ownership.')) {
// 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 NFTs state with an incorrect chainId.
} else {
// Any other error is unexpected and should be surfaced.
dispatch(displayWarning(err.message));
@ -2132,7 +2132,7 @@ export function updateSendAsset(
asset.balance = '0x1';
} else {
throw new Error(
'Send slice initialized as collectible send with a collectible not currently owned by the select account',
'Send slice initialized as NFT send with an NFT not currently owned by the select account',
);
}
await dispatch(

View File

@ -2497,12 +2497,12 @@ describe('Send Slice', () => {
);
});
it('should set up the appropriate state for editing a collectible asset transaction', async () => {
it('should set up the appropriate state for editing an NFT asset transaction', async () => {
getTokenStandardAndDetailsStub.mockImplementation(() =>
Promise.resolve({
standard: 'ERC721',
balance: '0x1',
address: '0xCollectibleAddress',
address: '0xNftAddress',
}),
);
const editTransactionState = {
@ -2539,7 +2539,7 @@ describe('Send Slice', () => {
tokenId: BigNumber.from(15000).toString(),
}),
from: '0xAddress',
to: '0xCollectibleAddress',
to: '0xNftAddress',
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
gasPrice: '0x3b9aca00', // 1000000000
value: '0x0',
@ -2621,7 +2621,7 @@ describe('Send Slice', () => {
expect(actionResult[4]).toStrictEqual({
type: 'send/addHistoryEntry',
payload:
'sendFlow - user set asset to NFT with tokenId 15000 and address 0xCollectibleAddress',
'sendFlow - user set asset to NFT with tokenId 15000 and address 0xNftAddress',
});
expect(actionResult[5]).toStrictEqual({
type: 'send/updateAsset',
@ -2629,7 +2629,7 @@ describe('Send Slice', () => {
asset: {
balance: '0x1',
details: {
address: '0xCollectibleAddress',
address: '0xNftAddress',
balance: '0x1',
standard: TokenStandard.ERC721,
tokenId: '15000',

View File

@ -49,7 +49,7 @@ const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status';
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
const ADD_COLLECTIBLE_ROUTE = '/add-collectible';
const ADD_NFT_ROUTE = '/add-nft';
const ONBOARDING_ROUTE = '/onboarding';
const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase';
@ -223,7 +223,7 @@ export {
SWAPS_ERROR_ROUTE,
SWAPS_MAINTENANCE_ROUTE,
SMART_TRANSACTION_STATUS_ROUTE,
ADD_COLLECTIBLE_ROUTE,
ADD_NFT_ROUTE,
ONBOARDING_ROUTE,
ONBOARDING_HELP_US_IMPROVE_ROUTE,
ONBOARDING_CREATE_PASSWORD_ROUTE,

View File

@ -1,3 +1,3 @@
export const getCollectibleImageAlt = ({ name, tokenId, description }) => {
export const getNftImageAlt = ({ name, tokenId, description }) => {
return description ?? `${name} ${tokenId}`;
};

View File

@ -1,10 +1,10 @@
import { getCollectibleImageAlt } from './nfts';
import { getNftImageAlt } from './nfts';
describe('Collectibles Utils', () => {
describe('getCollectibleImageAlt', () => {
describe('NFTs Utils', () => {
describe('getNftImageAlt', () => {
it('returns the description attribute when it is available', () => {
expect(
getCollectibleImageAlt({
getNftImageAlt({
name: 'Cool NFT',
tokenId: '555',
description: 'This is a really cool NFT',
@ -14,14 +14,14 @@ describe('Collectibles Utils', () => {
it('returns the formatted name and tokenId attributes when a description is not present', () => {
expect(
getCollectibleImageAlt({
getNftImageAlt({
name: 'Cool NFT',
tokenId: '555',
description: null,
}),
).toBe('Cool NFT 555');
expect(
getCollectibleImageAlt({
getNftImageAlt({
name: 'Cool NFT',
tokenId: '555',
}),

View File

@ -111,9 +111,9 @@ const t = (key) => {
return 'Enable OpenSea API';
case 'enableOpenSeaAPIDescription':
return "Use OpenSea's API to fetch NFT data. NFT auto-detection relies on OpenSea's API, and will not be available when this is turned off.";
case 'useCollectibleDetection':
case 'useNftDetection':
return 'Autodetect NFTs';
case 'useCollectibleDetectionDescription':
case 'useNftDetectionDescription':
return 'Displaying NFTs media & data may expose your IP address to centralized servers. Third-party APIs (like OpenSea) are used to detect NFTs in your wallet. This exposes your account address with those services. Leave this disabled if you dont want the app to pull data from those those services.';
case 'about':
return 'About';

View File

@ -217,7 +217,7 @@ export async function getAssetDetails(
tokenAddress,
currentUserAddress,
transactionData,
existingCollectibles,
existingNfts,
) {
const tokenData = parseStandardTokenTransactionData(transactionData);
if (!tokenData) {
@ -234,18 +234,18 @@ export async function getAssetDetails(
let tokenDetails;
// if a tokenId is present check if there is a collectible in state matching the address/tokenId
// if a tokenId is present check if there is an NFT in state matching the address/tokenId
// and avoid unnecessary network requests to query token details we already have
if (existingCollectibles?.length && tokenId) {
const existingCollectible = existingCollectibles.find(
if (existingNfts?.length && tokenId) {
const existingNft = existingNfts.find(
({ address, tokenId: _tokenId }) =>
isEqualCaseInsensitive(tokenAddress, address) && _tokenId === tokenId,
);
if (existingCollectible) {
if (existingNft) {
return {
toAddress,
...existingCollectible,
...existingNft,
};
}
}
@ -277,7 +277,7 @@ export async function getAssetDetails(
tokenId = undefined;
}
// else if not a collectible already in state or standard === ERC20 return tokenDetails and tokenId
// else if not an NFT already in state or standard === ERC20 return tokenDetails and tokenId
return {
tokenAmount,
toAddress,

View File

@ -1,7 +1,7 @@
import { isEqual } from 'lodash';
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getCollectibles, getTokens } from '../ducks/metamask/metamask';
import { getNfts, getTokens } from '../ducks/metamask/metamask';
import { getAssetDetails } from '../helpers/utils/token-util';
import { hideLoadingIndication, showLoadingIndication } from '../store/actions';
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
@ -11,7 +11,7 @@ import { useTokenTracker } from './useTokenTracker';
export function useAssetDetails(tokenAddress, userAddress, transactionData) {
const dispatch = useDispatch();
// state selectors
const collectibles = useSelector(getCollectibles);
const nfts = useSelector(getNfts);
const tokens = useSelector(getTokens, isEqual);
const currentToken = tokens.find((token) =>
isEqualCaseInsensitive(token.address, tokenAddress),
@ -36,7 +36,7 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
tokenAddress,
userAddress,
transactionData,
collectibles,
nfts,
);
setCurrentAsset(assetDetails);
dispatch(hideLoadingIndication());
@ -57,7 +57,7 @@ export function useAssetDetails(tokenAddress, userAddress, transactionData) {
tokenAddress,
userAddress,
transactionData,
collectibles,
nfts,
tokensWithBalances,
prevTokenBalance,
]);

View File

@ -1,79 +1,74 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
import {
getCollectibles,
getCollectibleContracts,
} from '../ducks/metamask/metamask';
import { getNfts, getNftContracts } from '../ducks/metamask/metamask';
import { getCurrentChainId, getSelectedAddress } from '../selectors';
import { usePrevious } from './usePrevious';
export function useCollectiblesCollections() {
export function useNftsCollections() {
const [collections, setCollections] = useState({});
const [previouslyOwnedCollection, setPreviouslyOwnedCollection] = useState({
collectionName: 'Previously Owned',
collectibles: [],
nfts: [],
});
const collectibles = useSelector(getCollectibles);
const [collectiblesLoading, setCollectiblesLoading] = useState(
() => collectibles?.length >= 0,
);
const nfts = useSelector(getNfts);
const [nftsLoading, setNftsLoading] = useState(() => nfts?.length >= 0);
const selectedAddress = useSelector(getSelectedAddress);
const chainId = useSelector(getCurrentChainId);
const collectibleContracts = useSelector(getCollectibleContracts);
const prevCollectibles = usePrevious(collectibles);
const nftContracts = useSelector(getNftContracts);
const prevNfts = usePrevious(nfts);
const prevChainId = usePrevious(chainId);
const prevSelectedAddress = usePrevious(selectedAddress);
useEffect(() => {
const getCollections = () => {
setCollectiblesLoading(true);
setNftsLoading(true);
if (selectedAddress === undefined || chainId === undefined) {
return;
}
const newCollections = {};
const newPreviouslyOwnedCollections = {
collectionName: 'Previously Owned',
collectibles: [],
nfts: [],
};
collectibles.forEach((collectible) => {
if (collectible?.isCurrentlyOwned === false) {
newPreviouslyOwnedCollections.collectibles.push(collectible);
} else if (newCollections[collectible.address]) {
newCollections[collectible.address].collectibles.push(collectible);
nfts.forEach((nft) => {
if (nft?.isCurrentlyOwned === false) {
newPreviouslyOwnedCollections.nfts.push(nft);
} else if (newCollections[nft.address]) {
newCollections[nft.address].nfts.push(nft);
} else {
const collectionContract = collectibleContracts.find(
({ address }) => address === collectible.address,
const collectionContract = nftContracts.find(
({ address }) => address === nft.address,
);
newCollections[collectible.address] = {
collectionName: collectionContract?.name || collectible.name,
collectionImage: collectionContract?.logo || collectible.image,
collectibles: [collectible],
newCollections[nft.address] = {
collectionName: collectionContract?.name || nft.name,
collectionImage: collectionContract?.logo || nft.image,
nfts: [nft],
};
}
});
setCollections(newCollections);
setPreviouslyOwnedCollection(newPreviouslyOwnedCollections);
setCollectiblesLoading(false);
setNftsLoading(false);
};
if (
!isEqual(prevCollectibles, collectibles) ||
!isEqual(prevNfts, nfts) ||
!isEqual(prevSelectedAddress, selectedAddress) ||
!isEqual(prevChainId, chainId)
) {
getCollections();
}
}, [
collectibles,
prevCollectibles,
collectibleContracts,
setCollectiblesLoading,
nfts,
prevNfts,
nftContracts,
setNftsLoading,
chainId,
prevChainId,
selectedAddress,
prevSelectedAddress,
]);
return { collectiblesLoading, collections, previouslyOwnedCollection };
return { nftsLoading, collections, previouslyOwnedCollection };
}

View File

@ -20,7 +20,7 @@ import {
PENDING_STATUS_HASH,
TOKEN_CATEGORY_HASH,
} from '../helpers/constants/transactions';
import { getCollectibles, getTokens } from '../ducks/metamask/metamask';
import { getNfts, getTokens } from '../ducks/metamask/metamask';
import {
TransactionType,
TransactionGroupCategory,
@ -92,7 +92,7 @@ export function useTransactionDisplayData(transactionGroup) {
const dispatch = useDispatch();
const currentAsset = useCurrentAsset();
const knownTokens = useSelector(getTokens);
const knownCollectibles = useSelector(getCollectibles);
const knownNfts = useSelector(getNfts);
const t = useI18nContext();
const { initialTransaction, primaryTransaction } = transactionGroup;
@ -145,9 +145,9 @@ export function useTransactionDisplayData(transactionGroup) {
const transactionDataTokenId =
getTokenIdParam(tokenData) ?? getTokenValueParam(tokenData);
const collectible =
const nft =
isTokenCategory &&
knownCollectibles.find(
knownNfts.find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, recipientAddress) &&
tokenId === transactionDataTokenId,
@ -258,7 +258,7 @@ export function useTransactionDisplayData(transactionGroup) {
) {
category = TransactionGroupCategory.send;
title = t('sendSpecifiedTokens', [
token?.symbol || collectible?.name || t('token'),
token?.symbol || nft?.name || t('token'),
]);
recipientAddress = getTokenAddressParam(tokenData);
subtitle = t('toAddress', [shortenAddress(recipientAddress)]);

View File

@ -18,8 +18,8 @@ import {
addNftVerifyOwnership,
getTokenStandardAndDetails,
ignoreTokens,
setNewCollectibleAddedMessage,
updateCollectibleDropDownState,
setNewNftAddedMessage,
updateNftDropDownState,
} from '../../store/actions';
import FormField from '../../components/ui/form-field';
import {
@ -28,69 +28,69 @@ import {
getSelectedAddress,
getUseNftDetection,
} from '../../selectors';
import { getCollectiblesDropdownState } from '../../ducks/metamask/metamask';
import CollectiblesDetectionNotice from '../../components/app/nfts-detection-notice';
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 { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
export default function AddCollectible() {
export default function AddNft() {
const t = useI18nContext();
const history = useHistory();
const dispatch = useDispatch();
const useNftDetection = useSelector(getUseNftDetection);
const isMainnet = useSelector(getIsMainnet);
const collectiblesDropdownState = useSelector(getCollectiblesDropdownState);
const nftsDropdownState = useSelector(getNftsDropdownState);
const selectedAddress = useSelector(getSelectedAddress);
const chainId = useSelector(getCurrentChainId);
const addressEnteredOnImportTokensPage =
history?.location?.state?.addressEnteredOnImportTokensPage;
const contractAddressToConvertFromTokenToCollectible =
const contractAddressToConvertFromTokenToNft =
history?.location?.state?.tokenAddress;
const [collectibleAddress, setCollectibleAddress] = useState(
const [nftAddress, setNftAddress] = useState(
addressEnteredOnImportTokensPage ??
contractAddressToConvertFromTokenToCollectible ??
contractAddressToConvertFromTokenToNft ??
'',
);
const [tokenId, setTokenId] = useState('');
const [disabled, setDisabled] = useState(true);
const [collectibleAddFailed, setCollectibleAddFailed] = useState(false);
const [nftAddFailed, setNftAddFailed] = useState(false);
const trackEvent = useContext(MetaMetricsContext);
const handleAddCollectible = async () => {
const handleAddNft = async () => {
try {
await dispatch(addNftVerifyOwnership(collectibleAddress, tokenId));
const newCollectibleDropdownState = {
...collectiblesDropdownState,
await dispatch(addNftVerifyOwnership(nftAddress, tokenId));
const newNftDropdownState = {
...nftsDropdownState,
[selectedAddress]: {
...collectiblesDropdownState?.[selectedAddress],
...nftsDropdownState?.[selectedAddress],
[chainId]: {
...collectiblesDropdownState?.[selectedAddress]?.[chainId],
[collectibleAddress]: true,
...nftsDropdownState?.[selectedAddress]?.[chainId],
[nftAddress]: true,
},
},
};
dispatch(updateCollectibleDropDownState(newCollectibleDropdownState));
dispatch(updateNftDropDownState(newNftDropdownState));
} catch (error) {
const { message } = error;
dispatch(setNewCollectibleAddedMessage(message));
setCollectibleAddFailed(true);
dispatch(setNewNftAddedMessage(message));
setNftAddFailed(true);
return;
}
if (contractAddressToConvertFromTokenToCollectible) {
if (contractAddressToConvertFromTokenToNft) {
await dispatch(
ignoreTokens({
tokensToIgnore: contractAddressToConvertFromTokenToCollectible,
tokensToIgnore: contractAddressToConvertFromTokenToNft,
dontShowLoadingIndicator: true,
}),
);
}
dispatch(setNewCollectibleAddedMessage('success'));
dispatch(setNewNftAddedMessage('success'));
const tokenDetails = await getTokenStandardAndDetails(
collectibleAddress,
nftAddress,
null,
tokenId.toString(),
);
@ -99,7 +99,7 @@ export default function AddCollectible() {
event: EVENT_NAMES.TOKEN_ADDED,
category: 'Wallet',
sensitiveProperties: {
token_contract_address: collectibleAddress,
token_contract_address: nftAddress,
token_symbol: tokenDetails?.symbol,
tokenId: tokenId.toString(),
asset_type: AssetType.NFT,
@ -113,13 +113,11 @@ export default function AddCollectible() {
const validateAndSetAddress = (val) => {
setDisabled(!isValidHexAddress(val) || !tokenId);
setCollectibleAddress(val);
setNftAddress(val);
};
const validateAndSetTokenId = (val) => {
setDisabled(
!isValidHexAddress(collectibleAddress) || !val || isNaN(Number(val)),
);
setDisabled(!isValidHexAddress(nftAddress) || !val || isNaN(Number(val)));
setTokenId(val);
};
@ -127,7 +125,7 @@ export default function AddCollectible() {
<PageContainer
title={t('importNFT')}
onSubmit={() => {
handleAddCollectible();
handleAddNft();
}}
submitText={t('add')}
onCancel={() => {
@ -139,10 +137,8 @@ export default function AddCollectible() {
disabled={disabled}
contentComponent={
<Box>
{isMainnet && !useNftDetection ? (
<CollectiblesDetectionNotice />
) : null}
{collectibleAddFailed && (
{isMainnet && !useNftDetection ? <NftsDetectionNotice /> : null}
{nftAddFailed && (
<Box marginLeft={4} marginRight={4}>
<ActionableMessage
type="danger"
@ -158,9 +154,9 @@ export default function AddCollectible() {
{t('nftAddFailedMessage')}
</Typography>
<button
className="fas fa-times add-collectible__close"
className="fas fa-times add-nft__close"
title={t('close')}
onClick={() => setCollectibleAddFailed(false)}
onClick={() => setNftAddFailed(false)}
/>
</Box>
}
@ -172,10 +168,10 @@ export default function AddCollectible() {
dataTestId="address"
titleText={t('address')}
placeholder="0x..."
value={collectibleAddress}
value={nftAddress}
onChange={(val) => {
validateAndSetAddress(val);
setCollectibleAddFailed(false);
setNftAddFailed(false);
}}
tooltipText={t('importNFTAddressToolTip')}
autoFocus
@ -187,7 +183,7 @@ export default function AddCollectible() {
value={tokenId}
onChange={(val) => {
validateAndSetTokenId(val);
setCollectibleAddFailed(false);
setNftAddFailed(false);
}}
tooltipText={t('importNFTTokenIdToolTip')}
/>

View File

@ -9,10 +9,10 @@ import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import {
addNftVerifyOwnership,
ignoreTokens,
setNewCollectibleAddedMessage,
updateCollectibleDropDownState,
setNewNftAddedMessage,
updateNftDropDownState,
} from '../../store/actions';
import AddCollectible from '.';
import AddNft from '.';
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9';
const INVALID_ADDRESS = 'aoinsafasdfa';
@ -39,15 +39,15 @@ jest.mock('../../store/actions.ts', () => ({
.mockReturnValue(jest.fn().mockResolvedValue()),
getTokenStandardAndDetails: jest.fn().mockResolvedValue(),
ignoreTokens: jest.fn().mockReturnValue(jest.fn().mockResolvedValue()),
setNewCollectibleAddedMessage: jest
setNewNftAddedMessage: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
updateCollectibleDropDownState: jest
updateNftDropDownState: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
}));
describe('AddCollectible', () => {
describe('AddNft', () => {
const store = configureMockStore([thunk])(mockState);
beforeEach(() => {
@ -55,10 +55,7 @@ describe('AddCollectible', () => {
});
it('should enable the "Add" button when valid entries are input into both Address and TokenId fields', () => {
const { getByTestId, getByText } = renderWithProvider(
<AddCollectible />,
store,
);
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
expect(getByText('Add')).not.toBeEnabled();
fireEvent.change(getByTestId('address'), {
target: { value: VALID_ADDRESS },
@ -70,10 +67,7 @@ describe('AddCollectible', () => {
});
it('should not enable the "Add" button when an invalid entry is input into one or both Address and TokenId fields', () => {
const { getByTestId, getByText } = renderWithProvider(
<AddCollectible />,
store,
);
const { getByTestId, getByText } = renderWithProvider(<AddNft />, store);
expect(getByText('Add')).not.toBeEnabled();
fireEvent.change(getByTestId('address'), {
target: { value: INVALID_ADDRESS },
@ -92,11 +86,8 @@ describe('AddCollectible', () => {
expect(getByText('Add')).not.toBeEnabled();
});
it('should call addNftVerifyOwnership, updateCollectibleDropDownState, setNewCollectibleAddedMessage, and ignoreTokens action with correct values (tokenId should not be in scientific notation)', async () => {
const { getByTestId, getByText } = renderWithProvider(
<AddCollectible />,
store,
);
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);
fireEvent.change(getByTestId('address'), {
target: { value: VALID_ADDRESS },
});
@ -113,7 +104,7 @@ describe('AddCollectible', () => {
'9007199254740992',
);
expect(updateCollectibleDropDownState).toHaveBeenCalledWith({
expect(updateNftDropDownState).toHaveBeenCalledWith({
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'0x5': {
'0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9': true,
@ -122,7 +113,7 @@ describe('AddCollectible', () => {
},
});
expect(setNewCollectibleAddedMessage).toHaveBeenCalledWith('success');
expect(setNewNftAddedMessage).toHaveBeenCalledWith('success');
expect(ignoreTokens).toHaveBeenCalledWith({
dontShowLoadingIndicator: true,
@ -137,7 +128,7 @@ describe('AddCollectible', () => {
);
const { getByTestId, getByText, queryByTitle } = renderWithProvider(
<AddCollectible />,
<AddNft />,
store,
);
fireEvent.change(getByTestId('address'), {
@ -151,16 +142,16 @@ describe('AddCollectible', () => {
fireEvent.click(getByText('Add'));
await waitFor(() => {
expect(setNewCollectibleAddedMessage).toHaveBeenCalledWith('error');
expect(setNewNftAddedMessage).toHaveBeenCalledWith('error');
});
const addCollectibleClose = queryByTitle('Close');
const addNftClose = queryByTitle('Close');
fireEvent.click(addCollectibleClose);
fireEvent.click(addNftClose);
});
it('should route to default route when cancel button is clicked', () => {
const { queryByTestId } = renderWithProvider(<AddCollectible />, store);
const { queryByTestId } = renderWithProvider(<AddNft />, store);
const cancelButton = queryByTestId('page-container-footer-cancel');
fireEvent.click(cancelButton);
@ -169,7 +160,7 @@ describe('AddCollectible', () => {
});
it('should route to default route when close button is clicked', () => {
const { queryByLabelText } = renderWithProvider(<AddCollectible />, store);
const { queryByLabelText } = renderWithProvider(<AddNft />, store);
const closeButton = queryByLabelText('close');
fireEvent.click(closeButton);

View File

@ -1,4 +1,4 @@
.add-collectible {
.add-nft {
&__close {
color: var(--color-icon-default);
background: none;

View File

@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Redirect, useParams } from 'react-router-dom';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import CollectibleDetails from '../../components/app/nft-details/nft-details';
import { getCollectibles, getTokens } from '../../ducks/metamask/metamask';
import NftDetails from '../../components/app/nft-details/nft-details';
import { getNfts, getTokens } from '../../ducks/metamask/metamask';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import NativeAsset from './components/native-asset';
@ -12,14 +12,14 @@ import TokenAsset from './components/token-asset';
const Asset = () => {
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency);
const tokens = useSelector(getTokens);
const collectibles = useSelector(getCollectibles);
const nfts = useSelector(getNfts);
const { asset, id } = useParams();
const token = tokens.find(({ address }) =>
isEqualCaseInsensitive(address, asset),
);
const collectible = collectibles.find(
const nft = nfts.find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, asset) && id === tokenId.toString(),
);
@ -30,8 +30,8 @@ const Asset = () => {
}, []);
let content;
if (collectible) {
content = <CollectibleDetails collectible={collectible} />;
if (nft) {
content = <NftDetails nft={nft} />;
} else if (token) {
content = <TokenAsset token={token} />;
} else if (asset === nativeCurrency) {

View File

@ -184,7 +184,7 @@ const mapStateToProps = (state, ownProps) => {
customTxParamsData,
);
const isCollectibleTransfer = Boolean(
const isNftTransfer = Boolean(
allNftContracts?.[selectedAddress]?.[chainId]?.find((contract) => {
return isEqualCaseInsensitive(contract.address, fullTxData.txParams.to);
}),
@ -235,7 +235,7 @@ const mapStateToProps = (state, ownProps) => {
useNonceField: getUseNonceField(state),
customNonceValue,
insufficientBalance,
hideSubtitle: !getShouldShowFiat(state) && !isCollectibleTransfer,
hideSubtitle: !getShouldShowFiat(state) && !isNftTransfer,
hideFiatConversion: !getShouldShowFiat(state),
type,
nextNonce,

View File

@ -7,7 +7,7 @@ import {
CONTEXT_PROPS,
} from '../../../shared/constants/metametrics';
import AssetList from '../../components/app/asset-list';
import CollectiblesTab from '../../components/app/nfts-tab';
import NftsTab from '../../components/app/nfts-tab';
import HomeNotification from '../../components/app/home-notification';
import MultipleNotifications from '../../components/app/multiple-notifications';
import TransactionList from '../../components/app/transaction-list';
@ -47,7 +47,7 @@ import {
BUILD_QUOTE_ROUTE,
VIEW_QUOTE_ROUTE,
CONFIRMATION_V_NEXT_ROUTE,
ADD_COLLECTIBLE_ROUTE,
ADD_NFT_ROUTE,
ONBOARDING_SECURE_YOUR_WALLET_ROUTE,
} from '../../helpers/constants/routes';
import ZENDESK_URLS from '../../helpers/constants/zendesk-url';
@ -140,9 +140,9 @@ export default class Home extends PureComponent {
// eslint-disable-next-line react/no-unused-prop-types
isSigningQRHardwareTransaction: PropTypes.bool.isRequired,
newNftAddedMessage: PropTypes.string,
setNewCollectibleAddedMessage: PropTypes.func.isRequired,
setNewNftAddedMessage: PropTypes.func.isRequired,
removeNftMessage: PropTypes.string,
setRemoveCollectibleMessage: PropTypes.func.isRequired,
setRemoveNftMessage: PropTypes.func.isRequired,
closeNotificationPopup: PropTypes.func.isRequired,
newTokensImported: PropTypes.string,
setNewTokensImported: PropTypes.func.isRequired,
@ -276,9 +276,9 @@ export default class Home extends PureComponent {
newNetworkAdded,
setNewNetworkAdded,
newNftAddedMessage,
setNewCollectibleAddedMessage,
setNewNftAddedMessage,
removeNftMessage,
setRemoveCollectibleMessage,
setRemoveNftMessage,
newTokensImported,
setNewTokensImported,
newCustomNetworkAdded,
@ -287,8 +287,8 @@ export default class Home extends PureComponent {
} = this.props;
const onAutoHide = () => {
setNewCollectibleAddedMessage('');
setRemoveCollectibleMessage('');
setNewNftAddedMessage('');
setRemoveNftMessage('');
};
const autoHideDelay = 5 * SECOND;
@ -763,9 +763,9 @@ export default class Home extends PureComponent {
name={this.context.t('nfts')}
tabKey="nfts"
>
<CollectiblesTab
<NftsTab
onAddNFT={() => {
history.push(ADD_COLLECTIBLE_ROUTE);
history.push(ADD_NFT_ROUTE);
}}
/>
</Tab>

View File

@ -21,11 +21,11 @@ import {
getNewNetworkAdded,
hasUnsignedQRHardwareTransaction,
hasUnsignedQRHardwareMessage,
getNewCollectibleAddedMessage,
getNewNftAddedMessage,
getNewTokensImported,
getShowPortfolioTooltip,
getShouldShowSeedPhraseReminder,
getRemoveCollectibleMessage,
getRemoveNftMessage,
} from '../../selectors';
import {
@ -39,8 +39,8 @@ import {
setRecoveryPhraseReminderLastShown,
setOutdatedBrowserWarningLastShown,
setNewNetworkAdded,
setNewCollectibleAddedMessage,
setRemoveCollectibleMessage,
setNewNftAddedMessage,
setRemoveNftMessage,
setNewTokensImported,
setRpcTarget,
///: BEGIN:ONLY_INCLUDE_IN(flask)
@ -151,8 +151,8 @@ const mapStateToProps = (state) => {
seedPhraseBackedUp,
newNetworkAdded: getNewNetworkAdded(state),
isSigningQRHardwareTransaction,
newNftAddedMessage: getNewCollectibleAddedMessage(state),
removeNftMessage: getRemoveCollectibleMessage(state),
newNftAddedMessage: getNewNftAddedMessage(state),
removeNftMessage: getRemoveNftMessage(state),
newTokensImported: getNewTokensImported(state),
newCustomNetworkAdded: appState.newCustomNetworkAdded,
onboardedInThisUISession: appState.onboardedInThisUISession,
@ -184,11 +184,11 @@ const mapDispatchToProps = (dispatch) => ({
console.log({ newNetwork });
dispatch(setNewNetworkAdded(newNetwork));
},
setNewCollectibleAddedMessage: (message) => {
dispatch(setNewCollectibleAddedMessage(message));
setNewNftAddedMessage: (message) => {
dispatch(setNewNftAddedMessage(message));
},
setRemoveCollectibleMessage: (message) => {
dispatch(setRemoveCollectibleMessage(message));
setRemoveNftMessage: (message) => {
dispatch(setRemoveNftMessage(message));
},
setNewTokensImported: (newTokens) => {
dispatch(setNewTokensImported(newTokens));

View File

@ -8,7 +8,7 @@ import {
} from '../../helpers/utils/util';
import { tokenInfoGetter } from '../../helpers/utils/token-util';
import {
ADD_COLLECTIBLE_ROUTE,
ADD_NFT_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE,
SECURITY_ROUTE,
} from '../../helpers/constants/routes';
@ -133,7 +133,7 @@ class ImportToken extends Component {
customAddressError: null,
customSymbolError: null,
customDecimalsError: null,
collectibleAddressError: null,
nftAddressError: null,
forceEditSymbol: false,
symbolAutoFilled: false,
decimalAutoFilled: false,
@ -198,7 +198,7 @@ class ImportToken extends Component {
customAddressError,
customSymbolError,
customDecimalsError,
collectibleAddressError,
nftAddressError,
} = this.state;
return (
@ -206,7 +206,7 @@ class ImportToken extends Component {
customAddressError ||
customSymbolError ||
customDecimalsError ||
collectibleAddressError
nftAddressError
);
}
@ -265,7 +265,7 @@ class ImportToken extends Component {
this.setState({
customAddress,
customAddressError: null,
collectibleAddressError: null,
nftAddressError: null,
tokenSelectorError: null,
symbolAutoFilled: false,
decimalAutoFilled: false,
@ -312,18 +312,18 @@ class ImportToken extends Component {
case process.env.NFTS_V1 &&
(standard === 'ERC1155' || standard === 'ERC721'):
this.setState({
collectibleAddressError: this.context.t('nftAddressError', [
nftAddressError: this.context.t('nftAddressError', [
<a
className="import-token__collectible-address-error-link"
className="import-token__nft-address-error-link"
onClick={() =>
this.props.history.push({
pathname: ADD_COLLECTIBLE_ROUTE,
pathname: ADD_NFT_ROUTE,
state: {
addressEnteredOnImportTokensPage: this.state.customAddress,
},
})
}
key="collectibleAddressError"
key="nftAddressError"
>
{this.context.t('importNFTPage')}
</a>,
@ -406,7 +406,7 @@ class ImportToken extends Component {
symbolAutoFilled,
decimalAutoFilled,
mainnetTokenWarning,
collectibleAddressError,
nftAddressError,
} = this.state;
const {
@ -493,9 +493,7 @@ class ImportToken extends Component {
type="text"
value={customAddress}
onChange={(e) => this.handleCustomAddressChange(e.target.value)}
error={
customAddressError || mainnetTokenWarning || collectibleAddressError
}
error={customAddressError || mainnetTokenWarning || nftAddressError}
fullWidth
autoFocus
margin="normal"

View File

@ -68,7 +68,7 @@
padding-right: 0;
}
&__collectible-address-error-link {
&__nft-address-error-link {
color: var(--color-primary-default);
cursor: pointer;

View File

@ -17,7 +17,7 @@ import RestoreVaultPage from '../keychains/restore-vault';
import RevealSeedConfirmation from '../keychains/reveal-seed';
import MobileSyncPage from '../mobile-sync';
import ImportTokenPage from '../import-token';
import AddCollectiblePage from '../add-nft';
import AddNftPage from '../add-nft';
import ConfirmImportTokenPage from '../confirm-import-token';
import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
import CreateAccountPage from '../create-account';
@ -57,7 +57,7 @@ import {
CONFIRMATION_V_NEXT_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE,
ONBOARDING_ROUTE,
ADD_COLLECTIBLE_ROUTE,
ADD_NFT_ROUTE,
ONBOARDING_UNLOCK_ROUTE,
TOKEN_DETAILS,
///: BEGIN:ONLY_INCLUDE_IN(flask)
@ -217,11 +217,7 @@ export default class Routes extends Component {
exact
/>
{process.env.NFTS_V1 ? (
<Authenticated
path={ADD_COLLECTIBLE_ROUTE}
component={AddCollectiblePage}
exact
/>
<Authenticated path={ADD_NFT_ROUTE} component={AddNftPage} exact />
) : null}
<Authenticated
path={CONFIRM_IMPORT_TOKEN_ROUTE}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SendAmountRow Component render Collectible Asset Type should match snapshot for token collectible type 1`] = `<div />`;
exports[`SendAmountRow Component render NFT Asset Type should match snapshot for token NFT type 1`] = `<div />`;
exports[`SendAmountRow Component render Native Asset Type should match snapshot for native asset type 1`] = `
<div>

View File

@ -62,9 +62,9 @@ describe('SendAmountRow Component', () => {
});
});
describe('Collectible Asset Type', () => {
it('should match snapshot for token collectible type', () => {
const collectibleState = {
describe('NFT Asset Type', () => {
it('should match snapshot for token NFT type', () => {
const nftState = {
...mockSendState,
send: {
currentTransactionUUID: '1-tx',
@ -81,7 +81,7 @@ describe('SendAmountRow Component', () => {
},
};
const mockStore = configureMockStore([thunk])(collectibleState);
const mockStore = configureMockStore([thunk])(nftState);
const { container } = renderWithProvider(<SendAmountRow />, mockStore);

View File

@ -29,7 +29,7 @@ export default class SendAssetRow extends Component {
updateSendAsset: PropTypes.func.isRequired,
nativeCurrency: PropTypes.string,
nativeCurrencyImage: PropTypes.string,
collectibles: PropTypes.arrayOf(
nfts: PropTypes.arrayOf(
PropTypes.shape({
address: PropTypes.string.isRequired,
tokenId: PropTypes.string.isRequired,
@ -62,17 +62,15 @@ export default class SendAssetRow extends Component {
state = {
isShowingDropdown: false,
sendableTokens: [],
sendableCollectibles: [],
sendableNfts: [],
};
async componentDidMount() {
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
const sendableCollectibles = this.props.collectibles.filter(
(collectible) =>
collectible.isCurrentlyOwned &&
collectible.standard === TokenStandard.ERC721,
const sendableNfts = this.props.nfts.filter(
(nft) => nft.isCurrentlyOwned && nft.standard === TokenStandard.ERC721,
);
this.setState({ sendableTokens, sendableCollectibles });
this.setState({ sendableTokens, sendableNfts });
}
openDropdown = () => this.setState({ isShowingDropdown: true });
@ -127,8 +125,7 @@ export default class SendAssetRow extends Component {
>
{this.renderSendAsset()}
</div>
{[...this.state.sendableTokens, ...this.state.sendableCollectibles]
.length > 0
{[...this.state.sendableTokens, ...this.state.sendableNfts].length > 0
? this.renderAssetDropdown()
: null}
</div>
@ -140,7 +137,7 @@ export default class SendAssetRow extends Component {
const {
sendAsset: { details, type },
tokens,
collectibles,
nfts,
} = this.props;
if (type === AssetType.token) {
@ -151,13 +148,13 @@ export default class SendAssetRow extends Component {
return this.renderToken(token);
}
} else if (type === AssetType.NFT) {
const collectible = collectibles.find(
const nft = nfts.find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, details.address) &&
tokenId === details.tokenId,
);
if (collectible) {
return this.renderCollectible(collectible);
if (nft) {
return this.renderNft(nft);
}
}
return this.renderNativeCurrency();
@ -177,9 +174,7 @@ export default class SendAssetRow extends Component {
clickHandler={(token) => this.selectToken(AssetType.token, token)}
/>
{this.state.sendableCollectibles.map((collectible) =>
this.renderCollectible(collectible, true),
)}
{this.state.sendableNfts.map((nft) => this.renderNft(nft, true))}
</div>
</div>
)
@ -191,13 +186,13 @@ export default class SendAssetRow extends Component {
const { accounts, selectedAddress, nativeCurrency, nativeCurrencyImage } =
this.props;
const { sendableTokens, sendableCollectibles } = this.state;
const { sendableTokens, sendableNfts } = this.state;
const balanceValue = accounts[selectedAddress]
? accounts[selectedAddress].balance
: '';
const sendableAssets = [...sendableTokens, ...sendableCollectibles];
const sendableAssets = [...sendableTokens, ...sendableNfts];
return (
<div
className={
@ -264,24 +259,24 @@ export default class SendAssetRow extends Component {
);
}
renderCollectible(collectible, insideDropdown = false) {
const { address, name, image, tokenId } = collectible;
renderNft(nft, insideDropdown = false) {
const { address, name, image, tokenId } = nft;
const { t } = this.context;
const collectibleCollection = this.props.collections.find(
const nftCollection = this.props.collections.find(
(collection) => collection.address === address,
);
return (
<div
key={address}
className="send-v2__asset-dropdown__asset"
onClick={() => this.selectToken(AssetType.NFT, collectible)}
onClick={() => this.selectToken(AssetType.NFT, nft)}
>
<div className="send-v2__asset-dropdown__asset-icon">
<Identicon address={address} diameter={36} image={image} />
</div>
<div className="send-v2__asset-dropdown__asset-data">
<div className="send-v2__asset-dropdown__symbol">
{collectibleCollection.name || name}
{nftCollection.name || name}
</div>
<div className="send-v2__asset-dropdown__name">
<span className="send-v2__asset-dropdown__name__label">

View File

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import {
getCollectibleContracts,
getCollectibles,
getNftContracts,
getNfts,
getNativeCurrency,
} from '../../../../ducks/metamask/metamask';
import {
@ -15,8 +15,8 @@ function mapStateToProps(state) {
return {
tokens: state.metamask.tokens,
selectedAddress: state.metamask.selectedAddress,
collectibles: getCollectibles(state),
collections: getCollectibleContracts(state),
nfts: getNfts(state),
collections: getNftContracts(state),
sendAsset: getSendAsset(state),
accounts: getMetaMaskAccounts(state),
nativeCurrency: getNativeCurrency(state),

View File

@ -22,7 +22,7 @@ const MIN_GAS_TOTAL = new Numeric(MIN_GAS_LIMIT_HEX, 16)
.toPrefixedHexString();
const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb';
const COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE = '0x23b872dd';
const NFT_TRANSFER_FROM_FUNCTION_SIGNATURE = '0x23b872dd';
const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds';
const INSUFFICIENT_FUNDS_FOR_GAS_ERROR = 'insufficientFundsForGas';
@ -72,6 +72,6 @@ export {
REQUIRED_ERROR,
CONFUSING_ENS_ERROR,
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE,
NFT_TRANSFER_FROM_FUNCTION_SIGNATURE,
RECIPIENT_TYPES,
};

View File

@ -5,7 +5,7 @@ import { TokenStandard } from '../../../shared/constants/transaction';
import { Numeric } from '../../../shared/modules/Numeric';
import {
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE,
NFT_TRANSFER_FROM_FUNCTION_SIGNATURE,
} from './send.constants';
export {
@ -100,7 +100,7 @@ function generateERC721TransferData({
return undefined;
}
return (
COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE +
NFT_TRANSFER_FROM_FUNCTION_SIGNATURE +
Array.prototype.map
.call(
abi.rawEncode(

View File

@ -1152,11 +1152,11 @@ export function doesAddressRequireLedgerHidConnection(state, address) {
);
}
export function getNewCollectibleAddedMessage(state) {
export function getNewNftAddedMessage(state) {
return state.appState.newNftAddedMessage;
}
export function getRemoveCollectibleMessage(state) {
export function getRemoveNftMessage(state) {
return state.appState.removeNftMessage;
}

View File

@ -78,9 +78,8 @@ export const SET_FIRST_TIME_FLOW_TYPE = 'SET_FIRST_TIME_FLOW_TYPE';
export const SET_SELECTED_SETTINGS_RPC_URL = 'SET_SELECTED_SETTINGS_RPC_URL';
export const SET_NEW_NETWORK_ADDED = 'SET_NEW_NETWORK_ADDED';
export const SET_NEW_COLLECTIBLE_ADDED_MESSAGE =
'SET_NEW_COLLECTIBLE_ADDED_MESSAGE';
export const SET_REMOVE_COLLECTIBLE_MESSAGE = 'SET_REMOVE_COLLECTIBLE_MESSAGE';
export const SET_NEW_NFT_ADDED_MESSAGE = 'SET_NEW_NFT_ADDED_MESSAGE';
export const SET_REMOVE_NFT_MESSAGE = 'SET_REMOVE_NFT_MESSAGE';
export const SET_NEW_CUSTOM_NETWORK_ADDED = 'SET_NEW_CUSTOM_NETWORK_ADDED';
export const LOADING_METHOD_DATA_STARTED = 'LOADING_METHOD_DATA_STARTED';

View File

@ -2103,10 +2103,10 @@ export function addNft(
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async (dispatch) => {
if (!address) {
throw new Error('MetaMask - Cannot add collectible without address');
throw new Error('MetaMask - Cannot add NFT without address');
}
if (!tokenID) {
throw new Error('MetaMask - Cannot add collectible without tokenID');
throw new Error('MetaMask - Cannot add NFT without tokenID');
}
if (!dontShowLoadingIndicator) {
dispatch(showLoadingIndication());
@ -2130,10 +2130,10 @@ export function addNftVerifyOwnership(
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async (dispatch) => {
if (!address) {
throw new Error('MetaMask - Cannot add collectible without address');
throw new Error('MetaMask - Cannot add NFT without address');
}
if (!tokenID) {
throw new Error('MetaMask - Cannot add collectible without tokenID');
throw new Error('MetaMask - Cannot add NFT without tokenID');
}
if (!dontShowLoadingIndicator) {
dispatch(showLoadingIndication());
@ -2168,10 +2168,10 @@ export function removeAndIgnoreNft(
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async (dispatch) => {
if (!address) {
throw new Error('MetaMask - Cannot ignore collectible without address');
throw new Error('MetaMask - Cannot ignore NFT without address');
}
if (!tokenID) {
throw new Error('MetaMask - Cannot ignore collectible without tokenID');
throw new Error('MetaMask - Cannot ignore NFT without tokenID');
}
if (!dontShowLoadingIndicator) {
dispatch(showLoadingIndication());
@ -2195,10 +2195,10 @@ export function removeNft(
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async (dispatch) => {
if (!address) {
throw new Error('MetaMask - Cannot remove collectible without address');
throw new Error('MetaMask - Cannot remove NFT without address');
}
if (!tokenID) {
throw new Error('MetaMask - Cannot remove collectible without tokenID');
throw new Error('MetaMask - Cannot remove NFT without tokenID');
}
if (!dontShowLoadingIndicator) {
dispatch(showLoadingIndication());
@ -2221,13 +2221,13 @@ export async function checkAndUpdateAllNftsOwnershipStatus() {
export async function isNftOwner(
ownerAddress: string,
collectibleAddress: string,
collectibleId: string,
nftAddress: string,
nftId: string,
): Promise<boolean> {
return await submitRequestToBackground('isNftOwner', [
ownerAddress,
collectibleAddress,
collectibleId,
nftAddress,
nftId,
]);
}
@ -2672,19 +2672,19 @@ export function hideAlert(): Action {
/**
* TODO: this should be moved somewhere else when it makese sense to do so
*/
interface CollectibleDropDownState {
interface NftDropDownState {
[address: string]: {
[chainId: string]: {
[collectibleAddress: string]: boolean;
[nftAddress: string]: boolean;
};
};
}
export function updateCollectibleDropDownState(
value: CollectibleDropDownState,
export function updateNftDropDownState(
value: NftDropDownState,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return async (dispatch) => {
await submitRequestToBackground('updateCollectibleDropDownState', [value]);
await submitRequestToBackground('updateNftDropDownState', [value]);
await forceUpdateMetamaskState(dispatch);
};
}
@ -3775,20 +3775,20 @@ export function setNewNetworkAdded(
};
}
export function setNewCollectibleAddedMessage(
export function setNewNftAddedMessage(
newNftAddedMessage: string,
): PayloadAction<string> {
return {
type: actionConstants.SET_NEW_COLLECTIBLE_ADDED_MESSAGE,
type: actionConstants.SET_NEW_NFT_ADDED_MESSAGE,
payload: newNftAddedMessage,
};
}
export function setRemoveCollectibleMessage(
export function setRemoveNftMessage(
removeNftMessage: string,
): PayloadAction<string> {
return {
type: actionConstants.SET_REMOVE_COLLECTIBLE_MESSAGE,
type: actionConstants.SET_REMOVE_NFT_MESSAGE,
payload: removeNftMessage,
};
}