diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.stories.js b/ui/components/app/assets/nfts/nft-details/nft-details.stories.js index 76554d40d43e..a20eadb7b303 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.stories.js +++ b/ui/components/app/assets/nfts/nft-details/nft-details.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import NftDetails from './nft-details'; +import { NftDetailsComponent } from './nft-details'; const nft = { name: 'Catnip Spicywright', @@ -18,6 +18,8 @@ const nft = { }, }; +const nftChainId = '0x1'; + export default { title: 'Components/App/NftsDetail', @@ -28,17 +30,18 @@ export default { }, args: { nft, + nftChainId, }, }; export const DefaultStory = (args) => { - return ; + return ; }; DefaultStory.storyName = 'Default'; export const NoImage = (args) => { - return ; + return ; }; NoImage.args = { diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.test.js b/ui/components/app/assets/nfts/nft-details/nft-details.test.js index e378fb4bb2bf..c93a294ab5a3 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.test.js +++ b/ui/components/app/assets/nfts/nft-details/nft-details.test.js @@ -1,5 +1,6 @@ import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; +import { useParams } from 'react-router-dom'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import copyToClipboard from 'copy-to-clipboard'; @@ -39,6 +40,7 @@ jest.mock('react-router-dom', () => ({ useHistory: () => ({ push: mockHistoryPush, }), + useParams: jest.fn(), })); jest.mock('../../../../../ducks/send/index.js', () => ({ @@ -72,6 +74,7 @@ describe('NFT Details', () => { }); it('should match minimal props and state snapshot', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.GOERLI }); getAssetImageURL.mockResolvedValue( 'https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link', ); @@ -88,6 +91,7 @@ describe('NFT Details', () => { }); it(`should route to '/' route when the back button is clicked`, () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const { queryByTestId } = renderWithProvider( , mockStore, @@ -101,6 +105,7 @@ describe('NFT Details', () => { }); it(`should call removeAndIgnoreNFT with proper nft details and route to '/' when removing nft`, async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const { queryByTestId } = renderWithProvider( , mockStore, @@ -122,6 +127,7 @@ describe('NFT Details', () => { }); it(`should call setRemoveNftMessage with error when removeAndIgnoreNft fails and route to '/'`, async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const { queryByTestId } = renderWithProvider( , mockStore, @@ -145,6 +151,7 @@ describe('NFT Details', () => { }); it('should copy nft address', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const { queryByTestId } = renderWithProvider( , mockStore, @@ -157,6 +164,7 @@ describe('NFT Details', () => { }); it('should navigate to draft transaction send route with ERC721 data', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const nftProps = { nft: nfts[5], }; @@ -180,6 +188,7 @@ describe('NFT Details', () => { }); it('should not render send button if isCurrentlyOwned is false', () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const sixthNftProps = { nft: nfts[6], }; @@ -195,6 +204,7 @@ describe('NFT Details', () => { }); it('should render send button if it is an ERC1155', () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); const nftProps = { nft: nfts[1], }; @@ -211,6 +221,7 @@ describe('NFT Details', () => { describe(`Alternative Networks' OpenSea Links`, () => { it('should open opeasea link with goeli testnet chainId', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.GOERLI }); global.platform = { openTab: jest.fn() }; const { queryByTestId } = renderWithProvider( @@ -234,6 +245,7 @@ describe('NFT Details', () => { }); it('should open tab to mainnet opensea url with nft info', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.MAINNET }); global.platform = { openTab: jest.fn() }; const mainnetState = { @@ -266,11 +278,15 @@ describe('NFT Details', () => { }); it('should open tab to polygon opensea url with nft info', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.POLYGON }); const polygonState = { ...mockState, metamask: { ...mockState.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), + ...mockNetworkState({ + chainId: CHAIN_IDS.POLYGON, + nickname: 'polygon', + }), }, }; const polygonMockStore = configureMockStore([thunk])(polygonState); @@ -296,11 +312,15 @@ describe('NFT Details', () => { }); it('should open tab to sepolia opensea url with nft info', async () => { + useParams.mockReturnValue({ chainId: CHAIN_IDS.SEPOLIA }); const sepoliaState = { ...mockState, metamask: { ...mockState.metamask, - ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), + ...mockNetworkState({ + chainId: CHAIN_IDS.SEPOLIA, + nickname: 'sepolia', + }), }, }; const sepoliaMockStore = configureMockStore([thunk])(sepoliaState); @@ -326,6 +346,7 @@ describe('NFT Details', () => { }); it('should not render opensea redirect button', async () => { + useParams.mockReturnValue({ chainId: '0x99' }); const randomNetworkState = { ...mockState, metamask: { diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.tsx b/ui/components/app/assets/nfts/nft-details/nft-details.tsx index a0de608a0e56..28e7ea9f0f95 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-details.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useParams } from 'react-router-dom'; import { isEqual } from 'lodash'; import { getTokenTrackerLink, getAccountLink } from '@metamask/etherscan-link'; import { Nft } from '@metamask/assets-controllers'; +import { Hex } from '@metamask/utils'; import { TextColor, IconColor, @@ -19,8 +20,15 @@ import { import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { shortenAddress } from '../../../../../helpers/utils/util'; import { getNftImageAlt } from '../../../../../helpers/utils/nfts'; -import { getCurrentChainId } from '../../../../../../shared/modules/selectors/networks'; -import { getCurrentNetwork, getIpfsGateway } from '../../../../../selectors'; +import { + getCurrentChainId, + getNetworkConfigurationsByChainId, +} from '../../../../../../shared/modules/selectors/networks'; +import { + getCurrentNetwork, + getIpfsGateway, + getNetworkConfigurationIdByChainId, +} from '../../../../../selectors'; import { ASSET_ROUTE, DEFAULT_ROUTE, @@ -31,6 +39,8 @@ import { removeAndIgnoreNft, setRemoveNftMessage, setNewNftAddedMessage, + setActiveNetworkWithError, + setSwitchedNetworkDetails, } from '../../../../../store/actions'; import { CHAIN_IDS } from '../../../../../../shared/constants/network'; import NftOptions from '../nft-options/nft-options'; @@ -71,13 +81,20 @@ import { Numeric } from '../../../../../../shared/modules/Numeric'; // eslint-disable-next-line import/no-restricted-paths import { addUrlProtocolPrefix } from '../../../../../../app/scripts/lib/util'; import useGetAssetImageUrl from '../../../../../hooks/useGetAssetImageUrl'; +import { getImageForChainId } from '../../../../../selectors/multichain'; import NftDetailInformationRow from './nft-detail-information-row'; import NftDetailInformationFrame from './nft-detail-information-frame'; import NftDetailDescription from './nft-detail-description'; const MAX_TOKEN_ID_LENGTH = 15; -export default function NftDetails({ nft }: { nft: Nft }) { +export function NftDetailsComponent({ + nft, + nftChainId, +}: { + nft: Nft; + nftChainId: string; +}) { const { image, imageOriginal, @@ -104,6 +121,14 @@ export default function NftDetails({ nft }: { nft: Nft }) { const currency = useSelector(getCurrentCurrency); const selectedNativeConversionRate = useSelector(getConversionRate); + const nftNetworkConfigs = useSelector(getNetworkConfigurationsByChainId); + const nftChainNetwork = nftNetworkConfigs[nftChainId as Hex]; + const nftChainImage = getImageForChainId(nftChainId as string); + const networks = useSelector(getNetworkConfigurationIdByChainId) as Record< + string, + string + >; + const [addressCopied, handleAddressCopy] = useCopyToClipboard(); const nftImageAlt = getNftImageAlt(nft); @@ -240,7 +265,28 @@ export default function NftDetails({ nft }: { nft: Nft }) { const sendDisabled = standard !== TokenStandard.ERC721 && standard !== TokenStandard.ERC1155; + const setCorrectChain = async () => { + // If we aren't presently on the chain of the nft, change to it + if (nftChainId !== currentChain.chainId) { + try { + const networkConfigurationId = networks[nftChainId as Hex]; + await dispatch(setActiveNetworkWithError(networkConfigurationId)); + await dispatch( + setSwitchedNetworkDetails({ + networkClientId: networkConfigurationId, + }), + ); + } catch (err) { + console.error(`Failed to switch chains for NFT. + Target chainId: ${nftChainId}, Current chainId: ${currentChain.chainId}. + ${err}`); + throw err; + } + } + }; + const onSend = async () => { + await setCorrectChain(); await dispatch( startNewDraftTransaction({ type: AssetType.NFT, @@ -350,8 +396,8 @@ export default function NftDetails({ nft }: { nft: Nft }) { ); } + +function NftDetails({ nft }: { nft: Nft }) { + const { chainId } = useParams(); + + return ; +} + +export default NftDetails;