Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show default petnames for watched NFTs #23438

Merged
merged 4 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
});
});
});