diff --git a/app/scripts/background.js b/app/scripts/background.js index ca5148f73..d696875b3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -11,6 +11,7 @@ import { storeAsStream, storeTransformStream } from '@metamask/obs-store'; import PortStream from 'extension-port-stream'; import { captureException } from '@sentry/browser'; +import { ethErrors } from 'eth-rpc-errors'; import { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, @@ -534,7 +535,9 @@ function setupController(initState, initLangCode) { ); // Finally, reject all approvals managed by the ApprovalController - controller.approvalController.clear(); + controller.approvalController.clear( + ethErrors.provider.userRejectedRequest(), + ); updateBadge(); } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9f4118f60..0bc90d362 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1161,6 +1161,10 @@ export default class MetamaskController extends EventEmitter { collectiblesController, ), + checkAndUpdateCollectiblesOwnershipStatus: collectiblesController.checkAndUpdateCollectiblesOwnershipStatus.bind( + collectiblesController, + ), + // AddressController setAddressBook: addressBookController.set.bind(addressBookController), removeFromAddressBook: addressBookController.delete.bind( diff --git a/package.json b/package.json index 6ced1b9ca..0142507ca 100644 --- a/package.json +++ b/package.json @@ -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": "^22.0.0", + "@metamask/controllers": "^23.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.10.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", diff --git a/ui/components/app/collectibles-items/collectibles-items.js b/ui/components/app/collectibles-items/collectibles-items.js index 6e070a0ee..257796002 100644 --- a/ui/components/app/collectibles-items/collectibles-items.js +++ b/ui/components/app/collectibles-items/collectibles-items.js @@ -24,8 +24,15 @@ const width = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP ? BLOCK_SIZES.ONE_THIRD : 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); Object.keys(collections).forEach((key) => { @@ -33,109 +40,138 @@ export default function CollectiblesItems({ collections = {} }) { }); const history = useHistory(); - const [dropdownState, setDropdownState] = useState(defaultDropdownState); + const renderCollectionImage = ( + isPreviouslyOwnedCollection, + collectionImage, + collectionName, + ) => { + if (isPreviouslyOwnedCollection) { + return null; + } + if (collectionImage) { + return ( + + ); + } + return ( +
+ {collectionName[0]} +
+ ); + }; + + const renderCollection = ({ + collectibles, + collectionName, + collectionImage, + key, + isPreviouslyOwnedCollection, + }) => { + if (!collectibles.length) { + return null; + } + + const isExpanded = dropdownState[key]; + return ( +
{ + setDropdownState((_dropdownState) => ({ + ..._dropdownState, + [key]: !isExpanded, + })); + }} + > + + + {renderCollectionImage( + isPreviouslyOwnedCollection, + collectionImage, + collectionName, + )} + + {`${collectionName} (${collectibles.length})`} + + + + + + + {isExpanded ? ( + + {collectibles.map((collectible, i) => { + const { image, address, tokenId, backgroundColor } = collectible; + const collectibleImage = getAssetImageURL(image, ipfsGateway); + return ( + +
+ + history.push(`${ASSET_ROUTE}/${address}/${tokenId}`) + } + className="collectibles-items__collection-item-image" + src={collectibleImage} + /> +
+
+ ); + })} +
+ ) : null} +
+ ); + }; + return (
<> - {Object.keys(collections).map((key, index) => { + {Object.keys(collections).map((key) => { const { collectibles, collectionName, collectionImage, } = collections[key]; - const isExpanded = dropdownState[key]; - return ( -
{ - setDropdownState((_dropdownState) => ({ - ..._dropdownState, - [key]: !isExpanded, - })); - }} - > - - - {collectionImage ? ( - - ) : ( -
- {collectionName[0]} -
- )} - - {`${collectionName} (${collectibles.length})`} - -
- - - -
- {isExpanded ? ( - - {collectibles.map((collectible, i) => { - const { - image, - address, - tokenId, - backgroundColor, - } = collectible; - const collectibleImage = getAssetImageURL( - image, - ipfsGateway, - ); - return ( - -
- - history.push( - `${ASSET_ROUTE}/${address}/${tokenId}`, - ) - } - className="collectibles-items__collection-item-image" - src={collectibleImage} - /> -
-
- ); - })} -
- ) : null} -
- ); + return renderCollection({ + collectibles, + collectionName, + collectionImage, + key, + isPreviouslyOwnedCollection: false, + }); + })} + {renderCollection({ + collectibles: previouslyOwnedCollection.collectibles, + collectionName: previouslyOwnedCollection.collectionName, + isPreviouslyOwnedCollection: true, + key: PREVIOUSLY_OWNED_KEY, })}
@@ -144,6 +180,26 @@ export default function CollectiblesItems({ collections = {} }) { } 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({ collectibles: PropTypes.arrayOf( PropTypes.shape({ diff --git a/ui/components/app/collectibles-tab/collectibles-tab.js b/ui/components/app/collectibles-tab/collectibles-tab.js index 9264144b7..32ce380be 100644 --- a/ui/components/app/collectibles-tab/collectibles-tab.js +++ b/ui/components/app/collectibles-tab/collectibles-tab.js @@ -24,7 +24,10 @@ import { } from '../../../ducks/metamask/metamask'; import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors'; import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; -import { detectCollectibles } from '../../../store/actions'; +import { + checkAndUpdateCollectiblesOwnershipStatus, + detectCollectibles, +} from '../../../store/actions'; export default function CollectiblesTab({ onAddNFT }) { const collectibles = useSelector(getCollectibles); @@ -38,31 +41,52 @@ export default function CollectiblesTab({ onAddNFT }) { 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 getCollections = () => { + const collections = {}; + const previouslyOwnedCollection = { + collectionName: 'Previously Owned', + collectibles: [], + }; + collectibles.forEach((collectible) => { + if (collectible?.isCurrentlyOwned === false) { + previouslyOwnedCollection.collectibles.push(collectible); + } else 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], + }; + } + }); + return [collections, previouslyOwnedCollection]; + }; + + const [collections, previouslyOwnedCollection] = getCollections(); const onEnableAutoDetect = () => { history.push(EXPERIMENTAL_ROUTE); }; + const onRefresh = () => { + if (isMainnet) { + dispatch(detectCollectibles()); + } + checkAndUpdateCollectiblesOwnershipStatus(); + }; + return (
{collectibles.length > 0 ? ( - + ) : ( {isMainnet && @@ -115,34 +139,27 @@ export default function CollectiblesTab({ onAddNFT }) { alignItems={ALIGN_ITEMS.CENTER} justifyContent={JUSTIFY_CONTENT.CENTER} > - {isMainnet ? ( - <> - - {useCollectibleDetection ? ( - - ) : ( - - )} - - - {t('or')} - - - ) : null} + + {isMainnet && !useCollectibleDetection ? ( + + ) : ( + + )} + + + {t('or')} + { const detectCollectiblesStub = jest.fn(); const setCollectiblesDetectionNoticeDismissedStub = jest.fn(); + const getStateStub = jest.fn(); + const checkAndUpdateCollectiblesOwnershipStatusStub = jest.fn(); setBackgroundConnection({ setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub, detectCollectibles: detectCollectiblesStub, + getState: getStateStub, + checkAndUpdateCollectiblesOwnershipStatus: checkAndUpdateCollectiblesOwnershipStatusStub, }); const historyPushMock = jest.fn(); @@ -264,15 +268,33 @@ describe('Collectible Items', () => { }); }); 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({ selectedAddress: ACCOUNT_1, collectibles: COLLECTIBLES, useCollectibleDetection: true, }); expect(detectCollectiblesStub).not.toHaveBeenCalled(); + expect( + checkAndUpdateCollectiblesOwnershipStatusStub, + ).not.toHaveBeenCalled(); fireEvent.click(screen.queryByText('Refresh list')); 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', () => { diff --git a/ui/store/actions.js b/ui/store/actions.js index c7c33157f..75be9893f 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -1391,6 +1391,10 @@ export function removeCollectible(address, tokenID, dontShowLoadingIndicator) { }; } +export async function checkAndUpdateCollectiblesOwnershipStatus() { + await promisifiedBackground.checkAndUpdateCollectiblesOwnershipStatus(); +} + export function removeToken(address) { return async (dispatch) => { dispatch(showLoadingIndication()); diff --git a/yarn.lock b/yarn.lock index 459cf818a..4488c6297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2643,10 +2643,10 @@ web3 "^0.20.7" web3-provider-engine "^16.0.3" -"@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== +"@metamask/controllers@^23.0.0": + version "23.0.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-23.0.0.tgz#81ea9fa37924a14b08668f37e7e31f46091aa610" + integrity sha512-6hKh5H0HM1YLTOdfuD8gRXGCG3brEhTagup204SrlmwabTReaIIb/zXTat7jCs6ZvN362a44GO5mboZ6W9MhIA== dependencies: "@ethereumjs/common" "^2.3.1" "@ethereumjs/tx" "^3.2.1" @@ -2667,7 +2667,6 @@ ethereumjs-wallet "^1.0.1" ethers "^5.4.1" ethjs-unit "^0.1.6" - ethjs-util "^0.1.6" human-standard-collectible-abi "^1.0.2" human-standard-multi-collectible-abi "^1.0.4" human-standard-token-abi "^2.0.0"