Skip to content

Commit f54a833

Browse files
runway-github[bot]OGPoyrazgauthierpetetin
authored
release(runway): cherry-pick fix: Fix account icons in send flow (#36917)
- fix: cp-13.5.0 Fix account icons in send flow (#36877) <!-- 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 centralise seed account icon map in a hook and use it in send flow. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/36877?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: #36657 ## **Manual testing steps** Account icon should be consistent as other parts of the wallet. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/466421d4-e7bc-4a35-98e9-236c7d85d558 ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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-extension/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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a hook to map account addresses to seed icon addresses and updates send flow to render consistent avatars using this map. > > - **Send Flow UI**: > - `Recipient`: Passes `seedIcon ?? address` to `PreferredAvatar`. > - `RecipientInput`: Uses mapped seed address for avatar when a recipient is resolved. > - **Hooks**: > - New `useAccountAddressSeedIconMap` builds a map from account groups to seed icon addresses. > - `useAccountRecipients`/`useContactRecipients`: Include `seedIcon` in `Recipient` objects and update memo deps. > - **Tests**: > - Add comprehensive tests for `useAccountAddressSeedIconMap`. > - Update related tests to mock the new hook and reflect avatar/seed behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 08a33ba. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [743b315](743b315) Co-authored-by: OGPoyraz <[email protected]> Co-authored-by: Gauthier Petetin <[email protected]>
1 parent fa8ad9f commit f54a833

File tree

11 files changed

+258
-11
lines changed

11 files changed

+258
-11
lines changed

ui/pages/confirmations/components/UI/recipient/recipient.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import configureStore from '../../../../../store/store';
55
import mockDefaultState from '../../../../../../test/data/mock-state.json';
66
import { Recipient } from './recipient';
77

8+
jest.mock('../../../hooks/send/useAccountAddressSeedIconMap', () => ({
9+
useAccountAddressSeedIconMap: jest.fn().mockReturnValue({
10+
accountAddressSeedIconMap: new Map(),
11+
}),
12+
}));
13+
814
const mockContactRecipient = {
915
address: '0x1234567890abcdef1234567890abcdef12345678',
1016
contactName: 'John Doe',

ui/pages/confirmations/components/UI/recipient/recipient.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const Recipient = ({
2424
recipient: RecipientType;
2525
onClick: (recipient: RecipientType) => void;
2626
}) => {
27-
const { address } = recipient;
27+
const { address, seedIcon } = recipient;
2828
const recipientName = isAccount
2929
? recipient.accountGroupName
3030
: recipient.contactName;
@@ -48,7 +48,7 @@ export const Recipient = ({
4848
onClick={() => onClick(recipient)}
4949
>
5050
<PreferredAvatar
51-
address={address}
51+
address={seedIcon ?? address}
5252
size={AvatarAccountSize.Lg}
5353
data-testid="avatar"
5454
/>

ui/pages/confirmations/components/send/recipient-input/recipient-input.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ jest.mock('../../../../../hooks/useI18nContext');
1515
jest.mock('../../../hooks/send/metrics/useRecipientSelectionMetrics');
1616
jest.mock('../../../context/send');
1717
jest.mock('../../../hooks/send/useRecipients');
18+
jest.mock('../../../hooks/send/useAccountAddressSeedIconMap', () => ({
19+
useAccountAddressSeedIconMap: jest.fn().mockReturnValue({
20+
accountAddressSeedIconMap: new Map(),
21+
}),
22+
}));
1823

1924
describe('RecipientInput', () => {
2025
const mockUseI18nContext = jest.mocked(useI18nContext);

ui/pages/confirmations/components/send/recipient-input/recipient-input.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { useI18nContext } from '../../../../../hooks/useI18nContext';
3030
import { useRecipientValidation } from '../../../hooks/send/useRecipientValidation';
3131
import { useRecipients } from '../../../hooks/send/useRecipients';
32+
import { useAccountAddressSeedIconMap } from '../../../hooks/send/useAccountAddressSeedIconMap';
3233
import { useRecipientSelectionMetrics } from '../../../hooks/send/metrics/useRecipientSelectionMetrics';
3334
import { useSendContext } from '../../../context/send';
3435
import { ConfusableRecipientName } from './confusable-recipient-name';
@@ -47,12 +48,15 @@ export const RecipientInput = ({
4748
const recipients = useRecipients();
4849
const t = useI18nContext();
4950
const { to, updateTo } = useSendContext();
51+
const { accountAddressSeedIconMap } = useAccountAddressSeedIconMap();
5052
const {
5153
recipientConfusableCharacters,
5254
recipientError,
5355
recipientResolvedLookup,
5456
toAddressValidated,
5557
} = recipientValidationResult;
58+
const avatarSeedAddress =
59+
accountAddressSeedIconMap.get(to?.toLowerCase() as string) ?? to ?? '';
5660

5761
const onToChange = useCallback(
5862
(e) => {
@@ -105,7 +109,7 @@ export const RecipientInput = ({
105109
>
106110
<Box alignItems={AlignItems.center} display={Display.Flex}>
107111
<PreferredAvatar
108-
address={resolvedAddress}
112+
address={avatarSeedAddress}
109113
size={AvatarAccountSize.Md}
110114
/>
111115
<Box
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { renderHookWithProvider } from '../../../../../test/lib/render-helpers';
2+
import mockState from '../../../../../test/data/mock-state.json';
3+
import * as accountTreeSelectors from '../../../../selectors/multichain-accounts/account-tree';
4+
import { AccountGroupWithInternalAccounts } from '../../../../selectors/multichain-accounts/account-tree.types';
5+
import { useAccountAddressSeedIconMap } from './useAccountAddressSeedIconMap';
6+
7+
jest.mock('../../../../selectors/multichain-accounts/account-tree');
8+
9+
describe('useAccountAddressSeedIconMap', () => {
10+
const mockGetAccountGroupWithInternalAccounts = jest.spyOn(
11+
accountTreeSelectors,
12+
'getAccountGroupWithInternalAccounts',
13+
);
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
mockGetAccountGroupWithInternalAccounts.mockReturnValue([]);
18+
});
19+
20+
it('creates seed address map from account groups with single account per group', () => {
21+
const mockAccountGroups = [
22+
{
23+
accounts: [{ address: '0x1234567890abcdef1234567890abcdef12345678' }],
24+
},
25+
{
26+
accounts: [{ address: '0xABCDEF1234567890ABCDEF1234567890ABCDEF12' }],
27+
},
28+
];
29+
30+
mockGetAccountGroupWithInternalAccounts.mockReturnValue(
31+
mockAccountGroups as AccountGroupWithInternalAccounts[],
32+
);
33+
34+
const { result } = renderHookWithProvider(
35+
() => useAccountAddressSeedIconMap(),
36+
mockState,
37+
);
38+
39+
expect(result.current.accountAddressSeedIconMap.size).toBe(2);
40+
expect(
41+
result.current.accountAddressSeedIconMap.get(
42+
'0x1234567890abcdef1234567890abcdef12345678',
43+
),
44+
).toBe('0x1234567890abcdef1234567890abcdef12345678');
45+
expect(
46+
result.current.accountAddressSeedIconMap.get(
47+
'0xabcdef1234567890abcdef1234567890abcdef12',
48+
),
49+
).toBe('0xABCDEF1234567890ABCDEF1234567890ABCDEF12');
50+
});
51+
52+
it('creates seed address map from account groups with multiple accounts per group', () => {
53+
const mockAccountGroups = [
54+
{
55+
accounts: [
56+
{ address: '0x1111111111111111111111111111111111111111' },
57+
{ address: '0x2222222222222222222222222222222222222222' },
58+
{ address: '0x3333333333333333333333333333333333333333' },
59+
],
60+
},
61+
{
62+
accounts: [
63+
{ address: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
64+
{ address: '0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' },
65+
],
66+
},
67+
];
68+
69+
mockGetAccountGroupWithInternalAccounts.mockReturnValue(
70+
mockAccountGroups as AccountGroupWithInternalAccounts[],
71+
);
72+
73+
const { result } = renderHookWithProvider(
74+
() => useAccountAddressSeedIconMap(),
75+
mockState,
76+
);
77+
78+
expect(result.current.accountAddressSeedIconMap.size).toBe(5);
79+
expect(
80+
result.current.accountAddressSeedIconMap.get(
81+
'0x1111111111111111111111111111111111111111',
82+
),
83+
).toBe('0x1111111111111111111111111111111111111111');
84+
expect(
85+
result.current.accountAddressSeedIconMap.get(
86+
'0x2222222222222222222222222222222222222222',
87+
),
88+
).toBe('0x1111111111111111111111111111111111111111');
89+
expect(
90+
result.current.accountAddressSeedIconMap.get(
91+
'0x3333333333333333333333333333333333333333',
92+
),
93+
).toBe('0x1111111111111111111111111111111111111111');
94+
expect(
95+
result.current.accountAddressSeedIconMap.get(
96+
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
97+
),
98+
).toBe('0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
99+
expect(
100+
result.current.accountAddressSeedIconMap.get(
101+
'0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
102+
),
103+
).toBe('0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
104+
});
105+
106+
it('returns empty map when no account groups exist', () => {
107+
const { result } = renderHookWithProvider(
108+
() => useAccountAddressSeedIconMap(),
109+
mockState,
110+
);
111+
112+
expect(result.current.accountAddressSeedIconMap.size).toBe(0);
113+
});
114+
115+
it('handles account groups with empty accounts array', () => {
116+
const mockAccountGroups = [
117+
{
118+
accounts: [],
119+
},
120+
{
121+
accounts: [{ address: '0x1111111111111111111111111111111111111111' }],
122+
},
123+
];
124+
125+
mockGetAccountGroupWithInternalAccounts.mockReturnValue(
126+
mockAccountGroups as AccountGroupWithInternalAccounts[],
127+
);
128+
129+
const { result } = renderHookWithProvider(
130+
() => useAccountAddressSeedIconMap(),
131+
mockState,
132+
);
133+
134+
expect(result.current.accountAddressSeedIconMap.size).toBe(1);
135+
expect(
136+
result.current.accountAddressSeedIconMap.get(
137+
'0x1111111111111111111111111111111111111111',
138+
),
139+
).toBe('0x1111111111111111111111111111111111111111');
140+
});
141+
142+
it('handles mixed case addresses by converting keys to lowercase', () => {
143+
const mockAccountGroups = [
144+
{
145+
accounts: [
146+
{ address: '0xAbCdEf1234567890AbCdEf1234567890AbCdEf12' },
147+
{ address: '0X1234567890ABCDEF1234567890ABCDEF12345678' },
148+
],
149+
},
150+
];
151+
152+
mockGetAccountGroupWithInternalAccounts.mockReturnValue(
153+
mockAccountGroups as AccountGroupWithInternalAccounts[],
154+
);
155+
156+
const { result } = renderHookWithProvider(
157+
() => useAccountAddressSeedIconMap(),
158+
mockState,
159+
);
160+
161+
expect(result.current.accountAddressSeedIconMap.size).toBe(2);
162+
expect(
163+
result.current.accountAddressSeedIconMap.get(
164+
'0xabcdef1234567890abcdef1234567890abcdef12',
165+
),
166+
).toBe('0xAbCdEf1234567890AbCdEf1234567890AbCdEf12');
167+
expect(
168+
result.current.accountAddressSeedIconMap.get(
169+
'0x1234567890abcdef1234567890abcdef12345678',
170+
),
171+
).toBe('0xAbCdEf1234567890AbCdEf1234567890AbCdEf12');
172+
});
173+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useMemo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { getAccountGroupWithInternalAccounts } from '../../../../selectors/multichain-accounts/account-tree';
4+
5+
export const useAccountAddressSeedIconMap = () => {
6+
const accountGroupsWithAddresses = useSelector(
7+
getAccountGroupWithInternalAccounts,
8+
);
9+
10+
const accountAddressSeedIconMap = useMemo(() => {
11+
const map = new Map<string, string>();
12+
13+
accountGroupsWithAddresses.forEach((accountGroup) => {
14+
const { accounts } = accountGroup;
15+
16+
if (accounts.length === 0) {
17+
return;
18+
}
19+
20+
const seedAddress = accounts[0].address;
21+
22+
accounts.forEach((account) => {
23+
map.set(account.address.toLowerCase(), seedAddress);
24+
});
25+
});
26+
27+
return map;
28+
}, [accountGroupsWithAddresses]);
29+
30+
return { accountAddressSeedIconMap };
31+
};

ui/pages/confirmations/hooks/send/useAccountRecipients.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ jest.mock('./useSendType');
1313
jest.mock('../../../../selectors/multichain-accounts/account-tree');
1414
jest.mock('../../context/send');
1515
jest.mock('../../utils/account');
16+
jest.mock('./useAccountAddressSeedIconMap', () => ({
17+
useAccountAddressSeedIconMap: jest.fn().mockReturnValue({
18+
accountAddressSeedIconMap: new Map(),
19+
}),
20+
}));
1621

1722
const mockUseSendType = jest.spyOn(useSendTypeModule, 'useSendType');
1823
const mockGetWalletsWithAccounts = jest.spyOn(

ui/pages/confirmations/hooks/send/useAccountRecipients.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
import { useSendContext } from '../../context/send';
1010
import { type Recipient } from './useRecipients';
1111
import { useSendType } from './useSendType';
12+
import { useAccountAddressSeedIconMap } from './useAccountAddressSeedIconMap';
1213

1314
export const useAccountRecipients = (): Recipient[] => {
1415
const { isEvmSendType, isSolanaSendType } = useSendType();
1516
const { from } = useSendContext();
17+
const { accountAddressSeedIconMap } = useAccountAddressSeedIconMap();
1618

1719
const walletsWithAccounts = useSelector(getWalletsWithAccounts);
1820

@@ -36,6 +38,9 @@ export const useAccountRecipients = (): Recipient[] => {
3638

3739
if (shouldInclude) {
3840
recipients.push({
41+
seedIcon: accountAddressSeedIconMap.get(
42+
account.address.toLowerCase(),
43+
),
3944
accountGroupName,
4045
address: account.address,
4146
walletName,
@@ -46,5 +51,11 @@ export const useAccountRecipients = (): Recipient[] => {
4651
});
4752

4853
return recipients;
49-
}, [walletsWithAccounts, isEvmSendType, isSolanaSendType, from]);
54+
}, [
55+
from,
56+
isEvmSendType,
57+
isSolanaSendType,
58+
accountAddressSeedIconMap,
59+
walletsWithAccounts,
60+
]);
5061
};

ui/pages/confirmations/hooks/send/useContactRecipients.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import { useSendType } from './useSendType';
99

1010
jest.mock('./useSendType');
1111
jest.mock('../../../../selectors');
12+
jest.mock('./useAccountAddressSeedIconMap', () => ({
13+
useAccountAddressSeedIconMap: jest.fn().mockReturnValue({
14+
accountAddressSeedIconMap: new Map(),
15+
}),
16+
}));
1217
jest.mock('ethers/lib/utils');
1318
jest.mock('@metamask/bridge-controller');
1419

ui/pages/confirmations/hooks/send/useContactRecipients.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,24 @@ import { AddressBookEntry } from '@metamask/address-book-controller';
77
import { getCompleteAddressBook } from '../../../../selectors';
88
import { type Recipient } from './useRecipients';
99
import { useSendType } from './useSendType';
10+
import { useAccountAddressSeedIconMap } from './useAccountAddressSeedIconMap';
1011

1112
export const useContactRecipients = (): Recipient[] => {
1213
const { isEvmSendType, isSolanaSendType } = useSendType();
1314
const addressBook = useSelector(getCompleteAddressBook);
15+
const { accountAddressSeedIconMap } = useAccountAddressSeedIconMap();
1416

15-
const processContacts = useCallback((contact: AddressBookEntry) => {
16-
return {
17-
address: contact.address,
18-
contactName: contact.name,
19-
isContact: true,
20-
};
21-
}, []);
17+
const processContacts = useCallback(
18+
(contact: AddressBookEntry) => {
19+
return {
20+
address: contact.address,
21+
contactName: contact.name,
22+
isContact: true,
23+
seedIcon: accountAddressSeedIconMap.get(contact.address.toLowerCase()),
24+
};
25+
},
26+
[accountAddressSeedIconMap],
27+
);
2228

2329
if (isEvmSendType) {
2430
return addressBook

0 commit comments

Comments
 (0)