Skip to content

Commit

Permalink
fix: fix nfts displayed on account and refactor collectibles component (
Browse files Browse the repository at this point in the history
#9206)

## **Description**

This PR fixes this issue
#9196.
Patch created considering this open PR on core
MetaMask/core#4143

When a user imports an account with Nfts then switches back and forth
betwen two accounts, at some point he will see nfts from one account
appear on another.

Link to core branch for the patch :
https://github.com/MetaMask/core/compare/patch/mobile-assets-controllers-v-12-0-0...patch/mobile-assets-controllers-v-12-0-0-nft-bug?expand=1


## **Related issues**

Fixes: #9196.
Core PR: MetaMask/core#4143

## **Manual testing steps**

1. Open app
2. Import an account that has nfts (use for example orangefox.eth)
3. Create a new account (You can either create a new account which will
be empty or use any account that you already have)
4. Switch back and forth between orangefox.eth and another account fast
and you will see nfts from orangefox.eth that appear on the other
account. (This should not happen on this PR)

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->


https://github.com/MetaMask/metamask-mobile/assets/10994169/611bd2c5-b005-48ed-9926-6d72ff244816


### **After**

<!-- [screenshots/recordings] -->
Switching back and forth between accounts


https://github.com/MetaMask/metamask-mobile/assets/10994169/5c00c78d-f3f2-4080-b441-eaf9828aec13


## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com>
  • Loading branch information
sahar-fehri and sethkfman authored Apr 11, 2024
1 parent f7466da commit cbc67f5
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 165 deletions.
6 changes: 0 additions & 6 deletions app/components/UI/CollectibleContractElement/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ function CollectibleContractElement({
chainId,
selectedAddress,
removeFavoriteCollectible,
toggleRemovingProgress,
}) {
const [collectiblesGrid, setCollectiblesGrid] = useState([]);
const [collectiblesVisible, setCollectiblesVisible] = useState(
Expand Down Expand Up @@ -150,11 +149,7 @@ function CollectibleContractElement({

const handleMenuAction = (index) => {
if (index === 1) {
// set toggle to true
toggleRemovingProgress();
removeNft();
// set toggle to false to indicate that removing the NFT has finished
toggleRemovingProgress();
} else if (index === 0) {
refreshMetadata();
}
Expand Down Expand Up @@ -306,7 +301,6 @@ CollectibleContractElement.propTypes = {
* Dispatch remove collectible from favorites action
*/
removeFavoriteCollectible: PropTypes.func,
toggleRemovingProgress: PropTypes.func,
};

const mapStateToProps = (state) => ({
Expand Down
156 changes: 53 additions & 103 deletions app/components/UI/CollectibleContracts/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
Expand All @@ -9,7 +9,7 @@ import {
FlatList,
RefreshControl,
} from 'react-native';
import { connect, useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { fontStyles } from '../../../styles/common';
import { strings } from '../../../../locales/i18n';
import Engine from '../../../core/Engine';
Expand All @@ -23,7 +23,7 @@ import {
import { removeFavoriteCollectible } from '../../../actions/collectibles';
import Text from '../../Base/Text';
import AppConstants from '../../../core/AppConstants';
import { isIPFSUri, toLowerCaseEquals } from '../../../util/general';
import { toLowerCaseEquals } from '../../../util/general';
import { compareTokenIds } from '../../../util/tokens';
import CollectibleDetectionModal from '../CollectibleDetectionModal';
import { useTheme } from '../../../util/theme';
Expand All @@ -37,13 +37,11 @@ import {
selectIsIpfsGatewayEnabled,
selectSelectedAddress,
selectUseNftDetection,
selectDisplayNftMedia,
} from '../../../selectors/preferencesController';
import {
IMPORT_NFT_BUTTON_ID,
NFT_TAB_CONTAINER_ID,
} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import Logger from '../../../util/Logger';
import { useMetrics } from '../../../components/hooks/useMetrics';

const createStyles = (colors) =>
Expand Down Expand Up @@ -114,13 +112,6 @@ const CollectibleContracts = ({
const [isAddNFTEnabled, setIsAddNFTEnabled] = useState(true);
const [refreshing, setRefreshing] = useState(false);

const [isRemovingNftInProgress, setIsRemovingNftInProgress] = useState(false);

const toggleRemovingProgress = () =>
setIsRemovingNftInProgress((value) => !value);

const displayNftMedia = useSelector(selectDisplayNftMedia);

const isCollectionDetectionBannerVisible =
networkType === MAINNET && !useNftDetection;

Expand Down Expand Up @@ -160,33 +151,68 @@ const CollectibleContracts = ({
typeof collectible.tokenId === 'number' ||
(typeof collectible.tokenId === 'string' && !isNaN(collectible.tokenId));

/**
* Method to updated collectible and avoid backwards compatibility issues.
* @param address - Collectible address.
* @param tokenId - Collectible token ID.
*/
const updateCollectibleMetadata = useCallback(
const updateAllCollectibleMetadata = useCallback(
async (collectibles) => {
const { NftController } = Engine.context;
// Filter out ignored collectibles
const filteredcollectibles = collectibles.filter(
(collectible) => !isCollectibleIgnored(collectible),
);

// filter removable collectible
const removable = filteredcollectibles.filter((single) =>
String(single.tokenId).includes('e+'),
);
const updatable = filteredcollectibles.filter(
(single) => !String(single.tokenId).includes('e+'),
);

removable.forEach((elm) => {
removeFavoriteCollectible(selectedAddress, chainId, elm);
});

filteredcollectibles.forEach((collectible) => {
if (String(collectible.tokenId).includes('e+')) {
removeFavoriteCollectible(selectedAddress, chainId, collectible);
}
});

if (updatable.length !== 0) {
await NftController.updateNftMetadata({
nfts: updatable,
userAddress: selectedAddress,
});
}
},
[isCollectibleIgnored, removeFavoriteCollectible, chainId, selectedAddress],
);

useEffect(() => {
// TO DO: Move this fix to the controllers layer
const updatableCollectibles = collectibles.filter((single) =>
shouldUpdateCollectibleMetadata(single),
);
if (updatableCollectibles.length !== 0) {
updateAllCollectibleMetadata(updatableCollectibles);
}
}, [collectibles, updateAllCollectibleMetadata]);

/* const updateCollectibleMetadata = useCallback(
async (collectible) => {
const { NftController } = Engine.context;
const { address, tokenId } = collectible;
const isIgnored = isCollectibleIgnored(collectible);
if (!isRemovingNftInProgress && !isIgnored) {
if (!isIgnored) {
if (String(tokenId).includes('e+')) {
removeFavoriteCollectible(selectedAddress, chainId, collectible);
} else {
await NftController.addNft(address, String(tokenId));
}
}
},
[
chainId,
removeFavoriteCollectible,
selectedAddress,
isCollectibleIgnored,
isRemovingNftInProgress,
],
[chainId, removeFavoriteCollectible, selectedAddress, isCollectibleIgnored],
);
useEffect(() => {
Expand All @@ -196,80 +222,7 @@ const CollectibleContracts = ({
updateCollectibleMetadata(collectible);
}
});
}, [collectibles, updateCollectibleMetadata]);

const memoizedCollectibles = useMemo(() => collectibles, [collectibles]);

const isNftUpdatableWithOpenSea = (collectible) =>
Boolean(
!collectible.image &&
!collectible.name &&
!collectible.description &&
// Preventing on a loop if the proxy or open sea api can't be fetched
!(
collectible.error?.startsWith('Opensea') ||
collectible.error?.startsWith('Both')
),
);

const updateOpenSeaUnfetchedMetadata = useCallback(async () => {
try {
if (displayNftMedia) {
const promises = memoizedCollectibles.map(async (collectible) => {
if (isNftUpdatableWithOpenSea(collectible)) {
await updateCollectibleMetadata(collectible);
}
});

await Promise.all(promises);
}
} catch (error) {
Logger.error(
error,
'error while trying to update metadata of stored nfts',
);
}
}, [updateCollectibleMetadata, memoizedCollectibles, displayNftMedia]);

useEffect(() => {
updateOpenSeaUnfetchedMetadata();
}, [updateOpenSeaUnfetchedMetadata]);

const isNftUpdatableWithThirdParties = (collectible) =>
Boolean(
!collectible.image &&
!collectible.name &&
!collectible.description &&
isIPFSUri(collectible.tokenURI) &&
// Preventing on a loop if the third party service can't be fetched
!(
collectible.error?.startsWith('URI') ||
collectible.error?.startsWith('Both')
),
);

const updateThirdPartyUnfetchedMetadata = useCallback(async () => {
try {
if (isIpfsGatewayEnabled) {
const promises = memoizedCollectibles.map(async (collectible) => {
if (isNftUpdatableWithThirdParties(collectible)) {
await updateCollectibleMetadata(collectible);
}
});

await Promise.all(promises);
}
} catch (error) {
Logger.error(
error,
'error while trying to update metadata of stored nfts',
);
}
}, [updateCollectibleMetadata, isIpfsGatewayEnabled, memoizedCollectibles]);

useEffect(() => {
updateThirdPartyUnfetchedMetadata();
}, [updateThirdPartyUnfetchedMetadata]);
}, [collectibles, updateCollectibleMetadata]); */

const goToAddCollectible = useCallback(() => {
setIsAddNFTEnabled(false);
Expand Down Expand Up @@ -310,7 +263,6 @@ const CollectibleContracts = ({
key={item.address}
contractCollectibles={contractCollectibles}
collectiblesVisible={index === 0}
toggleRemovingProgress={toggleRemovingProgress}
/>
);
},
Expand All @@ -333,12 +285,10 @@ const CollectibleContracts = ({
key={'Favorites'}
contractCollectibles={filteredCollectibles}
collectiblesVisible
toggleRemovingProgress={toggleRemovingProgress}
/>
)
);
}, [favoriteCollectibles, collectibles, onItemPress]);

const onRefresh = useCallback(async () => {
requestAnimationFrame(async () => {
setRefreshing(true);
Expand Down
10 changes: 6 additions & 4 deletions app/components/UI/CollectibleContracts/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jest.mock('../../../core/Engine', () => ({
context: {
NftController: {
addNft: jest.fn(),
updateNftMetadata: jest.fn(),
checkAndUpdateAllNftsOwnershipStatus: jest.fn(),
},
NftDetectionController: {
Expand Down Expand Up @@ -219,6 +220,7 @@ describe('CollectibleContracts', () => {
},
NftController: {
addNft: jest.fn(),
updateNftMetadata: jest.fn(),
allNfts: {
[CURRENT_ACCOUNT]: {
'1': [],
Expand All @@ -241,8 +243,8 @@ describe('CollectibleContracts', () => {
const spyOnContracts = jest
.spyOn(allSelectors, 'collectibleContractsSelector')
.mockReturnValue(collectibleData);
const spyOnAddNft = jest
.spyOn(Engine.context.NftController, 'addNft')
const spyOnUpdateNftMetadata = jest
.spyOn(Engine.context.NftController, 'updateNftMetadata')
.mockImplementation(async () => undefined);

const { getByTestId } = renderWithProvider(<CollectibleContracts />, {
Expand All @@ -256,7 +258,7 @@ describe('CollectibleContracts', () => {
});

await waitFor(() => {
expect(spyOnAddNft).toHaveBeenCalled();
expect(spyOnUpdateNftMetadata).toHaveBeenCalled();
const nftImageAfter = queryByTestId('nft-image');
expect(nftImageAfter.props.source.uri).toEqual(
nftItemDataUpdated[0].image,
Expand All @@ -265,6 +267,6 @@ describe('CollectibleContracts', () => {

spyOnCollectibles.mockRestore();
spyOnContracts.mockRestore();
spyOnAddNft.mockRestore();
spyOnUpdateNftMetadata.mockRestore();
});
});
Loading

0 comments on commit cbc67f5

Please sign in to comment.