mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
Feat/collectibles the return (#12970)
* Wire collectibles frontend UI with controller data
This commit is contained in:
parent
c03b6dd19b
commit
1b6e58c417
BIN
.storybook/images/catnip-spicywright.png
Normal file
BIN
.storybook/images/catnip-spicywright.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
@ -139,9 +139,6 @@
|
||||
"addNFT": {
|
||||
"message": "Add NFT"
|
||||
},
|
||||
"addNFTLowerCase": {
|
||||
"message": "add NFT"
|
||||
},
|
||||
"addNetwork": {
|
||||
"message": "Add Network"
|
||||
},
|
||||
@ -568,6 +565,9 @@
|
||||
"contract": {
|
||||
"message": "Contract"
|
||||
},
|
||||
"contractAddress": {
|
||||
"message": "Contract address"
|
||||
},
|
||||
"contractAddressError": {
|
||||
"message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens."
|
||||
},
|
||||
@ -717,6 +717,9 @@
|
||||
"depositEther": {
|
||||
"message": "Deposit Ether"
|
||||
},
|
||||
"description": {
|
||||
"message": "Description"
|
||||
},
|
||||
"details": {
|
||||
"message": "Details"
|
||||
},
|
||||
@ -908,6 +911,9 @@
|
||||
"editPermission": {
|
||||
"message": "Edit Permission"
|
||||
},
|
||||
"enableAutoDetect": {
|
||||
"message": " Enable Autodetect"
|
||||
},
|
||||
"enableFromSettings": {
|
||||
"message": " Enable it from Settings."
|
||||
},
|
||||
@ -1350,6 +1356,9 @@
|
||||
"importMyWallet": {
|
||||
"message": "Import My Wallet"
|
||||
},
|
||||
"importNFTs": {
|
||||
"message": "Import NFTs"
|
||||
},
|
||||
"importTokenQuestion": {
|
||||
"message": "Import token?"
|
||||
},
|
||||
@ -1485,6 +1494,9 @@
|
||||
"learnMore": {
|
||||
"message": "learn more"
|
||||
},
|
||||
"learnMoreUpperCase": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"learnScamRisk": {
|
||||
"message": "scams and security risks."
|
||||
},
|
||||
@ -1542,6 +1554,9 @@
|
||||
"likeToImportTokens": {
|
||||
"message": "Would you like to import these tokens?"
|
||||
},
|
||||
"link": {
|
||||
"message": "Link"
|
||||
},
|
||||
"links": {
|
||||
"message": "Links"
|
||||
},
|
||||
@ -1812,10 +1827,10 @@
|
||||
"message": "New Contract"
|
||||
},
|
||||
"newNFTsDetected": {
|
||||
"message": "New NFTs detected"
|
||||
"message": "New! NFT detection"
|
||||
},
|
||||
"newNFTsDetectedInfo": {
|
||||
"message": "One or more new NFTs were detected in your wallet."
|
||||
"message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your MetaMask wallet."
|
||||
},
|
||||
"newNetworkAdded": {
|
||||
"message": "“$1” was successfully added!"
|
||||
@ -2255,6 +2270,9 @@
|
||||
"removeAccountDescription": {
|
||||
"message": "This account will be removed from your wallet. Please make sure you have the original Secret Recovery Phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. "
|
||||
},
|
||||
"removeNFT": {
|
||||
"message": "Remove NFT"
|
||||
},
|
||||
"requestsAwaitingAcknowledgement": {
|
||||
"message": "requests waiting to be acknowledged"
|
||||
},
|
||||
@ -2443,7 +2461,7 @@
|
||||
"message": "Select HD Path"
|
||||
},
|
||||
"selectNFTPrivacyPreference": {
|
||||
"message": "Select NFT privacy preference"
|
||||
"message": "Turn on NFT detection in Settings"
|
||||
},
|
||||
"selectPathHelp": {
|
||||
"message": "If you don't see the accounts you expect, try switching the HD path."
|
||||
@ -2573,6 +2591,9 @@
|
||||
"somethingWentWrong": {
|
||||
"message": "Oops! Something went wrong."
|
||||
},
|
||||
"source": {
|
||||
"message": "Source"
|
||||
},
|
||||
"speedUp": {
|
||||
"message": "Speed Up"
|
||||
},
|
||||
@ -3359,6 +3380,9 @@
|
||||
"message": "View $1 on Etherscan",
|
||||
"description": "$1 is the action type. e.g (Account, Transaction, Swap)"
|
||||
},
|
||||
"viewOnOpensea": {
|
||||
"message": "View on Opensea"
|
||||
},
|
||||
"viewinExplorer": {
|
||||
"message": "View $1 in Explorer",
|
||||
"description": "$1 is the action type. e.g (Account, Transaction, Swap)"
|
||||
|
@ -31,6 +31,7 @@ export default class AppStateController extends EventEmitter {
|
||||
fullScreenGasPollTokens: [],
|
||||
recoveryPhraseReminderHasBeenShown: false,
|
||||
recoveryPhraseReminderLastShown: new Date().getTime(),
|
||||
collectiblesDetectionNoticeDismissed: false,
|
||||
showTestnetMessageInDropdown: true,
|
||||
trezorModel: null,
|
||||
...initState,
|
||||
@ -252,4 +253,15 @@ export default class AppStateController extends EventEmitter {
|
||||
setTrezorModel(trezorModel) {
|
||||
this.store.updateState({ trezorModel });
|
||||
}
|
||||
|
||||
/**
|
||||
* A setter for the `collectiblesDetectionNoticeDismissed` property
|
||||
*/
|
||||
setCollectiblesDetectionNoticeDismissed(
|
||||
collectiblesDetectionNoticeDismissed,
|
||||
) {
|
||||
this.store.updateState({
|
||||
collectiblesDetectionNoticeDismissed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ import { ObservableStore } from '@metamask/obs-store';
|
||||
import { normalize as normalizeAddress } from 'eth-sig-util';
|
||||
import { ethers } from 'ethers';
|
||||
import log from 'loglevel';
|
||||
import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network';
|
||||
import {
|
||||
IPFS_DEFAULT_GATEWAY_URL,
|
||||
NETWORK_TYPE_TO_ID_MAP,
|
||||
} from '../../../shared/constants/network';
|
||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
@ -61,7 +64,7 @@ export default class PreferencesController {
|
||||
hideZeroBalanceTokens: false,
|
||||
},
|
||||
// ENS decentralized website resolution
|
||||
ipfsGateway: 'dweb.link',
|
||||
ipfsGateway: IPFS_DEFAULT_GATEWAY_URL,
|
||||
infuraBlocked: null,
|
||||
ledgerTransportType: window.navigator.hid
|
||||
? LEDGER_TRANSPORT_TYPES.WEBHID
|
||||
|
@ -50,6 +50,7 @@ export default function setupEnsIpfsResolver({
|
||||
|
||||
async function attemptResolve({ tabId, name, pathname, search, fragment }) {
|
||||
const ipfsGateway = getIpfsGateway();
|
||||
|
||||
extension.tabs.update(tabId, { url: `loading.html` });
|
||||
let url = `https://app.ens.domains/name/${name}`;
|
||||
try {
|
||||
|
@ -206,32 +206,36 @@ export default class MetamaskController extends EventEmitter {
|
||||
provider: this.provider,
|
||||
});
|
||||
|
||||
this.collectiblesController = new CollectiblesController({
|
||||
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
|
||||
this.preferencesController.store,
|
||||
),
|
||||
onNetworkStateChange: this.networkController.store.subscribe.bind(
|
||||
this.networkController.store,
|
||||
),
|
||||
getAssetName: this.assetsContractController.getAssetName.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getAssetSymbol: this.assetsContractController.getAssetSymbol.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getCollectibleTokenURI: this.assetsContractController.getCollectibleTokenURI.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getOwnerOf: this.assetsContractController.getOwnerOf.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
balanceOfERC1155Collectible: this.assetsContractController.balanceOfERC1155Collectible.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
uriERC1155Collectible: this.assetsContractController.uriERC1155Collectible.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
});
|
||||
this.collectiblesController = new CollectiblesController(
|
||||
{
|
||||
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
|
||||
this.preferencesController.store,
|
||||
),
|
||||
onNetworkStateChange: this.networkController.store.subscribe.bind(
|
||||
this.networkController.store,
|
||||
),
|
||||
getAssetName: this.assetsContractController.getAssetName.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getAssetSymbol: this.assetsContractController.getAssetSymbol.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getCollectibleTokenURI: this.assetsContractController.getCollectibleTokenURI.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
getOwnerOf: this.assetsContractController.getOwnerOf.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
balanceOfERC1155Collectible: this.assetsContractController.balanceOfERC1155Collectible.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
uriERC1155Collectible: this.assetsContractController.uriERC1155Collectible.bind(
|
||||
this.assetsContractController,
|
||||
),
|
||||
},
|
||||
{},
|
||||
initState.CollectiblesController,
|
||||
);
|
||||
|
||||
process.env.COLLECTIBLES_V1 &&
|
||||
(this.collectibleDetectionController = new CollectibleDetectionController(
|
||||
@ -1182,7 +1186,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
setShowTestnetMessageInDropdown: appStateController.setShowTestnetMessageInDropdown.bind(
|
||||
appStateController,
|
||||
),
|
||||
|
||||
setCollectiblesDetectionNoticeDismissed: appStateController.setCollectiblesDetectionNoticeDismissed.bind(
|
||||
appStateController,
|
||||
),
|
||||
// EnsController
|
||||
tryReverseResolveAddress: ensController.reverseResolveAddress.bind(
|
||||
ensController,
|
||||
|
@ -534,6 +534,7 @@
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
@ -569,6 +570,7 @@
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
"jsonschema": true,
|
||||
"multiformats": true,
|
||||
"nanoid": true,
|
||||
"punycode": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
@ -3548,6 +3550,13 @@
|
||||
"varint": true
|
||||
}
|
||||
},
|
||||
"multiformats": {
|
||||
"globals": {
|
||||
"TextDecoder": true,
|
||||
"TextEncoder": true,
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"multihashes": {
|
||||
"packages": {
|
||||
"bs58": true,
|
||||
|
@ -534,6 +534,7 @@
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
@ -569,6 +570,7 @@
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
"jsonschema": true,
|
||||
"multiformats": true,
|
||||
"nanoid": true,
|
||||
"punycode": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
@ -3548,6 +3550,13 @@
|
||||
"varint": true
|
||||
}
|
||||
},
|
||||
"multiformats": {
|
||||
"globals": {
|
||||
"TextDecoder": true,
|
||||
"TextEncoder": true,
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"multihashes": {
|
||||
"packages": {
|
||||
"bs58": true,
|
||||
|
@ -534,6 +534,7 @@
|
||||
"@metamask/controllers": {
|
||||
"globals": {
|
||||
"Headers": true,
|
||||
"URL": true,
|
||||
"clearInterval": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
@ -569,6 +570,7 @@
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
"jsonschema": true,
|
||||
"multiformats": true,
|
||||
"nanoid": true,
|
||||
"punycode": true,
|
||||
"single-call-balance-checker-abi": true,
|
||||
@ -3548,6 +3550,13 @@
|
||||
"varint": true
|
||||
}
|
||||
},
|
||||
"multiformats": {
|
||||
"globals": {
|
||||
"TextDecoder": true,
|
||||
"TextEncoder": true,
|
||||
"console.warn": true
|
||||
}
|
||||
},
|
||||
"multihashes": {
|
||||
"packages": {
|
||||
"bs58": true,
|
||||
|
@ -107,7 +107,7 @@
|
||||
"@keystonehq/metamask-airgapped-keyring": "0.2.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@metamask/contract-metadata": "^1.31.0",
|
||||
"@metamask/controllers": "^20.1.0",
|
||||
"@metamask/controllers": "^22.0.0",
|
||||
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
|
||||
"@metamask/eth-token-tracker": "^3.0.1",
|
||||
"@metamask/etherscan-link": "^2.1.0",
|
||||
|
@ -171,3 +171,5 @@ export const UNSUPPORTED_RPC_METHODS = new Set([
|
||||
// eth-json-rpc-middleware – but our UI does not support it.
|
||||
'eth_signTransaction',
|
||||
]);
|
||||
|
||||
export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link';
|
||||
|
@ -7,6 +7,11 @@
|
||||
@import 'app-header/index';
|
||||
@import 'asset-list-item/asset-list-item';
|
||||
@import 'confirm-page-container/index';
|
||||
@import 'collectibles-items/index';
|
||||
@import 'collectibles-tab/index';
|
||||
@import 'collectible-details/index';
|
||||
@import 'collectible-options/index';
|
||||
@import 'collectibles-detection-notice/index';
|
||||
@import 'connected-accounts-list/index';
|
||||
@import 'connected-accounts-permissions/index';
|
||||
@import 'connected-sites-list/index';
|
||||
|
251
ui/components/app/collectible-details/collectible-details.js
Normal file
251
ui/components/app/collectible-details/collectible-details.js
Normal file
@ -0,0 +1,251 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getTokenTrackerLink } from '@metamask/etherscan-link';
|
||||
import Box from '../../ui/box';
|
||||
import Card from '../../ui/card';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
FONT_WEIGHT,
|
||||
JUSTIFY_CONTENT,
|
||||
FLEX_DIRECTION,
|
||||
OVERFLOW_WRAP,
|
||||
DISPLAY,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import {
|
||||
getAssetImageURL,
|
||||
isEqualCaseInsensitive,
|
||||
shortenAddress,
|
||||
} from '../../../helpers/utils/util';
|
||||
import {
|
||||
getCurrentChainId,
|
||||
getIpfsGateway,
|
||||
getRpcPrefsForCurrentProvider,
|
||||
getSelectedIdentity,
|
||||
} from '../../../selectors';
|
||||
import AssetNavigation from '../../../pages/asset/components/asset-navigation';
|
||||
import { getCollectibleContracts } from '../../../ducks/metamask/metamask';
|
||||
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { removeAndIgnoreCollectible } from '../../../store/actions';
|
||||
import {
|
||||
GOERLI_CHAIN_ID,
|
||||
KOVAN_CHAIN_ID,
|
||||
MAINNET_CHAIN_ID,
|
||||
POLYGON_CHAIN_ID,
|
||||
RINKEBY_CHAIN_ID,
|
||||
ROPSTEN_CHAIN_ID,
|
||||
} from '../../../../shared/constants/network';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import CollectibleOptions from '../collectible-options/collectible-options';
|
||||
|
||||
export default function CollectibleDetails({ collectible }) {
|
||||
const { image, name, description, address, tokenId } = collectible;
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
|
||||
const ipfsGateway = useSelector(getIpfsGateway);
|
||||
const collectibleContracts = useSelector(getCollectibleContracts);
|
||||
const currentNetwork = useSelector(getCurrentChainId);
|
||||
|
||||
const collectibleContractName = collectibleContracts.find(
|
||||
({ address: contractAddress }) =>
|
||||
isEqualCaseInsensitive(contractAddress, address),
|
||||
)?.name;
|
||||
const selectedAccountName = useSelector(
|
||||
(state) => getSelectedIdentity(state).name,
|
||||
);
|
||||
const collectibleImageURL = getAssetImageURL(image, ipfsGateway);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRemove = () => {
|
||||
dispatch(removeAndIgnoreCollectible(address, tokenId));
|
||||
history.push(DEFAULT_ROUTE);
|
||||
};
|
||||
|
||||
const getOpenSeaLink = () => {
|
||||
switch (currentNetwork) {
|
||||
case MAINNET_CHAIN_ID:
|
||||
return `https://opensea.io/assets/${address}/${tokenId}`;
|
||||
case POLYGON_CHAIN_ID:
|
||||
return `https://opensea.io/assets/matic/${address}/${tokenId}`;
|
||||
case GOERLI_CHAIN_ID:
|
||||
case KOVAN_CHAIN_ID:
|
||||
case ROPSTEN_CHAIN_ID:
|
||||
case RINKEBY_CHAIN_ID:
|
||||
return `https://testnets.opensea.io/assets/${address}/${tokenId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const openSeaLink = getOpenSeaLink();
|
||||
return (
|
||||
<>
|
||||
<AssetNavigation
|
||||
accountName={selectedAccountName}
|
||||
assetName={collectibleContractName}
|
||||
onBack={() => history.push(DEFAULT_ROUTE)}
|
||||
optionsButton={
|
||||
<CollectibleOptions
|
||||
onViewOnOpensea={
|
||||
openSeaLink
|
||||
? () => global.platform.openTab({ url: openSeaLink })
|
||||
: null
|
||||
}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Box className="collectible-details">
|
||||
<div className="collectible-details__top-section">
|
||||
<Card
|
||||
padding={0}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
className="collectible-details__card"
|
||||
>
|
||||
<img
|
||||
className="collectible-details__image"
|
||||
src={collectibleImageURL}
|
||||
/>
|
||||
</Card>
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
className="collectible-details__top-section__info"
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
>
|
||||
{`#${tokenId}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
className="collectible-details__description"
|
||||
boxProps={{ margin: 0, marginBottom: 2 }}
|
||||
>
|
||||
{t('description')}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI4}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ margin: 0 }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
<Box>
|
||||
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
boxProps={{
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
marginRight: 2,
|
||||
}}
|
||||
className="collectible-details__link-title"
|
||||
>
|
||||
{t('source')}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.PRIMARY1}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href={collectibleImageURL}
|
||||
rel="noopener noreferrer"
|
||||
className="collectible-details__image-link"
|
||||
>
|
||||
{image}
|
||||
</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW}>
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
boxProps={{
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
marginRight: 2,
|
||||
}}
|
||||
className="collectible-details__link-title"
|
||||
>
|
||||
{t('contractAddress')}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
boxProps={{
|
||||
margin: 0,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
className="collectible-details__contract-link"
|
||||
href={getTokenTrackerLink(
|
||||
address,
|
||||
currentNetwork,
|
||||
null,
|
||||
null,
|
||||
rpcPrefs,
|
||||
)}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
? shortenAddress(address)
|
||||
: address}
|
||||
</a>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CollectibleDetails.propTypes = {
|
||||
collectible: PropTypes.shape({
|
||||
address: PropTypes.string.isRequired,
|
||||
tokenId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
standard: PropTypes.string,
|
||||
imageThumbnail: PropTypes.string,
|
||||
imagePreview: PropTypes.string,
|
||||
creator: PropTypes.shape({
|
||||
address: PropTypes.string,
|
||||
config: PropTypes.string,
|
||||
profile_img_url: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import CollectibleDetails from './collectible-details';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/CollectiblesDetail',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
collectible: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const collectible = {
|
||||
name: 'Catnip Spicywright',
|
||||
tokenId: '1124157',
|
||||
address: '0x06012c8cf97bead5deae237070f9587f8e7a266d',
|
||||
image: './catnip-spicywright.png',
|
||||
description:
|
||||
"Good day. My name is Catnip Spicywight, which got me teased a lot in high school. If I want to put low fat mayo all over my hamburgers, I shouldn't have to answer to anyone about it, am I right? One time I beat Arlene in an arm wrestle.",
|
||||
};
|
||||
|
||||
export const DefaultStory = () => {
|
||||
return <CollectibleDetails collectible={collectible} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
collectible,
|
||||
};
|
72
ui/components/app/collectible-details/index.scss
Normal file
72
ui/components/app/collectible-details/index.scss
Normal file
@ -0,0 +1,72 @@
|
||||
$card-width-break-large: 224px;
|
||||
$link-title-width: 160px;
|
||||
$spacer-break-large: 24px;
|
||||
$spacer-break-small: 16px;
|
||||
|
||||
.collectible-details {
|
||||
padding: 0 $spacer-break-small;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
padding: 0 $spacer-break-large;
|
||||
}
|
||||
|
||||
&__top-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: $spacer-break-small;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
margin-bottom: $spacer-break-large;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__info {
|
||||
@media screen and (min-width: $break-large) {
|
||||
max-width: calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
|
||||
flex: 0 0 calc(100% - #{$card-width-break-large} - #{$spacer-break-large});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
overflow: hidden;
|
||||
margin-bottom: $spacer-break-small;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
margin-right: $spacer-break-large;
|
||||
margin-bottom: 0;
|
||||
max-width: $card-width-break-large;
|
||||
flex: 0 0 $card-width-break-large;
|
||||
height: $card-width-break-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
width: $card-width-break-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__address {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
&__contract-link,
|
||||
&__image-link {
|
||||
color: $primary-1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
|
||||
&:hover {
|
||||
color: $primary-3;
|
||||
}
|
||||
}
|
||||
|
||||
&__link-title {
|
||||
flex: 0 0 $link-title-width;
|
||||
max-width: 0 0 $link-title-width;
|
||||
}
|
||||
}
|
61
ui/components/app/collectible-options/collectible-options.js
Normal file
61
ui/components/app/collectible-options/collectible-options.js
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
import { Menu, MenuItem } from '../../ui/menu';
|
||||
|
||||
const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
|
||||
const t = useContext(I18nContext);
|
||||
const [
|
||||
collectibleOptionsButtonElement,
|
||||
setCollectibleOptionsButtonElement,
|
||||
] = useState(null);
|
||||
const [collectibleOptionsOpen, setCollectibleOptionsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="fas fa-ellipsis-v collectible-options__button"
|
||||
data-testid="collectible-options__button"
|
||||
onClick={() => setCollectibleOptionsOpen(true)}
|
||||
ref={setCollectibleOptionsButtonElement}
|
||||
/>
|
||||
{collectibleOptionsOpen ? (
|
||||
<Menu
|
||||
anchorElement={collectibleOptionsButtonElement}
|
||||
onHide={() => setCollectibleOptionsOpen(false)}
|
||||
>
|
||||
{onViewOnOpensea ? (
|
||||
<MenuItem
|
||||
iconClassName="fas fa-qrcode"
|
||||
data-testid="collectible-options__view-on-opensea"
|
||||
onClick={() => {
|
||||
setCollectibleOptionsOpen(false);
|
||||
onViewOnOpensea();
|
||||
}}
|
||||
>
|
||||
{t('viewOnOpensea')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
iconClassName="fas fa-trash-alt collectible-options__icon"
|
||||
data-testid="collectible-options__hide"
|
||||
onClick={() => {
|
||||
setCollectibleOptionsOpen(false);
|
||||
onRemove();
|
||||
}}
|
||||
>
|
||||
{t('removeNFT')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CollectibleOptions.propTypes = {
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onViewOnOpensea: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CollectibleOptions;
|
12
ui/components/app/collectible-options/index.scss
Normal file
12
ui/components/app/collectible-options/index.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.collectible-options {
|
||||
&__button {
|
||||
font-size: $font-size-paragraph;
|
||||
color: $Black-100;
|
||||
background-color: inherit;
|
||||
padding: 2px 0 2px 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import Box from '../../ui/box';
|
||||
import Dialog from '../../ui/dialog';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
@ -10,13 +11,22 @@ import {
|
||||
DISPLAY,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import Button from '../../ui/button';
|
||||
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { setCollectiblesDetectionNoticeDismissed } from '../../../store/actions';
|
||||
|
||||
export default function NewCollectiblesNotice() {
|
||||
export default function CollectiblesDetectionNotice() {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Box marginBottom={8}>
|
||||
<Dialog type="message">
|
||||
<Box marginBottom={8} className="collectibles-detection-notice">
|
||||
<Dialog type="message" className="collectibles-detection-notice__message">
|
||||
<button
|
||||
onClick={() => setCollectiblesDetectionNoticeDismissed()}
|
||||
className="collectibles-detection-notice__message__close-button"
|
||||
data-testid="collectibles-detection-notice-close"
|
||||
/>
|
||||
<Box display={DISPLAY.FLEX}>
|
||||
<Box paddingTop={2}>
|
||||
<i style={{ fontSize: '1rem' }} className="fa fa-info-circle" />
|
||||
@ -25,7 +35,7 @@ export default function NewCollectiblesNotice() {
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
align={TEXT_ALIGN.LEFT}
|
||||
variant={TYPOGRAPHY.Paragraph}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
>
|
||||
{t('newNFTsDetected')}
|
||||
@ -33,21 +43,20 @@ export default function NewCollectiblesNotice() {
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
align={TEXT_ALIGN.LEFT}
|
||||
variant={TYPOGRAPHY.Paragraph}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ marginBottom: 4 }}
|
||||
>
|
||||
{t('newNFTsDetectedInfo')}
|
||||
</Typography>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
console.log('show preference popover');
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
history.push(EXPERIMENTAL_ROUTE);
|
||||
}}
|
||||
style={{ fontSize: '.9rem' }}
|
||||
className="collectibles-detection-notice__message__link"
|
||||
>
|
||||
{t('selectNFTPrivacyPreference')}
|
||||
</a>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
1
ui/components/app/collectibles-detection-notice/index.js
Normal file
1
ui/components/app/collectibles-detection-notice/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './collectibles-detection-notice';
|
31
ui/components/app/collectibles-detection-notice/index.scss
Normal file
31
ui/components/app/collectibles-detection-notice/index.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.collectibles-detection-notice {
|
||||
&__message {
|
||||
position: relative;
|
||||
padding: 0 1rem 1rem 1rem !important;
|
||||
|
||||
&__close-button {
|
||||
background-color: transparent;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '\00D7';
|
||||
font-size: 29px;
|
||||
font-weight: 200;
|
||||
color: $black;
|
||||
background-color: transparent;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
a.collectibles-detection-notice__message__link {
|
||||
@include H6;
|
||||
|
||||
width: 60%;
|
||||
padding: 0;
|
||||
justify-content: flex-start;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
TEXT_ALIGN,
|
||||
JUSTIFY_CONTENT,
|
||||
FLEX_DIRECTION,
|
||||
ALIGN_ITEMS,
|
||||
DISPLAY,
|
||||
BLOCK_SIZES,
|
||||
SIZES,
|
||||
FLEX_WRAP,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
|
||||
export default function CollectiblesItems({ onAddNFT, onRefreshList }) {
|
||||
const t = useI18nContext();
|
||||
const collections = {};
|
||||
const defaultDropdownState = {};
|
||||
|
||||
Object.keys(collections).forEach((key) => {
|
||||
defaultDropdownState[key] = true;
|
||||
});
|
||||
|
||||
const [dropdownState, setDropdownState] = useState(defaultDropdownState);
|
||||
const width =
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
? BLOCK_SIZES.ONE_THIRD
|
||||
: BLOCK_SIZES.ONE_SIXTH;
|
||||
return (
|
||||
<div className="collectibles-items">
|
||||
<Box padding={[4, 6, 4, 6]} flexDirection={FLEX_DIRECTION.COLUMN}>
|
||||
<>
|
||||
{Object.keys(collections).map((key, index) => {
|
||||
const { icon, collectibles } = collections[key];
|
||||
const isExpanded = dropdownState[key];
|
||||
|
||||
return (
|
||||
<div key={`collection-${index}`}>
|
||||
<Box
|
||||
marginTop={4}
|
||||
marginBottom={4}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
|
||||
>
|
||||
<Box alignItems={ALIGN_ITEMS.CENTER}>
|
||||
<img width="28" src={icon} />
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
margin={[0, 0, 0, 2]}
|
||||
>
|
||||
{`${key} (${collectibles.length})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
||||
<i
|
||||
className={`fa fa-lg fa-chevron-${
|
||||
isExpanded ? 'down' : 'right'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setDropdownState((_dropdownState) => ({
|
||||
..._dropdownState,
|
||||
[key]: !isExpanded,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isExpanded ? (
|
||||
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP}>
|
||||
{collectibles.map((collectible, i) => {
|
||||
return (
|
||||
<Box width={width} padding={2} key={`collectible-${i}`}>
|
||||
<Box
|
||||
borderRadius={SIZES.MD}
|
||||
backgroundColor={collectible.backgroundColor}
|
||||
>
|
||||
<img src={collectible.icon} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Box
|
||||
marginTop={6}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('missingNFT')}
|
||||
</Typography>
|
||||
<Box
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
>
|
||||
<Box justifyContent={JUSTIFY_CONTENT.FLEX_END}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={onRefreshList}
|
||||
style={{ padding: '4px' }}
|
||||
>
|
||||
{t('refreshList')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('or')}
|
||||
</Typography>
|
||||
<Box justifyContent={JUSTIFY_CONTENT.FLEX_START}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={onAddNFT}
|
||||
style={{ padding: '4px' }}
|
||||
>
|
||||
{t('addNFTLowerCase')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CollectiblesItems.propTypes = {
|
||||
onAddNFT: PropTypes.func.isRequired,
|
||||
onRefreshList: PropTypes.func.isRequired,
|
||||
};
|
164
ui/components/app/collectibles-items/collectibles-items.js
Normal file
164
ui/components/app/collectibles-items/collectibles-items.js
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import Box from '../../ui/box';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
JUSTIFY_CONTENT,
|
||||
FLEX_DIRECTION,
|
||||
ALIGN_ITEMS,
|
||||
DISPLAY,
|
||||
BLOCK_SIZES,
|
||||
FLEX_WRAP,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { getIpfsGateway } from '../../../selectors';
|
||||
import { ASSET_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { getAssetImageURL } from '../../../helpers/utils/util';
|
||||
|
||||
const width =
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
? BLOCK_SIZES.ONE_THIRD
|
||||
: BLOCK_SIZES.ONE_SIXTH;
|
||||
export default function CollectiblesItems({ collections = {} }) {
|
||||
const defaultDropdownState = {};
|
||||
const ipfsGateway = useSelector(getIpfsGateway);
|
||||
|
||||
Object.keys(collections).forEach((key) => {
|
||||
defaultDropdownState[key] = true;
|
||||
});
|
||||
const history = useHistory();
|
||||
|
||||
const [dropdownState, setDropdownState] = useState(defaultDropdownState);
|
||||
return (
|
||||
<div className="collectibles-items">
|
||||
<Box padding={[6, 4]} flexDirection={FLEX_DIRECTION.COLUMN}>
|
||||
<>
|
||||
{Object.keys(collections).map((key, index) => {
|
||||
const {
|
||||
collectibles,
|
||||
collectionName,
|
||||
collectionImage,
|
||||
} = collections[key];
|
||||
|
||||
const isExpanded = dropdownState[key];
|
||||
return (
|
||||
<div
|
||||
className="collectibles-items__item"
|
||||
key={`collection-${index}`}
|
||||
onClick={() => {
|
||||
setDropdownState((_dropdownState) => ({
|
||||
..._dropdownState,
|
||||
[key]: !isExpanded,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
marginBottom={2}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
|
||||
className="collectibles-items__item__accordion-title"
|
||||
>
|
||||
<Box
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
className="collectibles-items__item__collection-header"
|
||||
>
|
||||
{collectionImage ? (
|
||||
<img
|
||||
src={collectionImage}
|
||||
className="collectibles-items__item__collection-image"
|
||||
/>
|
||||
) : (
|
||||
<div className="collectibles-items__item__collection-image-alt">
|
||||
{collectionName[0]}
|
||||
</div>
|
||||
)}
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
margin={[0, 0, 0, 2]}
|
||||
>
|
||||
{`${collectionName} (${collectibles.length})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
||||
<i
|
||||
className={`fa fa-chevron-${
|
||||
isExpanded ? 'down' : 'right'
|
||||
}`}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isExpanded ? (
|
||||
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
|
||||
{collectibles.map((collectible, i) => {
|
||||
const {
|
||||
image,
|
||||
address,
|
||||
tokenId,
|
||||
backgroundColor,
|
||||
} = collectible;
|
||||
const collectibleImage = getAssetImageURL(
|
||||
image,
|
||||
ipfsGateway,
|
||||
);
|
||||
return (
|
||||
<Box width={width} key={`collectible-${i}`}>
|
||||
<div
|
||||
className="collectibles-items__image__wrapper"
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onClick={() =>
|
||||
history.push(
|
||||
`${ASSET_ROUTE}/${address}/${tokenId}`,
|
||||
)
|
||||
}
|
||||
className="collectibles-items__image"
|
||||
src={collectibleImage}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CollectiblesItems.propTypes = {
|
||||
collections: PropTypes.shape({
|
||||
collectibles: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
address: PropTypes.string.isRequired,
|
||||
tokenId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
standard: PropTypes.string,
|
||||
imageThumbnail: PropTypes.string,
|
||||
imagePreview: PropTypes.string,
|
||||
creator: PropTypes.shape({
|
||||
address: PropTypes.string,
|
||||
config: PropTypes.string,
|
||||
profile_img_url: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
collectionImage: PropTypes.string,
|
||||
collectionName: PropTypes.string,
|
||||
}),
|
||||
};
|
@ -1 +1 @@
|
||||
export { default } from './collectibles-items.component';
|
||||
export { default } from './collectibles-items';
|
||||
|
41
ui/components/app/collectibles-items/index.scss
Normal file
41
ui/components/app/collectibles-items/index.scss
Normal file
@ -0,0 +1,41 @@
|
||||
.collectibles-items {
|
||||
&__image__wrapper {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__image {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&__accordion-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__collection-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__collection-image-alt {
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
background: $ui-4;
|
||||
color: $ui-white;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import NewCollectiblesNotice from '../new-collectibles-notice';
|
||||
import CollectiblesItems from '../collectibles-items';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
TEXT_ALIGN,
|
||||
JUSTIFY_CONTENT,
|
||||
FLEX_DIRECTION,
|
||||
FONT_WEIGHT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
|
||||
export default function CollectiblesTab({ onAddNFT }) {
|
||||
const collectibles = [];
|
||||
const newNFTsDetected = false;
|
||||
const t = useI18nContext();
|
||||
|
||||
return (
|
||||
<div className="collectibles-tab">
|
||||
{collectibles.length > 0 ? (
|
||||
<CollectiblesItems
|
||||
onAddNFT={onAddNFT}
|
||||
onRefreshList={() => {
|
||||
console.log('refreshing collectibles');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box padding={[6, 12, 6, 12]}>
|
||||
{newNFTsDetected ? <NewCollectiblesNotice /> : null}
|
||||
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||
<img src="./images/no-nfts.svg" />
|
||||
</Box>
|
||||
<Box
|
||||
marginTop={4}
|
||||
marginBottom={12}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
>
|
||||
{t('noNFTs')}
|
||||
</Typography>
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://metamask.zendesk.com/hc/en-us/articles/360058238591-NFT-tokens-in-MetaMask-wallet"
|
||||
style={{ padding: 0, fontSize: '1rem' }}
|
||||
>
|
||||
{t('learnMore')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
marginBottom={4}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('missingNFT')}
|
||||
</Typography>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={onAddNFT}
|
||||
style={{ padding: 0, fontSize: '1rem' }}
|
||||
>
|
||||
{t('addNFT')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CollectiblesTab.propTypes = {
|
||||
onAddNFT: PropTypes.func.isRequired,
|
||||
};
|
162
ui/components/app/collectibles-tab/collectibles-tab.js
Normal file
162
ui/components/app/collectibles-tab/collectibles-tab.js
Normal file
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import CollectiblesDetectionNotice from '../collectibles-detection-notice';
|
||||
import CollectiblesItems from '../collectibles-items';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
TEXT_ALIGN,
|
||||
JUSTIFY_CONTENT,
|
||||
FLEX_DIRECTION,
|
||||
FONT_WEIGHT,
|
||||
ALIGN_ITEMS,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import {
|
||||
getCollectibles,
|
||||
getCollectibleContracts,
|
||||
getCollectiblesDetectionNoticeDismissed,
|
||||
} from '../../../ducks/metamask/metamask';
|
||||
import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors';
|
||||
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { detectCollectibles } from '../../../store/actions';
|
||||
|
||||
export default function CollectiblesTab({ onAddNFT }) {
|
||||
const collectibles = useSelector(getCollectibles);
|
||||
const collectibleContracts = useSelector(getCollectibleContracts);
|
||||
const useCollectibleDetection = useSelector(getUseCollectibleDetection);
|
||||
const isMainnet = useSelector(getIsMainnet);
|
||||
const collectibleDetectionNoticeDismissed = useSelector(
|
||||
getCollectiblesDetectionNoticeDismissed,
|
||||
);
|
||||
const history = useHistory();
|
||||
const t = useI18nContext();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collections = {};
|
||||
collectibles.forEach((collectible) => {
|
||||
if (collections[collectible.address]) {
|
||||
collections[collectible.address].collectibles.push(collectible);
|
||||
} else {
|
||||
const collectionContract = collectibleContracts.find(
|
||||
({ address }) => address === collectible.address,
|
||||
);
|
||||
collections[collectible.address] = {
|
||||
collectionName: collectionContract?.name || collectible.name,
|
||||
collectionImage:
|
||||
collectionContract?.logo || collectible.collectionImage,
|
||||
collectibles: [collectible],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const onEnableAutoDetect = () => {
|
||||
history.push(EXPERIMENTAL_ROUTE);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="collectibles-tab">
|
||||
{collectibles.length > 0 ? (
|
||||
<CollectiblesItems collections={collections} />
|
||||
) : (
|
||||
<Box padding={[6, 12, 6, 12]}>
|
||||
{isMainnet &&
|
||||
!useCollectibleDetection &&
|
||||
!collectibleDetectionNoticeDismissed ? (
|
||||
<CollectiblesDetectionNotice />
|
||||
) : null}
|
||||
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||
<img src="./images/no-nfts.svg" />
|
||||
</Box>
|
||||
<Box
|
||||
marginTop={4}
|
||||
marginBottom={12}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
>
|
||||
{t('noNFTs')}
|
||||
</Typography>
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://metamask.zendesk.com/hc/en-us/articles/360058238591-NFT-tokens-in-MetaMask-wallet"
|
||||
style={{ padding: 0, fontSize: '1rem' }}
|
||||
>
|
||||
{t('learnMoreUpperCase')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
marginBottom={4}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H5}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('missingNFT')}
|
||||
</Typography>
|
||||
<Box
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
>
|
||||
{isMainnet ? (
|
||||
<>
|
||||
<Box
|
||||
className="collectibles-tab__link"
|
||||
justifyContent={JUSTIFY_CONTENT.FLEX_END}
|
||||
>
|
||||
{useCollectibleDetection ? (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => dispatch(detectCollectibles())}
|
||||
>
|
||||
{t('refreshList')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="link" onClick={onEnableAutoDetect}>
|
||||
{t('enableAutoDetect')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
color={COLORS.UI3}
|
||||
variant={TYPOGRAPHY.H4}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('or')}
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
<Box
|
||||
justifyContent={JUSTIFY_CONTENT.FLEX_START}
|
||||
className="collectibles-tab__link"
|
||||
>
|
||||
<Button type="link" onClick={onAddNFT}>
|
||||
{t('importNFTs')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CollectiblesTab.propTypes = {
|
||||
onAddNFT: PropTypes.func.isRequired,
|
||||
};
|
300
ui/components/app/collectibles-tab/collectibles-tab.test.js
Normal file
300
ui/components/app/collectibles-tab/collectibles-tab.test.js
Normal file
@ -0,0 +1,300 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import reactRouterDom from 'react-router-dom';
|
||||
import configureStore from '../../../store/store';
|
||||
import { renderWithProvider } from '../../../../test/jest/rendering';
|
||||
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { setBackgroundConnection } from '../../../../test/jest';
|
||||
import CollectiblesTab from '.';
|
||||
|
||||
const COLLECTIBLES = [
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
tokenId:
|
||||
'58076532811975507823669075598676816378162417803895263482849101575514658701313',
|
||||
name: 'Punk #4',
|
||||
creator: {
|
||||
user: {
|
||||
username: null,
|
||||
},
|
||||
profile_img_url: null,
|
||||
address: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
config: '',
|
||||
},
|
||||
description: 'Red Mohawk bam!',
|
||||
image:
|
||||
'https://lh3.googleusercontent.com/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE',
|
||||
standard: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
tokenId:
|
||||
'58076532811975507823669075598676816378162417803895263482849101574415147073537',
|
||||
name: 'Punk #3',
|
||||
creator: {
|
||||
user: {
|
||||
username: null,
|
||||
},
|
||||
profile_img_url: null,
|
||||
address: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
config: '',
|
||||
},
|
||||
description: 'Clown PUNK!!!',
|
||||
image:
|
||||
'https://lh3.googleusercontent.com/H7VrxaalZv4PF1B8U7ADuc8AfuqTVyzmMEDQ5OXKlx0Tqu5XiwsKYj4j_pAF6wUJjLMQbSN_0n3fuj84lNyRhFW9hyrxqDfY1IiQEQ',
|
||||
standard: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
tokenId:
|
||||
'58076532811975507823669075598676816378162417803895263482849101573315635445761',
|
||||
name: 'Punk #2',
|
||||
creator: {
|
||||
user: {
|
||||
username: null,
|
||||
},
|
||||
profile_img_url: null,
|
||||
address: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
config: '',
|
||||
},
|
||||
description: 'Got glasses and black hair!',
|
||||
image:
|
||||
'https://lh3.googleusercontent.com/CHNTSlKB_Gob-iwTq8jcag6XwBkTqBMLt_vEKeBv18Q4AoPFAEPceqK6mRzkad2s5djx6CT5zbGQwDy81WwtNzViK5dQbG60uAWv',
|
||||
standard: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
tokenId:
|
||||
'58076532811975507823669075598676816378162417803895263482849101572216123817985',
|
||||
name: 'Punk #1',
|
||||
creator: {
|
||||
user: {
|
||||
username: null,
|
||||
},
|
||||
profile_img_url: null,
|
||||
address: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
config: '',
|
||||
},
|
||||
image:
|
||||
'https://lh3.googleusercontent.com/4jfPi-nQNWCUXD5qVNVWX7LX2UufU_elEJcvICFlsTdcBXv70asnDEOlI8oKECZxlXq1wseeIXMwmP5tLyOUxMKk',
|
||||
standard: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
tokenId:
|
||||
'58076532811975507823669075598676816378162417803895263482849101571116612190209',
|
||||
name: 'Punk #4651',
|
||||
creator: {
|
||||
user: {
|
||||
username: null,
|
||||
},
|
||||
profile_img_url: null,
|
||||
address: '0x806627172af48bd5b0765d3449a7def80d6576ff',
|
||||
config: '',
|
||||
},
|
||||
image:
|
||||
'https://lh3.googleusercontent.com/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE',
|
||||
standard: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414',
|
||||
tokenId: '1',
|
||||
name: 'MUNK #1',
|
||||
description: null,
|
||||
image: 'ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL',
|
||||
standard: 'ERC721',
|
||||
},
|
||||
{
|
||||
address: '0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414',
|
||||
tokenId: '2',
|
||||
name: 'MUNK #2',
|
||||
description: null,
|
||||
image: 'ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL',
|
||||
standard: 'ERC721',
|
||||
},
|
||||
{
|
||||
address: '0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414',
|
||||
tokenId: '3',
|
||||
name: 'MUNK #3',
|
||||
description: null,
|
||||
image: 'ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL',
|
||||
standard: 'ERC721',
|
||||
},
|
||||
];
|
||||
|
||||
const COLLECTIBLES_CONTRACTS = [
|
||||
{
|
||||
address: '0x495f947276749Ce646f68AC8c248420045cb7b5e',
|
||||
name: 'PUNKS',
|
||||
symbol: 'PNKS',
|
||||
schemaName: 'ERC1155',
|
||||
},
|
||||
{
|
||||
address: '0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414',
|
||||
name: 'Munks',
|
||||
symbol: 'MNKS',
|
||||
},
|
||||
];
|
||||
|
||||
const ACCOUNT_1 = '0x123';
|
||||
const ACCOUNT_2 = '0x456';
|
||||
|
||||
const render = ({
|
||||
collectibleContracts = [],
|
||||
collectibles = [],
|
||||
selectedAddress,
|
||||
chainId = '0x1',
|
||||
collectiblesDetectionNoticeDismissed = false,
|
||||
useCollectibleDetection,
|
||||
onAddNFT = jest.fn(),
|
||||
}) => {
|
||||
const store = configureStore({
|
||||
metamask: {
|
||||
allCollectibles: {
|
||||
[ACCOUNT_1]: {
|
||||
[chainId]: collectibles,
|
||||
},
|
||||
},
|
||||
allCollectibleContracts: {
|
||||
[ACCOUNT_1]: {
|
||||
[chainId]: collectibleContracts,
|
||||
},
|
||||
},
|
||||
provider: { chainId },
|
||||
selectedAddress,
|
||||
collectiblesDetectionNoticeDismissed,
|
||||
useCollectibleDetection,
|
||||
},
|
||||
});
|
||||
return renderWithProvider(<CollectiblesTab onAddNFT={onAddNFT} />, store);
|
||||
};
|
||||
|
||||
describe('Collectible Items', () => {
|
||||
const detectCollectiblesStub = jest.fn();
|
||||
const setCollectiblesDetectionNoticeDismissedStub = jest.fn();
|
||||
setBackgroundConnection({
|
||||
setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub,
|
||||
detectCollectibles: detectCollectiblesStub,
|
||||
});
|
||||
const historyPushMock = jest.fn();
|
||||
|
||||
jest
|
||||
.spyOn(reactRouterDom, 'useHistory')
|
||||
.mockImplementation()
|
||||
.mockReturnValue({ push: historyPushMock });
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Collectibles Detection Notice', () => {
|
||||
it('should render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_2,
|
||||
collectibles: COLLECTIBLES,
|
||||
});
|
||||
expect(screen.queryByText('New! NFT detection')).toBeInTheDocument();
|
||||
});
|
||||
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has collectibles', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
});
|
||||
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should take user to the experimental settings tab in setings when user clicks "Turn on NFT detection in Settings"', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_2,
|
||||
collectibles: COLLECTIBLES,
|
||||
});
|
||||
fireEvent.click(screen.queryByText('Turn on NFT detection in Settings'));
|
||||
expect(historyPushMock).toHaveBeenCalledTimes(1);
|
||||
expect(historyPushMock).toHaveBeenCalledWith(EXPERIMENTAL_ROUTE);
|
||||
});
|
||||
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles but use collectible autodetection preference is set to true', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
useCollectibleDetection: true,
|
||||
});
|
||||
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should not render the Collectibles Detection Notice when currently selected network is Mainnet and currently selected account has no collectibles but user has dismissed the notice before', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
collectiblesDetectionNoticeDismissed: true,
|
||||
});
|
||||
expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call setCollectibesDetectionNoticeDismissed when users clicks "X"', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_2,
|
||||
collectibles: COLLECTIBLES,
|
||||
});
|
||||
expect(
|
||||
setCollectiblesDetectionNoticeDismissedStub,
|
||||
).not.toHaveBeenCalled();
|
||||
fireEvent.click(
|
||||
screen.queryByTestId('collectibles-detection-notice-close'),
|
||||
);
|
||||
expect(setCollectiblesDetectionNoticeDismissedStub).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Collections', () => {
|
||||
it('should render the name of the collections and number of collectibles in each collection if current account/chainId combination has collectibles', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
collectibleContracts: COLLECTIBLES_CONTRACTS,
|
||||
});
|
||||
expect(screen.queryByText('PUNKS (5)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Munks (3)')).toBeInTheDocument();
|
||||
});
|
||||
it('should not render collections if current account/chainId combination has collectibles', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_2,
|
||||
collectibles: COLLECTIBLES,
|
||||
collectibleContracts: COLLECTIBLES_CONTRACTS,
|
||||
});
|
||||
expect(screen.queryByText('PUNKS (5)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Munks (3)')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('Collectibles options', () => {
|
||||
it('should render a link "Refresh list" when some collectibles are present and collectible auto-detection preference is set to true, which, when clicked calls a method DetectCollectibles', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
useCollectibleDetection: true,
|
||||
});
|
||||
expect(detectCollectiblesStub).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.queryByText('Refresh list'));
|
||||
expect(detectCollectiblesStub).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render a link "Enable Autodetect" when some collectibles are present and collectible auto-detection preference is set to false, which, when clicked sends user to the experimental tab of settings', () => {
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
});
|
||||
expect(historyPushMock).toHaveBeenCalledTimes(0);
|
||||
fireEvent.click(screen.queryByText('Enable Autodetect'));
|
||||
expect(historyPushMock).toHaveBeenCalledTimes(1);
|
||||
expect(historyPushMock).toHaveBeenCalledWith(EXPERIMENTAL_ROUTE);
|
||||
});
|
||||
it('should render a link "Import NFTs" when some collectibles are present, which, when clicked calls the passed in onAddNFT method', () => {
|
||||
const onAddNFTStub = jest.fn();
|
||||
render({
|
||||
selectedAddress: ACCOUNT_1,
|
||||
collectibles: COLLECTIBLES,
|
||||
onAddNFT: onAddNFTStub,
|
||||
});
|
||||
expect(onAddNFTStub).toHaveBeenCalledTimes(0);
|
||||
fireEvent.click(screen.queryByText('Import NFTs'));
|
||||
expect(onAddNFTStub).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +1 @@
|
||||
export { default } from './collectibles-tab.component';
|
||||
export { default } from './collectibles-tab';
|
||||
|
8
ui/components/app/collectibles-tab/index.scss
Normal file
8
ui/components/app/collectibles-tab/index.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.collectibles-tab {
|
||||
&__link {
|
||||
a {
|
||||
padding: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { default } from './new-collectibles-notice.component';
|
@ -7,6 +7,7 @@ import {
|
||||
FONT_STYLE,
|
||||
TEXT_ALIGN,
|
||||
TYPOGRAPHY,
|
||||
OVERFLOW_WRAP,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box, { MultipleSizes } from '../box';
|
||||
|
||||
@ -21,6 +22,7 @@ export default function Typography({
|
||||
fontWeight = 'normal',
|
||||
fontStyle = 'normal',
|
||||
align,
|
||||
overflowWrap,
|
||||
boxProps = {},
|
||||
margin = [1, 0],
|
||||
}) {
|
||||
@ -33,6 +35,7 @@ export default function Typography({
|
||||
{
|
||||
[`typography--align-${align}`]: Boolean(align),
|
||||
[`typography--color-${color}`]: Boolean(color),
|
||||
[`typography--overflowwrap-${overflowWrap}`]: Boolean(overflowWrap),
|
||||
},
|
||||
);
|
||||
|
||||
@ -67,6 +70,7 @@ Typography.propTypes = {
|
||||
margin: MultipleSizes,
|
||||
fontWeight: PropTypes.oneOf(Object.values(FONT_WEIGHT)),
|
||||
fontStyle: PropTypes.oneOf(Object.values(FONT_STYLE)),
|
||||
overflowWrap: PropTypes.oneOf(Object.values(OVERFLOW_WRAP)),
|
||||
tag: PropTypes.oneOf([
|
||||
'p',
|
||||
'h1',
|
||||
|
@ -39,6 +39,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@each $overflow in design-system.$overflow-wrap {
|
||||
&--overflowwrap-#{$overflow} {
|
||||
overflow-wrap: $overflow;
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 8 {
|
||||
&--spacing-#{$i} {
|
||||
margin: #{$i * 4}px auto;
|
||||
|
@ -44,6 +44,7 @@ $border-style: solid, double, none, dashed, dotted;
|
||||
$directions: top, right, bottom, left;
|
||||
$display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item;
|
||||
$text-align: left, right, center, justify, end;
|
||||
$overflow-wrap: normal, break-word;
|
||||
$font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900;
|
||||
$font-style: normal, italic, oblique;
|
||||
$font-size: 10px, 12px;
|
||||
|
@ -256,6 +256,34 @@ export const getUnconnectedAccountAlertShown = (state) =>
|
||||
|
||||
export const getTokens = (state) => state.metamask.tokens;
|
||||
|
||||
export function getCollectiblesDetectionNoticeDismissed(state) {
|
||||
return state.metamask.collectiblesDetectionNoticeDismissed;
|
||||
}
|
||||
|
||||
export const getCollectibles = (state) => {
|
||||
const {
|
||||
metamask: {
|
||||
allCollectibles,
|
||||
provider: { chainId },
|
||||
selectedAddress,
|
||||
},
|
||||
} = state;
|
||||
|
||||
return allCollectibles?.[selectedAddress]?.[chainId] ?? [];
|
||||
};
|
||||
|
||||
export const getCollectibleContracts = (state) => {
|
||||
const {
|
||||
metamask: {
|
||||
allCollectibleContracts,
|
||||
provider: { chainId },
|
||||
selectedAddress,
|
||||
},
|
||||
} = state;
|
||||
|
||||
return allCollectibleContracts?.[selectedAddress]?.[chainId] ?? [];
|
||||
};
|
||||
|
||||
export function getBlockGasLimit(state) {
|
||||
return state.metamask.currentBlockGasLimit;
|
||||
}
|
||||
|
@ -172,6 +172,11 @@ export const FONT_WEIGHT = {
|
||||
900: 900,
|
||||
};
|
||||
|
||||
export const OVERFLOW_WRAP = {
|
||||
BREAK_WORD: 'break-word',
|
||||
NORMAL: 'normal',
|
||||
};
|
||||
|
||||
export const FONT_STYLE = {
|
||||
ITALIC: 'italic',
|
||||
NORMAL: 'normal',
|
||||
|
@ -93,7 +93,7 @@ const PATH_NAME_MAP = {
|
||||
[DEFAULT_ROUTE]: 'Home',
|
||||
[UNLOCK_ROUTE]: 'Unlock Page',
|
||||
[LOCK_ROUTE]: 'Lock Page',
|
||||
[`${ASSET_ROUTE}/:asset`]: `Asset Page`,
|
||||
[`${ASSET_ROUTE}/:asset/:id`]: `Asset Page`,
|
||||
[SETTINGS_ROUTE]: 'Settings Page',
|
||||
[GENERAL_ROUTE]: 'General Settings Page',
|
||||
[ADVANCED_ROUTE]: 'Advanced Settings Page',
|
||||
|
@ -3,6 +3,7 @@ import abi from 'human-standard-token-abi';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import { DateTime } from 'luxon';
|
||||
import { util } from '@metamask/controllers';
|
||||
import { addHexPrefix } from '../../../app/scripts/lib/util';
|
||||
import {
|
||||
GOERLI_CHAIN_ID,
|
||||
@ -546,3 +547,14 @@ export const sanitizeMessage = (msg, baseType, types) => {
|
||||
});
|
||||
return sanitizedMessage;
|
||||
};
|
||||
|
||||
export function getAssetImageURL(image, ipfsGateway) {
|
||||
if (!image || !ipfsGateway || typeof image !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (image.startsWith('ipfs://')) {
|
||||
return util.getFormattedIpfsUrl(ipfsGateway, image, true);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { util } from '@metamask/controllers';
|
||||
import { useI18nContext } from '../../hooks/useI18nContext';
|
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
||||
|
||||
@ -19,6 +20,7 @@ export default function AddCollectible() {
|
||||
|
||||
const [address, setAddress] = useState('');
|
||||
const [tokenId, setTokenId] = useState('');
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
|
||||
const handleAddCollectible = async () => {
|
||||
try {
|
||||
@ -33,6 +35,16 @@ export default function AddCollectible() {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
};
|
||||
|
||||
const validateAndSetAddress = (val) => {
|
||||
setDisabled(!util.isValidHexAddress(val) || !tokenId);
|
||||
setAddress(val);
|
||||
};
|
||||
|
||||
const validateAndSetTokenId = (val) => {
|
||||
setDisabled(!util.isValidHexAddress(address) || !val);
|
||||
setTokenId(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={t('addNFT')}
|
||||
@ -46,7 +58,7 @@ export default function AddCollectible() {
|
||||
onClose={() => {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}}
|
||||
disabled={false}
|
||||
disabled={disabled}
|
||||
contentComponent={
|
||||
<Box padding={4}>
|
||||
<Box>
|
||||
@ -56,7 +68,7 @@ export default function AddCollectible() {
|
||||
placeholder="0x..."
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
onChange={(e) => validateAndSetAddress(e.target.value)}
|
||||
fullWidth
|
||||
autoFocus
|
||||
margin="normal"
|
||||
@ -69,7 +81,7 @@ export default function AddCollectible() {
|
||||
placeholder={t('nftTokenIdPlaceholder')}
|
||||
type="number"
|
||||
value={tokenId}
|
||||
onChange={(e) => setTokenId(e.target.value)}
|
||||
onChange={(e) => validateAndSetTokenId(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
@ -1 +1 @@
|
||||
export { default } from './add-collectible.component';
|
||||
export { default } from './add-collectible';
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Redirect, useParams } from 'react-router-dom';
|
||||
import { getTokens } from '../../ducks/metamask/metamask';
|
||||
import CollectibleDetails from '../../components/app/collectible-details/collectible-details';
|
||||
import { getCollectibles, getTokens } from '../../ducks/metamask/metamask';
|
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
||||
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
|
||||
|
||||
@ -11,19 +12,27 @@ import TokenAsset from './components/token-asset';
|
||||
const Asset = () => {
|
||||
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency);
|
||||
const tokens = useSelector(getTokens);
|
||||
const { asset } = useParams();
|
||||
const collectibles = useSelector(getCollectibles);
|
||||
const { asset, id } = useParams();
|
||||
|
||||
const token = tokens.find(({ address }) =>
|
||||
isEqualCaseInsensitive(address, asset),
|
||||
);
|
||||
|
||||
const collectible = collectibles.find(
|
||||
({ address, tokenId }) =>
|
||||
isEqualCaseInsensitive(address, asset) && id === tokenId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector('.app');
|
||||
el.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
if (token) {
|
||||
if (collectible) {
|
||||
content = <CollectibleDetails collectible={collectible} />;
|
||||
} else if (token) {
|
||||
content = <TokenAsset token={token} />;
|
||||
} else if (asset === nativeCurrency) {
|
||||
content = <NativeAsset nativeCurrency={nativeCurrency} />;
|
||||
|
@ -184,7 +184,8 @@ export default class Routes extends Component {
|
||||
path={`${CONNECT_ROUTE}/:id`}
|
||||
component={PermissionsConnect}
|
||||
/>
|
||||
<Authenticated path={`${ASSET_ROUTE}/:asset`} component={Asset} />
|
||||
<Authenticated path={`${ASSET_ROUTE}/:asset/:id`} component={Asset} />
|
||||
<Authenticated path={`${ASSET_ROUTE}/:asset/`} component={Asset} />
|
||||
<Authenticated path={DEFAULT_ROUTE} component={Home} />
|
||||
</Switch>
|
||||
);
|
||||
|
@ -2203,6 +2203,16 @@ export function setOpenSeaEnabled(val) {
|
||||
};
|
||||
}
|
||||
|
||||
export function detectCollectibles() {
|
||||
return async (dispatch) => {
|
||||
dispatch(showLoadingIndication());
|
||||
log.debug(`background.detectCollectibles`);
|
||||
await promisifiedBackground.detectCollectibles();
|
||||
dispatch(hideLoadingIndication());
|
||||
await forceUpdateMetamaskState(dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
export function setAdvancedGasFee(val) {
|
||||
return (dispatch) => {
|
||||
dispatch(showLoadingIndication());
|
||||
@ -3042,10 +3052,15 @@ export async function detectNewTokens() {
|
||||
return promisifiedBackground.detectNewTokens();
|
||||
}
|
||||
|
||||
// App state
|
||||
export function hideTestNetMessage() {
|
||||
return promisifiedBackground.setShowTestnetMessageInDropdown(false);
|
||||
}
|
||||
|
||||
export function setCollectiblesDetectionNoticeDismissed() {
|
||||
return promisifiedBackground.setCollectiblesDetectionNoticeDismissed(true);
|
||||
}
|
||||
|
||||
// QR Hardware Wallets
|
||||
export async function submitQRHardwareCryptoHDKey(cbor) {
|
||||
await promisifiedBackground.submitQRHardwareCryptoHDKey(cbor);
|
||||
|
28
yarn.lock
28
yarn.lock
@ -2601,7 +2601,7 @@
|
||||
semver "^7.3.5"
|
||||
yargs "^17.0.1"
|
||||
|
||||
"@metamask/contract-metadata@^1.29.0", "@metamask/contract-metadata@^1.30.0", "@metamask/contract-metadata@^1.31.0":
|
||||
"@metamask/contract-metadata@^1.29.0", "@metamask/contract-metadata@^1.31.0":
|
||||
version "1.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.31.0.tgz#9e3e46de7a955ea1ca61f7db20d9a17b5e91d3d0"
|
||||
integrity sha512-4FBJkg/vDiYp/thIiZknxrJ0lfsj2eWIPenwlNZmoqOhoL4VqhK5eKWxi+EuGMvv9taP+QBRk6Key7wC1uL78A==
|
||||
@ -2643,14 +2643,14 @@
|
||||
web3 "^0.20.7"
|
||||
web3-provider-engine "^16.0.3"
|
||||
|
||||
"@metamask/controllers@^20.1.0":
|
||||
version "20.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-20.1.0.tgz#1d8386dc22d14f9fd9feb8b3cc8314d663587550"
|
||||
integrity sha512-Z/7uLGXZWbCBbtCybR3jo1bx3mcvZRUSm1i43od4dnJoQo2+Veq4ePrFVgPKS3WtLIM/hzZuI7UTAQ9HNX9aew==
|
||||
"@metamask/controllers@^22.0.0":
|
||||
version "22.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-22.0.0.tgz#54f172be2ae7e32ce47536a1ff06e35cc6ee3c80"
|
||||
integrity sha512-5m4aT+B87IOAvvlbfgqI5n7Pd6VSQUjHBfm34qMBBL5jjUFUSfK6BL0h6ef2jxTE2VCuyBibQ8A7sETQ1+Hd+Q==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^2.3.1"
|
||||
"@ethereumjs/tx" "^3.2.1"
|
||||
"@metamask/contract-metadata" "^1.30.0"
|
||||
"@metamask/contract-metadata" "^1.31.0"
|
||||
"@types/uuid" "^8.3.0"
|
||||
abort-controller "^3.0.0"
|
||||
async-mutex "^0.2.6"
|
||||
@ -2669,11 +2669,12 @@
|
||||
ethjs-unit "^0.1.6"
|
||||
ethjs-util "^0.1.6"
|
||||
human-standard-collectible-abi "^1.0.2"
|
||||
human-standard-multi-collectible-abi "^1.0.2"
|
||||
human-standard-multi-collectible-abi "^1.0.4"
|
||||
human-standard-token-abi "^2.0.0"
|
||||
immer "^9.0.6"
|
||||
isomorphic-fetch "^3.0.0"
|
||||
jsonschema "^1.2.4"
|
||||
multiformats "^9.5.2"
|
||||
nanoid "^3.1.12"
|
||||
punycode "^2.1.1"
|
||||
single-call-balance-checker-abi "^1.0.0"
|
||||
@ -14732,10 +14733,10 @@ human-standard-collectible-abi@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/human-standard-collectible-abi/-/human-standard-collectible-abi-1.0.2.tgz#077bae9ed1b0b0b82bc46932104b4b499c941aa0"
|
||||
integrity sha512-nD3ITUuSAIBgkaCm9J2BGwlHL8iEzFjJfTleDAC5Wi8RBJEXXhxV0JeJjd95o+rTwf98uTE5MW+VoBKOIYQh0g==
|
||||
|
||||
human-standard-multi-collectible-abi@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/human-standard-multi-collectible-abi/-/human-standard-multi-collectible-abi-1.0.3.tgz#be5896b13f8622289cff70040e478366931bf3d7"
|
||||
integrity sha512-1VXqats7JQqDZozLKhpmFG0S33hVePrkLNRJNKfJTxewR0heYKjSoz72kqs+6O/Tywi0zW4fWe7dfTaPX4j7gQ==
|
||||
human-standard-multi-collectible-abi@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/human-standard-multi-collectible-abi/-/human-standard-multi-collectible-abi-1.0.4.tgz#981625bc1a6bea5fef90567f9e12c11581fac497"
|
||||
integrity sha512-ylR9JDXClDJAxWD/QJxsjXJJdLTUmhipTquMAgrfybXL3qX3x3P/vmKg92A7qFu7SqVOf2hyv5dA8vX0j+0Thg==
|
||||
|
||||
human-standard-token-abi@^1.0.2:
|
||||
version "1.0.2"
|
||||
@ -19812,6 +19813,11 @@ multicodec@^3.0.1:
|
||||
uint8arrays "^2.1.5"
|
||||
varint "^6.0.0"
|
||||
|
||||
multiformats@^9.5.2:
|
||||
version "9.5.2"
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.5.2.tgz#14256e49bac8b6a5ecb558c4d3c347bb94873d65"
|
||||
integrity sha512-nLQ9s7YOVtZdeNOVvCkNyFiZdS3wyq0gvCIvdm7Zy1zw3zBoColJKjMkIPXNdTqT7ruuq+G7HrezIN0cXiAZ0w==
|
||||
|
||||
multihashes@^0.4.12, multihashes@^0.4.15, multihashes@~0.4.12, multihashes@~0.4.13, multihashes@~0.4.14, multihashes@~0.4.15:
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.15.tgz#6dbc55f7f312c6782f5367c03c9783681589d8a6"
|
||||
|
Loading…
Reference in New Issue
Block a user