Skip to content

Commit

Permalink
Add NFT details page
Browse files Browse the repository at this point in the history
  • Loading branch information
muliswilliam committed Apr 29, 2022
1 parent 7a7eaba commit ae7404e
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 97 deletions.
11 changes: 10 additions & 1 deletion components/brave_wallet_ui/common/async/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,16 @@ handler.on(WalletActions.getAllTokensList.getType(), async (store) => {
})

handler.on(WalletActions.addUserAsset.getType(), async (store: Store, payload: BraveWallet.BlockchainToken) => {
const { braveWalletService } = getAPIProxy()
const { braveWalletService, jsonRpcService } = getAPIProxy()
if (payload.isErc721) {
// Get NFTMetadata
const result = await jsonRpcService.getERC721Metadata(payload.contractAddress, payload.tokenId, payload.chainId)
if (!result.error) {
const response = JSON.parse(result.response)
payload.logo = response.image || payload.logo
}
}

const result = await braveWalletService.addUserAsset(payload)
store.dispatch(WalletActions.addUserAssetError(!result.success))
})
Expand Down
24 changes: 20 additions & 4 deletions components/brave_wallet_ui/common/hooks/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ import usePricing from './pricing'
import useBalance from './balance'
import { useIsMounted } from './useIsMounted'
import { useLib } from './useLib'
import { httpifyIpfsUrl } from '../../utils/string-utils'

const assetsLogo = (assets: BraveWallet.BlockchainToken[]) => {
return assets.map(token => {
let logo = token.logo
if (token.logo?.startsWith('ipfs://')) {
logo = httpifyIpfsUrl(token.logo)
} else if (token.logo?.startsWith('data:image/')) {
logo = token.logo
} else {
logo = `chrome://erc-token-images/${token.logo}`
}

return {
...token,
logo
} as BraveWallet.BlockchainToken
})
}

export function useAssets () {
// redux
Expand Down Expand Up @@ -54,10 +73,7 @@ export function useAssets () {
React.useEffect(() => {
isMounted && getBuyAssets().then(tokens => {
if (isMounted) {
setBuyAssetOptions(tokens.map(token => ({
...token,
logo: `chrome://erc-token-images/${token.logo}`
}) as BraveWallet.BlockchainToken))
setBuyAssetOptions(assetsLogo(tokens))
}
}).catch(e => console.error(e))
}, [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ const EditVisibleAssetsModal = (props: 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
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,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 +111,7 @@ const PortfolioAssetItem = (props: Props) => {
{token.visible &&
// Selecting an erc721 token is temp disabled until UI is ready for viewing NFTs
// or when showing loading skeleton
<StyledWrapper disabled={token.isErc721 || isLoading} onClick={action}>
<StyledWrapper disabled={isLoading} onClick={action}>
<NameAndIcon>
<IconsWrapper>
{isLoading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,23 @@ import { useExplorer } from '../../../../../../common/hooks'

// Utils
import Amount from '../../../../../../utils/amount'
import { CurrencySymbols } from '../../../../../../utils/currency-symbols'
import { getLocale } from '../../../../../../../common/locale'
import { getLocale } from '$web-common/locale'

// Styled Components
import {
StyledWrapper,
LoadIcon,
LoadingOverlay,
NFTImage,
DetailColumn,
TokenName,
TokenFiatValue,
TokenCryptoValue,
DetailSectionRow,
DetailSectionColumn,
DetailSectionTitle,
DetailSectionValue,
ProjectDetailRow,
ProjectDetailName,
ProjectDetailDescription,
ProjectDetailImage,
ProjectDetailButtonRow,
ProjectDetailButton,
ProjectDetailButtonSeperator,
Expand All @@ -39,44 +37,48 @@ import {
ProjectFacebookIcon,
ProjectDetailIDRow,
ExplorerIcon,
ExplorerButton
ExplorerButton,
NFTImageSkeletonWrapper
} from './style'
import { LoadingSkeleton } from '../../../../../shared'

export interface Props {
isLoading: boolean
selectedAsset: BraveWallet.BlockchainToken
nftMetadata: NFTMetadataReturnType
nftMetadata: NFTMetadataReturnType | undefined
defaultCurrencies: DefaultCurrencies
selectedNetwork: BraveWallet.EthereumChain
selectedNetwork: BraveWallet.NetworkInfo
}

const NFTDetails = (props: Props) => {
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
const {
isLoading,
selectedAsset,
nftMetadata,
defaultCurrencies,
selectedNetwork
} = props

const onClickViewOnBlockExplorer = useExplorer(selectedNetwork)

const onClickWebsite = () => {
chrome.tabs.create({ url: nftMetadata.contractInformation.website }, () => {
chrome.tabs.create({ url: nftMetadata?.contractInformation?.website }, () => {
if (chrome.runtime.lastError) {
console.error('tabs.create failed: ' + chrome.runtime.lastError.message)
}
})
}

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)
}
})
}

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)
}
Expand All @@ -85,61 +87,81 @@ const NFTDetails = (props: Props) => {

return (
<StyledWrapper>
<NFTImage src={selectedAsset.logo} />
<DetailColumn>
<TokenName>
{selectedAsset.name} {
selectedAsset.tokenId
? '#' + new Amount(selectedAsset.tokenId).toNumber()
: ''
{isLoading &&
<LoadingOverlay isLoading={isLoading}>
<LoadIcon />
</LoadingOverlay>
}
{nftMetadata &&
<>
<NFTImage
isLoading={!isImageLoaded}
src={nftMetadata.imageURL}
onLoad={() => setIsImageLoaded(true)}
/>
{!isImageLoaded &&
<LoadingSkeleton wrapper={NFTImageSkeletonWrapper} />
}
</TokenName>
<TokenFiatValue>{CurrencySymbols[defaultCurrencies.fiat]}{nftMetadata.floorFiatPrice}</TokenFiatValue>
<TokenCryptoValue>{nftMetadata.floorCryptoPrice} {selectedNetwork.symbol}</TokenCryptoValue>
<DetailSectionRow>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailBlockchain')}</DetailSectionTitle>
<DetailSectionValue>{nftMetadata.chainName}</DetailSectionValue>
</DetailSectionColumn>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailTokenStandard')}</DetailSectionTitle>
<DetailSectionValue>{nftMetadata.tokenType}</DetailSectionValue>
</DetailSectionColumn>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailTokenID')}</DetailSectionTitle>
<ProjectDetailIDRow>
<DetailSectionValue>
{
selectedAsset.tokenId
? '#' + new Amount(selectedAsset.tokenId).toNumber()
: ''
}
</DetailSectionValue>
<ExplorerButton onClick={onClickViewOnBlockExplorer('contract', selectedAsset.contractAddress, selectedAsset.tokenId)}>
<ExplorerIcon />
</ExplorerButton>
</ProjectDetailIDRow>
</DetailSectionColumn>
</DetailSectionRow>
<ProjectDetailRow>
<ProjectDetailImage src={nftMetadata.contractInformation.logo} />
<ProjectDetailName>{nftMetadata.contractInformation.name}</ProjectDetailName>
<ProjectDetailButtonRow>
<ProjectDetailButton onClick={onClickWebsite}>
<ProjectWebsiteIcon />
</ProjectDetailButton>
<ProjectDetailButtonSeperator />
<ProjectDetailButton onClick={onClickTwitter}>
<ProjectTwitterIcon />
</ProjectDetailButton>
<ProjectDetailButtonSeperator />
<ProjectDetailButton onClick={onClickFacebook}>
<ProjectFacebookIcon />
</ProjectDetailButton>
</ProjectDetailButtonRow>
</ProjectDetailRow>
<ProjectDetailDescription>{nftMetadata.contractInformation.description}</ProjectDetailDescription>
</DetailColumn>
<DetailColumn>
<TokenName>
{selectedAsset.name} {
selectedAsset.tokenId
? '#' + new Amount(selectedAsset.tokenId).toNumber()
: ''
}
</TokenName>
{/* TODO: Add floorFiatPrice & floorCryptoPrice when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */}
{/* <TokenFiatValue>{CurrencySymbols[defaultCurrencies.fiat]}{nftMetadata.floorFiatPrice}</TokenFiatValue> */}
{/* <TokenCryptoValue>{nftMetadata.floorCryptoPrice} {selectedNetwork.symbol}</TokenCryptoValue> */}
<DetailSectionRow>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailBlockchain')}</DetailSectionTitle>
<DetailSectionValue>{nftMetadata.chainName}</DetailSectionValue>
</DetailSectionColumn>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailTokenStandard')}</DetailSectionTitle>
<DetailSectionValue>{nftMetadata.tokenType}</DetailSectionValue>
</DetailSectionColumn>
<DetailSectionColumn>
<DetailSectionTitle>{getLocale('braveWalletNFTDetailTokenID')}</DetailSectionTitle>
<ProjectDetailIDRow>
<DetailSectionValue>
{
selectedAsset.tokenId
? '#' + new Amount(selectedAsset.tokenId).toNumber()
: ''
}
</DetailSectionValue>
<ExplorerButton onClick={onClickViewOnBlockExplorer('contract', selectedAsset.contractAddress, selectedAsset.tokenId)}>
<ExplorerIcon />
</ExplorerButton>
</ProjectDetailIDRow>
</DetailSectionColumn>
</DetailSectionRow>
<ProjectDetailRow>
{/* TODO: Add nft logo when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */}
{/* <ProjectDetailImage src={nftMetadata.contractInformation.logo} /> */}
<ProjectDetailName>{nftMetadata.contractInformation.name}</ProjectDetailName>
{nftMetadata.contractInformation.website && nftMetadata.contractInformation.twitter && nftMetadata.contractInformation.facebook &&
<ProjectDetailButtonRow>
<ProjectDetailButton onClick={onClickWebsite}>
<ProjectWebsiteIcon/>
</ProjectDetailButton>
<ProjectDetailButtonSeperator/>
<ProjectDetailButton onClick={onClickTwitter}>
<ProjectTwitterIcon/>
</ProjectDetailButton>
<ProjectDetailButtonSeperator/>
<ProjectDetailButton onClick={onClickFacebook}>
<ProjectFacebookIcon/>
</ProjectDetailButton>
</ProjectDetailButtonRow>
}
</ProjectDetailRow>
<ProjectDetailDescription>{nftMetadata.contractInformation.description}</ProjectDetailDescription>
</DetailColumn>
</>
}
</StyledWrapper>

)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,37 @@ import { WalletButton } from '../../../../../shared/style'
import WebsiteIcon from '../../../../../../assets/svg-icons/website-icon.svg'
import TwitterIcon from '../../../../../../assets/svg-icons/twitter-icon.svg'
import FacebookIcon from '../../../../../../assets/svg-icons/facebook-icon.svg'
import { OpenNewIcon } from 'brave-ui/components/icons'
import { LoaderIcon, OpenNewIcon } from 'brave-ui/components/icons'

export interface StyleProps {
isLoading: boolean
}

const nftImageDimension = '440px'

export const StyledWrapper = styled.div`
display: flex;
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 NFTImage = styled.img<Partial<StyleProps>>`
display: ${p => p.isLoading ? 'none' : 'block'};
width: ${nftImageDimension};
height: ${nftImageDimension};
margin-right: 28px;
border-radius: 12px;
`

export const NFTImageSkeletonWrapper = styled.div`
min-width: ${nftImageDimension};
height: ${nftImageDimension};
margin-right: 28px;
border-radius: 12px;
`

export const DetailColumn = styled.div`
Expand Down Expand Up @@ -205,3 +222,22 @@ export const ExplorerIcon = styled(OpenNewIcon)`
height: 14px;
color: ${(p) => p.theme.color.interactive05};
`

export const LoadingOverlay = styled.div<Partial<StyleProps>>`
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;
`
Loading

0 comments on commit ae7404e

Please sign in to comment.