1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Feat/check update collectible ownership (#13110)

* Use method to check and update collectible ownership
This commit is contained in:
Alex Donesky 2022-01-03 14:39:41 -06:00 committed by GitHub
parent 211c5afb7b
commit c266d4e6af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 149 deletions

View File

@ -11,6 +11,7 @@ import { storeAsStream, storeTransformStream } from '@metamask/obs-store';
import PortStream from 'extension-port-stream'; import PortStream from 'extension-port-stream';
import { captureException } from '@sentry/browser'; import { captureException } from '@sentry/browser';
import { ethErrors } from 'eth-rpc-errors';
import { import {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
@ -534,7 +535,9 @@ function setupController(initState, initLangCode) {
); );
// Finally, reject all approvals managed by the ApprovalController // Finally, reject all approvals managed by the ApprovalController
controller.approvalController.clear(); controller.approvalController.clear(
ethErrors.provider.userRejectedRequest(),
);
updateBadge(); updateBadge();
} }

View File

@ -1161,6 +1161,10 @@ export default class MetamaskController extends EventEmitter {
collectiblesController, collectiblesController,
), ),
checkAndUpdateCollectiblesOwnershipStatus: collectiblesController.checkAndUpdateCollectiblesOwnershipStatus.bind(
collectiblesController,
),
// AddressController // AddressController
setAddressBook: addressBookController.set.bind(addressBookController), setAddressBook: addressBookController.set.bind(addressBookController),
removeFromAddressBook: addressBookController.delete.bind( removeFromAddressBook: addressBookController.delete.bind(

View File

@ -107,7 +107,7 @@
"@keystonehq/metamask-airgapped-keyring": "0.2.1", "@keystonehq/metamask-airgapped-keyring": "0.2.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.31.0", "@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^22.0.0", "@metamask/controllers": "^23.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-ledger-bridge-keyring": "^0.10.0",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^2.1.0", "@metamask/etherscan-link": "^2.1.0",

View File

@ -24,8 +24,15 @@ const width =
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
? BLOCK_SIZES.ONE_THIRD ? BLOCK_SIZES.ONE_THIRD
: BLOCK_SIZES.ONE_SIXTH; : BLOCK_SIZES.ONE_SIXTH;
export default function CollectiblesItems({ collections = {} }) {
const defaultDropdownState = {}; const PREVIOUSLY_OWNED_KEY = 'previouslyOwned';
export default function CollectiblesItems({
collections = {},
previouslyOwnedCollection = {},
}) {
const defaultDropdownState = { [PREVIOUSLY_OWNED_KEY]: false };
const [dropdownState, setDropdownState] = useState(defaultDropdownState);
const ipfsGateway = useSelector(getIpfsGateway); const ipfsGateway = useSelector(getIpfsGateway);
Object.keys(collections).forEach((key) => { Object.keys(collections).forEach((key) => {
@ -33,109 +40,138 @@ export default function CollectiblesItems({ collections = {} }) {
}); });
const history = useHistory(); const history = useHistory();
const [dropdownState, setDropdownState] = useState(defaultDropdownState); const renderCollectionImage = (
isPreviouslyOwnedCollection,
collectionImage,
collectionName,
) => {
if (isPreviouslyOwnedCollection) {
return null;
}
if (collectionImage) {
return (
<img
src={collectionImage}
className="collectibles-items__collection-image"
/>
);
}
return (
<div className="collectibles-items__collection-image-alt">
{collectionName[0]}
</div>
);
};
const renderCollection = ({
collectibles,
collectionName,
collectionImage,
key,
isPreviouslyOwnedCollection,
}) => {
if (!collectibles.length) {
return null;
}
const isExpanded = dropdownState[key];
return (
<div
className="collectibles-items__collection"
key={`collection-${key}`}
onClick={() => {
setDropdownState((_dropdownState) => ({
..._dropdownState,
[key]: !isExpanded,
}));
}}
>
<Box
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
>
<Box
alignItems={ALIGN_ITEMS.CENTER}
className="collectibles-items__collection-header"
>
{renderCollectionImage(
isPreviouslyOwnedCollection,
collectionImage,
collectionName,
)}
<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}`}
className="collectibles-items__collection-item-wrapper"
>
<div
className="collectibles-items__collection-item"
style={{
backgroundColor,
}}
>
<img
onClick={() =>
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
}
className="collectibles-items__collection-item-image"
src={collectibleImage}
/>
</div>
</Box>
);
})}
</Box>
) : null}
</div>
);
};
return ( return (
<div className="collectibles-items"> <div className="collectibles-items">
<Box padding={[6, 4]} flexDirection={FLEX_DIRECTION.COLUMN}> <Box padding={[6, 4]} flexDirection={FLEX_DIRECTION.COLUMN}>
<> <>
{Object.keys(collections).map((key, index) => { {Object.keys(collections).map((key) => {
const { const {
collectibles, collectibles,
collectionName, collectionName,
collectionImage, collectionImage,
} = collections[key]; } = collections[key];
const isExpanded = dropdownState[key]; return renderCollection({
return ( collectibles,
<div collectionName,
className="collectibles-items__collection" collectionImage,
key={`collection-${index}`} key,
onClick={() => { isPreviouslyOwnedCollection: false,
setDropdownState((_dropdownState) => ({ });
..._dropdownState, })}
[key]: !isExpanded, {renderCollection({
})); collectibles: previouslyOwnedCollection.collectibles,
}} collectionName: previouslyOwnedCollection.collectionName,
> isPreviouslyOwnedCollection: true,
<Box key: PREVIOUSLY_OWNED_KEY,
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
>
<Box
alignItems={ALIGN_ITEMS.CENTER}
className="collectibles-items__collection-header"
>
{collectionImage ? (
<img
src={collectionImage}
className="collectibles-items__collection-image"
/>
) : (
<div className="collectibles-items__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}`}
className="collectibles-items__collection-item-wrapper"
>
<div
className="collectibles-items__collection-item"
style={{
backgroundColor,
}}
>
<img
onClick={() =>
history.push(
`${ASSET_ROUTE}/${address}/${tokenId}`,
)
}
className="collectibles-items__collection-item-image"
src={collectibleImage}
/>
</div>
</Box>
);
})}
</Box>
) : null}
</div>
);
})} })}
</> </>
</Box> </Box>
@ -144,6 +180,26 @@ export default function CollectiblesItems({ collections = {} }) {
} }
CollectiblesItems.propTypes = { CollectiblesItems.propTypes = {
previouslyOwnedCollection: 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,
}),
}),
),
collectionName: PropTypes.string,
}),
collections: PropTypes.shape({ collections: PropTypes.shape({
collectibles: PropTypes.arrayOf( collectibles: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({

View File

@ -24,7 +24,10 @@ import {
} from '../../../ducks/metamask/metamask'; } from '../../../ducks/metamask/metamask';
import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors'; import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors';
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
import { detectCollectibles } from '../../../store/actions'; import {
checkAndUpdateCollectiblesOwnershipStatus,
detectCollectibles,
} from '../../../store/actions';
export default function CollectiblesTab({ onAddNFT }) { export default function CollectiblesTab({ onAddNFT }) {
const collectibles = useSelector(getCollectibles); const collectibles = useSelector(getCollectibles);
@ -38,31 +41,52 @@ export default function CollectiblesTab({ onAddNFT }) {
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
const collections = {}; const getCollections = () => {
collectibles.forEach((collectible) => { const collections = {};
if (collections[collectible.address]) { const previouslyOwnedCollection = {
collections[collectible.address].collectibles.push(collectible); collectionName: 'Previously Owned',
} else { collectibles: [],
const collectionContract = collectibleContracts.find( };
({ address }) => address === collectible.address, collectibles.forEach((collectible) => {
); if (collectible?.isCurrentlyOwned === false) {
collections[collectible.address] = { previouslyOwnedCollection.collectibles.push(collectible);
collectionName: collectionContract?.name || collectible.name, } else if (collections[collectible.address]) {
collectionImage: collections[collectible.address].collectibles.push(collectible);
collectionContract?.logo || collectible.collectionImage, } else {
collectibles: [collectible], const collectionContract = collectibleContracts.find(
}; ({ address }) => address === collectible.address,
} );
}); collections[collectible.address] = {
collectionName: collectionContract?.name || collectible.name,
collectionImage:
collectionContract?.logo || collectible.collectionImage,
collectibles: [collectible],
};
}
});
return [collections, previouslyOwnedCollection];
};
const [collections, previouslyOwnedCollection] = getCollections();
const onEnableAutoDetect = () => { const onEnableAutoDetect = () => {
history.push(EXPERIMENTAL_ROUTE); history.push(EXPERIMENTAL_ROUTE);
}; };
const onRefresh = () => {
if (isMainnet) {
dispatch(detectCollectibles());
}
checkAndUpdateCollectiblesOwnershipStatus();
};
return ( return (
<div className="collectibles-tab"> <div className="collectibles-tab">
{collectibles.length > 0 ? ( {collectibles.length > 0 ? (
<CollectiblesItems collections={collections} /> <CollectiblesItems
collections={collections}
previouslyOwnedCollection={previouslyOwnedCollection}
/>
) : ( ) : (
<Box padding={[6, 12, 6, 12]}> <Box padding={[6, 12, 6, 12]}>
{isMainnet && {isMainnet &&
@ -115,34 +139,27 @@ export default function CollectiblesTab({ onAddNFT }) {
alignItems={ALIGN_ITEMS.CENTER} alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER} justifyContent={JUSTIFY_CONTENT.CENTER}
> >
{isMainnet ? ( <Box
<> className="collectibles-tab__link"
<Box justifyContent={JUSTIFY_CONTENT.FLEX_END}
className="collectibles-tab__link" >
justifyContent={JUSTIFY_CONTENT.FLEX_END} {isMainnet && !useCollectibleDetection ? (
> <Button type="link" onClick={onEnableAutoDetect}>
{useCollectibleDetection ? ( {t('enableAutoDetect')}
<Button </Button>
type="link" ) : (
onClick={() => dispatch(detectCollectibles())} <Button type="link" onClick={onRefresh}>
> {t('refreshList')}
{t('refreshList')} </Button>
</Button> )}
) : ( </Box>
<Button type="link" onClick={onEnableAutoDetect}> <Typography
{t('enableAutoDetect')} color={COLORS.UI3}
</Button> variant={TYPOGRAPHY.H4}
)} align={TEXT_ALIGN.CENTER}
</Box> >
<Typography {t('or')}
color={COLORS.UI3} </Typography>
variant={TYPOGRAPHY.H4}
align={TEXT_ALIGN.CENTER}
>
{t('or')}
</Typography>
</>
) : null}
<Box <Box
justifyContent={JUSTIFY_CONTENT.FLEX_START} justifyContent={JUSTIFY_CONTENT.FLEX_START}
className="collectibles-tab__link" className="collectibles-tab__link"

View File

@ -172,9 +172,13 @@ const render = ({
describe('Collectible Items', () => { describe('Collectible Items', () => {
const detectCollectiblesStub = jest.fn(); const detectCollectiblesStub = jest.fn();
const setCollectiblesDetectionNoticeDismissedStub = jest.fn(); const setCollectiblesDetectionNoticeDismissedStub = jest.fn();
const getStateStub = jest.fn();
const checkAndUpdateCollectiblesOwnershipStatusStub = jest.fn();
setBackgroundConnection({ setBackgroundConnection({
setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub, setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub,
detectCollectibles: detectCollectiblesStub, detectCollectibles: detectCollectiblesStub,
getState: getStateStub,
checkAndUpdateCollectiblesOwnershipStatus: checkAndUpdateCollectiblesOwnershipStatusStub,
}); });
const historyPushMock = jest.fn(); const historyPushMock = jest.fn();
@ -264,15 +268,33 @@ describe('Collectible Items', () => {
}); });
}); });
describe('Collectibles options', () => { 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', () => { it('should render a link "Refresh list" when some collectibles are present on mainnet and collectible auto-detection preference is set to true, which, when clicked calls methods DetectCollectibles and checkAndUpdateCollectiblesOwnershipStatus', () => {
render({ render({
selectedAddress: ACCOUNT_1, selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES, collectibles: COLLECTIBLES,
useCollectibleDetection: true, useCollectibleDetection: true,
}); });
expect(detectCollectiblesStub).not.toHaveBeenCalled(); expect(detectCollectiblesStub).not.toHaveBeenCalled();
expect(
checkAndUpdateCollectiblesOwnershipStatusStub,
).not.toHaveBeenCalled();
fireEvent.click(screen.queryByText('Refresh list')); fireEvent.click(screen.queryByText('Refresh list'));
expect(detectCollectiblesStub).toHaveBeenCalled(); expect(detectCollectiblesStub).toHaveBeenCalled();
expect(checkAndUpdateCollectiblesOwnershipStatusStub).toHaveBeenCalled();
});
it('should render a link "Refresh list" when some collectibles are present on a non-mainnet chain, which, when clicked calls a method checkAndUpdateCollectiblesOwnershipStatus', () => {
render({
chainId: '0x4',
selectedAddress: ACCOUNT_1,
collectibles: COLLECTIBLES,
useCollectibleDetection: true,
});
expect(
checkAndUpdateCollectiblesOwnershipStatusStub,
).not.toHaveBeenCalled();
fireEvent.click(screen.queryByText('Refresh list'));
expect(checkAndUpdateCollectiblesOwnershipStatusStub).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', () => { 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', () => {

View File

@ -1391,6 +1391,10 @@ export function removeCollectible(address, tokenID, dontShowLoadingIndicator) {
}; };
} }
export async function checkAndUpdateCollectiblesOwnershipStatus() {
await promisifiedBackground.checkAndUpdateCollectiblesOwnershipStatus();
}
export function removeToken(address) { export function removeToken(address) {
return async (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());

View File

@ -2643,10 +2643,10 @@
web3 "^0.20.7" web3 "^0.20.7"
web3-provider-engine "^16.0.3" web3-provider-engine "^16.0.3"
"@metamask/controllers@^22.0.0": "@metamask/controllers@^23.0.0":
version "22.0.0" version "23.0.0"
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-22.0.0.tgz#54f172be2ae7e32ce47536a1ff06e35cc6ee3c80" resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-23.0.0.tgz#81ea9fa37924a14b08668f37e7e31f46091aa610"
integrity sha512-5m4aT+B87IOAvvlbfgqI5n7Pd6VSQUjHBfm34qMBBL5jjUFUSfK6BL0h6ef2jxTE2VCuyBibQ8A7sETQ1+Hd+Q== integrity sha512-6hKh5H0HM1YLTOdfuD8gRXGCG3brEhTagup204SrlmwabTReaIIb/zXTat7jCs6ZvN362a44GO5mboZ6W9MhIA==
dependencies: dependencies:
"@ethereumjs/common" "^2.3.1" "@ethereumjs/common" "^2.3.1"
"@ethereumjs/tx" "^3.2.1" "@ethereumjs/tx" "^3.2.1"
@ -2667,7 +2667,6 @@
ethereumjs-wallet "^1.0.1" ethereumjs-wallet "^1.0.1"
ethers "^5.4.1" ethers "^5.4.1"
ethjs-unit "^0.1.6" ethjs-unit "^0.1.6"
ethjs-util "^0.1.6"
human-standard-collectible-abi "^1.0.2" human-standard-collectible-abi "^1.0.2"
human-standard-multi-collectible-abi "^1.0.4" human-standard-multi-collectible-abi "^1.0.4"
human-standard-token-abi "^2.0.0" human-standard-token-abi "^2.0.0"