Skip to content

Commit

Permalink
feat: show default petnames for watched NFTs (#23438)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewwalsh0 committed Mar 15, 2024
1 parent 466e7ec commit 805c81e
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,31 @@ exports[`Identicon should match snapshot with default props 1`] = `
/>
</div>
`;

exports[`Identicon should match snapshot with token icon 1`] = `
<div>
<div
class="identicon test-address"
style="height: 46px; width: 46px; border-radius: 23px;"
>
<img
src="https://test.com/testTokenIcon.jpg"
style="width: 100%;"
/>
</div>
</div>
`;

exports[`Identicon should match snapshot with watched NFT logo 1`] = `
<div>
<div
class="identicon test-address"
style="height: 46px; width: 46px; border-radius: 23px;"
>
<img
src="https://test.com/testNftLogo.jpg"
style="width: 100%;"
/>
</div>
</div>
`;
38 changes: 35 additions & 3 deletions ui/components/ui/identicon/identicon.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export default class Identicon extends Component {
* User preferred IPFS gateway
*/
ipfsGateway: PropTypes.string,
/**
* Watched NFT contract data keyed by address
*/
watchedNftContracts: PropTypes.object,
};

state = {
Expand All @@ -72,6 +76,7 @@ export default class Identicon extends Component {
useBlockie: false,
alt: '',
tokenList: {},
watchedNftContracts: {},
};

renderImage() {
Expand Down Expand Up @@ -105,7 +110,9 @@ export default class Identicon extends Component {
}

renderJazzicon() {
const { address, className, diameter, alt, tokenList } = this.props;
const { address, className, diameter, alt } = this.props;
const tokenList = this.getTokenList();

return (
<Jazzicon
address={address}
Expand Down Expand Up @@ -141,8 +148,33 @@ export default class Identicon extends Component {
return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state);
}

getTokenImage() {
const { address, tokenList } = this.props;
return tokenList[address?.toLowerCase()]?.iconUrl;
}

getNftImage() {
const { address, watchedNftContracts } = this.props;
return watchedNftContracts[address?.toLowerCase()]?.logo;
}

getTokenList() {
const { address } = this.props;
const tokenImage = this.getTokenImage();
const nftImage = this.getNftImage();
const iconUrl = tokenImage || nftImage;

if (!iconUrl) {
return {};
}

return {
[address.toLowerCase()]: { iconUrl },
};
}

render() {
const { address, image, addBorder, diameter, tokenList } = this.props;
const { address, image, addBorder, diameter } = this.props;
const { imageLoadingError } = this.state;
const size = diameter + 8;

Expand All @@ -155,7 +187,7 @@ export default class Identicon extends Component {
}

if (address) {
if (tokenList[address.toLowerCase()]?.iconUrl) {
if (this.getTokenImage() || this.getNftImage()) {
return this.renderJazzicon();
}

Expand Down
84 changes: 74 additions & 10 deletions ui/components/ui/identicon/identicon.component.test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { getTokenList } from '../../../selectors';
import { getNftContractsByAddressOnCurrentChain } from '../../../selectors/nft';
import Identicon from '.';

describe('Identicon', () => {
const mockState = {
metamask: {
providerConfig: {
chainId: '0x99',
},
useBlockie: false,
jest.mock('../../../selectors', () => ({
...jest.requireActual('../../../selectors'),
getTokenList: jest.fn(),
}));

jest.mock('../../../selectors/nft', () => ({
...jest.requireActual('../../../selectors/nft'),
getNftContractsByAddressOnCurrentChain: jest.fn(),
}));

const ADDRESS_MOCK = '0x0000000000000000000000000000000000000000';

const mockState = {
metamask: {
providerConfig: {
chainId: '0x99',
},
};
useBlockie: false,
},
};

const mockStore = configureMockStore()(mockState);
const mockStore = configureMockStore()(mockState);

describe('Identicon', () => {
const getTokenListMock = jest.mocked(getTokenList);
const getNftContractsByAddressOnCurrentChainMock = jest.mocked(
getNftContractsByAddressOnCurrentChain,
);

beforeEach(() => {
jest.resetAllMocks();
getNftContractsByAddressOnCurrentChainMock.mockReturnValue({});
});

it('should match snapshot with default props', () => {
const { container } = renderWithProvider(<Identicon />, mockStore);
Expand All @@ -38,7 +62,7 @@ describe('Identicon', () => {
it('should match snapshot with address prop div', () => {
const props = {
className: 'test-address',
address: '0x0000000000000000000000000000000000000000',
address: ADDRESS_MOCK,
};

const { container } = renderWithProvider(
Expand All @@ -48,4 +72,44 @@ describe('Identicon', () => {

expect(container).toMatchSnapshot();
});

it('should match snapshot with token icon', () => {
const props = {
className: 'test-address',
address: ADDRESS_MOCK,
};

getTokenListMock.mockReturnValue({
[ADDRESS_MOCK]: {
iconUrl: 'https://test.com/testTokenIcon.jpg',
},
});

const { container } = renderWithProvider(
<Identicon {...props} />,
mockStore,
);

expect(container).toMatchSnapshot();
});

it('should match snapshot with watched NFT logo', () => {
const props = {
className: 'test-address',
address: ADDRESS_MOCK,
};

getNftContractsByAddressOnCurrentChainMock.mockReturnValue({
[ADDRESS_MOCK]: {
logo: 'https://test.com/testNftLogo.jpg',
},
});

const { container } = renderWithProvider(
<Identicon {...props} />,
mockStore,
);

expect(container).toMatchSnapshot();
});
});
2 changes: 2 additions & 0 deletions ui/components/ui/identicon/identicon.container.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { getTokenList } from '../../../selectors';
import { getNftContractsByAddressOnCurrentChain } from '../../../selectors/nft';
import Identicon from './identicon.component';

const mapStateToProps = (state) => {
Expand All @@ -11,6 +12,7 @@ const mapStateToProps = (state) => {
useBlockie,
tokenList: getTokenList(state),
ipfsGateway,
watchedNftContracts: getNftContractsByAddressOnCurrentChain(state),
};
};

Expand Down
1 change: 1 addition & 0 deletions ui/components/ui/identicon/identicon.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default {
imageBorder: { control: 'boolean' },
useTokenDetection: { control: 'boolean' },
tokenList: { control: 'object' },
watchedNftContracts: { control: 'object' },
},
};

Expand Down
57 changes: 47 additions & 10 deletions ui/hooks/useDisplayName.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NameEntry, NameType } from '@metamask/name-controller';
import { NftContract } from '@metamask/assets-controllers';
import { getMemoizedMetadataContractName } from '../selectors';
import { getNftContractsByAddressOnCurrentChain } from '../selectors/nft';
import { useDisplayName } from './useDisplayName';
import { useName } from './useName';
import { useFirstPartyContractName } from './useFirstPartyContractName';
Expand All @@ -21,40 +23,60 @@ jest.mock('../selectors', () => ({
getCurrentChainId: jest.fn(),
}));

jest.mock('../selectors/nft', () => ({
getNftContractsByAddressOnCurrentChain: jest.fn(),
}));

const VALUE_MOCK = '0xabc123';
const TYPE_MOCK = NameType.ETHEREUM_ADDRESS;
const NAME_MOCK = 'TestName';
const CONTRACT_NAME_MOCK = 'TestContractName';
const FIRST_PARTY_CONTRACT_NAME_MOCK = 'MetaMask Bridge';
const WATCHED_NFT_NAME_MOCK = 'TestWatchedNFTName';

const NO_PETNAME_FOUND_RETURN_VALUE = {
name: null,
} as NameEntry;
const NO_CONTRACT_NAME_FOUND_RETURN_VALUE = '';
const NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE = null;
const NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE = {};

const PETNAME_FOUND_RETURN_VALUE = {
name: NAME_MOCK,
} as NameEntry;

const WATCHED_NFT_FOUND_RETURN_VALUE = {
[VALUE_MOCK]: {
name: WATCHED_NFT_NAME_MOCK,
} as NftContract,
};

describe('useDisplayName', () => {
const useNameMock = jest.mocked(useName);
const getMemoizedMetadataContractNameMock = jest.mocked(
getMemoizedMetadataContractName,
);
const useFirstPartyContractNameMock = jest.mocked(useFirstPartyContractName);
const getNftContractsByAddressOnCurrentChainMock = jest.mocked(
getNftContractsByAddressOnCurrentChain,
);

beforeEach(() => {
jest.resetAllMocks();
});

it('handles no name found', () => {
useNameMock.mockReturnValue(NO_PETNAME_FOUND_RETURN_VALUE);
useFirstPartyContractNameMock.mockReturnValue(null);
useFirstPartyContractNameMock.mockReturnValue(
NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE,
);
getMemoizedMetadataContractNameMock.mockReturnValue(
NO_CONTRACT_NAME_FOUND_RETURN_VALUE,
);
getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
NO_WATCHED_NFT_NAME_FOUND_RETURN_VALUE,
);
});

it('handles no name found', () => {
expect(useDisplayName(VALUE_MOCK, TYPE_MOCK)).toEqual({
name: null,
hasPetname: false,
Expand All @@ -67,36 +89,51 @@ describe('useDisplayName', () => {
FIRST_PARTY_CONTRACT_NAME_MOCK,
);
getMemoizedMetadataContractNameMock.mockReturnValue(CONTRACT_NAME_MOCK);
getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
WATCHED_NFT_FOUND_RETURN_VALUE,
);

expect(useDisplayName(VALUE_MOCK, TYPE_MOCK)).toEqual({
name: NAME_MOCK,
hasPetname: true,
});
});

it('prioritizes a first-party contract name over a contract name', () => {
useNameMock.mockReturnValue(NO_PETNAME_FOUND_RETURN_VALUE);
it('prioritizes a first-party contract name over a contract name and watched NFT name', () => {
useFirstPartyContractNameMock.mockReturnValue(
FIRST_PARTY_CONTRACT_NAME_MOCK,
);
getMemoizedMetadataContractNameMock.mockReturnValue(CONTRACT_NAME_MOCK);
getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
WATCHED_NFT_FOUND_RETURN_VALUE,
);

expect(useDisplayName(VALUE_MOCK, TYPE_MOCK)).toEqual({
name: FIRST_PARTY_CONTRACT_NAME_MOCK,
hasPetname: false,
});
});

it('returns a contract name if no other name is found', () => {
useNameMock.mockReturnValue(NO_PETNAME_FOUND_RETURN_VALUE);
useFirstPartyContractNameMock.mockReturnValue(
NO_FIRST_PARTY_CONTRACT_NAME_FOUND_RETURN_VALUE,
);
it('prioritizes a contract name over a watched NFT name', () => {
getMemoizedMetadataContractNameMock.mockReturnValue(CONTRACT_NAME_MOCK);
getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
WATCHED_NFT_FOUND_RETURN_VALUE,
);

expect(useDisplayName(VALUE_MOCK, TYPE_MOCK)).toEqual({
name: CONTRACT_NAME_MOCK,
hasPetname: false,
});
});

it('returns a watched NFT name if no other name is found', () => {
getNftContractsByAddressOnCurrentChainMock.mockReturnValue(
WATCHED_NFT_FOUND_RETURN_VALUE,
);

expect(useDisplayName(VALUE_MOCK, TYPE_MOCK)).toEqual({
name: WATCHED_NFT_NAME_MOCK,
hasPetname: false,
});
});
});

0 comments on commit 805c81e

Please sign in to comment.