From ae7404e4d447747ac30e1f16af02824df4c9f02d Mon Sep 17 00:00:00 2001 From: William Muli Date: Wed, 13 Apr 2022 19:04:46 +0300 Subject: [PATCH] Add NFT details page --- .../brave_wallet_ui/common/async/handlers.ts | 11 +- .../brave_wallet_ui/common/hooks/assets.ts | 24 ++- .../edit-visible-assets-modal/index.tsx | 2 +- .../desktop/portfolio-asset-item/index.tsx | 6 +- .../components/nft-details/index.tsx | 154 ++++++++++-------- .../portfolio/components/nft-details/style.ts | 46 +++++- .../desktop/views/portfolio/index.tsx | 16 +- .../shared/create-placeholder-icon/index.tsx | 7 +- components/brave_wallet_ui/constants/types.ts | 6 +- .../page/actions/wallet_page_actions.ts | 4 + .../page/async/wallet_page_async_handler.ts | 40 +++++ .../page/reducers/page_reducer.ts | 20 ++- .../brave_wallet_ui/utils/string-utils.ts | 16 +- 13 files changed, 255 insertions(+), 97 deletions(-) diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index 8e284559462f..c736134db2bf 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -288,7 +288,16 @@ handler.on(WalletActions.getAllTokensList.getType(), async (store) => { }) handler.on(WalletActions.addUserAsset.getType(), async (store: Store, payload: BraveWallet.BlockchainToken) => { - const { braveWalletService } = getAPIProxy() + const { braveWalletService, jsonRpcService } = getAPIProxy() + if (payload.isErc721) { + // Get NFTMetadata + const result = await jsonRpcService.getERC721Metadata(payload.contractAddress, payload.tokenId, payload.chainId) + if (!result.error) { + const response = JSON.parse(result.response) + payload.logo = response.image || payload.logo + } + } + const result = await braveWalletService.addUserAsset(payload) store.dispatch(WalletActions.addUserAssetError(!result.success)) }) diff --git a/components/brave_wallet_ui/common/hooks/assets.ts b/components/brave_wallet_ui/common/hooks/assets.ts index 911f7e3ef834..ab10fcfd1676 100644 --- a/components/brave_wallet_ui/common/hooks/assets.ts +++ b/components/brave_wallet_ui/common/hooks/assets.ts @@ -20,6 +20,25 @@ import usePricing from './pricing' import useBalance from './balance' import { useIsMounted } from './useIsMounted' import { useLib } from './useLib' +import { httpifyIpfsUrl } from '../../utils/string-utils' + +const assetsLogo = (assets: BraveWallet.BlockchainToken[]) => { + return assets.map(token => { + let logo = token.logo + if (token.logo?.startsWith('ipfs://')) { + logo = httpifyIpfsUrl(token.logo) + } else if (token.logo?.startsWith('data:image/')) { + logo = token.logo + } else { + logo = `chrome://erc-token-images/${token.logo}` + } + + return { + ...token, + logo + } as BraveWallet.BlockchainToken + }) +} export function useAssets () { // redux @@ -54,10 +73,7 @@ export function useAssets () { React.useEffect(() => { isMounted && getBuyAssets().then(tokens => { if (isMounted) { - setBuyAssetOptions(tokens.map(token => ({ - ...token, - logo: `chrome://erc-token-images/${token.logo}` - }) as BraveWallet.BlockchainToken)) + setBuyAssetOptions(assetsLogo(tokens)) } }).catch(e => console.error(e)) }, []) diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/edit-visible-assets-modal/index.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/edit-visible-assets-modal/index.tsx index 84ed852c8cbb..f1cb91234962 100644 --- a/components/brave_wallet_ui/components/desktop/popup-modals/edit-visible-assets-modal/index.tsx +++ b/components/brave_wallet_ui/components/desktop/popup-modals/edit-visible-assets-modal/index.tsx @@ -558,7 +558,7 @@ const EditVisibleAssetsModal = (props: Props) => { <> {filteredTokenList.slice(0, tokenDisplayAmount).map((token) => { }, [fiatBalance]) const isLoading = React.useMemo(() => { - return formattedAssetBalance === '' - }, [formattedAssetBalance]) + return formattedAssetBalance === '' && !token.isErc721 + }, [formattedAssetBalance, token]) const tokensNetwork = React.useMemo(() => { return getTokensNetwork(networks, token) @@ -111,7 +111,7 @@ const PortfolioAssetItem = (props: Props) => { {token.visible && // Selecting an erc721 token is temp disabled until UI is ready for viewing NFTs // or when showing loading skeleton - + {isLoading diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx index 46e10339424e..a3f542397940 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx @@ -12,17 +12,16 @@ import { useExplorer } from '../../../../../../common/hooks' // Utils import Amount from '../../../../../../utils/amount' -import { CurrencySymbols } from '../../../../../../utils/currency-symbols' -import { getLocale } from '../../../../../../../common/locale' +import { getLocale } from '$web-common/locale' // Styled Components import { StyledWrapper, + LoadIcon, + LoadingOverlay, NFTImage, DetailColumn, TokenName, - TokenFiatValue, - TokenCryptoValue, DetailSectionRow, DetailSectionColumn, DetailSectionTitle, @@ -30,7 +29,6 @@ import { ProjectDetailRow, ProjectDetailName, ProjectDetailDescription, - ProjectDetailImage, ProjectDetailButtonRow, ProjectDetailButton, ProjectDetailButtonSeperator, @@ -39,28 +37,32 @@ import { ProjectFacebookIcon, ProjectDetailIDRow, ExplorerIcon, - ExplorerButton + ExplorerButton, + NFTImageSkeletonWrapper } from './style' +import { LoadingSkeleton } from '../../../../../shared' export interface Props { + isLoading: boolean selectedAsset: BraveWallet.BlockchainToken - nftMetadata: NFTMetadataReturnType + nftMetadata: NFTMetadataReturnType | undefined defaultCurrencies: DefaultCurrencies - selectedNetwork: BraveWallet.EthereumChain + selectedNetwork: BraveWallet.NetworkInfo } const NFTDetails = (props: Props) => { + const [isImageLoaded, setIsImageLoaded] = React.useState(false) const { + isLoading, selectedAsset, nftMetadata, - defaultCurrencies, selectedNetwork } = props const onClickViewOnBlockExplorer = useExplorer(selectedNetwork) const onClickWebsite = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.website }, () => { + chrome.tabs.create({ url: nftMetadata?.contractInformation?.website }, () => { if (chrome.runtime.lastError) { console.error('tabs.create failed: ' + chrome.runtime.lastError.message) } @@ -68,7 +70,7 @@ const NFTDetails = (props: Props) => { } const onClickTwitter = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.twitter }, () => { + chrome.tabs.create({ url: nftMetadata?.contractInformation?.twitter }, () => { if (chrome.runtime.lastError) { console.error('tabs.create failed: ' + chrome.runtime.lastError.message) } @@ -76,7 +78,7 @@ const NFTDetails = (props: Props) => { } const onClickFacebook = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.facebook }, () => { + chrome.tabs.create({ url: nftMetadata?.contractInformation?.facebook }, () => { if (chrome.runtime.lastError) { console.error('tabs.create failed: ' + chrome.runtime.lastError.message) } @@ -85,61 +87,81 @@ const NFTDetails = (props: Props) => { return ( - - - - {selectedAsset.name} { - selectedAsset.tokenId - ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' + {isLoading && + + + + } + {nftMetadata && + <> + setIsImageLoaded(true)} + /> + {!isImageLoaded && + } - - {CurrencySymbols[defaultCurrencies.fiat]}{nftMetadata.floorFiatPrice} - {nftMetadata.floorCryptoPrice} {selectedNetwork.symbol} - - - {getLocale('braveWalletNFTDetailBlockchain')} - {nftMetadata.chainName} - - - {getLocale('braveWalletNFTDetailTokenStandard')} - {nftMetadata.tokenType} - - - {getLocale('braveWalletNFTDetailTokenID')} - - - { - selectedAsset.tokenId - ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' - } - - - - - - - - - - {nftMetadata.contractInformation.name} - - - - - - - - - - - - - - - {nftMetadata.contractInformation.description} - + + + {selectedAsset.name} { + selectedAsset.tokenId + ? '#' + new Amount(selectedAsset.tokenId).toNumber() + : '' + } + + {/* TODO: Add floorFiatPrice & floorCryptoPrice when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */} + {/* {CurrencySymbols[defaultCurrencies.fiat]}{nftMetadata.floorFiatPrice} */} + {/* {nftMetadata.floorCryptoPrice} {selectedNetwork.symbol} */} + + + {getLocale('braveWalletNFTDetailBlockchain')} + {nftMetadata.chainName} + + + {getLocale('braveWalletNFTDetailTokenStandard')} + {nftMetadata.tokenType} + + + {getLocale('braveWalletNFTDetailTokenID')} + + + { + selectedAsset.tokenId + ? '#' + new Amount(selectedAsset.tokenId).toNumber() + : '' + } + + + + + + + + + {/* TODO: Add nft logo when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */} + {/* */} + {nftMetadata.contractInformation.name} + {nftMetadata.contractInformation.website && nftMetadata.contractInformation.twitter && nftMetadata.contractInformation.facebook && + + + + + + + + + + + + + + } + + {nftMetadata.contractInformation.description} + + + } ) diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts index 78b7e922c778..dcc59009b81d 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts @@ -3,7 +3,13 @@ import { WalletButton } from '../../../../../shared/style' import WebsiteIcon from '../../../../../../assets/svg-icons/website-icon.svg' import TwitterIcon from '../../../../../../assets/svg-icons/twitter-icon.svg' import FacebookIcon from '../../../../../../assets/svg-icons/facebook-icon.svg' -import { OpenNewIcon } from 'brave-ui/components/icons' +import { LoaderIcon, OpenNewIcon } from 'brave-ui/components/icons' + +export interface StyleProps { + isLoading: boolean +} + +const nftImageDimension = '440px' export const StyledWrapper = styled.div` display: flex; @@ -11,12 +17,23 @@ export const StyledWrapper = styled.div` align-items: flex-start; justify-content: flex-start; width: 100%; + min-height: ${nftImageDimension}; + margin: 16px 0 50px 0; ` -export const NFTImage = styled.img` - width: 440px; - height: 440px; - margin-right: 10px; +export const NFTImage = styled.img>` + display: ${p => p.isLoading ? 'none' : 'block'}; + width: ${nftImageDimension}; + height: ${nftImageDimension}; + margin-right: 28px; + border-radius: 12px; +` + +export const NFTImageSkeletonWrapper = styled.div` + min-width: ${nftImageDimension}; + height: ${nftImageDimension}; + margin-right: 28px; + border-radius: 12px; ` export const DetailColumn = styled.div` @@ -205,3 +222,22 @@ export const ExplorerIcon = styled(OpenNewIcon)` height: 14px; color: ${(p) => p.theme.color.interactive05}; ` + +export const LoadingOverlay = styled.div>` + display: ${(p) => p.isLoading ? 'flex' : 'none'}; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 440px; + position: absolute; + z-index: 10; + backdrop-filter: blur(5px); +` + +export const LoadIcon = styled(LoaderIcon)` + color: ${p => p.theme.color.interactive08}; + height: 70px; + width: 70px; + opacity: .4; +` diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/index.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/index.tsx index 75d15da58f47..0bdc18de4e33 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/index.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/index.tsx @@ -32,7 +32,7 @@ import { WithHideBalancePlaceholder } from '../../' -// import NFTDetails from './components/nft-details' +import NFTDetails from './components/nft-details' import TokenLists from './components/token-lists' import AccountsAndTransactionsList from './components/accounts-and-transctions-list' @@ -87,7 +87,6 @@ const Portfolio = (props: Props) => { const history = useHistory() // redux - const dispatch = useDispatch() const { defaultCurrencies, addUserAssetError, @@ -204,6 +203,8 @@ const Portfolio = (props: Props) => { // more custom hooks const parseTransaction = useTransactionParser(selectedAssetsNetwork, accounts, transactionSpotPrices, userVisibleTokensInfo) + const { isFetchingNFTMetadata, nftMetadata } = useSelector((state: { page: PageState }) => state.page) + const dispatch = useDispatch() // memos / computed @@ -304,6 +305,9 @@ const Portfolio = (props: Props) => { const goBack = () => { selectAsset(undefined) + if (nftMetadata) { + dispatch(WalletPageActions.updateNFTMetadata(undefined)) + } setfilteredAssetList(userAssetList) toggleNav() } @@ -422,15 +426,15 @@ const Portfolio = (props: Props) => { /> } - {/* Temp Commented out until we have an API to get NFT MetaData */} - {/* {selectedAsset?.isErc721 && + {selectedAsset?.isErc721 && - } */} + } , config const isValidIcon = React.useMemo(() => { if (isRemoteURL || isDataURL) { - const url = new URL(asset.logo) - return isValidIconExtension(url.pathname) + return tokenImageURL?.includes('data:image/') ? true : isValidIconExtension(new URL(asset.logo).pathname) } if (isStorybook) { return true @@ -76,7 +75,7 @@ function withPlaceholderIcon (WrappedComponent: React.ComponentType, config const remoteImage = React.useMemo(() => { if (isRemoteURL) { - return `chrome://image?${tokenImageURL}` + return httpifyIpfsUrl(tokenImageURL) } return '' }, [tokenImageURL]) diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 3b8a468e9c75..c78f15bb63cf 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -250,6 +250,8 @@ export interface PageState { invalidMnemonic: boolean selectedTimeline: BraveWallet.AssetPriceTimeframe selectedAsset: BraveWallet.BlockchainToken | undefined + isFetchingNFTMetadata: boolean + nftMetadata: NFTMetadataReturnType | undefined selectedAssetFiatPrice: BraveWallet.AssetPrice | undefined selectedAssetCryptoPrice: BraveWallet.AssetPrice | undefined selectedAssetPriceHistory: GetPriceHistoryReturnInfo[] @@ -587,11 +589,7 @@ export interface CreateAccountOptionsType { icon: string } -// This is mostly speculative -// will likely change once we have an api for getting -// nft metadata export interface NFTMetadataReturnType { - chain: string chainName: string tokenType: string tokenID: string diff --git a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts index 4b7a30058be1..175eabef4e03 100644 --- a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts +++ b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts @@ -24,6 +24,7 @@ import { } from '../constants/action_types' import { BraveWallet, + NFTMetadataReturnType, UpdateAccountNamePayloadType } from '../../constants/types' @@ -60,3 +61,6 @@ export const setMetaMaskInitialized = createAction('setMetaMaskInitiali export const importFromCryptoWallets = createAction('importFromCryptoWallets') export const importFromMetaMask = createAction('importFromMetaMask') export const openWalletSettings = createAction('openWalletSettings') +export const getNFTMetadata = createAction('getNFTMetadata') +export const setIsFetchingNFTMetadata = createAction('setIsFetchingNFTMetadata') +export const updateNFTMetadata = createAction('updateNFTMetadata') diff --git a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts index 47acaed2f321..d57531b1c052 100644 --- a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts +++ b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts @@ -8,6 +8,7 @@ import * as WalletPageActions from '../actions/wallet_page_actions' import * as WalletActions from '../../common/actions/wallet_actions' import { BraveWallet, + NFTMetadataReturnType, UpdateAccountNamePayloadType, WalletState } from '../../constants/types' @@ -31,6 +32,9 @@ import { import { NewUnapprovedTxAdded } from '../../common/constants/action_types' import { Store } from '../../common/async/types' import { getTokenParam } from '../../utils/api-utils' +import getAPIProxy from '../../common/async/bridge' +import { getTokensNetwork } from '../../utils/network-utils' +import { httpifyIpfsUrl } from '../../utils/string-utils' const handler = new AsyncActionHandler() @@ -91,6 +95,10 @@ handler.on(WalletPageActions.selectAsset.getType(), async (store: Store, payload const defaultPrices = await assetRatioService.getPrice([getTokenParam(selectedAsset)], [defaultFiat, defaultCrypto], payload.timeFrame) const priceHistory = await assetRatioService.getPriceHistory(getTokenParam(selectedAsset), defaultFiat, payload.timeFrame) store.dispatch(WalletPageActions.updatePriceInfo({ priceHistory: priceHistory, defaultFiatPrice: defaultPrices.values[0], defaultCryptoPrice: defaultPrices.values[1], timeFrame: payload.timeFrame })) + + if (payload.asset.isErc721) { + store.dispatch(WalletPageActions.getNFTMetadata(payload.asset)) + } } else { store.dispatch(WalletPageActions.updatePriceInfo({ priceHistory: undefined, defaultFiatPrice: undefined, defaultCryptoPrice: undefined, timeFrame: payload.timeFrame })) } @@ -225,4 +233,36 @@ handler.on(WalletPageActions.openWalletSettings.getType(), async (store) => { }) }) +handler.on(WalletPageActions.getNFTMetadata.getType(), async (store, payload: BraveWallet.BlockchainToken) => { + store.dispatch(WalletPageActions.setIsFetchingNFTMetadata(true)) + const { jsonRpcService } = getAPIProxy() + const result = await jsonRpcService.getERC721Metadata(payload.contractAddress, payload.tokenId, payload.chainId) + + if (!result.error) { + const response = JSON.parse(result.response) + const tokenNetwork = getTokensNetwork(getWalletState(store).networkList, payload) + const nftMetadata: NFTMetadataReturnType = { + chainName: tokenNetwork.chainName, + tokenType: 'ERC721', // getNFTMetadata currently supports only ERC721 standard. When other standards are supported, this value should be dynamic + tokenID: payload.tokenId, + imageURL: response.image.startsWith('data:image/') ? response.image : httpifyIpfsUrl(response.image), + floorFiatPrice: '', + floorCryptoPrice: '', + contractInformation: { + address: payload.contractAddress, + name: response.name, + description: response.description, + website: '', + twitter: '', + facebook: '', + logo: '' + } + } + store.dispatch(WalletPageActions.updateNFTMetadata(nftMetadata)) + } else { + console.error(result.errorMessage) + } + store.dispatch(WalletPageActions.setIsFetchingNFTMetadata(false)) +}) + export default handler.middleware diff --git a/components/brave_wallet_ui/page/reducers/page_reducer.ts b/components/brave_wallet_ui/page/reducers/page_reducer.ts index 78862d2538d6..2d93cd4a8807 100644 --- a/components/brave_wallet_ui/page/reducers/page_reducer.ts +++ b/components/brave_wallet_ui/page/reducers/page_reducer.ts @@ -8,7 +8,8 @@ import * as Actions from '../actions/wallet_page_actions' import { BraveWallet, PageState, - ImportWalletError + ImportWalletError, + NFTMetadataReturnType } from '../../constants/types' import { WalletCreatedPayloadType, @@ -26,6 +27,8 @@ const defaultState: PageState = { importWalletError: { hasError: false }, selectedTimeline: BraveWallet.AssetPriceTimeframe.OneDay, selectedAsset: undefined, + isFetchingNFTMetadata: true, + nftMetadata: undefined, selectedAssetFiatPrice: undefined, selectedAssetCryptoPrice: undefined, selectedAssetPriceHistory: [], @@ -166,6 +169,21 @@ export const createPageReducer = (initialState: PageState) => { isMetaMaskInitialized: payload } }) + + reducer.on(Actions.updateNFTMetadata, (state: PageState, payload: NFTMetadataReturnType) => { + return { + ...state, + nftMetadata: payload + } + }) + + reducer.on(Actions.setIsFetchingNFTMetadata, (state: PageState, payload: boolean) => { + return { + ...state, + isFetchingNFTMetadata: payload + } + }) + return reducer } diff --git a/components/brave_wallet_ui/utils/string-utils.ts b/components/brave_wallet_ui/utils/string-utils.ts index a742243d2034..a5b8b47f656e 100644 --- a/components/brave_wallet_ui/utils/string-utils.ts +++ b/components/brave_wallet_ui/utils/string-utils.ts @@ -6,7 +6,19 @@ export const toProperCase = (value: string) => (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()) export const isRemoteImageURL = (url?: string) => - url?.startsWith('http://') || url?.startsWith('https://') || url?.startsWith('data:image/') + url?.startsWith('http://') || url?.startsWith('https://') || url?.startsWith('data:image/') || url?.startsWith('ipfs://') export const isValidIconExtension = (url?: string) => - url?.endsWith('.jpg') || url?.endsWith('.jpeg') || url?.endsWith('.png') || url?.endsWith('.svg') + url?.endsWith('.jpg') || url?.endsWith('.jpeg') || url?.endsWith('.png') || url?.endsWith('.svg') || url?.endsWith('.gif') + +export const httpifyIpfsUrl = (url: string | undefined) => { + if (!url) { + return '' + } + + if (url.startsWith('data:image/')) { + return url + } + + return `chrome://image/?${url.includes('ipfs://') ? url.replace('ipfs://', 'https://ipfs.io/ipfs/') : url}` +}