Skip to content

Commit

Permalink
fix: add tokenList iconUrl to IdentIcon component (#10163)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

This PR aims to add support for `IdentIcon` component to use iconUrl for
token entity from `tokenList`

## **Related issues**

Fixes:

## **Manual testing steps**

## **Screenshots/Recordings**

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

### **Before**

![Screenshot 2024-06-28 at 11 56
58](https://github.com/MetaMask/metamask-mobile/assets/7644512/c7786923-1eff-4530-a534-0a09556a68b9)


### **After**

![Screenshot 2024-06-28 at 11 41
13](https://github.com/MetaMask/metamask-mobile/assets/7644512/760dbd7e-e5ff-4a9c-b2a9-7444ebf9ee6e)


## **Pre-merge author checklist**

- [X] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] 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.
  • Loading branch information
OGPoyraz authored Jul 2, 2024
1 parent 1f9af51 commit 53fc082
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 114 deletions.
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
36 changes: 25 additions & 11 deletions app/components/UI/Identicon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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 { useTokenListEntry } from '../../../components/hooks/DisplayName/useTokenListEntry';
import { NameType } from '../../UI/Name/Name.types';

interface IdenticonProps {
/**
Expand Down Expand Up @@ -43,23 +45,35 @@ const Identicon: React.FC<IdenticonProps> = ({
useBlockieIcon = true,
}) => {
const { colors } = useTheme();
const tokenListIcon = useTokenListEntry(
address as string,
NameType.EthereumAddress,
)?.iconUrl;

if (!address) return null;

const uri = useBlockieIcon && toDataUrl(address);

const styleForBlockieAndTokenIcon = [
{
height: diameter,
width: diameter,
borderRadius: diameter / 2,
},
customStyle,
];

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
31 changes: 10 additions & 21 deletions app/components/hooks/DisplayName/useTokenList.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
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,
},
};
}, {});
}

const NORMALIZED_MAINNET_TOKEN_LIST = normalizeTokenAddresses(contractMap);
const NORMALIZED_MAINNET_TOKEN_ARRAY = Object.values(
contractMap,
) as TokenListToken[];

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(
selectTokenList,
tokenListToArray,
);

0 comments on commit 53fc082

Please sign in to comment.