diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 843440fb07b6..cb31596ee420 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -601,6 +601,8 @@ source_set("ui") { "views/wallet_bubble_focus_observer.h", "wallet_bubble_manager_delegate_impl.cc", "wallet_bubble_manager_delegate_impl.h", + "webui/brave_wallet/nft/nft_ui.cc", + "webui/brave_wallet/nft/nft_ui.h", "webui/brave_wallet/page_handler/wallet_page_handler.cc", "webui/brave_wallet/page_handler/wallet_page_handler.h", "webui/brave_wallet/trezor/trezor_ui.cc", @@ -620,6 +622,7 @@ source_set("ui") { "//brave/components/brave_wallet/common", "//brave/components/brave_wallet/common:mojom", "//brave/components/brave_wallet_ui:resources", + "//brave/components/brave_wallet_ui/nft:nft_display_generated", "//brave/components/brave_wallet_ui/page:brave_wallet_page_generated", "//brave/components/brave_wallet_ui/panel:brave_wallet_panel_generated", "//brave/components/brave_wallet_ui/trezor:trezor_bridge_generated", diff --git a/browser/ui/webui/brave_wallet/nft/nft_ui.cc b/browser/ui/webui/brave_wallet/nft/nft_ui.cc new file mode 100644 index 000000000000..76a6dfafff00 --- /dev/null +++ b/browser/ui/webui/brave_wallet/nft/nft_ui.cc @@ -0,0 +1,54 @@ +/* Copyright (c) 2021 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/webui/brave_wallet/nft/nft_ui.h" + +#include + +#include "brave/common/webui_url_constants.h" +#include "brave/components/nft_display/resources/grit/nft_display_generated_map.h" +#include "components/grit/brave_components_resources.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui_data_source.h" +#include "ui/resources/grit/webui_generated_resources.h" + +namespace nft { + +UntrustedNftUI::UntrustedNftUI(content::WebUI* web_ui) + : ui::UntrustedWebUIController(web_ui) { + auto* untrusted_source = content::WebUIDataSource::Create(kUntrustedNftURL); + untrusted_source->SetDefaultResource(IDR_BRAVE_WALLET_NFT_DISPLAY_HTML); + untrusted_source->AddResourcePaths( + base::make_span(kNftDisplayGenerated, kNftDisplayGeneratedSize)); + untrusted_source->AddFrameAncestor(GURL(kBraveUIWalletPageURL)); + untrusted_source->AddFrameAncestor(GURL(kBraveUIWalletPanelURL)); + + // TODO(nvonpentz) Determine CSP. Commented below was copied from trezor_ui.cc + // + // untrusted_source->OverrideContentSecurityPolicy( + // network::mojom::CSPDirectiveName::StyleSrc, + // std::string("style-src 'unsafe-inline';")); + untrusted_source->AddResourcePath("load_time_data.js", + IDR_WEBUI_JS_LOAD_TIME_DATA_JS); + untrusted_source->UseStringsJs(); + untrusted_source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + std::string("img-src 'self' https: data:;")); + auto* browser_context = web_ui->GetWebContents()->GetBrowserContext(); + content::WebUIDataSource::Add(browser_context, untrusted_source); +} + +UntrustedNftUI::~UntrustedNftUI() = default; + +std::unique_ptr +UntrustedNftUIConfig::CreateWebUIController(content::WebUI* web_ui) { + return std::make_unique(web_ui); +} + +UntrustedNftUIConfig::UntrustedNftUIConfig() + : WebUIConfig(content::kChromeUIUntrustedScheme, kUntrustedNftHost) {} + +} // namespace nft diff --git a/browser/ui/webui/brave_wallet/nft/nft_ui.h b/browser/ui/webui/brave_wallet/nft/nft_ui.h new file mode 100644 index 000000000000..f77e5efebc67 --- /dev/null +++ b/browser/ui/webui/brave_wallet/nft/nft_ui.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2021 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_NFT_NFT_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_NFT_NFT_UI_H_ + +#include + +#include "content/public/browser/web_ui.h" +#include "content/public/common/url_constants.h" +#include "ui/webui/untrusted_web_ui_controller.h" +#include "ui/webui/webui_config.h" + +namespace nft { + +class UntrustedNftUI : public ui::UntrustedWebUIController { + public: + explicit UntrustedNftUI(content::WebUI* web_ui); + UntrustedNftUI(const UntrustedNftUI&) = delete; + UntrustedNftUI& operator=(const UntrustedNftUI&) = delete; + ~UntrustedNftUI() override; +}; + +class UntrustedNftUIConfig : public ui::WebUIConfig { + public: + UntrustedNftUIConfig(); + ~UntrustedNftUIConfig() override = default; + + std::unique_ptr CreateWebUIController( + content::WebUI* web_ui) override; +}; + +} // namespace nft + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_NFT_NFT_UI_H_ diff --git a/browser/ui/webui/brave_wallet/wallet_page_ui.cc b/browser/ui/webui/brave_wallet/wallet_page_ui.cc index 81592073b499..3f115b0234bc 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.cc @@ -59,6 +59,9 @@ WalletPageUI::WalletPageUI(content::WebUI* web_ui) source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::FrameSrc, std::string("frame-src ") + kUntrustedTrezorURL + ";"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameSrc, + std::string("frame-src ") + kUntrustedNftURL + ";"); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); auto* profile = Profile::FromWebUI(web_ui); content::WebUIDataSource::Add(profile, source); diff --git a/common/webui_url_constants.cc b/common/webui_url_constants.cc index 7246948cb36c..68edf41459be 100644 --- a/common/webui_url_constants.cc +++ b/common/webui_url_constants.cc @@ -33,6 +33,8 @@ const char kWalletSettingsURL[] = "brave://settings/wallet"; const char kBraveSyncPath[] = "braveSync"; const char kBraveSyncSetupPath[] = "braveSync/setup"; const char kTorInternalsHost[] = "tor-internals"; +const char kUntrustedNftHost[] = "nft-display"; +const char kUntrustedNftURL[] = "chrome-untrusted://nft-display/"; const char kUntrustedTrezorHost[] = "trezor-bridge"; const char kUntrustedTrezorURL[] = "chrome-untrusted://trezor-bridge/"; const char kShieldsPanelURL[] = "chrome://brave-shields.top-chrome"; diff --git a/common/webui_url_constants.h b/common/webui_url_constants.h index 1ca26f07c4aa..8c10a1d8198c 100644 --- a/common/webui_url_constants.h +++ b/common/webui_url_constants.h @@ -35,6 +35,8 @@ extern const char kWalletSettingsURL[]; extern const char kBraveSyncPath[]; extern const char kBraveSyncSetupPath[]; extern const char kTorInternalsHost[]; +extern const char kUntrustedNftHost[]; +extern const char kUntrustedNftURL[]; extern const char kUntrustedTrezorHost[]; extern const char kUntrustedTrezorURL[]; extern const char kShieldsPanelURL[]; diff --git a/components/brave_wallet_ui/BUILD.gn b/components/brave_wallet_ui/BUILD.gn index 259c0d6de44a..2c9401080412 100644 --- a/components/brave_wallet_ui/BUILD.gn +++ b/components/brave_wallet_ui/BUILD.gn @@ -10,6 +10,7 @@ import("//tools/grit/repack.gni") repack("resources") { deps = [ + "//brave/components/brave_wallet_ui/nft:nft_display_generated", "//brave/components/brave_wallet_ui/page:brave_wallet_page_generated", "//brave/components/brave_wallet_ui/panel:brave_wallet_panel_generated", "//brave/components/brave_wallet_ui/trezor:trezor_bridge_generated", @@ -17,6 +18,7 @@ repack("resources") { sources = [ "$root_gen_dir/brave/components/brave_wallet_page/resources/brave_wallet_page_generated.pak", "$root_gen_dir/brave/components/brave_wallet_panel/resources/brave_wallet_panel_generated.pak", + "$root_gen_dir/brave/components/nft_display/resources/nft_display_generated.pak", "$root_gen_dir/brave/components/trezor_bridge/resources/trezor_bridge_generated.pak", ] output = diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index e065936a39f3..99b10e40180b 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -292,7 +292,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 502153138bbb..a29a4e1f0984 100644 --- a/components/brave_wallet_ui/common/hooks/assets.ts +++ b/components/brave_wallet_ui/common/hooks/assets.ts @@ -17,12 +17,24 @@ 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 => ({ - ...token, - logo: `chrome://erc-token-images/${token.logo}` - }) as 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 () { 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 8ba074a683d8..101604f8e616 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 @@ -566,7 +566,7 @@ const EditVisibleAssetsModal = ({ onClose }: Props) => { <> {filteredTokenList.slice(0, tokenDisplayAmount).map((token) => { const [assetNetworkSkeletonWidth, setAssetNetworkSkeletonWidth] = React.useState(0) const AssetIconWithPlaceholder = React.useMemo(() => { - return withPlaceholderIcon(AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) + return withPlaceholderIcon(token.isErc721 ? NFTAssetIcon : AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) }, []) const formattedAssetBalance = token.isErc721 @@ -78,8 +79,8 @@ const PortfolioAssetItem = (props: Props) => { }, [fiatBalance]) const isLoading = React.useMemo(() => { - return formattedAssetBalance === '' - }, [formattedAssetBalance]) + return formattedAssetBalance === '' && !token.isErc721 + }, [formattedAssetBalance, token]) const tokensNetwork = React.useMemo(() => { return getTokensNetwork(networks, token) @@ -111,7 +112,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/portfolio-asset-item/style.ts b/components/brave_wallet_ui/components/desktop/portfolio-asset-item/style.ts index 5ed823510cfe..69ab75d1a91b 100644 --- a/components/brave_wallet_ui/components/desktop/portfolio-asset-item/style.ts +++ b/components/brave_wallet_ui/components/desktop/portfolio-asset-item/style.ts @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { AssetIconProps, AssetIconFactory, WalletButton } from '../../shared/style' +import { AssetIconProps, AssetIconFactory, WalletButton, AssetIconIframe } from '../../shared/style' interface StyleProps { disabled: boolean @@ -60,9 +60,15 @@ export const AssetBalanceText = styled.span` // support with custom AssetIconFactory. // // Ref: https://styled-components.com/docs/advanced#style-objects -export const AssetIcon = AssetIconFactory({ +const assetIconProps = { width: '40px', height: 'auto' +} +export const AssetIcon = AssetIconFactory(assetIconProps) +export const NFTAssetIcon = AssetIconIframe({ + ...assetIconProps, + height: '40px', + border: 'transparent' }) export const IconsWrapper = styled.div` 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..be661c97d5ef 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,15 @@ 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, - NFTImage, + LoadIcon, + LoadingOverlay, DetailColumn, TokenName, - TokenFiatValue, - TokenCryptoValue, DetailSectionRow, DetailSectionColumn, DetailSectionTitle, @@ -30,7 +28,6 @@ import { ProjectDetailRow, ProjectDetailName, ProjectDetailDescription, - ProjectDetailImage, ProjectDetailButtonRow, ProjectDetailButton, ProjectDetailButtonSeperator, @@ -39,28 +36,33 @@ import { ProjectFacebookIcon, ProjectDetailIDRow, ExplorerIcon, - ExplorerButton + ExplorerButton, + NFTImageSkeletonWrapper, + NTFImageIframe } 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,80 @@ const NFTDetails = (props: Props) => { return ( - - - - {selectedAsset.name} { - selectedAsset.tokenId - ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' + {isLoading && + + + + } + {nftMetadata && + <> + setIsImageLoaded(true)} + src={`chrome-untrusted://nft-display?imageUrl=${encodeURIComponent(nftMetadata.imageURL)}`} + /> + {!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..59c1b596a416 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 +} + +export const nftImageDimension = '440px' export const StyledWrapper = styled.div` display: flex; @@ -11,12 +17,24 @@ 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 NTFImageIframe = styled.iframe` + display: block; + width: ${nftImageDimension}; + height: ${nftImageDimension}; + margin-right: 28px; + border-radius: 12px; + border: transparent; +` + +export const NFTImageSkeletonWrapper = styled.div` + min-width: ${nftImageDimension}; + height: ${nftImageDimension}; + margin-right: 28px; + border-radius: 12px; ` export const DetailColumn = styled.div` @@ -205,3 +223,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/portfolio-asset.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx index 55e170f00d4d..62c4341d35f5 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx @@ -64,6 +64,7 @@ import { NetworkDescription } from './style' import { Skeleton } from '../../../shared/loading-skeleton/styles' +import NFTDetails from './components/nft-details' const AssetIconWithPlaceholder = withPlaceholderIcon(AssetIcon, { size: 'big', marginLeft: 0, marginRight: 12 }) @@ -94,7 +95,9 @@ export const PortfolioAsset = () => { selectedAssetCryptoPrice, selectedAssetFiatPrice, selectedAssetPriceHistory, - selectedTimeline + selectedTimeline, + isFetchingNFTMetadata, + nftMetadata } = useSelector(({ page }: { page: PageState }) => page) // custom hooks @@ -378,7 +381,7 @@ export const PortfolioAsset = () => { /> } - {/* {selectedAsset?.isErc721 && + {selectedAsset?.isErc721 && { defaultCurrencies={defaultCurrencies} selectedNetwork={selectedNetwork} /> - } */} + } , 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/components/shared/style.tsx b/components/brave_wallet_ui/components/shared/style.tsx index cac829091e56..58cdd1e2cbc4 100644 --- a/components/brave_wallet_ui/components/shared/style.tsx +++ b/components/brave_wallet_ui/components/shared/style.tsx @@ -55,6 +55,10 @@ export const AssetIconFactory = styled.img.attrs(props => ({ loading: 'lazy' })) +export const AssetIconIframe = styled.iframe.attrs(props => ({ + src: `chrome-untrusted://nft-display?imageUrl=${encodeURIComponent(props.icon || '')}` +})) + export const WalletButton = styled.button<{ isDraggedOver?: boolean }>` diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 80e538afc87c..b278180cc996 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -252,6 +252,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[] @@ -613,11 +615,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/nft/BUILD.gn b/components/brave_wallet_ui/nft/BUILD.gn new file mode 100644 index 000000000000..2070fde29f71 --- /dev/null +++ b/components/brave_wallet_ui/nft/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2021 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. + +import("//brave/components/common/typescript.gni") +import("//chrome/common/features.gni") +import("//mojo/public/tools/bindings/mojom.gni") +import("//tools/grit/preprocess_if_expr.gni") +import("//tools/grit/repack.gni") + +transpile_web_ui("nft_display_ui") { + entry_points = [ [ + "nft", + rebase_path("nft.ts"), + ] ] + webpack_aliases = [ "browser" ] + resource_name = "nft_display" +} + +pack_web_resources("nft_display_generated") { + resource_name = "nft_display" + output_dir = "$root_gen_dir/brave/components/nft_display/resources" + deps = [ ":nft_display_ui" ] +} diff --git a/components/brave_wallet_ui/nft/nft.html b/components/brave_wallet_ui/nft/nft.html new file mode 100644 index 000000000000..18f6100ea98b --- /dev/null +++ b/components/brave_wallet_ui/nft/nft.html @@ -0,0 +1,11 @@ + + + + + + NFT iframe + + + + + diff --git a/components/brave_wallet_ui/nft/nft.ts b/components/brave_wallet_ui/nft/nft.ts new file mode 100644 index 000000000000..db2f91813cf6 --- /dev/null +++ b/components/brave_wallet_ui/nft/nft.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => { + if (typeof prop === 'string') { + return searchParams.get(prop) + } + return null + } +}) +/* eslint-disable @typescript-eslint/dot-notation */ +const imageUrl = params['imageUrl'] +/* eslint-enable @typescript-eslint/dot-notation */ +const imageEl = document.getElementById('image') +if (imageUrl && imageEl) { + imageEl.src = imageUrl +} 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 ffa82c5ac011..bdb7f407cff5 100644 --- a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts +++ b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts @@ -23,6 +23,7 @@ import { } from '../constants/action_types' import { BraveWallet, + NFTMetadataReturnType, UpdateAccountNamePayloadType } from '../../constants/types' @@ -58,3 +59,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 c72fc85a9fea..0a638527490d 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 @@ -9,6 +9,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() @@ -86,6 +90,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 })) } @@ -220,4 +228,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/stories/mock-data/mock-nft-metadata.ts b/components/brave_wallet_ui/stories/mock-data/mock-nft-metadata.ts index 3849c20f592e..9c2f318e6043 100644 --- a/components/brave_wallet_ui/stories/mock-data/mock-nft-metadata.ts +++ b/components/brave_wallet_ui/stories/mock-data/mock-nft-metadata.ts @@ -4,7 +4,6 @@ import MooncatProjectIcon from '../../assets/png-icons/mooncat-project-icon.png' export const mockNFTMetadata: NFTMetadataReturnType[] = [ { - chain: '0x1', chainName: 'Ethereum', tokenType: 'ERC721', tokenID: '0x42a5', diff --git a/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts b/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts index 9140f9dd262e..099b59065c89 100644 --- a/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts +++ b/components/brave_wallet_ui/stories/mock-data/mock-page-state.ts @@ -1,6 +1,9 @@ import { BraveWallet, PageState } from '../../constants/types' +import { mockNFTMetadata } from './mock-nft-metadata' export const mockPageState: PageState = { + isFetchingNFTMetadata: false, + nftMetadata: mockNFTMetadata[0], hasInitialized: false, importAccountError: false, importWalletError: { 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}` +} diff --git a/components/resources/brave_components_resources.grd b/components/resources/brave_components_resources.grd index 0f5bdfe91843..8da103191960 100644 --- a/components/resources/brave_components_resources.grd +++ b/components/resources/brave_components_resources.grd @@ -38,6 +38,7 @@ + diff --git a/resources/resource_ids.spec b/resources/resource_ids.spec index 522ca35b5ea9..9bc4dbeedff8 100644 --- a/resources/resource_ids.spec +++ b/resources/resource_ids.spec @@ -182,4 +182,9 @@ "META": {"sizes": {"includes": [250]}}, "includes": [58750] }, + # This file is generated during the build. + "<(SHARED_INTERMEDIATE_DIR)/brave/web-ui-nft_display/nft_display.grd": { + "META": {"sizes": {"includes": [250]}}, + "includes": [59000] + }, }