From 0b6165ff9dab255d3a9edc17a49728663f184f08 Mon Sep 17 00:00:00 2001 From: William Muli <48117473+muliswilliam@users.noreply.github.com> Date: Thu, 28 Jul 2022 20:01:14 +0300 Subject: [PATCH] NFT Details Screen (#13370) * Add NFT details screen * chore: remove console.log * chore: remove chrome://image prefix * chore: fix loading of nft asset use url params * chore: add skeleton loader for nft image * chore: check token id when deleting assets * chore: remove redundant lines * chore: add tokenId to dep list * feat: display visible asset modal asset icon in chrome-untrusted * chore: trim url * chore: add tokenId check when removing asset from visible assets list * chore: fix tokenBalanceRegistry keys to avoid duplicates for erc721 tokens * chore: refactor nft details screen * chore: add border-radius to nft image * feat: create react app for rendering nft details in a chrome-untrusted tab * Add string to access via loadTimeData object * Add configurations nft ui untrusted tab * Update build requirements for nft ui untrusted tab * Send message to nft iframe to update state * Added iframe component * Create components for diplaying nft content in chrome untrusted tab * Added utilities for communicating with nft chrome untrusted tab using postmessage * chore: remove unused component * chore: validate url to avoid xss attack * feat: change nft icon display from query parameter(imageUrl) to postmessage api * use specific targetOrigin in postMessage * chore: remove console.log * chore: fix feedback * chore: feedback * chore: add license header and documentation * Revert "chore: add tokenId check when removing asset from visible assets list" This reverts commit 440134d27366c9e08a483b00b7878c8649496595. This will be fixed in https://github.com/brave/brave-browser/issues/24238 * chore: fix unstable test behaviour * chore: fix tests * chore: rename function * chore: refactor to avoid adding unnecessary string * chore: change copyright year * chore: add braveWalletLedgerBridgeUrl string * chore: use getWalletPageProxy * chore: useCallback --- browser/ui/BUILD.gn | 3 + browser/ui/webui/brave_wallet/nft/nft_ui.cc | 78 +++++++++ browser/ui/webui/brave_wallet/nft/nft_ui.h | 37 +++++ .../ui/webui/brave_wallet/wallet_page_ui.cc | 4 + .../ui/webui/brave_wallet/wallet_panel_ui.cc | 1 + .../webui/chrome_untrusted_web_ui_configs.cc | 3 + components/brave_wallet_ui/BUILD.gn | 2 + .../brave_wallet_ui/common/async/handlers.ts | 11 +- .../common/hooks/assets-management.ts | 2 +- .../brave_wallet_ui/common/hooks/assets.ts | 20 ++- .../brave_wallet_ui/common/hooks/balance.ts | 4 +- .../brave_wallet_ui/common/hooks/explorer.ts | 2 +- .../common/reducers/wallet_reducer.ts | 6 +- .../desktop/asset-watchlist-item/index.tsx | 5 +- .../edit-visible-assets-modal/index.tsx | 2 +- .../desktop/portfolio-asset-item/index.tsx | 11 +- .../desktop/portfolio-asset-item/style.ts | 5 +- .../components/nft-details/index.tsx | 148 ----------------- .../views/portfolio/portfolio-asset.tsx | 117 +++++++++---- .../views/portfolio/portfolio-overview.tsx | 11 +- .../desktop/views/portfolio/style.ts | 18 +- .../shared/create-placeholder-icon/index.tsx | 7 +- .../shared/nft-icon/nft-icon-styles.ts | 12 ++ .../components/shared/nft-icon/nft-icon.tsx | 39 +++++ components/brave_wallet_ui/constants/types.ts | 8 +- components/brave_wallet_ui/nft/BUILD.gn | 29 ++++ .../nft-content/nft-content-styles.ts | 35 ++++ .../components/nft-content/nft-content.tsx | 52 ++++++ .../nft-details/nft-details-styles.ts} | 43 ++++- .../components/nft-details/nft-details.tsx | 155 ++++++++++++++++++ .../brave_wallet_ui/nft/css/nft-global.css | 12 ++ .../brave_wallet_ui/nft/nft-ui-messages.ts | 51 ++++++ components/brave_wallet_ui/nft/nft.html | 16 ++ components/brave_wallet_ui/nft/nft.tsx | 113 +++++++++++++ .../page/actions/wallet_page_actions.ts | 4 + .../page/async/wallet_page_async_handler.ts | 39 +++++ .../page/reducers/page_reducer.ts | 20 ++- .../stories/mock-data/mock-nft-metadata.ts | 1 - .../stories/mock-data/mock-page-state.ts | 3 + .../brave_wallet_ui/utils/account-utils.ts | 6 +- .../utils/copy-to-clipboard.ts | 2 +- .../brave_wallet_ui/utils/string-utils.ts | 14 +- components/constants/webui_url_constants.cc | 2 + components/constants/webui_url_constants.h | 2 + .../resources/brave_components_resources.grd | 1 + resources/resource_ids.spec | 5 + 46 files changed, 925 insertions(+), 236 deletions(-) create mode 100644 browser/ui/webui/brave_wallet/nft/nft_ui.cc create mode 100644 browser/ui/webui/brave_wallet/nft/nft_ui.h delete mode 100644 components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx create mode 100644 components/brave_wallet_ui/components/shared/nft-icon/nft-icon-styles.ts create mode 100644 components/brave_wallet_ui/components/shared/nft-icon/nft-icon.tsx create mode 100644 components/brave_wallet_ui/nft/BUILD.gn create mode 100644 components/brave_wallet_ui/nft/components/nft-content/nft-content-styles.ts create mode 100644 components/brave_wallet_ui/nft/components/nft-content/nft-content.tsx rename components/brave_wallet_ui/{components/desktop/views/portfolio/components/nft-details/style.ts => nft/components/nft-details/nft-details-styles.ts} (76%) create mode 100644 components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx create mode 100644 components/brave_wallet_ui/nft/css/nft-global.css create mode 100644 components/brave_wallet_ui/nft/nft-ui-messages.ts create mode 100644 components/brave_wallet_ui/nft/nft.html create mode 100644 components/brave_wallet_ui/nft/nft.tsx diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index f5890619481ac..70959e8babbfb 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -628,6 +628,8 @@ source_set("ui") { "wallet_bubble_manager_delegate_impl.h", "webui/brave_wallet/ledger/ledger_ui.cc", "webui/brave_wallet/ledger/ledger_ui.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", @@ -648,6 +650,7 @@ source_set("ui") { "//brave/components/brave_wallet/common:mojom", "//brave/components/brave_wallet_ui:resources", "//brave/components/brave_wallet_ui/ledger:ledger_bridge_generated", + "//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 0000000000000..8adc2cfbf2d98 --- /dev/null +++ b/browser/ui/webui/brave_wallet/nft/nft_ui.cc @@ -0,0 +1,78 @@ +/* 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/components/brave_wallet/browser/brave_wallet_constants.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/l10n/common/locale_util.h" +#include "brave/components/nft_display/resources/grit/nft_display_generated_map.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "chrome/grit/browser_resources.h" +#include "chrome/grit/generated_resources.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); + + for (const auto& str : brave_wallet::kLocalizedStrings) { + std::u16string l10n_str = + brave_l10n::GetLocalizedResourceUTF16String(str.id); + untrusted_source->AddString(str.name, l10n_str); + } + + 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)); + webui::SetupWebUIDataSource( + untrusted_source, + base::make_span(kNftDisplayGenerated, kNftDisplayGeneratedSize), + IDR_BRAVE_WALLET_NFT_DISPLAY_HTML); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ScriptSrc, + std::string("script-src 'self' chrome-untrusted://resources " + "chrome-untrusted://brave-resources;")); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::StyleSrc, + std::string("style-src 'self' 'unsafe-inline';")); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FontSrc, + std::string("font-src 'self' data:;")); + untrusted_source->AddResourcePath("load_time_data.js", + IDR_WEBUI_JS_LOAD_TIME_DATA_JS); + untrusted_source->UseStringsJs(); + untrusted_source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); + untrusted_source->AddString("braveWalletTrezorBridgeUrl", + kUntrustedTrezorURL); + untrusted_source->AddString("braveWalletLedgerBridgeUrl", + kUntrustedLedgerURL); + 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 0000000000000..21777843cebd3 --- /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/browser/webui_config.h" +#include "content/public/common/url_constants.h" +#include "ui/webui/untrusted_web_ui_controller.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 content::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 751dddb71e0e2..0b6f1159d19d2 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.cc @@ -61,7 +61,11 @@ WalletPageUI::WalletPageUI(content::WebUI* web_ui) network::mojom::CSPDirectiveName::FrameSrc, std::string("frame-src ") + kUntrustedTrezorURL + " " + kUntrustedLedgerURL + ";"); + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameSrc, + std::string("frame-src ") + kUntrustedNftURL + ";"); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); + source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); auto* profile = Profile::FromWebUI(web_ui); content::WebUIDataSource::Add(profile, source); content::URLDataSource::Add(profile, diff --git a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc index 2335c18390409..0a4c92584289d 100644 --- a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc @@ -64,6 +64,7 @@ WalletPanelUI::WalletPanelUI(content::WebUI* web_ui) std::string("frame-src ") + kUntrustedTrezorURL + " " + kUntrustedLedgerURL + ";"); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); + source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); if (ShouldDisableCSPForTesting()) { source->DisableContentSecurityPolicy(); } diff --git a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc index 8926979703e99..47bb94dc38230 100644 --- a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc +++ b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc @@ -6,6 +6,7 @@ #include "chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.h" #include "brave/browser/ui/webui/brave_wallet/ledger/ledger_ui.h" +#include "brave/browser/ui/webui/brave_wallet/nft/nft_ui.h" #include "brave/browser/ui/webui/brave_wallet/trezor/trezor_ui.h" #include "brave/components/brave_vpn/buildflags/buildflags.h" #include "build/build_config.h" @@ -30,6 +31,8 @@ void RegisterChromeUntrustedWebUIConfigs() { std::make_unique()); content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( std::make_unique()); + content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( + std::make_unique()); #if BUILDFLAG(ENABLE_BRAVE_VPN) if (brave_vpn::IsBraveVPNEnabled()) { content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( diff --git a/components/brave_wallet_ui/BUILD.gn b/components/brave_wallet_ui/BUILD.gn index 78b683284389c..d9410c78d3150 100644 --- a/components/brave_wallet_ui/BUILD.gn +++ b/components/brave_wallet_ui/BUILD.gn @@ -11,6 +11,7 @@ import("//tools/grit/repack.gni") repack("resources") { deps = [ "//brave/components/brave_wallet_ui/ledger:ledger_bridge_generated", + "//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", @@ -19,6 +20,7 @@ repack("resources") { "$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/ledger_bridge/resources/ledger_bridge_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 34e7666577d59..3573e9648f858 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -302,7 +302,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-management.ts b/components/brave_wallet_ui/common/hooks/assets-management.ts index 75af5db3301ee..36756c2040ed1 100644 --- a/components/brave_wallet_ui/common/hooks/assets-management.ts +++ b/components/brave_wallet_ui/common/hooks/assets-management.ts @@ -17,7 +17,7 @@ const onlyInLeft = (left: BraveWallet.BlockchainToken[], right: BraveWallet.Bloc left.filter(leftValue => !right.some(rightValue => leftValue.contractAddress.toLowerCase() === rightValue.contractAddress.toLowerCase() && - leftValue.chainId === rightValue.chainId)) + leftValue.chainId === rightValue.chainId && leftValue.tokenId === rightValue.tokenId)) export default function useAssetManagement () { // redux diff --git a/components/brave_wallet_ui/common/hooks/assets.ts b/components/brave_wallet_ui/common/hooks/assets.ts index de1bbf70a50aa..974dab1f81c21 100644 --- a/components/brave_wallet_ui/common/hooks/assets.ts +++ b/components/brave_wallet_ui/common/hooks/assets.ts @@ -18,12 +18,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/common/hooks/balance.ts b/components/brave_wallet_ui/common/hooks/balance.ts index f5ff6a928ca10..37cd4aa292c75 100644 --- a/components/brave_wallet_ui/common/hooks/balance.ts +++ b/components/brave_wallet_ui/common/hooks/balance.ts @@ -7,6 +7,7 @@ import * as React from 'react' import { BraveWallet, WalletAccountType } from '../../constants/types' import { getTokensCoinType } from '../../utils/network-utils' +import { createTokenBalanceRegistryKey } from '../../utils/account-utils' export default function useBalance (networks: BraveWallet.NetworkInfo[]) { const getBalance = React.useCallback((account?: WalletAccountType, token?: BraveWallet.BlockchainToken) => { @@ -31,7 +32,8 @@ export default function useBalance (networks: BraveWallet.NetworkInfo[]) { return (account.nativeBalanceRegistry || {})[token.chainId || ''] || '' } - return (account.tokenBalanceRegistry || {})[token.contractAddress.toLowerCase()] || '' + const registryKey = createTokenBalanceRegistryKey(token) + return (account.tokenBalanceRegistry || {})[registryKey] || '' }, [networks]) return getBalance diff --git a/components/brave_wallet_ui/common/hooks/explorer.ts b/components/brave_wallet_ui/common/hooks/explorer.ts index c9bca58d83ed5..b8f34ec26d038 100644 --- a/components/brave_wallet_ui/common/hooks/explorer.ts +++ b/components/brave_wallet_ui/common/hooks/explorer.ts @@ -15,7 +15,7 @@ export function buildExplorerUrl ( const explorerURL = network.blockExplorerUrls[0] if (type === 'contract') { - return `${explorerURL}/${value}?a=${new Amount(id ?? '').format()}` + return id ? `${explorerURL}/${value}?a=${new Amount(id).format()}` : `${explorerURL}/${value}}` } const isFileCoinNet = diff --git a/components/brave_wallet_ui/common/reducers/wallet_reducer.ts b/components/brave_wallet_ui/common/reducers/wallet_reducer.ts index a2e46017dc89c..f3d71bca7bf81 100644 --- a/components/brave_wallet_ui/common/reducers/wallet_reducer.ts +++ b/components/brave_wallet_ui/common/reducers/wallet_reducer.ts @@ -35,6 +35,7 @@ import { mojoTimeDeltaToJSDate } from '../../../common/mojomUtils' import { sortTransactionByDate } from '../../utils/tx-utils' import Amount from '../../utils/amount' import { AllNetworksOption } from '../../options/network-filter-options' +import { createTokenBalanceRegistryKey } from '../../utils/account-utils' const defaultState: WalletState = { hasInitialized: false, @@ -219,8 +220,9 @@ export const createWalletReducer = (initialState: WalletState) => { accounts.forEach((account, accountIndex) => { payload.balances[accountIndex]?.forEach((info, tokenIndex) => { if (info.error === BraveWallet.ProviderError.kSuccess) { - const contractAddress = visibleTokens[tokenIndex].contractAddress.toLowerCase() - accounts[accountIndex].tokenBalanceRegistry[contractAddress] = Amount.normalize(info.balance) + const token = visibleTokens[tokenIndex] + const registryKey = createTokenBalanceRegistryKey(token) + accounts[accountIndex].tokenBalanceRegistry[registryKey] = Amount.normalize(info.balance) } }) }) diff --git a/components/brave_wallet_ui/components/desktop/asset-watchlist-item/index.tsx b/components/brave_wallet_ui/components/desktop/asset-watchlist-item/index.tsx index 06eb58cfb8737..85fa24186396b 100644 --- a/components/brave_wallet_ui/components/desktop/asset-watchlist-item/index.tsx +++ b/components/brave_wallet_ui/components/desktop/asset-watchlist-item/index.tsx @@ -24,6 +24,7 @@ import { NameAndSymbol, AssetSymbol } from './style' +import { NftIcon } from '../../shared/nft-icon/nft-icon' export interface Props { onSelectAsset: (key: string, selected: boolean, token: BraveWallet.BlockchainToken, isCustom: boolean) => void @@ -57,8 +58,8 @@ const AssetWatchlistItem = (props: Props) => { } const AssetIconWithPlaceholder = React.useMemo(() => { - return withPlaceholderIcon(AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) - }, []) + return withPlaceholderIcon(token.isErc721 ? NftIcon : AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) + }, [token]) const tokensNetwork = React.useMemo(() => { if (!token) { 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 e04a443b3539e..55c0c717697e9 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 @@ -570,7 +570,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 ? NftIcon : AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) + }, [token]) const formattedAssetBalance = token.isErc721 ? new Amount(assetBalance) @@ -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 - + {!token.logo 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 5ed823510cfec..88b82d8fab91f 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 @@ -60,10 +60,11 @@ 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 IconsWrapper = styled.div` display: flex; 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 deleted file mode 100644 index 46e10339424ef..0000000000000 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/index.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import * as React from 'react' - -// Types -import { - BraveWallet, - NFTMetadataReturnType, - DefaultCurrencies -} from '../../../../../../constants/types' - -// Hooks -import { useExplorer } from '../../../../../../common/hooks' - -// Utils -import Amount from '../../../../../../utils/amount' -import { CurrencySymbols } from '../../../../../../utils/currency-symbols' -import { getLocale } from '../../../../../../../common/locale' - -// Styled Components -import { - StyledWrapper, - NFTImage, - DetailColumn, - TokenName, - TokenFiatValue, - TokenCryptoValue, - DetailSectionRow, - DetailSectionColumn, - DetailSectionTitle, - DetailSectionValue, - ProjectDetailRow, - ProjectDetailName, - ProjectDetailDescription, - ProjectDetailImage, - ProjectDetailButtonRow, - ProjectDetailButton, - ProjectDetailButtonSeperator, - ProjectWebsiteIcon, - ProjectTwitterIcon, - ProjectFacebookIcon, - ProjectDetailIDRow, - ExplorerIcon, - ExplorerButton -} from './style' - -export interface Props { - selectedAsset: BraveWallet.BlockchainToken - nftMetadata: NFTMetadataReturnType - defaultCurrencies: DefaultCurrencies - selectedNetwork: BraveWallet.EthereumChain -} - -const NFTDetails = (props: Props) => { - const { - selectedAsset, - nftMetadata, - defaultCurrencies, - selectedNetwork - } = props - - const onClickViewOnBlockExplorer = useExplorer(selectedNetwork) - - const onClickWebsite = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.website }, () => { - if (chrome.runtime.lastError) { - console.error('tabs.create failed: ' + chrome.runtime.lastError.message) - } - }) - } - - const onClickTwitter = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.twitter }, () => { - if (chrome.runtime.lastError) { - console.error('tabs.create failed: ' + chrome.runtime.lastError.message) - } - }) - } - - const onClickFacebook = () => { - chrome.tabs.create({ url: nftMetadata.contractInformation.facebook }, () => { - if (chrome.runtime.lastError) { - console.error('tabs.create failed: ' + chrome.runtime.lastError.message) - } - }) - } - - return ( - - - - - {selectedAsset.name} { - selectedAsset.tokenId - ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' - } - - {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} - - - - ) -} - -export default NFTDetails 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 65831ffac8a7a..6152bb52b9cd6 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 @@ -9,20 +9,26 @@ import { Redirect, useHistory, useParams } from 'react-router' // types import { - BraveWallet, - UserAssetInfoType, AddAccountNavTypes, - WalletState, + BraveWallet, PageState, SupportedTestNetworks, - WalletRoutes + UserAssetInfoType, + WalletRoutes, + WalletState } from '../../../../constants/types' // Utils import Amount from '../../../../utils/amount' import { mojoTimeDeltaToJSDate } from '../../../../../common/mojomUtils' import { sortTransactionByDate } from '../../../../utils/tx-utils' -import { getTokensNetwork, getTokensCoinType } from '../../../../utils/network-utils' +import { getTokensCoinType, getTokensNetwork } from '../../../../utils/network-utils' +import { + NftUiCommand, + sendMessageToNftUiFrame, + UpdateLoadingMessage, UpdateNFtMetadataMessage, + UpdateSelectedAssetMessage, UpdateTokenNetworkMessage +} from '../../../../nft/nft-ui-messages' // actions import { WalletPageActions } from '../../../../page/actions' @@ -32,12 +38,9 @@ import { ChartTimelineOptions } from '../../../../options/chart-timeline-options import { AllNetworksOption } from '../../../../options/network-filter-options' // Components -import { BackButton, withPlaceholderIcon } from '../../../shared' -import { - ChartControlBar, - LineChart -} from '../../' -// import NFTDetails from './components/nft-details' +import { BackButton } from '../../../shared' +import withPlaceholderIcon from '../../../shared/create-placeholder-icon' +import { ChartControlBar, LineChart } from '../../' import AccountsAndTransactionsList from './components/accounts-and-transctions-list' // Hooks @@ -45,22 +48,23 @@ import { useBalance, usePricing, useTransactionParser } from '../../../../common // Styled Components import { - StyledWrapper, - TopRow, - AssetIcon, - AssetRow, + ArrowIcon, AssetColumn, - PriceRow, + AssetIcon, AssetNameText, + AssetRow, + BalanceRow, DetailText, InfoColumn, - PriceText, + NetworkDescription, + NftDetails, PercentBubble, PercentText, - ArrowIcon, - BalanceRow, + PriceRow, + PriceText, ShowBalanceButton, - NetworkDescription + StyledWrapper, + TopRow } from './style' import { Skeleton } from '../../../shared/loading-skeleton/styles' @@ -69,8 +73,9 @@ const AssetIconWithPlaceholder = withPlaceholderIcon(AssetIcon, { size: 'big', m export const PortfolioAsset = () => { // routing const history = useHistory() - const { id: assetId } = useParams<{ id?: string }>() - + const { id: assetId, tokenId } = useParams<{ id?: string, tokenId?: string }>() + const nftDetailsRef = React.useRef(null) + const [nftIframeLoaded, setNftIframeLoaded] = React.useState(false) // redux const dispatch = useDispatch() const { @@ -93,9 +98,10 @@ export const PortfolioAsset = () => { selectedAssetCryptoPrice, selectedAssetFiatPrice, selectedAssetPriceHistory, - selectedTimeline + selectedTimeline, + isFetchingNFTMetadata, + nftMetadata } = useSelector(({ page }: { page: PageState }) => page) - // custom hooks const getAccountBalance = useBalance(networkList) @@ -164,9 +170,9 @@ export const PortfolioAsset = () => { // If the id length is greater than 15 assumes it's a contractAddress return assetId.length > 15 - ? userVisibleTokensInfo.find((token) => token.contractAddress === assetId) + ? userVisibleTokensInfo.find((token) => tokenId ? token.contractAddress === assetId && token.tokenId === tokenId : token.contractAddress === assetId) : userVisibleTokensInfo.find((token) => token.symbol.toLowerCase() === assetId?.toLowerCase()) - }, [assetId, userVisibleTokensInfo, selectedTimeline]) + }, [assetId, userVisibleTokensInfo, selectedTimeline, tokenId]) // This will scrape all of the user's accounts and combine the fiat value for every asset const fullPortfolioFiatBalance = React.useMemo(() => { @@ -287,6 +293,8 @@ export const PortfolioAsset = () => { setHideBalances(prevHideBalances => !prevHideBalances) }, []) + const onNftDetailsLoad = React.useCallback(() => setNftIframeLoaded(true), []) + // effects React.useEffect(() => { setfilteredAssetList(userAssetList) @@ -299,6 +307,47 @@ export const PortfolioAsset = () => { } }, [selectedAssetFromParams]) + React.useEffect(() => { + if (!nftIframeLoaded) return + + if (nftDetailsRef?.current) { + const command: UpdateLoadingMessage = { + command: NftUiCommand.UpdateLoading, + payload: isFetchingNFTMetadata + } + sendMessageToNftUiFrame(nftDetailsRef.current.contentWindow, command) + } + }, [nftIframeLoaded, nftDetailsRef, isFetchingNFTMetadata]) + + React.useEffect(() => { + if (!nftIframeLoaded) return + + if (selectedAsset && nftDetailsRef?.current) { + const command: UpdateSelectedAssetMessage = { + command: NftUiCommand.UpdateSelectedAsset, + payload: selectedAsset + } + sendMessageToNftUiFrame(nftDetailsRef.current.contentWindow, command) + } + + if (selectedAsset && networkList && nftDetailsRef?.current) { + const tokenNetwork = getTokensNetwork(networkList, selectedAsset) + const command: UpdateTokenNetworkMessage = { + command: NftUiCommand.UpdateTokenNetwork, + payload: tokenNetwork + } + sendMessageToNftUiFrame(nftDetailsRef.current.contentWindow, command) + } + + if (nftMetadata && nftDetailsRef?.current) { + const command: UpdateNFtMetadataMessage = { + command: NftUiCommand.UpdateNFTMetadata, + payload: nftMetadata + } + sendMessageToNftUiFrame(nftDetailsRef.current.contentWindow, command) + } + }, [nftIframeLoaded, nftDetailsRef, selectedAsset, nftMetadata, networkList]) + // token list needs to load before we can find an asset to select from the url params if (userVisibleTokensInfo.length === 0) { return @@ -377,15 +426,13 @@ export const PortfolioAsset = () => { /> } - {/* {selectedAsset?.isErc721 && - - } */} + { selectedNetworkFilter } = useSelector(({ wallet }: { wallet: WalletState }) => wallet) - const selectedTimeline = useSelector(({ page }: { page: PageState }) => page.selectedTimeline) + const { selectedTimeline, nftMetadata } = useSelector(({ page }: { page: PageState }) => page) // custom hooks const getAccountAssetBalance = useBalance(networkList) @@ -173,10 +173,17 @@ export const PortfolioOverview = () => { if (asset.contractAddress === '') { history.push(`${WalletRoutes.Portfolio}/${asset.symbol}`) return + } else if (asset.isErc721) { + history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}/${asset.tokenId}`) + } else { + history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}`) } - history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}`) dispatch(WalletPageActions.selectAsset({ asset, timeFrame: selectedTimeline })) + if (asset.isErc721 && nftMetadata) { + // reset nft metadata + dispatch(WalletPageActions.updateNFTMetadata(undefined)) + } }, [selectedTimeline]) const onUpdateBalance = React.useCallback((value: number | undefined) => { diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts b/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts index bd11d26328dcf..fd31ce9b8624f 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts @@ -4,11 +4,6 @@ import EyeOnIcon from '../../../../assets/svg-icons/eye-on-icon.svg' import EyeOffIcon from '../../../../assets/svg-icons/eye-off-icon.svg' import { AssetIconProps, AssetIconFactory, WalletButton } from '../../../shared/style' -interface StyleProps { - isDown: boolean - hideBalances: boolean -} - export const StyledWrapper = styled.div` display: flex; flex-direction: column; @@ -150,7 +145,7 @@ export const DividerText = styled.span` color: ${(p) => p.theme.color.text03}; ` -export const PercentBubble = styled.div>` +export const PercentBubble = styled.div<{ isDown?: boolean}>` display: flex; align-items: center; justify-conent: center; @@ -168,7 +163,7 @@ export const PercentText = styled.span` color: ${(p) => p.theme.palette.white}; ` -export const ArrowIcon = styled(ArrowUpIcon) >` +export const ArrowIcon = styled(ArrowUpIcon) <{ isDown?: boolean }>` width: 12px; height: 12px; margin-right: 2px; @@ -227,7 +222,7 @@ export const CoinGeckoText = styled.span` margin: 15px 0px; ` -export const ShowBalanceButton = styled(WalletButton) >` +export const ShowBalanceButton = styled(WalletButton) <{ hideBalances?: boolean}>` display: flex; align-items: center; justify-content: center; @@ -253,3 +248,10 @@ export const FilterTokenRow = styled.div` flex-direction: row; width: 100%; ` + +export const NftDetails = styled.iframe<{ visible?: boolean }>` + width: 100%; + min-height: ${p => p.visible ? '490px' : 'hidden'}; + border: none; + visibility: ${p => p.visible ? 'visible' : 'hidden'}; +` diff --git a/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx b/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx index 735e59677cbad..51634228fe50c 100644 --- a/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx +++ b/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx @@ -5,7 +5,7 @@ import { background } from 'ethereum-blockies' import { BraveWallet } from '../../../constants/types' // Utils -import { stripERC20TokenImageURL, isRemoteImageURL, isValidIconExtension } from '../../../utils/string-utils' +import { stripERC20TokenImageURL, isRemoteImageURL, isValidIconExtension, httpifyIpfsUrl } from '../../../utils/string-utils' // Styled components import { IconWrapper, PlaceholderText } from './style' @@ -55,8 +55,7 @@ function withPlaceholderIcon (WrappedComponent: React.ComponentType, 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/nft-icon/nft-icon-styles.ts b/components/brave_wallet_ui/components/shared/nft-icon/nft-icon-styles.ts new file mode 100644 index 0000000000000..0796c005e6381 --- /dev/null +++ b/components/brave_wallet_ui/components/shared/nft-icon/nft-icon-styles.ts @@ -0,0 +1,12 @@ +// 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/. + +import styled from 'styled-components' + +export const NftImageIframe = styled.iframe` + border: none; + width: ${p => p.width ? p.width : '40px'}; + height: ${p => p.height ? p.height : '40px'}; +` diff --git a/components/brave_wallet_ui/components/shared/nft-icon/nft-icon.tsx b/components/brave_wallet_ui/components/shared/nft-icon/nft-icon.tsx new file mode 100644 index 0000000000000..29e02697015bc --- /dev/null +++ b/components/brave_wallet_ui/components/shared/nft-icon/nft-icon.tsx @@ -0,0 +1,39 @@ +// 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/. + +import * as React from 'react' + +import { AssetIconProps } from '../style' +import { + NftUiCommand, + sendMessageToNftUiFrame, + UpdateNftImageUrl +} from '../../../nft/nft-ui-messages' + +import { NftImageIframe } from './nft-icon-styles' + +export const NftIcon = ({ icon }: AssetIconProps) => { + const [loaded, setLoaded] = React.useState() + const nftImageIframeRef = React.useRef(null) + + React.useEffect(() => { + if (loaded && icon && nftImageIframeRef?.current) { + const command: UpdateNftImageUrl = { + command: NftUiCommand.UpdateNFTImageUrl, + payload: icon + } + sendMessageToNftUiFrame(nftImageIframeRef.current.contentWindow, command) + } + }, [loaded, icon, nftImageIframeRef]) + + return ( + setLoaded(true)} + ref={nftImageIframeRef} + src="chrome-untrusted://nft-display" + sandbox="allow-scripts allow-same-origin" + /> + ) +} diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index e828f52fe4030..1904813cbf4f0 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -265,6 +265,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[] @@ -609,7 +611,7 @@ export enum WalletRoutes { // portfolio Portfolio = '/crypto/portfolio', - PortfolioAsset = '/crypto/portfolio/:id', + PortfolioAsset = '/crypto/portfolio/:id/:tokenId?', // portfolio asset modals AddAssetModal = '/crypto/portfolio/add-asset', @@ -630,11 +632,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 0000000000000..5ff181c8adb46 --- /dev/null +++ b/components/brave_wallet_ui/nft/BUILD.gn @@ -0,0 +1,29 @@ +# 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/. + +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.tsx"), + ] ] + webpack_aliases = [ "browser" ] + resource_name = "nft_display" + deps = [ + "//brave/components/brave_wallet/common:mojom_js", + "//brave/components/brave_wallet/common:preprocess_mojo", + ] +} + +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/components/nft-content/nft-content-styles.ts b/components/brave_wallet_ui/nft/components/nft-content/nft-content-styles.ts new file mode 100644 index 0000000000000..75c6a3bde21d7 --- /dev/null +++ b/components/brave_wallet_ui/nft/components/nft-content/nft-content-styles.ts @@ -0,0 +1,35 @@ +// 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/. + +import styled from 'styled-components' +import { LoaderIcon } from 'brave-ui/components/icons' + +export const Image = styled.img<{ + customWidth?: string + customHeight?: string +}>` + border-radius: 4px; + width: 100%; + height: auto +` + +export const LoadingOverlay = styled.div<{isLoading: boolean}>` + 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/nft/components/nft-content/nft-content.tsx b/components/brave_wallet_ui/nft/components/nft-content/nft-content.tsx new file mode 100644 index 0000000000000..e3908b88b937a --- /dev/null +++ b/components/brave_wallet_ui/nft/components/nft-content/nft-content.tsx @@ -0,0 +1,52 @@ +// 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/. + +import * as React from 'react' + +// components +import { Image, LoadingOverlay, LoadIcon } from './nft-content-styles' +import { NftDetails } from '../nft-details/nft-details' + +// utils +import { BraveWallet, NFTMetadataReturnType } from '../../../constants/types' + +interface Props { + isLoading?: boolean + selectedAsset?: BraveWallet.BlockchainToken + nftMetadata?: NFTMetadataReturnType + tokenNetwork?: BraveWallet.NetworkInfo + imageUrl?: string +} + +export const NftContent = (props: Props) => { + const { + isLoading, + selectedAsset, + imageUrl + } = props + + return ( + <> + {imageUrl + ? + : <> + {isLoading && + + + + } + {selectedAsset && + + } + + } + + ) +} diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts b/components/brave_wallet_ui/nft/components/nft-details/nft-details-styles.ts similarity index 76% rename from components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts rename to components/brave_wallet_ui/nft/components/nft-details/nft-details-styles.ts index 78b7e922c7781..226545a40ef8b 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/nft-details/style.ts +++ b/components/brave_wallet_ui/nft/components/nft-details/nft-details-styles.ts @@ -1,22 +1,49 @@ +// 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/. + import styled from 'styled-components' -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 { WalletButton } from '../../../components/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' +export const nftImageDimension = '440px' + export const StyledWrapper = styled.div` display: flex; flex-direction: row; 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 NftImageWrapper = styled.div<{ isLoading: boolean }>` + display: flex; + width: ${p => p.isLoading ? 0 : nftImageDimension}; + height: ${p => p.isLoading ? 0 : nftImageDimension}; + visibility: ${p => p.isLoading ? 'hidden' : 'visible'}; + margin-right: ${p => p.isLoading ? 0 : '28px'};; + border-radius: 12px; +` + +export const NTFImage = styled.img` + display: block; + width: ${nftImageDimension}; + height: ${nftImageDimension}; + border: transparent; + border-radius: 4px; +` + +export const NFTImageSkeletonWrapper = styled.div` + min-width: ${nftImageDimension}; + height: ${nftImageDimension}; + margin-right: 28px; + border-radius: 12px; ` export const DetailColumn = styled.div` diff --git a/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx b/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx new file mode 100644 index 0000000000000..a8398f31995df --- /dev/null +++ b/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx @@ -0,0 +1,155 @@ +// 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/. + +import * as React from 'react' + +// Types +import { + BraveWallet, + NFTMetadataReturnType +} from '../../../constants/types' + +// Hooks +import { useExplorer } from '../../../common/hooks' + +// Utils +import Amount from '../../../utils/amount' +import { getLocale } from '$web-common/locale' + +// Styled Components +import { + StyledWrapper, + DetailColumn, + TokenName, + DetailSectionRow, + DetailSectionColumn, + DetailSectionTitle, + DetailSectionValue, + ProjectDetailRow, + ProjectDetailName, + ProjectDetailDescription, + ProjectDetailButtonRow, + ProjectDetailButton, + ProjectDetailButtonSeperator, + ProjectWebsiteIcon, + ProjectTwitterIcon, + ProjectFacebookIcon, + ProjectDetailIDRow, + ExplorerIcon, + ExplorerButton, + NTFImage, + NftImageWrapper, + NFTImageSkeletonWrapper +} from './nft-details-styles' +import { LoadingSkeleton } from '../../../components/shared' +import { isValidateUrl } from '../../../utils/string-utils' + +interface Props { + isLoading?: boolean + selectedAsset: BraveWallet.BlockchainToken + nftMetadata?: NFTMetadataReturnType + tokenNetwork?: BraveWallet.NetworkInfo +} + +export const NftDetails = ({ selectedAsset, nftMetadata, tokenNetwork }: Props) => { + const [isImageLoaded, setIsImageLoaded] = React.useState() + const onClickViewOnBlockExplorer = useExplorer(tokenNetwork || new BraveWallet.NetworkInfo()) + + const onClickLink = React.useCallback((url?: string) => { + if (url && isValidateUrl(url)) { + chrome.tabs.create({ url }, () => { + if (chrome.runtime.lastError) { + console.error('tabs.create failed: ' + chrome.runtime.lastError.message) + } + }) + } + }, []) + + const onClickWebsite = () => { + onClickLink(nftMetadata?.contractInformation?.website) + } + + const onClickTwitter = () => { + onClickLink(nftMetadata?.contractInformation?.twitter) + } + + const onClickFacebook = () => { + onClickLink(nftMetadata?.contractInformation?.facebook) + } + + return ( + + {nftMetadata && + <> + + setIsImageLoaded(true)} /> + + {!isImageLoaded && + + } + + + {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/nft/css/nft-global.css b/components/brave_wallet_ui/nft/css/nft-global.css new file mode 100644 index 0000000000000..f6f779910a565 --- /dev/null +++ b/components/brave_wallet_ui/nft/css/nft-global.css @@ -0,0 +1,12 @@ +/*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/.*/ + +html, body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} diff --git a/components/brave_wallet_ui/nft/nft-ui-messages.ts b/components/brave_wallet_ui/nft/nft-ui-messages.ts new file mode 100644 index 0000000000000..6f89b7b1d7e7c --- /dev/null +++ b/components/brave_wallet_ui/nft/nft-ui-messages.ts @@ -0,0 +1,51 @@ +// 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/. + +import { loadTimeData } from '../../common/loadTimeData' +import { BraveWallet, NFTMetadataReturnType } from '../constants/types' + +const nftDisplayOrigin = loadTimeData.getString('braveWalletNftBridgeUrl') +// remove trailing / +export const braveNftDisplayOrigin = nftDisplayOrigin.endsWith('/') ? nftDisplayOrigin.slice(0, -1) : nftDisplayOrigin + +export const braveWalletOrigin = 'chrome://wallet' + +export const enum NftUiCommand { + UpdateLoading = 'update-loading', + UpdateSelectedAsset = 'update-selected-asset', + UpdateNFTMetadata = 'update-nft-metadata', + UpdateTokenNetwork = 'update-token-network', + UpdateNFTImageUrl = 'update-nft-image-url' +} + +export type CommandMessage = { + command: NftUiCommand +} + +export type UpdateLoadingMessage = CommandMessage & { + payload: boolean +} + +export type UpdateSelectedAssetMessage = CommandMessage & { + payload: BraveWallet.BlockchainToken +} + +export type UpdateNFtMetadataMessage = CommandMessage & { + payload: NFTMetadataReturnType +} + +export type UpdateTokenNetworkMessage = CommandMessage & { + payload: BraveWallet.NetworkInfo +} + +export type UpdateNftImageUrl = CommandMessage & { + payload: string +} + +export const sendMessageToNftUiFrame = (targetWindow: Window | null, message: CommandMessage) => { + if (targetWindow) { + targetWindow.postMessage(message, braveNftDisplayOrigin) + } +} diff --git a/components/brave_wallet_ui/nft/nft.html b/components/brave_wallet_ui/nft/nft.html new file mode 100644 index 0000000000000..e63501df6790a --- /dev/null +++ b/components/brave_wallet_ui/nft/nft.html @@ -0,0 +1,16 @@ + + + + + + + + + NFT iframe + + + + + +
+ diff --git a/components/brave_wallet_ui/nft/nft.tsx b/components/brave_wallet_ui/nft/nft.tsx new file mode 100644 index 0000000000000..8ce82df9be218 --- /dev/null +++ b/components/brave_wallet_ui/nft/nft.tsx @@ -0,0 +1,113 @@ +// 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/. + +import * as React from 'react' +import { render } from 'react-dom' +import { BrowserRouter } from 'react-router-dom' +import { initLocale } from 'brave-ui' +import { loadTimeData } from '../../common/loadTimeData' + +// css +import 'emptykit.css' +import '../../../ui/webui/resources/fonts/poppins.css' +import './css/nft-global.css' + +// theme setup +import BraveCoreThemeProvider from '../../common/BraveCoreThemeProvider' +import walletDarkTheme from '../theme/wallet-dark' +import walletLightTheme from '../theme/wallet-light' + +// utils +import { + braveWalletOrigin, + CommandMessage, + NftUiCommand, + UpdateLoadingMessage, UpdateNftImageUrl, + UpdateNFtMetadataMessage, + UpdateSelectedAssetMessage, + UpdateTokenNetworkMessage +} from './nft-ui-messages' +import { BraveWallet, NFTMetadataReturnType } from '../constants/types' + +// components +import { NftContent } from './components/nft-content/nft-content' + +const App = () => { + const [loadingNftMetadata, setLoadingNftMetadata] = React.useState(true) + const [selectedAsset, setSelectedAsset] = React.useState() + const [nftMetadata, setNftMetadata] = React.useState() + const [tokenNetwork, setTokenNetwork] = React.useState() + const [imageUrl, setImageUrl] = React.useState() + + // handle postMessage from wallet ui by setting component state + // each message has a payload parameter containing the event data + const onMessageEventListener = React.useCallback((event: MessageEvent) => { + // validate message origin + if (event.origin !== braveWalletOrigin) return + + const message = event.data + switch (message.command) { + case NftUiCommand.UpdateLoading: { + const { payload } = message as UpdateLoadingMessage + setLoadingNftMetadata(payload) + break + } + + case NftUiCommand.UpdateSelectedAsset: { + const { payload } = message as UpdateSelectedAssetMessage + setSelectedAsset(payload) + break + } + + case NftUiCommand.UpdateNFTMetadata: { + const { payload } = message as UpdateNFtMetadataMessage + setNftMetadata(payload) + break + } + + case NftUiCommand.UpdateTokenNetwork: { + const { payload } = message as UpdateTokenNetworkMessage + setTokenNetwork(payload) + break + } + + case NftUiCommand.UpdateNFTImageUrl: { + const { payload } = message as UpdateNftImageUrl + setImageUrl(payload) + break + } + } + }, []) + + React.useEffect(() => { + // add event listener for postMessage from wallet ui + window.addEventListener('message', onMessageEventListener) + return () => window.removeEventListener('message', onMessageEventListener) + }, []) + + return ( + + + + + + ) +} + +function initialize () { + initLocale(loadTimeData.data_) + render(, document.getElementById('root')) +} + +document.addEventListener('DOMContentLoaded', initialize) 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 1b1d49d25e41f..fb976c409647c 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,4 +59,7 @@ 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') export const onOnboardingShown = createAction('onOnboardingShown') 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 ae7f30f1f72bb..1f6cb532f2d21 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,8 @@ import { import { NewUnapprovedTxAdded } from '../../common/constants/action_types' import { Store } from '../../common/async/types' import { getTokenParam } from '../../utils/api-utils' +import { getTokensNetwork } from '../../utils/network-utils' +import { httpifyIpfsUrl } from '../../utils/string-utils' const handler = new AsyncActionHandler() @@ -86,6 +89,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 })) } @@ -218,6 +225,38 @@ handler.on(WalletPageActions.openWalletSettings.getType(), async (store) => { }) }) +handler.on(WalletPageActions.getNFTMetadata.getType(), async (store, payload: BraveWallet.BlockchainToken) => { + store.dispatch(WalletPageActions.setIsFetchingNFTMetadata(true)) + const jsonRpcService = getWalletPageApiProxy().jsonRpcService + 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)) +}) + handler.on(WalletPageActions.onOnboardingShown.getType(), async (store: Store) => { const braveWalletService = getWalletPageApiProxy().braveWalletService await braveWalletService.onOnboardingShown() diff --git a/components/brave_wallet_ui/page/reducers/page_reducer.ts b/components/brave_wallet_ui/page/reducers/page_reducer.ts index 78862d2538d65..2d93cd4a8807d 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 3849c20f592e3..9c2f318e6043c 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 9140f9dd262e2..099b59065c890 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/account-utils.ts b/components/brave_wallet_ui/utils/account-utils.ts index ffbd7c207cfc8..9a0bac874e7b7 100644 --- a/components/brave_wallet_ui/utils/account-utils.ts +++ b/components/brave_wallet_ui/utils/account-utils.ts @@ -3,7 +3,7 @@ // 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 { WalletAccountType } from '../constants/types' +import { BraveWallet, WalletAccountType } from '../constants/types' export const sortAccountsByName = (accounts: WalletAccountType[]) => { return [...accounts].sort(function (a: WalletAccountType, b: WalletAccountType) { @@ -29,3 +29,7 @@ export const groupAccountsById = (accounts: WalletAccountType[], key: string) => export const findAccountName = (accounts: WalletAccountType[], address: string) => { return accounts.find((account) => account.address.toLowerCase() === address.toLowerCase())?.name } + +export const createTokenBalanceRegistryKey = (token: BraveWallet.BlockchainToken) => { + return token.isErc721 ? `${token.contractAddress.toLowerCase()}#${token.tokenId}` : token.contractAddress.toLowerCase() +} diff --git a/components/brave_wallet_ui/utils/copy-to-clipboard.ts b/components/brave_wallet_ui/utils/copy-to-clipboard.ts index 2d09d70a54d8b..c21ae08ae9050 100644 --- a/components/brave_wallet_ui/utils/copy-to-clipboard.ts +++ b/components/brave_wallet_ui/utils/copy-to-clipboard.ts @@ -1,6 +1,6 @@ export const copyToClipboard = async (data: string) => { try { - await navigator.clipboard.writeText(data) + await navigator.clipboard?.writeText(data) } catch (e) { console.log(`Could not copy address ${e.toString()}`) } diff --git a/components/brave_wallet_ui/utils/string-utils.ts b/components/brave_wallet_ui/utils/string-utils.ts index c08432b0b0251..b8fa5696aba4e 100644 --- a/components/brave_wallet_ui/utils/string-utils.ts +++ b/components/brave_wallet_ui/utils/string-utils.ts @@ -8,10 +8,15 @@ 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) => { + const trimmedUrl = url ? url.trim() : '' + return trimmedUrl.startsWith('ipfs://') ? trimmedUrl.replace('ipfs://', 'https://ipfs.io/ipfs/') : trimmedUrl +} export const getRampNetworkPrefix = (chainId: string) => { switch (chainId) { @@ -37,3 +42,8 @@ export const getRampNetworkPrefix = (chainId: string) => { export const formatAsDouble = (value: string): string => // Removes all characters except numbers, commas and decimals value.replace(/[^0-9.,]+/g, '') + +export const isValidateUrl = (url: string) => { + const re = /^\s*https?:\/\// + return re.test(url) +} diff --git a/components/constants/webui_url_constants.cc b/components/constants/webui_url_constants.cc index a58c0bddba4fb..ac53b351f84d6 100644 --- a/components/constants/webui_url_constants.cc +++ b/components/constants/webui_url_constants.cc @@ -39,6 +39,8 @@ const char kBraveSyncSetupPath[] = "braveSync/setup"; const char kTorInternalsHost[] = "tor-internals"; const char kUntrustedLedgerHost[] = "ledger-bridge"; const char kUntrustedLedgerURL[] = "chrome-untrusted://ledger-bridge/"; +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/components/constants/webui_url_constants.h b/components/constants/webui_url_constants.h index 7731467345474..29e02836e2826 100644 --- a/components/constants/webui_url_constants.h +++ b/components/constants/webui_url_constants.h @@ -40,6 +40,8 @@ extern const char kBraveSyncSetupPath[]; extern const char kTorInternalsHost[]; extern const char kUntrustedLedgerHost[]; extern const char kUntrustedLedgerURL[]; +extern const char kUntrustedNftHost[]; +extern const char kUntrustedNftURL[]; extern const char kUntrustedTrezorHost[]; extern const char kUntrustedTrezorURL[]; extern const char kShieldsPanelURL[]; diff --git a/components/resources/brave_components_resources.grd b/components/resources/brave_components_resources.grd index 3b09ad185d860..d1a23b4acb507 100644 --- a/components/resources/brave_components_resources.grd +++ b/components/resources/brave_components_resources.grd @@ -39,6 +39,7 @@ + diff --git a/resources/resource_ids.spec b/resources/resource_ids.spec index d41fbe2d1f378..b69a334d45288 100644 --- a/resources/resource_ids.spec +++ b/resources/resource_ids.spec @@ -200,4 +200,9 @@ "META": {"sizes": {"includes": [250]}}, "includes": [59270] }, + # This file is generated during the build. + "<(SHARED_INTERMEDIATE_DIR)/brave/web-ui-nft_display/nft_display.grd": { + "META": {"sizes": {"includes": [250]}}, + "includes": [59520] + }, }