import React, { useEffect } 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 Card from '../../ui/card'; import { TextColor, IconColor, TextVariant, FontWeight, JustifyContent, FLEX_DIRECTION, OverflowWrap, DISPLAY, BLOCK_SIZES, } 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, getIpfsGateway, getSelectedIdentity, } 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 NftDefaultImage from '../nft-default-image'; import { ButtonIcon, IconName, Text } from '../../component-library'; import Tooltip from '../../ui/tooltip'; import { decWEIToDecETH } from '../../../../shared/modules/conversion.utils'; export default function NftDetails({ nft }) { const { image, imageOriginal, name, description, address, tokenId, standard, isCurrentlyOwned, lastSale, imageThumbnail, } = nft; const t = useI18nContext(); const history = useHistory(); const dispatch = useDispatch(); const ipfsGateway = useSelector(getIpfsGateway); const nftContracts = useSelector(getNftContracts); const currentNetwork = useSelector(getCurrentChainId); const [addressCopied, handleAddressCopy] = useCopyToClipboard(); const nftContractName = nftContracts.find(({ address: contractAddress }) => isEqualCaseInsensitive(contractAddress, address), )?.name; const selectedAccountName = useSelector( (state) => getSelectedIdentity(state).name, ); 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', ); const onRemove = () => { dispatch(removeAndIgnoreNft(address, tokenId)); dispatch(setNewNftAddedMessage('')); dispatch(setRemoveNftMessage('success')); 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/${address}/${tokenId}`; case CHAIN_IDS.POLYGON: return `https://opensea.io/assets/matic/${address}/${tokenId}`; case CHAIN_IDS.GOERLI: case CHAIN_IDS.SEPOLIA: return `https://testnets.opensea.io/assets/${address}/${tokenId}`; default: return null; } }; const openSeaLink = getOpenSeaLink(); const sendDisabled = standard !== TokenStandard.ERC721; const inPopUp = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; const onSend = async () => { await dispatch( startNewDraftTransaction({ type: AssetType.NFT, details: nft, }), ); history.push(SEND_ROUTE); }; const renderSendButton = () => { if (isCurrentlyOwned === false) { return <div style={{ height: '30px' }} />; } return ( <Box display={DISPLAY.FLEX} width={inPopUp ? BLOCK_SIZES.FULL : BLOCK_SIZES.HALF} margin={inPopUp ? [4, 0] : null} > <Button type="primary" onClick={onSend} disabled={sendDisabled} className="nft-details__send-button" data-testid="nft-send-button" > {t('send')} </Button> {sendDisabled ? ( <InfoTooltip position="top" contentText={t('sendingDisabled')} /> ) : null} </Box> ); }; return ( <> <AssetNavigation accountName={selectedAccountName} assetName={nftContractName} onBack={() => history.push(DEFAULT_ROUTE)} optionsButton={ <NftOptions onViewOnOpensea={ openSeaLink ? () => global.platform.openTab({ url: openSeaLink }) : null } onRemove={onRemove} /> } /> <Box className="nft-details"> <div className="nft-details__top-section"> <Card padding={0} justifyContent={JustifyContent.center} className="nft-details__card" > {image ? ( <img className="nft-details__image" src={nftImageURL} alt={nftImageAlt} /> ) : ( <NftDefaultImage name={name} tokenId={tokenId} /> )} </Card> <Box flexDirection={FLEX_DIRECTION.COLUMN} className="nft-details__info" justifyContent={JustifyContent.spaceBetween} > <div> <Text color={TextColor.textDefault} variant={TextVariant.headingSm} as="h4" fontWeight={FontWeight.Bold} marginBottom={2} > {name} </Text> <Text color={TextColor.textMuted} variant={TextVariant.bodyMd} as="h5" marginBottom={4} overflowWrap={OverflowWrap.BreakWord} > #{tokenId} </Text> </div> {description ? ( <div> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={2} className="nft-details__description" > {t('description')} </Text> <Text color={TextColor.textAlternative} variant={TextVariant.bodySm} as="h6" overflowWrap={OverflowWrap.BreakWord} marginBottom={4} > {description} </Text> </div> ) : null} {inPopUp ? null : renderSendButton()} </Box> </div> <Box marginBottom={2}> {lastSale ? ( <> <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={4} marginRight={2} className="nft-details__link-title" > {t('lastSold')} </Text> <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} className="nft-details__contract-wrapper" > <Text color={TextColor.textAlternative} variant={TextVariant.bodySm} as="h6" overflowWrap={OverflowWrap.BreakWord} marginBottom={4} > {formattedTimestamp} </Text> </Box> </Box> <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={4} marginRight={2} className="nft-details__link-title" > {t('lastPriceSold')} </Text> <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} className="nft-details__contract-wrapper" > <Text color={TextColor.textAlternative} variant={TextVariant.bodySm} as="h6" overflowWrap={OverflowWrap.BreakWord} marginBottom={4} > {`${Number(decWEIToDecETH(lastSale.total_price))} ${ lastSale.payment_token.symbol }`} </Text> </Box> </Box> </> ) : null} <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={4} marginRight={2} className="nft-details__link-title" > {t('source')} </Text> <Text variant={TextVariant.bodySm} as="h6" marginBottom={4} className="nft-details__image-source" color={ isDataURI ? TextColor.textDefault : TextColor.primaryDefault } > {isDataURI ? ( <>{nftImageURL}</> ) : ( <a target="_blank" rel="noopener noreferrer" href={nftImageURL} title={nftImageURL} > {nftImageURL} </a> )} </Text> </Box> {imageThumbnail ? ( <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={4} marginRight={2} className="nft-details__link-title" > {t('link')} </Text> <Text variant={TextVariant.bodySm} as="h6" marginBottom={4} className="nft-details__image-source" color={ isDataURI ? TextColor.textDefault : TextColor.primaryDefault } > <a target="_blank" rel="noopener noreferrer" href={nftImageURL} title={nftImageURL} > {imageThumbnail} </a> </Text> </Box> ) : null} <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}> <Text color={TextColor.textDefault} variant={TextVariant.bodySmBold} as="h6" marginBottom={4} marginRight={2} className="nft-details__link-title" > {t('contractAddress')} </Text> <Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} className="nft-details__contract-wrapper" > <Text color={TextColor.textAlternative} variant={TextVariant.bodySm} as="h6" overflowWrap={OverflowWrap.BreakWord} marginBottom={4} > {shortenAddress(address)} </Text> <Tooltip wrapperClassName="nft-details__tooltip-wrapper" position="bottom" title={ addressCopied ? t('copiedExclamation') : t('copyToClipboard') } > <ButtonIcon ariaLabel="copy" color={IconColor.iconAlternative} className="nft-details__contract-copy-button" data-testid="nft-address-copy" onClick={() => { handleAddressCopy(address); }} iconName={ addressCopied ? IconName.CopySuccess : IconName.Copy } /> </Tooltip> </Box> </Box> {inPopUp ? renderSendButton() : null} <Text color={TextColor.textAlternative} variant={TextVariant.bodySm} as="h6" > {t('nftDisclaimer')} </Text> </Box> </Box> </> ); } 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({ event_timestamp: PropTypes.string, total_price: PropTypes.string, payment_token: PropTypes.shape({ symbol: PropTypes.string, }), }), }), };