Skip to content

Commit

Permalink
NFT Details Screen (#13370)
Browse files Browse the repository at this point in the history
* 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 440134d. This will be fixed in brave/brave-browser#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
  • Loading branch information
muliswilliam committed Jul 28, 2022
1 parent 15ece28 commit 0b6165f
Show file tree
Hide file tree
Showing 46 changed files with 925 additions and 236 deletions.
3 changes: 3 additions & 0 deletions browser/ui/BUILD.gn
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
78 changes: 78 additions & 0 deletions 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 <string>

#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<content::WebUIController>
UntrustedNftUIConfig::CreateWebUIController(content::WebUI* web_ui) {
return std::make_unique<UntrustedNftUI>(web_ui);
}

UntrustedNftUIConfig::UntrustedNftUIConfig()
: WebUIConfig(content::kChromeUIUntrustedScheme, kUntrustedNftHost) {}

} // namespace nft
37 changes: 37 additions & 0 deletions 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 <memory>

#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<content::WebUIController> CreateWebUIController(
content::WebUI* web_ui) override;
};

} // namespace nft

#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_NFT_NFT_UI_H_
4 changes: 4 additions & 0 deletions browser/ui/webui/brave_wallet/wallet_page_ui.cc
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions browser/ui/webui/brave_wallet/wallet_panel_ui.cc
Expand Up @@ -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();
}
Expand Down
Expand Up @@ -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"
Expand All @@ -30,6 +31,8 @@ void RegisterChromeUntrustedWebUIConfigs() {
std::make_unique<ledger::UntrustedLedgerUIConfig>());
content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<trezor::UntrustedTrezorUIConfig>());
content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<nft::UntrustedNftUIConfig>());
#if BUILDFLAG(ENABLE_BRAVE_VPN)
if (brave_vpn::IsBraveVPNEnabled()) {
content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
Expand Down
2 changes: 2 additions & 0 deletions components/brave_wallet_ui/BUILD.gn
Expand Up @@ -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",
Expand All @@ -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 =
Expand Down
11 changes: 10 additions & 1 deletion components/brave_wallet_ui/common/async/handlers.ts
Expand Up @@ -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))
})
Expand Down
Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions components/brave_wallet_ui/common/hooks/assets.ts
Expand Up @@ -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 () {
Expand Down
4 changes: 3 additions & 1 deletion components/brave_wallet_ui/common/hooks/balance.ts
Expand Up @@ -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) => {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion components/brave_wallet_ui/common/hooks/explorer.ts
Expand Up @@ -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 =
Expand Down
6 changes: 4 additions & 2 deletions components/brave_wallet_ui/common/reducers/wallet_reducer.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
})
})
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Expand Up @@ -570,7 +570,7 @@ const EditVisibleAssetsModal = ({ onClose }: Props) => {
<>
{filteredTokenList.slice(0, tokenDisplayAmount).map((token) =>
<AssetWatchlistItem
key={`${token.contractAddress}-${token.symbol}-${token.chainId}`}
key={`${token.contractAddress}-${token.symbol}-${token.chainId}-${token.tokenId}`}
isCustom={isCustomToken(token)}
token={token}
networkList={networkList}
Expand Down
Expand Up @@ -30,6 +30,7 @@ import { getTokensNetwork } from '../../../utils/network-utils'
// Hooks
import { usePricing } from '../../../common/hooks'
import { unbiasedRandom } from '../../../utils/random-utils'
import { NftIcon } from '../../shared/nft-icon/nft-icon'

interface Props {
spotPrices: BraveWallet.AssetPrice[]
Expand Down Expand Up @@ -57,8 +58,8 @@ const PortfolioAssetItem = (props: Props) => {
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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
<StyledWrapper disabled={token.isErc721 || isLoading} onClick={action}>
<StyledWrapper disabled={isLoading} onClick={action}>
<NameAndIcon>
<IconsWrapper>
{!token.logo
Expand Down
Expand Up @@ -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<AssetIconProps>({
const assetIconProps = {
width: '40px',
height: 'auto'
})
}
export const AssetIcon = AssetIconFactory<AssetIconProps>(assetIconProps)

export const IconsWrapper = styled.div`
display: flex;
Expand Down

0 comments on commit 0b6165f

Please sign in to comment.