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"