diff --git a/.yarn/patches/@metamask-assets-controllers-npm-34.0.0-ea790e90a1.patch b/.yarn/patches/@metamask-assets-controllers-npm-34.0.0-ea790e90a1.patch
new file mode 100644
index 000000000000..3f4563ad938f
--- /dev/null
+++ b/.yarn/patches/@metamask-assets-controllers-npm-34.0.0-ea790e90a1.patch
@@ -0,0 +1,236 @@
+diff --git a/dist/chunk-354SINOH.js b/dist/chunk-354SINOH.js
+index 7f87776370b755bf04765b8a0ae0145bf3a0b5e6..e0b47123b31b3c7779e903180afc6c692953b6c2 100644
+--- a/dist/chunk-354SINOH.js
++++ b/dist/chunk-354SINOH.js
+@@ -12,7 +12,8 @@ var _basecontroller = require('@metamask/base-controller');
+
+
+
+-
++var MAX_GET_COLLECTION_BATCH_SIZE = 20;
++var _chunkNYVA7ZTQjs = require('./chunk-NYVA7ZTQ.js');
+
+ var _controllerutils = require('@metamask/controller-utils');
+ var _utils = require('@metamask/utils');
+@@ -134,6 +135,60 @@ var NftDetectionController = class extends _basecontroller.BaseController {
+ apiNfts = resultNftApi.tokens.filter(
+ (elm) => elm.token.isSpam === false && (elm.blockaidResult?.result_type ? elm.blockaidResult?.result_type === "Benign" /* Benign */ : true)
+ );
++ const collections = apiNfts.reduce((acc, currValue) => {
++ if (!acc.includes(currValue.token.contract) && currValue.token.contract === currValue?.token?.collection?.id) {
++ acc.push(currValue.token.contract);
++ }
++ return acc;
++ }, []);
++ if (collections.length !== 0) {
++ const collectionResponse = await _chunkNYVA7ZTQjs.reduceInBatchesSerially.call(void 0, {
++ values: collections,
++ batchSize: MAX_GET_COLLECTION_BATCH_SIZE,
++ eachBatch: async (allResponses, batch) => {
++ const params = new URLSearchParams(
++ batch.map((s) => ["contract", s])
++ );
++ params.append("chainId", "1");
++ const collectionResponseForBatch = await _controllerutils.fetchWithErrorHandling.call(void 0,
++ {
++ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${params.toString()}`,
++ options: {
++ headers: {
++ Version: _controllerutils.NFT_API_VERSION
++ }
++ },
++ timeout: _controllerutils.NFT_API_TIMEOUT
++ }
++ );
++ return {
++ ...allResponses,
++ ...collectionResponseForBatch
++ };
++ },
++ initialResult: {}
++ });
++ if (collectionResponse.collections?.length) {
++ apiNfts.forEach((singleNFT) => {
++ const found = collectionResponse.collections.find(
++ (elm) => elm.id?.toLowerCase() === singleNFT.token.contract.toLowerCase()
++ );
++ if (found) {
++ singleNFT.token = {
++ ...singleNFT.token,
++ collection: {
++ ...singleNFT.token.collection ? singleNFT.token.collection : {},
++ creator: found?.creator,
++ openseaVerificationStatus: found?.openseaVerificationStatus,
++ contractDeployedAt: found.contractDeployedAt,
++ ownerCount: found.ownerCount,
++ topBid: found.topBid
++ }
++ };
++ }
++ });
++ }
++ }
+ const addNftPromises = apiNfts.map(async (nft) => {
+ const {
+ tokenId,
+diff --git a/dist/chunk-7JWDWDXT.js b/dist/chunk-7JWDWDXT.js
+index af5d78416658763da52305f9e08b286733310898..5f1d7268ed8b102e0aab9f09c3896ea6fba6a0a8 100644
+--- a/dist/chunk-7JWDWDXT.js
++++ b/dist/chunk-7JWDWDXT.js
+@@ -881,6 +881,18 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
+ }
+ }
+ });
++ const getCollectionParams = new URLSearchParams({
++ chainId: "1",
++ id: `${nftInformation?.tokens[0]?.token?.collection?.id}`
++ }).toString();
++ const collectionInformation = await _controllerutils.fetchWithErrorHandling.call(void 0, {
++ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${getCollectionParams}`,
++ options: {
++ headers: {
++ Version: _controllerutils.NFT_API_VERSION
++ }
++ }
++ });
+ if (!nftInformation?.tokens?.[0]?.token) {
+ return {
+ name: null,
+@@ -918,7 +930,16 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
+ },
+ rarityRank && { rarityRank },
+ rarity && { rarity },
+- collection && { collection }
++ (collection || collectionInformation) && {
++ collection: {
++ ...collection || {},
++ creator: collection?.creator || collectionInformation?.collections[0].creator,
++ openseaVerificationStatus: collectionInformation?.collections[0].openseaVerificationStatus,
++ contractDeployedAt: collectionInformation?.collections[0].contractDeployedAt,
++ ownerCount: collectionInformation?.collections[0].ownerCount,
++ topBid: collectionInformation?.collections[0].topBid
++ }
++ }
+ );
+ return nftMetadata;
+ };
+@@ -1095,7 +1116,8 @@ addIndividualNft_fn = async function(tokenAddress, tokenId, nftMetadata, nftCont
+ nftMetadata,
+ existingEntry
+ );
+- if (!differentMetadata && existingEntry.isCurrentlyOwned) {
++ const hasNewFields = hasNewCollectionFields(nftMetadata, existingEntry);
++ if (!differentMetadata && existingEntry.isCurrentlyOwned && !hasNewFields) {
+ return;
+ }
+ const indexToUpdate = nfts.findIndex(
+diff --git a/dist/chunk-NYVA7ZTQ.js b/dist/chunk-NYVA7ZTQ.js
+index f31fdabedc067227407a6320e57a670f86b972f4..c0ff7ece56dc5f3e68149d114ff16f7d10eb1741 100644
+--- a/dist/chunk-NYVA7ZTQ.js
++++ b/dist/chunk-NYVA7ZTQ.js
+@@ -27,6 +27,11 @@ function compareNftMetadata(newNftMetadata, nft) {
+ }, 0);
+ return differentValues > 0;
+ }
++function hasNewCollectionFields(newNftMetadata, nft) {
++ const keysNewNftMetadata = Object.keys(newNftMetadata.collection || {});
++ const keysExistingNft = new Set(Object.keys(nft.collection || {}));
++ return keysNewNftMetadata.some((key) => !keysExistingNft.has(key));
++}
+ var aggregatorNameByKey = {
+ aave: "Aave",
+ bancor: "Bancor",
+@@ -205,5 +210,5 @@ async function fetchTokenContractExchangeRates({
+
+
+
+-exports.TOKEN_PRICES_BATCH_SIZE = TOKEN_PRICES_BATCH_SIZE; exports.compareNftMetadata = compareNftMetadata; exports.formatAggregatorNames = formatAggregatorNames; exports.formatIconUrlWithProxy = formatIconUrlWithProxy; exports.SupportedTokenDetectionNetworks = SupportedTokenDetectionNetworks; exports.isTokenDetectionSupportedForNetwork = isTokenDetectionSupportedForNetwork; exports.isTokenListSupportedForNetwork = isTokenListSupportedForNetwork; exports.removeIpfsProtocolPrefix = removeIpfsProtocolPrefix; exports.getIpfsCIDv1AndPath = getIpfsCIDv1AndPath; exports.getFormattedIpfsUrl = getFormattedIpfsUrl; exports.addUrlProtocolPrefix = addUrlProtocolPrefix; exports.ethersBigNumberToBN = ethersBigNumberToBN; exports.divideIntoBatches = divideIntoBatches; exports.reduceInBatchesSerially = reduceInBatchesSerially; exports.fetchTokenContractExchangeRates = fetchTokenContractExchangeRates;
++exports.TOKEN_PRICES_BATCH_SIZE = TOKEN_PRICES_BATCH_SIZE; exports.compareNftMetadata = compareNftMetadata; exports.hasNewCollectionFields = hasNewCollectionFields; exports.formatAggregatorNames = formatAggregatorNames; exports.formatIconUrlWithProxy = formatIconUrlWithProxy; exports.SupportedTokenDetectionNetworks = SupportedTokenDetectionNetworks; exports.isTokenDetectionSupportedForNetwork = isTokenDetectionSupportedForNetwork; exports.isTokenListSupportedForNetwork = isTokenListSupportedForNetwork; exports.removeIpfsProtocolPrefix = removeIpfsProtocolPrefix; exports.getIpfsCIDv1AndPath = getIpfsCIDv1AndPath; exports.getFormattedIpfsUrl = getFormattedIpfsUrl; exports.addUrlProtocolPrefix = addUrlProtocolPrefix; exports.ethersBigNumberToBN = ethersBigNumberToBN; exports.divideIntoBatches = divideIntoBatches; exports.reduceInBatchesSerially = reduceInBatchesSerially; exports.fetchTokenContractExchangeRates = fetchTokenContractExchangeRates;
+ //# sourceMappingURL=chunk-NYVA7ZTQ.js.map
+\ No newline at end of file
+diff --git a/dist/types/NftController.d.ts b/dist/types/NftController.d.ts
+index b663e265475fee486f1e570736a08f2c06ce5479..0252b138bb4f1cbcbfab7c6eadc8ba28fe5af674 100644
+--- a/dist/types/NftController.d.ts
++++ b/dist/types/NftController.d.ts
+@@ -7,7 +7,7 @@ import type { PreferencesControllerStateChangeEvent } from '@metamask/preference
+ import type { Hex } from '@metamask/utils';
+ import type { AssetsContractController } from './AssetsContractController';
+ import { Source } from './constants';
+-import type { Collection, Attributes, LastSale } from './NftDetectionController';
++import type { Collection, Attributes, LastSale, TopBid } from './NftDetectionController';
+ type NFTStandardType = 'ERC721' | 'ERC1155';
+ type SuggestedNftMeta = {
+ asset: {
+@@ -110,9 +110,10 @@ export type NftMetadata = {
+ tokenURI?: string | null;
+ collection?: Collection;
+ address?: string;
+- attributes?: Attributes;
++ attributes?: Attributes[];
+ lastSale?: LastSale;
+ rarityRank?: string;
++ topBid?: TopBid;
+ };
+ /**
+ * @type NftControllerState
+diff --git a/dist/types/NftDetectionController.d.ts b/dist/types/NftDetectionController.d.ts
+index c645b3ada1ad9dd862428e94adb788f7892c99ad..ad2df53b8225c105b67245f6498702920f882f95 100644
+--- a/dist/types/NftDetectionController.d.ts
++++ b/dist/types/NftDetectionController.d.ts
+@@ -227,7 +227,43 @@ export type Attributes = {
+ topBidValue?: number | null;
+ createdAt?: string;
+ };
+-export type Collection = {
++
++export type GetCollectionsResponse = {
++ collections: CollectionResponse[];
++ };
++
++export type CollectionResponse = {
++ id?: string;
++ openseaVerificationStatus?: string;
++ contractDeployedAt?: string;
++ creator?: string;
++ ownerCount?: string;
++ topBid?: TopBid & {
++ sourceDomain?: string;
++ };
++};
++
++export type FloorAskCollection = {
++ id?: string;
++ price?: Price;
++ maker?: string;
++ kind?: string;
++ validFrom?: number;
++ validUntil?: number;
++ source?: SourceCollection;
++ rawData?: Metadata;
++ isNativeOffChainCancellable?: boolean;
++};
++
++export type SourceCollection = {
++ id: string;
++ domain: string;
++ name: string;
++ icon: string;
++ url: string;
++};
++
++export type TokenCollection = {
+ id?: string;
+ name?: string;
+ slug?: string;
+@@ -243,7 +279,10 @@ export type Collection = {
+ floorAskPrice?: Price;
+ royaltiesBps?: number;
+ royalties?: Royalties[];
+-};
++ floorAsk?: FloorAskCollection;
++ };
++
++export type Collection = TokenCollection & CollectionResponse;
+ export type Royalties = {
+ bps?: number;
+ recipient?: string;
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index 7566d8c1872d..8b4f7c8e5d34 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Zuletzt verbunden"
},
- "lastPriceSold": {
- "message": "Letzter Verkaufspreis"
- },
"lastSold": {
"message": "Zuletzt verkauft"
},
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index 5ca813ae6c72..ab8e04b978d5 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Τελευταία σύνδεση"
},
- "lastPriceSold": {
- "message": "Τελευταία τιμή πώλησης"
- },
"lastSold": {
"message": "Τελευταία πώληση"
},
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 8683dc1bb47d..6fe394a278a9 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -635,6 +635,9 @@
"attemptToCancelSwapForFree": {
"message": "Attempt to cancel swap for free"
},
+ "attributes": {
+ "message": "Attributes"
+ },
"attributions": {
"message": "Attributions"
},
@@ -835,6 +838,9 @@
"blockies": {
"message": "Blockies"
},
+ "boughtFor": {
+ "message": "Bought for"
+ },
"bridge": {
"message": "Bridge"
},
@@ -956,6 +962,9 @@
"coingecko": {
"message": "CoinGecko"
},
+ "collectionName": {
+ "message": "Collection name"
+ },
"comboNoOptions": {
"message": "No options found",
"description": "Default text shown in the combo field dropdown if no options."
@@ -1261,6 +1270,9 @@
"createSnapAccountTitle": {
"message": "Create account"
},
+ "creatorAddress": {
+ "message": "Creator address"
+ },
"crossChainSwapsLink": {
"message": "Swap across networks with MetaMask Portfolio"
},
@@ -1448,6 +1460,12 @@
"dataHex": {
"message": "Hex"
},
+ "dataUnavailable": {
+ "message": "data unavailable"
+ },
+ "dateCreated": {
+ "message": "Date created"
+ },
"dcent": {
"message": "D'Cent"
},
@@ -2197,6 +2215,12 @@
"highLowercase": {
"message": "high"
},
+ "highestCurrentBid": {
+ "message": "Highest current bid"
+ },
+ "highestFloorPrice": {
+ "message": "Highest floor price"
+ },
"history": {
"message": "History"
},
@@ -2557,9 +2581,6 @@
"lastConnected": {
"message": "Last connected"
},
- "lastPriceSold": {
- "message": "Last price sold"
- },
"lastSold": {
"message": "Last sold"
},
@@ -3419,6 +3440,9 @@
"numberOfNewTokensDetectedSingular": {
"message": "1 new token found in this account"
},
+ "numberOfTokens": {
+ "message": "Number of tokens"
+ },
"ofTextNofM": {
"message": "of"
},
@@ -4006,6 +4030,12 @@
"prev": {
"message": "Prev"
},
+ "price": {
+ "message": "Price"
+ },
+ "priceUnavailable": {
+ "message": "price unavailable"
+ },
"primaryCurrencySetting": {
"message": "Primary currency"
},
@@ -4158,6 +4188,9 @@
"quoteRate": {
"message": "Quote rate"
},
+ "rank": {
+ "message": "Rank"
+ },
"reAddAccounts": {
"message": "re-add any other accounts"
},
@@ -4720,6 +4753,9 @@
"showIncomingTransactionsExplainer": {
"message": "This relies on different third-party APIs for each network, which expose your Ethereum address and your IP address."
},
+ "showLess": {
+ "message": "Show less"
+ },
"showMore": {
"message": "Show more"
},
@@ -5958,6 +5994,9 @@
"tokenShowUp": {
"message": "Your tokens may not automatically show up in your wallet. "
},
+ "tokenStandard": {
+ "message": "Token standard"
+ },
"tokenSymbol": {
"message": "Token symbol"
},
@@ -5968,6 +6007,9 @@
"message": "$1 new tokens found",
"description": "$1 is the number of new tokens detected"
},
+ "tokensInCollection": {
+ "message": "Tokens in collection"
+ },
"tooltipApproveButton": {
"message": "I understand"
},
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index 4d3ec28c7198..32f1e0ad689e 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "Última conexión"
},
- "lastPriceSold": {
- "message": "Precio de la última venta"
- },
"lastSold": {
"message": "Última venta"
},
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index 35973f9e23ff..0745dffeccab 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Dernière connexion"
},
- "lastPriceSold": {
- "message": "Prix de la dernière vente"
- },
"lastSold": {
"message": "Dernière vente"
},
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 59aebe129f11..1e6e15af58de 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "अंतिम बार जुड़ा"
},
- "lastPriceSold": {
- "message": "पिछली बार की बिक्री दर"
- },
"lastSold": {
"message": "पिछली बार बेचा गया"
},
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 0d8144d1f0fe..4dc34c85ae76 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Terakhir terhubung"
},
- "lastPriceSold": {
- "message": "Harga terakhir terjual"
- },
"lastSold": {
"message": "Terakhir terjual"
},
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 464bd37d8236..73e6e50c7243 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "前回の接続"
},
- "lastPriceSold": {
- "message": "前回の売値"
- },
"lastSold": {
"message": "前回の売却"
},
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index 2eaf06ce7afd..fd341daf8251 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "마지막 연결"
},
- "lastPriceSold": {
- "message": "최근 판매 가격"
- },
"lastSold": {
"message": "최근 판매"
},
diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json
index c391a94b5f6a..64ba59e24785 100644
--- a/app/_locales/pt/messages.json
+++ b/app/_locales/pt/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Última conexão"
},
- "lastPriceSold": {
- "message": "Último preço de venda"
- },
"lastSold": {
"message": "Última venda"
},
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index 3a0e2ed664c6..14b5bf02009c 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Последнее подключение"
},
- "lastPriceSold": {
- "message": "Последняя цена продажи"
- },
"lastSold": {
"message": "Последняя продажа"
},
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index 90e423b1fe9c..a5cf6d94503e 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "Huling Kumonekta"
},
- "lastPriceSold": {
- "message": "Huling presyong naibenta"
- },
"lastSold": {
"message": "Huling naibenta"
},
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index 3f599c8c6162..e6b8548d18c4 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -2324,9 +2324,6 @@
"lastConnected": {
"message": "Son bağlanma"
},
- "lastPriceSold": {
- "message": "Son satış fiyatı"
- },
"lastSold": {
"message": "Son satış"
},
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index 56279bd7062d..e16996caf2d3 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "Đã kết nối lần cuối"
},
- "lastPriceSold": {
- "message": "Giá bán gần nhất"
- },
"lastSold": {
"message": "Đã bán gần nhất"
},
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index 21fe318fab45..77b5e57644d1 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -2321,9 +2321,6 @@
"lastConnected": {
"message": "最后连接"
},
- "lastPriceSold": {
- "message": "最后售价"
- },
"lastSold": {
"message": "最后售出"
},
diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json
index 4506020c2bee..ee49384b9b1d 100644
--- a/lavamoat/browserify/beta/policy.json
+++ b/lavamoat/browserify/beta/policy.json
@@ -882,6 +882,7 @@
"clearTimeout": true,
"console.error": true,
"console.log": true,
+ "hasNewCollectionFields": true,
"setInterval": true,
"setTimeout": true
},
diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json
index 4506020c2bee..ee49384b9b1d 100644
--- a/lavamoat/browserify/flask/policy.json
+++ b/lavamoat/browserify/flask/policy.json
@@ -882,6 +882,7 @@
"clearTimeout": true,
"console.error": true,
"console.log": true,
+ "hasNewCollectionFields": true,
"setInterval": true,
"setTimeout": true
},
diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json
index 4506020c2bee..ee49384b9b1d 100644
--- a/lavamoat/browserify/main/policy.json
+++ b/lavamoat/browserify/main/policy.json
@@ -882,6 +882,7 @@
"clearTimeout": true,
"console.error": true,
"console.log": true,
+ "hasNewCollectionFields": true,
"setInterval": true,
"setTimeout": true
},
diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json
index bc5d084d0243..f83e97c1a725 100644
--- a/lavamoat/browserify/mmi/policy.json
+++ b/lavamoat/browserify/mmi/policy.json
@@ -974,6 +974,7 @@
"clearTimeout": true,
"console.error": true,
"console.log": true,
+ "hasNewCollectionFields": true,
"setInterval": true,
"setTimeout": true
},
diff --git a/package.json b/package.json
index b956c03ec007..5a1e1830a1ca 100644
--- a/package.json
+++ b/package.json
@@ -291,7 +291,7 @@
"@metamask/address-book-controller": "^4.0.1",
"@metamask/announcement-controller": "^6.1.0",
"@metamask/approval-controller": "^7.0.0",
- "@metamask/assets-controllers": "^34.0.0",
+ "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A34.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-34.0.0-ea790e90a1.patch",
"@metamask/base-controller": "^5.0.1",
"@metamask/bitcoin-wallet-snap": "^0.2.4",
"@metamask/browser-passworder": "^4.3.0",
diff --git a/test/e2e/tests/tokens/nft/send-nft.spec.js b/test/e2e/tests/tokens/nft/send-nft.spec.js
index a9b89a2abb9b..37b81eca6794 100644
--- a/test/e2e/tests/tokens/nft/send-nft.spec.js
+++ b/test/e2e/tests/tokens/nft/send-nft.spec.js
@@ -101,9 +101,6 @@ describe('Send NFT', function () {
await driver.clickElement('[data-testid="account-overview__nfts-tab"]');
await driver.clickElement('[data-testid="nft-network-badge"]');
- await driver.clickElement(
- '.nft-item__container .mm-badge-wrapper__badge-container',
- );
await driver.clickElement({ text: 'Send', tag: 'button' });
await driver.fill(
diff --git a/test/e2e/tests/tokens/nft/view-erc1155-details.spec.js b/test/e2e/tests/tokens/nft/view-erc1155-details.spec.js
index 1bf8d6adf286..fd06b4f36b27 100644
--- a/test/e2e/tests/tokens/nft/view-erc1155-details.spec.js
+++ b/test/e2e/tests/tokens/nft/view-erc1155-details.spec.js
@@ -3,6 +3,7 @@ const {
withFixtures,
unlockWallet,
} = require('../../../helpers');
+
const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts');
const FixtureBuilder = require('../../../fixture-builder');
@@ -37,26 +38,22 @@ describe('View ERC1155 NFT details', function () {
await driver.clickElement('.nft-item__container');
- await driver.findElement({
- css: '.asset-breadcrumb span:nth-of-type(2)',
- text: 'Account 1',
- });
+ await driver.findElement('[data-testid="nft__back"]');
- // Check the displayed ERC1155 NFT details
await driver.findElement({
- css: '.nft-details__info h4',
+ css: '[data-testid="nft-details__name"]',
text: 'Rocks',
});
await driver.findElement({
- css: '.nft-details__info h6:nth-of-type(2)',
+ css: '[data-testid="nft-details__description"]',
text: 'This is a collection of Rock NFTs.',
});
await driver.findVisibleElement('.nft-item__container');
await driver.findElement({
- css: '.nft-details__contract-wrapper',
+ css: '.nft-details__nft-frame',
text: '0x581c3...45947',
});
},
diff --git a/test/e2e/tests/tokens/nft/view-nft-details.spec.js b/test/e2e/tests/tokens/nft/view-nft-details.spec.js
index ef8aba0cd9b3..410bed887ab4 100644
--- a/test/e2e/tests/tokens/nft/view-nft-details.spec.js
+++ b/test/e2e/tests/tokens/nft/view-nft-details.spec.js
@@ -26,31 +26,27 @@ describe('View NFT details', function () {
await driver.clickElement('[data-testid="account-overview__nfts-tab"]');
await driver.clickElement('.nft-item__container');
- const detailsPageTitle = await driver.findElement('.asset-breadcrumb');
- assert.equal(
- await detailsPageTitle.getText(),
- 'Account 1 / TestDappNFTs',
- );
+ await driver.findElement('[data-testid="nft__back"]');
// Check the displayed NFT details
- const nftName = await driver.findElement('.nft-details__info h4');
- assert.equal(await nftName.getText(), 'Test Dapp NFTs #1');
- const nftDescription = await driver.findElement(
- '.nft-details__info h6:nth-of-type(2)',
- );
- assert.equal(
- await nftDescription.getText(),
- 'Test Dapp NFTs for testing.',
- );
+ await driver.findElement({
+ css: '[data-testid="nft-details__name"]',
+ text: 'Test Dapp NFTs #1',
+ });
+
+ await driver.findElement({
+ css: '[data-testid="nft-details__description"]',
+ text: 'Test Dapp NFTs for testing.',
+ });
const nftImage = await driver.findElement('.nft-item__container');
assert.equal(await nftImage.isDisplayed(), true);
- const nftContract = await driver.findElement(
- '.nft-details__contract-wrapper',
- );
- assert.equal(await nftContract.getText(), '0x581c3...45947');
+ await driver.findElement({
+ css: '.nft-details__nft-frame',
+ text: '0x581c3...45947',
+ });
},
);
});
diff --git a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap
index 037f736f6829..67338dfd167d 100644
--- a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap
+++ b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap
@@ -3,186 +3,179 @@
exports[`NFT Details should match minimal props and state snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Last price sold
-
-
-
-
-
-
-
-
-
- Contract address
-
-
-
+ MUNK #1
+
+
+
+
+
+
+ Token standard
+
+
+ ERC721
+
+
+
+
+
+
+
+ Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted.
+
+
-
- Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted.
-
diff --git a/ui/components/app/nft-details/index.scss b/ui/components/app/nft-details/index.scss
index 6f0a020bce4e..fe01b3ade2e9 100644
--- a/ui/components/app/nft-details/index.scss
+++ b/ui/components/app/nft-details/index.scss
@@ -1,123 +1,117 @@
@use "design-system";
-$card-width-break-large: 224px;
+$card-width-break-large: 144px;
$link-title-width: 160px;
$spacer-break-large: 24px;
$spacer-break-small: 16px;
-.nft-details {
- padding: 0 $spacer-break-small;
-
- @include design-system.screen-sm-min {
- padding: 0 $spacer-break-large;
- }
+.buttonDescriptionContainer {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ background: linear-gradient(90deg, transparent 0%, var(--color-background-default) 33%);
- &__tooltip-wrapper {
- width: 100%;
+ @include design-system.screen-md-min {
+ bottom: 3px;
}
+}
- &__top-section {
- display: flex;
- flex-direction: column;
- margin-bottom: $spacer-break-small;
- box-shadow: var(--shadow-size-xs) var(--color-shadow-default);
- padding: $spacer-break-large;
+.badge-wrapper {
+ height: 70vh;
+}
- @include design-system.screen-sm-min {
- margin-bottom: $spacer-break-large;
- flex-direction: row;
- }
- }
+.fade-in {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
- &__info {
- @include design-system.screen-sm-min {
- max-width:
- calc(
- 100% - #{$card-width-break-large} - #{$spacer-break-large}
- );
- flex: 0 0 calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
- }
- }
+.fade-in.visible {
+ opacity: 1;
+}
- &__card {
- overflow: hidden;
+.nft-details {
+ &__nft-item {
margin-bottom: $spacer-break-small;
- @include design-system.screen-sm-min {
- margin-right: $spacer-break-large;
+ @include design-system.screen-sm-max {
margin-bottom: 0;
max-width: $card-width-break-large;
flex: 0 0 $card-width-break-large;
- height: $card-width-break-large;
+ height: calc(100% - 8px);
}
- }
- &__nft-item {
- margin-bottom: $spacer-break-small;
+ margin-bottom: 0;
+ max-width: $card-width-break-large;
+ flex: 0 0 $card-width-break-large;
+ height: calc(100% - 8px);
+ }
- @include design-system.screen-sm-min {
- margin-right: $spacer-break-large;
- margin-bottom: 0;
- max-width: $card-width-break-large;
- flex: 0 0 $card-width-break-large;
- height: $card-width-break-large;
+ &__full-image-container {
+ @include design-system.screen-lg-min {
+ margin: 10px;
+ align-items: center;
}
- }
- &__image {
- width: 100%;
- object-fit: contain;
+ margin: 10px;
+ align-items: center;
}
- &__address {
- overflow-wrap: break-word;
+ &__content {
+ @include design-system.screen-lg-min {
+ padding-left: 192px;
+ padding-right: 192px;
+ width: auto !important;
+ }
}
- &__contract-wrapper {
- max-width: calc(100% - #{$link-title-width});
+ &__addressButton {
+ background-color: transparent;
+ padding-right: 0;
}
- &__contract-copy-button {
- @include design-system.H6;
- width: 80px;
- display: flex;
- align-items: flex-start;
- justify-content: center;
- background-color: transparent;
- cursor: pointer;
- color: var(--color-text-alternative);
- border: 0;
- margin-top: -4px;
+ &__show-more {
+ max-height: 2.5rem;
- &:active {
- transform: scale(0.97);
+ &__buttonContainer {
+ position: 'absolute';
+ bottom: 0;
+ right: 0;
+ background: linear-gradient(90deg, transparent 0%, var(--color-background-default) 33%);
}
- }
- &__contract-link {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
+ @include design-system.screen-md-min {
+ max-height: 3rem;
+ }
- &__image-source {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 332px;
+ &__button {
+ background: linear-gradient(90deg, transparent 0%, var(--color-background-default) 33%);
+ vertical-align: baseline;
+ }
}
- &__link-title {
- flex: 0 0 $link-title-width;
- max-width: 0 0 $link-title-width;
+ &__nft-frame {
+ flex: 1 0 33%;
+ padding-top: 12px;
+ padding-bottom: 12px;
+ padding-left: 16px;
+ padding-right: 16px;
+ border-radius: var(--Spacing-sm, 8px);
+ border: 1px solid var(--border-muted, #d6d9dc);
}
- &__send-button {
- margin-inline-end: 8px;
-
- @include design-system.screen-sm-min {
- max-width: 160px;
+ &__nft-attribute-frame {
+ @include design-system.screen-sm-max {
+ width: 48.51%;
}
+
+ display: inline-block;
+ width: 49%;
+ padding-left: 16px;
+ padding-right: 16px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ border-radius: var(--Spacing-sm, 8px);
+ border: 1px solid var(--border-muted, #d6d9dc);
}
}
diff --git a/ui/components/app/nft-details/nft-detail-description.stories.js b/ui/components/app/nft-details/nft-detail-description.stories.js
new file mode 100644
index 000000000000..10c3482d1c17
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-description.stories.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import NftDetailDescription from './nft-detail-description';
+
+export default {
+ title: 'Components/App/NftDetailDescription',
+ args: {
+ value:
+ 'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.',
+ },
+};
+
+export const DefaultStory = (args) => {
+ return ;
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/nft-details/nft-detail-description.tsx b/ui/components/app/nft-details/nft-detail-description.tsx
new file mode 100644
index 000000000000..d29aacc5ab0b
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-description.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react';
+import useIsOverflowing from '../../../hooks/snaps/useIsOverflowing';
+import { Box, Button, ButtonVariant, Text } from '../../component-library';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ FontWeight,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+
+const NftDetailDescription = ({ value }: { value: string | null }) => {
+ const t = useI18nContext();
+ const { contentRef, isOverflowing } = useIsOverflowing();
+ const [isOpen, setIsOpen] = useState(false);
+
+ const shouldDisplayButton = !isOpen && isOverflowing;
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsOpen(!isOpen);
+ };
+
+ return (
+ <>
+
+
+ {value}
+
+ {shouldDisplayButton && (
+
+
+ {t('showMore')}
+
+
+ )}
+
+ {isOpen && (
+
+
+ {t('showLess')}
+
+
+ )}
+ >
+ );
+};
+
+export default NftDetailDescription;
diff --git a/ui/components/app/nft-details/nft-detail-information-frame.stories.js b/ui/components/app/nft-details/nft-detail-information-frame.stories.js
new file mode 100644
index 000000000000..241cf98ef9e1
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-information-frame.stories.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import {
+ IconColor,
+ TextAlign,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import { ButtonIcon, IconName, IconSize } from '../../component-library';
+import NftDetailInformationFrame from './nft-detail-information-frame';
+
+export default {
+ title: 'Components/App/NftDetailInformationFrame',
+
+ argTypes: {
+ nft: {
+ control: 'object',
+ },
+ },
+ args: {
+ title: 'Bought for',
+ value: '$500',
+ frameClassname: 'nft-details__nft-frame',
+ frameTextTitleProps: {
+ textAlign: TextAlign.Center,
+ color: TextColor.textAlternative,
+ variant: TextVariant.bodyMdMedium,
+ },
+ frameTextTitleStyle: {
+ fontSize: '10px',
+ lineHeight: '16px',
+ },
+ frameTextValueProps: {
+ color: TextColor.textDefault,
+ variant: TextVariant.headingSm,
+ },
+ frameTextValueStyle: {
+ fontSize: '16px',
+ lineHeight: '24px',
+ },
+ },
+};
+
+export const DefaultStory = (args) => {
+ return (
+ {
+ global.platform.openTab({
+ url: 'test',
+ });
+ }}
+ iconName={IconName.Export}
+ />
+ }
+ />
+ );
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/nft-details/nft-detail-information-frame.tsx b/ui/components/app/nft-details/nft-detail-information-frame.tsx
new file mode 100644
index 000000000000..7e0a629fe470
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-information-frame.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { Box, Text } from '../../component-library';
+import {
+ AlignItems,
+ Display,
+ JustifyContent,
+} from '../../../helpers/constants/design-system';
+
+type NftDetailInformationFrameProps = {
+ title?: string;
+ value?: string;
+ frameClassname: string;
+ frameTextTitleProps: Record;
+ frameTextValueProps?: Record;
+ frameTextTitleStyle?: React.CSSProperties;
+ frameTextValueStyle?: React.CSSProperties;
+ icon?: React.ReactNode;
+ buttonAddressValue?: React.ButtonHTMLAttributes;
+};
+
+const NftDetailInformationFrame = ({
+ title,
+ value,
+ buttonAddressValue,
+ frameClassname,
+ frameTextTitleProps,
+ frameTextTitleStyle,
+ frameTextValueStyle,
+ frameTextValueProps,
+ icon,
+}: NftDetailInformationFrameProps) => {
+ return (
+
+
+ {title}
+
+
+ {icon ? (
+
+ {' '}
+ {buttonAddressValue ? (
+ { ...buttonAddressValue }
+ ) : (
+
+ {value}
+
+ )}
+ {icon}
+
+ ) : (
+
+ {value}
+
+ )}
+
+ );
+};
+
+export default NftDetailInformationFrame;
diff --git a/ui/components/app/nft-details/nft-detail-information-row.stories.js b/ui/components/app/nft-details/nft-detail-information-row.stories.js
new file mode 100644
index 000000000000..e5001cecce2d
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-information-row.stories.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import NftDetailInformationRow from './nft-detail-information-row';
+
+export default {
+ title: 'Components/App/NftDetailInformationRow',
+
+ argTypes: {
+ nft: {
+ control: 'object',
+ },
+ },
+ args: {
+ title: 'Token ID',
+ value: '345',
+ },
+};
+
+export const DefaultStory = (args) => {
+ return ;
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/nft-details/nft-detail-information-row.tsx b/ui/components/app/nft-details/nft-detail-information-row.tsx
new file mode 100644
index 000000000000..0d320f1426b4
--- /dev/null
+++ b/ui/components/app/nft-details/nft-detail-information-row.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+
+import { Box, Text } from '../../component-library';
+import {
+ Display,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+
+type NftDetailInformationRowProps = {
+ title: string;
+ valueColor?: TextColor;
+ value?: string | null;
+ icon?: React.ReactNode;
+ buttonAddressValue?: React.ButtonHTMLAttributes | null;
+};
+
+const NftDetailInformationRow: React.FC = ({
+ title,
+ valueColor,
+ value,
+ icon,
+ buttonAddressValue,
+}) => {
+ if (!value && !buttonAddressValue) {
+ return null;
+ }
+ return (
+
+
+ {title}
+
+ {icon ? (
+
+ {buttonAddressValue ? (
+ { ...buttonAddressValue }
+ ) : (
+
+ {value}
+
+ )}
+ {icon}
+
+ ) : (
+
+ {value}
+
+ )}
+
+ );
+};
+
+export default NftDetailInformationRow;
diff --git a/ui/components/app/nft-details/nft-details.js b/ui/components/app/nft-details/nft-details.js
deleted file mode 100644
index 55f5cdf32b75..000000000000
--- a/ui/components/app/nft-details/nft-details.js
+++ /dev/null
@@ -1,439 +0,0 @@
-import React, { useEffect, useContext } from 'react';
-import PropTypes from 'prop-types';
-import { useDispatch, useSelector } from 'react-redux';
-import { useHistory } from 'react-router-dom';
-import { isEqual } from 'lodash';
-import Box from '../../ui/box';
-import {
- TextColor,
- IconColor,
- TextVariant,
- FontWeight,
- JustifyContent,
- OverflowWrap,
- FlexDirection,
- Display,
-} from '../../../helpers/constants/design-system';
-import { useI18nContext } from '../../../hooks/useI18nContext';
-import {
- formatDate,
- getAssetImageURL,
- shortenAddress,
-} from '../../../helpers/utils/util';
-import { getNftImageAlt } from '../../../helpers/utils/nfts';
-import {
- getCurrentChainId,
- getCurrentNetwork,
- getIpfsGateway,
- getSelectedInternalAccount,
-} from '../../../selectors';
-import AssetNavigation from '../../../pages/asset/components/asset-navigation';
-import { getNftContracts } from '../../../ducks/metamask/metamask';
-import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
-import {
- checkAndUpdateSingleNftOwnershipStatus,
- removeAndIgnoreNft,
- setRemoveNftMessage,
- setNewNftAddedMessage,
-} 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 NftOptions from '../nft-options/nft-options';
-import Button from '../../ui/button';
-import { startNewDraftTransaction } from '../../../ducks/send';
-import InfoTooltip from '../../ui/info-tooltip';
-import { usePrevious } from '../../../hooks/usePrevious';
-import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
-import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
-import {
- AssetType,
- TokenStandard,
-} from '../../../../shared/constants/transaction';
-import { ButtonIcon, IconName, Text } from '../../component-library';
-import Tooltip from '../../ui/tooltip';
-import { NftItem } from '../../multichain/nft-item';
-import {
- MetaMetricsEventName,
- MetaMetricsEventCategory,
-} from '../../../../shared/constants/metametrics';
-import { MetaMetricsContext } from '../../../contexts/metametrics';
-
-export default function NftDetails({ nft }) {
- const {
- image,
- imageOriginal,
- name,
- description,
- address,
- tokenId,
- standard,
- isCurrentlyOwned,
- lastSale,
- } = nft;
- const t = useI18nContext();
- const history = useHistory();
- const dispatch = useDispatch();
- const ipfsGateway = useSelector(getIpfsGateway);
- const nftContracts = useSelector(getNftContracts);
- const currentNetwork = useSelector(getCurrentChainId);
- const currentChain = useSelector(getCurrentNetwork);
- const trackEvent = useContext(MetaMetricsContext);
-
- const [addressCopied, handleAddressCopy] = useCopyToClipboard();
-
- const nftContractName = nftContracts.find(({ address: contractAddress }) =>
- isEqualCaseInsensitive(contractAddress, address),
- )?.name;
- const {
- metadata: { name: selectedAccountName },
- } = useSelector(getSelectedInternalAccount);
- const nftImageAlt = getNftImageAlt(nft);
- const nftSrcUrl = imageOriginal ?? image;
- const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway);
- const isIpfsURL = nftSrcUrl?.startsWith('ipfs:');
- const isImageHosted = image?.startsWith('https:');
-
- const formattedTimestamp = formatDate(
- new Date(lastSale?.timestamp).getTime(),
- 'M/d/y',
- );
-
- const { chainId } = currentChain;
- useEffect(() => {
- trackEvent({
- event: MetaMetricsEventName.NftDetailsOpened,
- category: MetaMetricsEventCategory.Tokens,
- properties: {
- chain_id: chainId,
- },
- });
- }, [trackEvent, chainId]);
-
- const onRemove = async () => {
- let isSuccessfulEvent = false;
- try {
- await dispatch(removeAndIgnoreNft(address, tokenId));
- dispatch(setNewNftAddedMessage(''));
- dispatch(setRemoveNftMessage('success'));
- isSuccessfulEvent = true;
- } catch (err) {
- dispatch(setNewNftAddedMessage(''));
- dispatch(setRemoveNftMessage('error'));
- } finally {
- // track event
- trackEvent({
- event: MetaMetricsEventName.NFTRemoved,
- category: 'Wallet',
- properties: {
- token_contract_address: address,
- tokenId: tokenId.toString(),
- asset_type: AssetType.NFT,
- token_standard: standard,
- chain_id: currentNetwork,
- isSuccessful: isSuccessfulEvent,
- },
- });
- history.push(DEFAULT_ROUTE);
- }
- };
-
- const prevNft = usePrevious(nft);
- useEffect(() => {
- if (!isEqual(prevNft, nft)) {
- checkAndUpdateSingleNftOwnershipStatus(nft);
- }
- }, [nft, prevNft]);
-
- const getOpenSeaLink = () => {
- switch (currentNetwork) {
- case CHAIN_IDS.MAINNET:
- return `https://opensea.io/assets/ethereum/${address}/${tokenId}`;
- case CHAIN_IDS.POLYGON:
- return `https://opensea.io/assets/matic/${address}/${tokenId}`;
- case CHAIN_IDS.GOERLI:
- return `https://testnets.opensea.io/assets/goerli/${address}/${tokenId}`;
- case CHAIN_IDS.SEPOLIA:
- return `https://testnets.opensea.io/assets/sepolia/${address}/${tokenId}`;
- default:
- return null;
- }
- };
-
- const openSeaLink = getOpenSeaLink();
- const sendDisabled =
- standard !== TokenStandard.ERC721 && standard !== TokenStandard.ERC1155;
- const inPopUp = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
-
- const onSend = async () => {
- await dispatch(
- startNewDraftTransaction({
- type: AssetType.NFT,
- details: nft,
- }),
- );
- // We only allow sending one NFT at a time
- history.push(SEND_ROUTE);
- };
-
- const renderSendButton = () => {
- if (isCurrentlyOwned === false) {
- return ;
- }
- return (
-
-
- {t('send')}
-
- {sendDisabled ? (
-
- ) : null}
-
- );
- };
-
- return (
- <>
- history.push(DEFAULT_ROUTE)}
- optionsButton={
- global.platform.openTab({ url: openSeaLink })
- : null
- }
- onRemove={onRemove}
- />
- }
- />
-
-
-
-
-
-
-
-
- {name}
-
-
- #{tokenId}
-
-
- {description ? (
-
-
- {t('description')}
-
-
- {description}
-
-
- ) : null}
- {inPopUp ? null : renderSendButton()}
-
-
-
- {lastSale ? (
- <>
-
-
- {t('lastSold')}
-
-
-
- {formattedTimestamp}
-
-
-
-
-
- {t('lastPriceSold')}
-
-
-
- {lastSale?.price?.amount?.decimal}{' '}
- {lastSale?.price?.currency?.symbol}
-
-
-
- >
- ) : null}
-
-
- {t('contractAddress')}
-
-
-
- {shortenAddress(address)}
-
-
- {
- handleAddressCopy(address);
- }}
- iconName={
- addressCopied ? IconName.CopySuccess : IconName.Copy
- }
- />
-
-
-
- {inPopUp ? renderSendButton() : null}
-
- {t('nftDisclaimer')}
-
-
-
- >
- );
-}
-
-NftDetails.propTypes = {
- nft: PropTypes.shape({
- address: PropTypes.string.isRequired,
- tokenId: PropTypes.string.isRequired,
- isCurrentlyOwned: PropTypes.bool,
- name: PropTypes.string,
- description: PropTypes.string,
- image: PropTypes.string,
- standard: PropTypes.string,
- imageThumbnail: PropTypes.string,
- imagePreview: PropTypes.string,
- imageOriginal: PropTypes.string,
- creator: PropTypes.shape({
- address: PropTypes.string,
- config: PropTypes.string,
- profile_img_url: PropTypes.string,
- }),
- lastSale: PropTypes.shape({
- timestamp: PropTypes.string,
- price: PropTypes.shape({
- amount: PropTypes.shape({
- native: PropTypes.string,
- decimal: PropTypes.string,
- }),
- currency: PropTypes.shape({
- symbol: PropTypes.string,
- }),
- }),
- }),
- }),
-};
diff --git a/ui/components/app/nft-details/nft-details.test.js b/ui/components/app/nft-details/nft-details.test.js
index 91eea81ff634..e2504179e60e 100644
--- a/ui/components/app/nft-details/nft-details.test.js
+++ b/ui/components/app/nft-details/nft-details.test.js
@@ -78,7 +78,7 @@ describe('NFT Details', () => {
mockStore,
);
- const backButton = queryByTestId('asset__back');
+ const backButton = queryByTestId('nft__back');
fireEvent.click(backButton);
@@ -141,8 +141,12 @@ describe('NFT Details', () => {
});
it('should navigate to draft transaction send route with ERC721 data', async () => {
+ const nftProps = {
+ nft: nfts[5],
+ };
+ nfts[5].isCurrentlyOwned = true;
const { queryByTestId } = renderWithProvider(
- ,
+ ,
mockStore,
);
@@ -152,7 +156,7 @@ describe('NFT Details', () => {
await waitFor(() => {
expect(startNewDraftTransaction).toHaveBeenCalledWith({
type: AssetType.NFT,
- details: nfts[5],
+ details: { ...nfts[5], tokenId: 1 },
});
expect(mockHistoryPush).toHaveBeenCalledWith(SEND_ROUTE);
@@ -178,6 +182,7 @@ describe('NFT Details', () => {
const nftProps = {
nft: nfts[1],
};
+ nfts[1].isCurrentlyOwned = true;
const { queryByTestId } = renderWithProvider(
,
mockStore,
diff --git a/ui/components/app/nft-details/nft-details.tsx b/ui/components/app/nft-details/nft-details.tsx
new file mode 100644
index 000000000000..ca6a77cddbc0
--- /dev/null
+++ b/ui/components/app/nft-details/nft-details.tsx
@@ -0,0 +1,873 @@
+import React, { useEffect, useContext } from 'react';
+import PropTypes from 'prop-types';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import { isEqual } from 'lodash';
+import { getTokenTrackerLink, getAccountLink } from '@metamask/etherscan-link';
+import { Nft } from '@metamask/assets-controllers';
+import {
+ TextColor,
+ IconColor,
+ TextVariant,
+ FontWeight,
+ JustifyContent,
+ Display,
+ FlexWrap,
+ FontStyle,
+ TextAlign,
+ AlignItems,
+} from '../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { getAssetImageURL, shortenAddress } from '../../../helpers/utils/util';
+import { getNftImageAlt } from '../../../helpers/utils/nfts';
+import {
+ getCurrentChainId,
+ getCurrentCurrency,
+ getCurrentNetwork,
+ getIpfsGateway,
+} from '../../../selectors';
+import {
+ ASSET_ROUTE,
+ DEFAULT_ROUTE,
+ SEND_ROUTE,
+} from '../../../helpers/constants/routes';
+import {
+ checkAndUpdateSingleNftOwnershipStatus,
+ removeAndIgnoreNft,
+ setRemoveNftMessage,
+ setNewNftAddedMessage,
+} from '../../../store/actions';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import NftOptions from '../nft-options/nft-options';
+import { startNewDraftTransaction } from '../../../ducks/send';
+import InfoTooltip from '../../ui/info-tooltip';
+import { usePrevious } from '../../../hooks/usePrevious';
+import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
+import {
+ AssetType,
+ TokenStandard,
+} from '../../../../shared/constants/transaction';
+import {
+ ButtonIcon,
+ IconName,
+ Text,
+ Box,
+ ButtonIconSize,
+ ButtonPrimarySize,
+ ButtonPrimary,
+ Icon,
+} from '../../component-library';
+import { NftItem } from '../../multichain/nft-item';
+import {
+ MetaMetricsEventName,
+ MetaMetricsEventCategory,
+} from '../../../../shared/constants/metametrics';
+import { MetaMetricsContext } from '../../../contexts/metametrics';
+import { Content, Footer, Page } from '../../multichain/pages/page';
+import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
+import { getShortDateFormatterV2 } from '../../../pages/asset/util';
+import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps';
+import { getConversionRate } from '../../../ducks/metamask/metamask';
+import { Numeric } from '../../../../shared/modules/Numeric';
+import NftDetailInformationRow from './nft-detail-information-row';
+import NftDetailInformationFrame from './nft-detail-information-frame';
+import NftDetailDescription from './nft-detail-description';
+
+export default function NftDetails({ nft }: { nft: Nft }) {
+ const {
+ image,
+ imageOriginal,
+ name,
+ description,
+ address,
+ tokenId,
+ standard,
+ isCurrentlyOwned,
+ lastSale,
+ collection,
+ rarityRank,
+ topBid,
+ attributes,
+ } = nft;
+
+ const t = useI18nContext();
+ const history = useHistory();
+ const dispatch = useDispatch();
+ const ipfsGateway = useSelector(getIpfsGateway);
+ const currentNetwork = useSelector(getCurrentChainId);
+ const currentChain = useSelector(getCurrentNetwork);
+ const trackEvent = useContext(MetaMetricsContext);
+ const currency = useSelector(getCurrentCurrency);
+ const selectedNativeConversionRate = useSelector(getConversionRate);
+
+ const [addressCopied, handleAddressCopy] = useCopyToClipboard();
+
+ const nftImageAlt = getNftImageAlt(nft);
+ const nftSrcUrl = imageOriginal ?? image;
+ const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway);
+ const isIpfsURL = nftSrcUrl?.startsWith('ipfs:');
+ const isImageHosted = image?.startsWith('https:');
+
+ const hasFloorAskPrice = Boolean(collection?.floorAsk?.price?.amount?.usd);
+ const hasLastSalePrice = Boolean(lastSale?.price?.amount?.usd);
+
+ const getFloorAskSource = () => {
+ if (hasFloorAskPrice && Boolean(collection?.floorAsk?.source?.url)) {
+ return collection?.floorAsk?.source?.url;
+ }
+ return undefined;
+ };
+
+ const getCurrentHighestBidValue = () => {
+ if (
+ topBid?.price?.amount?.native &&
+ collection?.topBid?.price?.amount?.native
+ ) {
+ // return the max between collection top Bid and token topBid
+ const topBidValue = Math.max(
+ topBid?.price?.amount?.native,
+ collection?.topBid?.price?.amount?.native,
+ );
+ const currentChainSymbol = currentChain.ticker;
+ return `${topBidValue}${currentChainSymbol}`;
+ }
+ // return the one that is available
+ const topBidValue =
+ topBid?.price?.amount?.native ||
+ collection?.topBid?.price?.amount?.native;
+ if (!topBidValue) {
+ return undefined;
+ }
+ const currentChainSymbol = currentChain.ticker;
+ return `${topBidValue}${currentChainSymbol}`;
+ };
+
+ const getTopBidSourceDomain = () => {
+ return (
+ topBid?.source?.url ||
+ (collection?.topBid?.sourceDomain
+ ? `https://${collection.topBid?.sourceDomain}`
+ : undefined)
+ );
+ };
+
+ const { chainId } = currentChain;
+ useEffect(() => {
+ trackEvent({
+ event: MetaMetricsEventName.NftDetailsOpened,
+ category: MetaMetricsEventCategory.Tokens,
+ properties: {
+ chain_id: chainId,
+ },
+ });
+ }, [trackEvent, chainId]);
+
+ const onRemove = async () => {
+ let isSuccessfulEvent = false;
+ try {
+ await dispatch(removeAndIgnoreNft(address, tokenId));
+ dispatch(setNewNftAddedMessage(''));
+ dispatch(setRemoveNftMessage('success'));
+ isSuccessfulEvent = true;
+ } catch (err) {
+ dispatch(setNewNftAddedMessage(''));
+ dispatch(setRemoveNftMessage('error'));
+ } finally {
+ // track event
+ trackEvent({
+ event: MetaMetricsEventName.NFTRemoved,
+ category: 'Wallet',
+ properties: {
+ token_contract_address: address,
+ tokenId: tokenId.toString(),
+ asset_type: AssetType.NFT,
+ token_standard: standard,
+ chain_id: currentNetwork,
+ isSuccessful: isSuccessfulEvent,
+ },
+ });
+ history.push(DEFAULT_ROUTE);
+ }
+ };
+
+ const prevNft = usePrevious(nft);
+ useEffect(() => {
+ if (!isEqual(prevNft, nft)) {
+ checkAndUpdateSingleNftOwnershipStatus(nft);
+ }
+ }, [nft, prevNft]);
+
+ const getOpenSeaLink = () => {
+ switch (currentNetwork) {
+ case CHAIN_IDS.MAINNET:
+ return `https://opensea.io/assets/ethereum/${address}/${tokenId}`;
+ case CHAIN_IDS.POLYGON:
+ return `https://opensea.io/assets/matic/${address}/${tokenId}`;
+ case CHAIN_IDS.GOERLI:
+ return `https://testnets.opensea.io/assets/goerli/${address}/${tokenId}`;
+ case CHAIN_IDS.SEPOLIA:
+ return `https://testnets.opensea.io/assets/sepolia/${address}/${tokenId}`;
+ default:
+ return null;
+ }
+ };
+
+ const openSeaLink = getOpenSeaLink();
+ const sendDisabled =
+ standard !== TokenStandard.ERC721 && standard !== TokenStandard.ERC1155;
+
+ const onSend = async () => {
+ await dispatch(
+ startNewDraftTransaction({
+ type: AssetType.NFT,
+ details: {
+ ...nft,
+ tokenId: Number(nft.tokenId),
+ image: nft.image ?? undefined,
+ },
+ }),
+ );
+ // We only allow sending one NFT at a time
+ history.push(SEND_ROUTE);
+ };
+
+ const getDateCreatedTimestamp = (dateString: string) => {
+ const date = new Date(dateString);
+ return Math.floor(date.getTime() / 1000);
+ };
+
+ const getFormattedDate = (dateString: number) => {
+ const date = new Date(dateString * 1000).getTime();
+ return getShortDateFormatterV2().format(date);
+ };
+
+ const hasPriceSection = getCurrentHighestBidValue() || lastSale?.timestamp;
+ const hasCollectionSection =
+ collection?.name || collection?.tokenCount || collection?.creator;
+ const hasAttributesSection = attributes && attributes?.length !== 0;
+
+ const blockExplorerTokenLink = (tokenAddress: string) => {
+ return getTokenTrackerLink(
+ tokenAddress,
+ chainId,
+ null as unknown as string, // no networkId
+ null as unknown as string, // no holderAddress
+ {
+ blockExplorerUrl:
+ SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null,
+ },
+ );
+ };
+
+ const handleImageClick = () => {
+ return history.push(`${ASSET_ROUTE}/image/${address}/${tokenId}`);
+ };
+
+ const getValueInFormattedCurrency = (
+ nativeValue: number,
+ usdValue: number,
+ ) => {
+ const numericVal = new Numeric(nativeValue, 16);
+ // if current currency is usd or if fetching conversion rate failed then always return USD value
+ if (!selectedNativeConversionRate || currency === 'usd') {
+ const usdValueFormatted = formatCurrency(usdValue.toString(), 'usd');
+ return usdValueFormatted;
+ }
+
+ const value = numericVal
+ .applyConversionRate(selectedNativeConversionRate)
+ .toNumber();
+
+ return formatCurrency(new Numeric(value, 10).toString(), currency);
+ };
+
+ return (
+
+
+
+ history.push(DEFAULT_ROUTE)}
+ data-testid="nft__back"
+ />
+ global.platform.openTab({ url: openSeaLink })
+ : null
+ }
+ onRemove={onRemove}
+ />
+
+
+
+
+
+
+
+ {name || collection?.name ? (
+
+
+ {name || collection?.name}
+
+ {collection?.openseaVerificationStatus === 'verified' ? (
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+ {hasLastSalePrice || hasFloorAskPrice ? (
+ <>
+ {
+ global.platform.openTab({
+ url: lastSale?.orderSource as string,
+ });
+ }}
+ iconName={IconName.Export}
+ ariaLabel="redirect"
+ />
+ ) : undefined
+ }
+ />
+ {
+ global.platform.openTab({
+ url: collection?.floorAsk?.source?.url as string,
+ });
+ }}
+ iconName={IconName.Export}
+ ariaLabel="redirect"
+ />
+ ) : undefined
+ }
+ />
+ >
+ ) : null}
+
+ {rarityRank ? (
+
+ ) : null}
+
+ {
+ global.platform.openTab({
+ url: blockExplorerTokenLink(address),
+ });
+ }}
+ >
+
+ {shortenAddress(address)}
+
+
+ }
+ icon={
+ {
+ (handleAddressCopy as (text: string) => void)?.(
+ address || '',
+ );
+ }}
+ iconName={
+ addressCopied ? IconName.CopySuccess : IconName.Copy
+ }
+ />
+ }
+ />
+
+
+
+
+
+
+ {hasPriceSection ? (
+
+
+ {t('price')}
+
+
+ ) : null}
+ {
+ global.platform.openTab({
+ url: lastSale?.orderSource as string,
+ });
+ }}
+ iconName={IconName.Export}
+ justifyContent={JustifyContent.flexEnd}
+ ariaLabel="export"
+ />
+ ) : undefined
+ }
+ />
+ {
+ global.platform.openTab({
+ url: getTopBidSourceDomain() as string, // Adding cast here because verification has been done on line 594
+ });
+ }}
+ iconName={IconName.Export}
+ justifyContent={JustifyContent.flexEnd}
+ ariaLabel="redirect"
+ />
+ ) : undefined
+ }
+ />
+ {hasCollectionSection ? (
+
+
+ {t('notificationItemCollection')}
+
+
+ ) : null}
+
+
+ {
+ global.platform.openTab({
+ url: getAccountLink(
+ collection?.creator as string,
+ chainId,
+ ),
+ });
+ }}
+ >
+
+ {shortenAddress(collection?.creator)}
+
+
+ ) : null
+ }
+ valueColor={TextColor.primaryDefault}
+ icon={
+ {
+ (handleAddressCopy as (text: string) => void)?.(
+ collection?.creator || '',
+ );
+ }}
+ iconName={addressCopied ? IconName.CopySuccess : IconName.Copy}
+ justifyContent={JustifyContent.flexEnd}
+ />
+ }
+ />
+ {hasAttributesSection ? (
+
+
+ {t('attributes')}
+
+
+ ) : null}
+
+ {' '}
+ {attributes?.map((elm, idx) => {
+ const { key, value } = elm;
+ return (
+
+ );
+ })}
+
+
+
+ {t('nftDisclaimer')}
+
+
+
+
+ {isCurrentlyOwned === true ? (
+
+ ) : null}
+
+ );
+}
+
+NftDetails.propTypes = {
+ nft: PropTypes.shape({
+ address: PropTypes.string.isRequired,
+ tokenId: PropTypes.string.isRequired,
+ isCurrentlyOwned: PropTypes.bool,
+ name: PropTypes.string,
+ description: PropTypes.string,
+ image: PropTypes.string,
+ standard: PropTypes.string,
+ imageThumbnail: PropTypes.string,
+ imagePreview: PropTypes.string,
+ imageOriginal: PropTypes.string,
+ rarityRank: PropTypes.string,
+
+ creator: PropTypes.shape({
+ address: PropTypes.string,
+ config: PropTypes.string,
+ profile_img_url: PropTypes.string,
+ }),
+ attributes: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string,
+ value: PropTypes.string,
+ }),
+ ),
+ lastSale: PropTypes.shape({
+ timestamp: PropTypes.string,
+ orderSource: PropTypes.string,
+ price: PropTypes.shape({
+ amount: PropTypes.shape({
+ native: PropTypes.string,
+ decimal: PropTypes.string,
+ usd: PropTypes.string,
+ }),
+ currency: PropTypes.shape({
+ symbol: PropTypes.string,
+ }),
+ }),
+ }),
+ topBid: PropTypes.shape({
+ source: PropTypes.shape({
+ id: PropTypes.string,
+ domain: PropTypes.string,
+ name: PropTypes.string,
+ icon: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ price: PropTypes.shape({
+ amount: PropTypes.shape({
+ native: PropTypes.string,
+ decimal: PropTypes.string,
+ usd: PropTypes.string,
+ }),
+ currency: PropTypes.shape({
+ symbol: PropTypes.string,
+ }),
+ }),
+ }),
+ collection: PropTypes.shape({
+ openseaVerificationStatus: PropTypes.string,
+ tokenCount: PropTypes.string,
+ name: PropTypes.string,
+ ownerCount: PropTypes.string,
+ creator: PropTypes.string,
+ symbol: PropTypes.string,
+ contractDeployedAt: PropTypes.string,
+ floorAsk: PropTypes.shape({
+ sourceDomain: PropTypes.string,
+ source: PropTypes.shape({
+ id: PropTypes.string,
+ domain: PropTypes.string,
+ name: PropTypes.string,
+ icon: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ price: PropTypes.shape({
+ amount: PropTypes.shape({
+ native: PropTypes.string,
+ decimal: PropTypes.string,
+ usd: PropTypes.string,
+ }),
+ currency: PropTypes.shape({
+ symbol: PropTypes.string,
+ }),
+ }),
+ }),
+ topBid: PropTypes.shape({
+ sourceDomain: PropTypes.string,
+ price: PropTypes.shape({
+ amount: PropTypes.shape({
+ native: PropTypes.string,
+ decimal: PropTypes.string,
+ usd: PropTypes.string,
+ }),
+ currency: PropTypes.shape({
+ symbol: PropTypes.string,
+ }),
+ }),
+ }),
+ }),
+ }),
+};
diff --git a/ui/components/app/nft-details/nft-full-image.tsx b/ui/components/app/nft-details/nft-full-image.tsx
new file mode 100644
index 000000000000..8565150f7602
--- /dev/null
+++ b/ui/components/app/nft-details/nft-full-image.tsx
@@ -0,0 +1,96 @@
+import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useHistory, useParams } from 'react-router-dom';
+import { getAssetImageURL } from '../../../helpers/utils/util';
+import { getNftImageAlt } from '../../../helpers/utils/nfts';
+import { getCurrentNetwork, getIpfsGateway } from '../../../selectors';
+
+import {
+ Box,
+ ButtonIcon,
+ ButtonIconSize,
+ IconName,
+} from '../../component-library';
+import { NftItem } from '../../multichain/nft-item';
+import { Content, Header, Page } from '../../multichain/pages/page';
+
+import { getNfts } from '../../../ducks/metamask/metamask';
+import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
+import {
+ Display,
+ IconColor,
+ JustifyContent,
+} from '../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { ASSET_ROUTE } from '../../../helpers/constants/routes';
+
+export default function NftFullImage() {
+ const t = useI18nContext();
+ const { asset, id } = useParams<{ asset: string; id: string }>();
+ const nfts = useSelector(getNfts);
+ const nft = nfts.find(
+ ({ address, tokenId }: { address: string; tokenId: string }) =>
+ isEqualCaseInsensitive(address, asset) && id === tokenId.toString(),
+ );
+
+ const { image, imageOriginal, name, tokenId } = nft;
+
+ const ipfsGateway = useSelector(getIpfsGateway);
+ const currentChain = useSelector(getCurrentNetwork);
+
+ const nftImageAlt = getNftImageAlt(nft);
+ const nftSrcUrl = imageOriginal ?? image;
+ const nftImageURL = getAssetImageURL(imageOriginal ?? image, ipfsGateway);
+ const isIpfsURL = nftSrcUrl?.startsWith('ipfs:');
+ const isImageHosted = image?.startsWith('https:');
+ const history = useHistory();
+
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ setVisible(true);
+ }, []);
+
+ return (
+
+
+ history.push(`${ASSET_ROUTE}/${asset}/${id}`)}
+ data-testid="nft-details__close"
+ paddingLeft={0}
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/components/app/nft-options/nft-options.js b/ui/components/app/nft-options/nft-options.js
index 0343667d3ec2..c9ff893f710f 100644
--- a/ui/components/app/nft-options/nft-options.js
+++ b/ui/components/app/nft-options/nft-options.js
@@ -15,7 +15,6 @@ const NftOptions = ({ onRemove, onViewOnOpensea }) => {
setNftOptionsOpen(true)}
color={Color.textDefault}
diff --git a/ui/components/app/snaps/show-more/show-more.js b/ui/components/app/snaps/show-more/show-more.js
index bc7b787f4f10..d6939b9f546f 100644
--- a/ui/components/app/snaps/show-more/show-more.js
+++ b/ui/components/app/snaps/show-more/show-more.js
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
+import classnames from 'classnames';
import useIsOverflowing from '../../../../hooks/snaps/useIsOverflowing';
import { Box, Button, ButtonVariant, Text } from '../../../component-library';
import {
@@ -9,7 +10,7 @@ import {
} from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
-export const ShowMore = ({ children, ...props }) => {
+export const ShowMore = ({ children, className = '', ...props }) => {
const t = useI18nContext();
const { contentRef, isOverflowing } = useIsOverflowing();
const [isOpen, setIsOpen] = useState(false);
@@ -23,7 +24,7 @@ export const ShowMore = ({ children, ...props }) => {
return (
{
ShowMore.propTypes = {
children: PropTypes.node,
buttonBackground: PropTypes.string,
+ className: PropTypes.string,
};
diff --git a/ui/components/multichain/nft-item/index.scss b/ui/components/multichain/nft-item/index.scss
index 0ca5fded8d09..e331c6653131 100644
--- a/ui/components/multichain/nft-item/index.scss
+++ b/ui/components/multichain/nft-item/index.scss
@@ -1,7 +1,6 @@
.nft-item {
&__container {
width: 100%;
- height: 100%;
padding: 0;
border-radius: 8px;
cursor: unset;
diff --git a/ui/components/multichain/nft-item/nft-item.js b/ui/components/multichain/nft-item/nft-item.js
index d955db8a32f3..d8df3e26a9cb 100644
--- a/ui/components/multichain/nft-item/nft-item.js
+++ b/ui/components/multichain/nft-item/nft-item.js
@@ -31,6 +31,7 @@ export const NftItem = ({
onClick,
clickable,
isIpfsURL,
+ badgeWrapperClassname,
}) => {
const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor);
const isIpfsEnabled = useSelector(getIpfsGateway);
@@ -68,9 +69,13 @@ export const NftItem = ({
onClick={onClick}
>
3 && day < 21) {
+ return 'th';
+ } // because 11th, 12th, 13th
+ switch (day % 10) {
+ case 1:
+ return 'st';
+ case 2:
+ return 'nd';
+ case 3:
+ return 'rd';
+ default:
+ return 'th';
+ }
+}
/**
* Determines if the provided chainId is a default MetaMask chain
*
diff --git a/ui/pages/asset/components/asset-breadcrumb.js b/ui/pages/asset/components/asset-breadcrumb.js
index 42218ef43a9c..710842a2e5da 100644
--- a/ui/pages/asset/components/asset-breadcrumb.js
+++ b/ui/pages/asset/components/asset-breadcrumb.js
@@ -16,7 +16,6 @@ const AssetBreadcrumb = ({ accountName, assetName, onBack }) => {
size={IconSize.Xs}
/>
{accountName}
- /
{assetName}
);
diff --git a/ui/pages/asset/util.ts b/ui/pages/asset/util.ts
index 0f99d8b585b4..824040fa6560 100644
--- a/ui/pages/asset/util.ts
+++ b/ui/pages/asset/util.ts
@@ -9,6 +9,14 @@ export const getShortDateFormatter = () =>
minute: 'numeric',
});
+/** Formats a datetime in a short human readable format like 'Feb 8, 2030' */
+export const getShortDateFormatterV2 = () =>
+ Intl.DateTimeFormat(navigator.language, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+
/**
* Formats a potentially large number to the nearest unit.
* e.g. 1T for trillions, 2.3B for billions, 4.56M for millions, 7,890 for thousands, etc.
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js
index 510d8322244c..5b1a2194255f 100644
--- a/ui/pages/routes/routes.component.js
+++ b/ui/pages/routes/routes.component.js
@@ -132,6 +132,7 @@ import {
import { MILLISECOND, SECOND } from '../../../shared/constants/time';
import { MultichainMetaFoxLogo } from '../../components/multichain/app-header/multichain-meta-fox-logo';
import NetworkConfirmationPopover from '../../components/multichain/network-list-menu/network-confirmation-popover/network-confirmation-popover';
+import NftFullImage from '../../components/app/nft-details/nft-full-image';
const isConfirmTransactionRoute = (pathname) =>
Boolean(
@@ -419,6 +420,11 @@ export default class Routes extends Component {
path={`${CONNECT_ROUTE}/:id`}
component={PermissionsConnect}
/>
+
+