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

fix: add tokenList iconUrl to IdentIcon component #10163

Merged
merged 7 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/components/UI/AddressInputs/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const initialState = {
name: 'Account 2',
},
},
useTokenDetection: false,
},
AddressBookController: {
addressBook: {
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('AddressInputs', () => {
fromAccountBalance="0x5"
fromAccountName="DUMMY_ACCOUNT"
/>,
{},
{ state: initialState },
);
expect(container).toMatchSnapshot();
});
Expand All @@ -74,7 +75,7 @@ describe('AddressInputs', () => {
fromAccountName="DUMMY_ACCOUNT"
layout="vertical"
/>,
{},
{ state: initialState },
);
expect(container).toMatchSnapshot();
});
Expand Down
20 changes: 20 additions & 0 deletions app/components/UI/Identicon/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Identicon should render correctly when provided address found in tokenList and iconUrl is available 1`] = `
<Image
source={
{
"uri": "https://example.com/icon.png",
}
}
style={
[
{
"borderRadius": 23,
"height": 46,
"width": 46,
},
undefined,
]
}
/>
`;

exports[`Identicon should render correctly when useBlockieIcon is false 1`] = `
<ContextProvider
value={
Expand Down
29 changes: 29 additions & 0 deletions app/components/UI/Identicon/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import React from 'react';
import { shallow } from 'enzyme';
import { render } from '@testing-library/react-native';
import Identicon from './';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import useTokenList from '../../../components/hooks/DisplayName/useTokenList';

jest.mock('../../../components/hooks/DisplayName/useTokenList');

describe('Identicon', () => {
const mockStore = configureMockStore();
const mockUseTokenList = jest
.mocked(useTokenList)
.mockImplementation(() => ({}));

it('should render correctly when provided address found in tokenList and iconUrl is available', () => {
const addressMock = '0x0439e60f02a8900a951603950d8d4527f400c3f1';
mockUseTokenList.mockImplementation(() => [
{
address: addressMock,
iconUrl: 'https://example.com/icon.png',
},
]);

const initialState = {
settings: { useBlockieIcon: true },
};
const store = mockStore(initialState);

const wrapper = render(
<Provider store={store}>
<Identicon address={addressMock} />
</Provider>,
);
expect(wrapper).toMatchSnapshot();
});
it('should render correctly when useBlockieIcon is true', () => {
const initialState = {
settings: { useBlockieIcon: true },
Expand Down
37 changes: 26 additions & 11 deletions app/components/UI/Identicon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable react/prop-types */
import React, { memo } from 'react';
import { Image, ImageStyle, View } from 'react-native';
import { TokenListToken } from '@metamask/assets-controllers';
import { toDataUrl } from '../../../util/blockies';
import FadeIn from 'react-native-fade-in-image';
import Jazzicon from 'react-native-jazzicon';
import { connect } from 'react-redux';
import { useTheme } from '../../../util/theme';
import useTokenList from '../../../components/hooks/DisplayName/useTokenList';

interface IdenticonProps {
/**
Expand Down Expand Up @@ -43,23 +45,36 @@ const Identicon: React.FC<IdenticonProps> = ({
useBlockieIcon = true,
}) => {
const { colors } = useTheme();
const tokenListArray: TokenListToken[] = useTokenList();
const normalizedAddress = address?.toLowerCase();
const tokenListIcon = tokenListArray.find(
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved
(token: TokenListToken) => token.address === normalizedAddress,
)?.iconUrl;

if (!address) return null;

const uri = useBlockieIcon && toDataUrl(address);

const styleForBlockieAndTokenIcon = [
{
height: diameter,
width: diameter,
borderRadius: diameter / 2,
},
customStyle,
];
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved

if (tokenListIcon) {
return (
<Image
source={{ uri: tokenListIcon }}
style={styleForBlockieAndTokenIcon}
/>
);
}

const image = useBlockieIcon ? (
<Image
source={{ uri }}
style={[
{
height: diameter,
width: diameter,
borderRadius: diameter / 2,
},
customStyle,
]}
/>
<Image source={{ uri }} style={styleForBlockieAndTokenIcon} />
) : (
<View style={customStyle}>
<Jazzicon size={diameter} address={address} />
Expand Down
5 changes: 5 additions & 0 deletions app/components/UI/Name/Name.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ jest.mock('../../hooks/DisplayName/useDisplayName', () => ({
default: jest.fn(),
}));

jest.mock('../Identicon', () => ({
__esModule: true,
default: () => 'Identicon',
}));

const UNKNOWN_ADDRESS_CHECKSUMMED =
'0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e';
const EXPECTED_UNKNOWN_ADDRESS_CHECKSUMMED = '0x29900...A5B4e';
Expand Down
61 changes: 2 additions & 59 deletions app/components/UI/Name/__snapshots__/Name.test.tsx.snap

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions app/components/Views/confirmations/Send/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const initialState = {
TokenBalancesController: {
contractBalances: {},
},
TokenListController: {
tokenList: [],
},
PreferencesController: {
featureFlags: {},
identities: {
Expand Down
30 changes: 16 additions & 14 deletions app/components/hooks/DisplayName/useTokenList.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React from 'react';
import { type TokenListMap } from '@metamask/assets-controllers';
import { type TokenListToken } from '@metamask/assets-controllers';
import { selectChainId } from '../../../selectors/networkController';
import { selectUseTokenDetection } from '../../../selectors/preferencesController';
import { selectTokenList } from '../../../selectors/tokenListController';
import { selectTokenListArray } from '../../../selectors/tokenListController';
import { isMainnetByChainId } from '../../../util/networks';

import useTokenList from './useTokenList';

const MAINNET_TOKEN_ADDRESS_MOCK = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const MAINNET_TOKEN_NAME_MOCK = 'Tether USD';
const normalizedMainnetTokenListMock = {
[MAINNET_TOKEN_ADDRESS_MOCK.toLowerCase()]: {
const normalizedMainnetTokenListMock = [
{
name: MAINNET_TOKEN_NAME_MOCK,
},
};
];
jest.mock('@metamask/contract-metadata', () => ({
__esModule: true,
default: {
Expand All @@ -38,7 +38,7 @@ jest.mock('../../../selectors/preferencesController', () => ({
}));

jest.mock('../../../selectors/tokenListController', () => ({
selectTokenList: jest.fn(),
selectTokenListArray: jest.fn(),
}));

jest.mock('../../../util/networks', () => ({
Expand All @@ -48,27 +48,29 @@ jest.mock('../../../util/networks', () => ({
const CHAIN_ID_MOCK = '0x1';
const TOKEN_NAME_MOCK = 'MetaMask Token';
const TOKEN_ADDRESS_MOCK = '0x0439e60F02a8900a951603950d8D4527f400C3f1';
const TOKEN_LIST_MOCK = {
[TOKEN_ADDRESS_MOCK]: {
const TOKEN_LIST_ARRAY_MOCK = [
{
name: TOKEN_NAME_MOCK,
address: TOKEN_ADDRESS_MOCK,
},
} as unknown as TokenListMap;
const normalizedTokenListMock = {
[TOKEN_ADDRESS_MOCK.toLowerCase()]: {
] as unknown as TokenListToken[];
const normalizedTokenListMock = [
{
address: TOKEN_ADDRESS_MOCK,
name: TOKEN_NAME_MOCK,
},
};
];

describe('useTokenList', () => {
const selectChainIdMock = jest.mocked(selectChainId);
const selectUseTokenDetectionMock = jest.mocked(selectUseTokenDetection);
const selectTokenListMock = jest.mocked(selectTokenList);
const selectTokenListArrayMock = jest.mocked(selectTokenListArray);
const isMainnetByChainIdMock = jest.mocked(isMainnetByChainId);
beforeEach(() => {
jest.resetAllMocks();
selectChainIdMock.mockReturnValue(CHAIN_ID_MOCK);
selectUseTokenDetectionMock.mockReturnValue(true);
selectTokenListMock.mockReturnValue(TOKEN_LIST_MOCK);
selectTokenListArrayMock.mockReturnValue(TOKEN_LIST_ARRAY_MOCK);
isMainnetByChainIdMock.mockReturnValue(true);

const memoizedValues = new Map();
Expand Down
29 changes: 8 additions & 21 deletions app/components/hooks/DisplayName/useTokenList.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
import { useMemo } from 'react';
import { type TokenListMap } from '@metamask/assets-controllers';
import contractMap from '@metamask/contract-metadata';

import { TokenListToken } from '@metamask/assets-controllers';
import { useSelector } from 'react-redux';
import { selectChainId } from '../../../selectors/networkController';
import { selectUseTokenDetection } from '../../../selectors/preferencesController';
import { selectTokenList } from '../../../selectors/tokenListController';
import { selectTokenListArray } from '../../../selectors/tokenListController';
import { isMainnetByChainId } from '../../../util/networks';

function normalizeTokenAddresses(tokenMap: TokenListMap) {
return Object.keys(tokenMap).reduce((acc, address) => {
const tokenMetadata = tokenMap[address];
return {
...acc,
[address.toLowerCase()]: {
...tokenMetadata,
},
};
}, {});
}
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved

const NORMALIZED_MAINNET_TOKEN_LIST = normalizeTokenAddresses(contractMap);
const NORMALIZED_MAINNET_TOKEN_ARRAY = Object.values(contractMap);

export default function useTokenList(): TokenListMap {
export default function useTokenList(): TokenListToken[] {
const chainId = useSelector(selectChainId);
const isMainnet = isMainnetByChainId(chainId);
const isTokenDetectionEnabled = useSelector(selectUseTokenDetection);
const tokenList = useSelector(selectTokenList);
const tokenListArray = useSelector(selectTokenListArray);
const shouldUseStaticList = !isTokenDetectionEnabled && isMainnet;

return useMemo(() => {
if (shouldUseStaticList) {
return NORMALIZED_MAINNET_TOKEN_LIST;
return NORMALIZED_MAINNET_TOKEN_ARRAY;
}
return normalizeTokenAddresses(tokenList);
}, [shouldUseStaticList, tokenList]);
return tokenListArray;
}, [shouldUseStaticList, tokenListArray]);
}
9 changes: 5 additions & 4 deletions app/components/hooks/DisplayName/useTokenListEntry.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TokenListMap } from '@metamask/assets-controllers';
import { TokenListToken } from '@metamask/assets-controllers';
import { NameType } from '../../UI/Name/Name.types';
import { useTokenListEntry } from './useTokenListEntry';
import useTokenList from './useTokenList';
Expand All @@ -18,12 +18,13 @@ describe('useTokenListEntry', () => {
beforeEach(() => {
jest.resetAllMocks();

useTokenListMock.mockReturnValue({
[TOKEN_ADDRESS_MOCK.toLowerCase()]: {
useTokenListMock.mockReturnValue([
{
address: TOKEN_ADDRESS_MOCK.toLowerCase(),
name: TOKEN_NAME_MOCK,
symbol: TOKEN_SYMBOL_MOCK,
},
} as TokenListMap);
] as unknown as TokenListToken[]);
});

it('returns undefined if no token found', () => {
Expand Down
7 changes: 5 additions & 2 deletions app/components/hooks/DisplayName/useTokenListEntry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TokenListToken } from '@metamask/assets-controllers';
import { NameType } from '../../UI/Name/Name.types';
import useTokenList from './useTokenList';

Expand All @@ -7,7 +8,7 @@ export interface UseTokenListEntriesRequest {
}

export function useTokenListEntries(requests: UseTokenListEntriesRequest[]) {
const tokenList = useTokenList();
const tokenListArray = useTokenList();

return requests.map(({ value, type }) => {
if (type !== NameType.EthereumAddress) {
Expand All @@ -16,7 +17,9 @@ export function useTokenListEntries(requests: UseTokenListEntriesRequest[]) {

const normalizedValue = value.toLowerCase();

return tokenList[normalizedValue];
return tokenListArray.find(
(token: TokenListToken) => token.address === normalizedValue,
);
});
}

Expand Down
3 changes: 2 additions & 1 deletion app/selectors/tokenListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSelector } from 'reselect';
import { TokenListState } from '@metamask/assets-controllers';
import { RootState } from '../reducers';
import { tokenListToArray } from '../util/tokens';
import { createDeepEqualSelector } from '../selectors/util';

const selectTokenLIstConstrollerState = (state: RootState) =>
state.engine.backgroundState.TokenListController;
Expand All @@ -20,7 +21,7 @@ export const selectTokenList = createSelector(
* Return token list array from TokenListController.
* Can pass directly into useSelector.
*/
export const selectTokenListArray = createSelector(
export const selectTokenListArray = createDeepEqualSelector(
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved
selectTokenList,
tokenListToArray,
);
Loading