Skip to content

Commit 058c052

Browse files
authored
feat: add MultichainAddressRow component (#17328)
<!-- 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 introduces a new MultichainAddressRow component for displaying network information and addresses in MetaMask's multichain interface. The component provides a clean, consistent way to show network icons, names, truncated addresses, and action buttons (copy and QR code) across the application. **What is the reason for the change?** As part of the multichain accounts feature development, we need a reusable component to display network-address pairs in a standardized format throughout the UI. **What is the improvement/solution?** The new MultichainAddressRow component: - Displays network icon using AvatarNetwork component - Shows network name and truncated address in a two-line layout - Provides copy-to-clipboard functionality with visual feedback - Includes QR code button for address sharing - Integrates with the existing MultichainNetwork type system **Out of scope** - QR code button click implementation ## **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: Added MultichainAddressRow component for displaying network information and addresses in multichain interface ## **Related issues** Fixes: Jira ticket: https://consensyssoftware.atlassian.net/browse/MUL-401 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> <img width="376" height="738" alt="Screenshot 2025-07-17 at 15 43 56" src="https://github.com/user-attachments/assets/ea5b41b0-206d-4cd4-8e66-320599cc4cb8" /> <img width="376" height="728" alt="Screenshot 2025-07-17 at 15 44 04" src="https://github.com/user-attachments/assets/b83ce713-d605-42df-9728-2763517999d8" /> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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.
1 parent 4f751a4 commit 058c052

File tree

9 files changed

+571
-0
lines changed

9 files changed

+571
-0
lines changed

.storybook/storybook.requires.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { MultichainAddressRowProps } from './MultichainAddressRow.types';
2+
import { ProviderConfig } from '../../../../selectors/networkController';
3+
4+
export const SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS: MultichainAddressRowProps = {
5+
network: {
6+
nickname: 'Ethereum Mainnet',
7+
chainId: '0x1',
8+
ticker: 'ETH',
9+
type: 'mainnet',
10+
rpcPrefs: {},
11+
} as ProviderConfig,
12+
address: '0x1234567890123456789012345678901234567890',
13+
};
14+
15+
export const MULTICHAIN_ADDRESS_ROW_TEST_ID = 'multichain-address-row';
16+
export const MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID =
17+
'multichain-address-row-network-icon';
18+
export const MULTICHAIN_ADDRESS_ROW_NETWORK_NAME_TEST_ID =
19+
'multichain-address-row-network-name';
20+
export const MULTICHAIN_ADDRESS_ROW_ADDRESS_TEST_ID =
21+
'multichain-address-row-address';
22+
export const MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID =
23+
'multichain-address-row-copy-button';
24+
export const MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID =
25+
'multichain-address-row-qr-button';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* eslint-disable react-native/no-inline-styles */
2+
import React from 'react';
3+
import { View } from 'react-native';
4+
import { mockTheme } from '../../../../util/theme';
5+
import { default as MultichainAddressRowComponent } from './MultichainAddressRow';
6+
import { ProviderConfig } from '../../../../selectors/networkController';
7+
import { SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS } from './MultichainAddressRow.constants';
8+
9+
const MultichainAddressRowMeta = {
10+
title: 'Component Library / MultichainAccounts',
11+
component: MultichainAddressRowComponent,
12+
argTypes: {
13+
networkName: {
14+
control: { type: 'text' },
15+
defaultValue: SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS.network.nickname,
16+
},
17+
address: {
18+
control: { type: 'text' },
19+
defaultValue: SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS.address,
20+
},
21+
},
22+
};
23+
export default MultichainAddressRowMeta;
24+
25+
export const MultichainAddressRow = {
26+
render: (args: { networkName: string; address: string }) => (
27+
<View
28+
style={{
29+
alignItems: 'center',
30+
justifyContent: 'center',
31+
flex: 1,
32+
backgroundColor: mockTheme.colors.background.default,
33+
}}
34+
>
35+
<MultichainAddressRowComponent
36+
network={{
37+
...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS.network,
38+
nickname: args.networkName,
39+
}}
40+
address={args.address}
41+
/>
42+
</View>
43+
),
44+
};
45+
46+
export const WithLongNetworkName = {
47+
render: (args: { address: string }) => (
48+
<View
49+
style={{
50+
alignItems: 'center',
51+
justifyContent: 'center',
52+
flex: 1,
53+
backgroundColor: mockTheme.colors.background.default,
54+
}}
55+
>
56+
<MultichainAddressRowComponent
57+
network={{
58+
...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS.network,
59+
nickname: 'Very Long Network Name That Might Wrap',
60+
}}
61+
address={args.address}
62+
/>
63+
</View>
64+
),
65+
};
66+
67+
export const WithCustomNetwork = {
68+
render: () => (
69+
<View
70+
style={{
71+
alignItems: 'center',
72+
justifyContent: 'center',
73+
flex: 1,
74+
backgroundColor: mockTheme.colors.background.default,
75+
}}
76+
>
77+
<MultichainAddressRowComponent
78+
network={
79+
{
80+
nickname: 'Polygon Mainnet',
81+
chainId: '0x89',
82+
ticker: 'MATIC',
83+
type: 'rpc',
84+
rpcPrefs: {},
85+
} as ProviderConfig
86+
}
87+
address="0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
88+
/>
89+
</View>
90+
),
91+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { StyleSheet, ViewStyle } from 'react-native';
2+
import { Theme } from '../../../../util/theme/models';
3+
import { MultichainAddressRowStyleSheetVars } from './MultichainAddressRow.types';
4+
5+
/**
6+
* Style sheet function for MultichainAddressRow component.
7+
*
8+
* @param params Style sheet params.
9+
* @param params.theme App theme from ThemeContext.
10+
* @param params.vars Inputs that the style sheet depends on.
11+
* @returns StyleSheet object.
12+
*/
13+
const styleSheet = (params: {
14+
theme: Theme;
15+
vars: MultichainAddressRowStyleSheetVars;
16+
}) => {
17+
const { theme, vars } = params;
18+
const { colors } = theme;
19+
const { style } = vars;
20+
21+
return StyleSheet.create({
22+
base: {
23+
flexDirection: 'row',
24+
alignItems: 'center',
25+
justifyContent: 'space-between',
26+
padding: 16,
27+
gap: 16,
28+
backgroundColor: colors.background.default,
29+
...StyleSheet.flatten(style),
30+
} as ViewStyle,
31+
content: {
32+
flex: 1,
33+
flexDirection: 'column',
34+
alignItems: 'flex-start',
35+
} as ViewStyle,
36+
actions: {
37+
flexDirection: 'row',
38+
alignItems: 'center',
39+
gap: 8,
40+
} as ViewStyle,
41+
});
42+
};
43+
44+
export default styleSheet;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import MultichainAddressRow from './MultichainAddressRow';
4+
import {
5+
SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS,
6+
MULTICHAIN_ADDRESS_ROW_TEST_ID,
7+
MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID,
8+
MULTICHAIN_ADDRESS_ROW_NETWORK_NAME_TEST_ID,
9+
MULTICHAIN_ADDRESS_ROW_ADDRESS_TEST_ID,
10+
MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID,
11+
MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID,
12+
} from './MultichainAddressRow.constants';
13+
14+
// Mock useCopyClipboard hook
15+
jest.mock(
16+
'../../../../components/Views/Notifications/Details/hooks/useCopyClipboard',
17+
() => jest.fn(() => jest.fn()),
18+
);
19+
20+
// Mock getNetworkImageSource utility
21+
jest.mock('../../../../util/networks', () => ({
22+
getNetworkImageSource: jest.fn(() => ({ uri: 'mock-image-url' })),
23+
}));
24+
25+
describe('MultichainAddressRow', () => {
26+
it('should render correctly', () => {
27+
const wrapper = render(
28+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
29+
);
30+
expect(wrapper).toMatchSnapshot();
31+
});
32+
33+
it('should render the network name', () => {
34+
const { getByTestId } = render(
35+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
36+
);
37+
const networkName = getByTestId(
38+
MULTICHAIN_ADDRESS_ROW_NETWORK_NAME_TEST_ID,
39+
);
40+
expect(networkName.props.children).toBe('Ethereum Mainnet');
41+
});
42+
43+
it('should render the truncated address', () => {
44+
const { getByTestId } = render(
45+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
46+
);
47+
const address = getByTestId(MULTICHAIN_ADDRESS_ROW_ADDRESS_TEST_ID);
48+
expect(address.props.children).toBe('0x12345...67890');
49+
});
50+
51+
it('should render the network icon', () => {
52+
const { getByTestId } = render(
53+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
54+
);
55+
const networkIcon = getByTestId(
56+
MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID,
57+
);
58+
expect(networkIcon).toBeTruthy();
59+
});
60+
61+
it('should render copy and QR buttons', () => {
62+
const { getByTestId } = render(
63+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
64+
);
65+
const copyButton = getByTestId(MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID);
66+
const qrButton = getByTestId(MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID);
67+
68+
expect(copyButton).toBeTruthy();
69+
expect(qrButton).toBeTruthy();
70+
});
71+
72+
it('should accept custom testID', () => {
73+
const customTestID = 'custom-test-id';
74+
const { getByTestId } = render(
75+
<MultichainAddressRow
76+
{...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS}
77+
testID={customTestID}
78+
/>,
79+
);
80+
const component = getByTestId(customTestID);
81+
expect(component).toBeTruthy();
82+
});
83+
84+
it('should handle network without rpcPrefs', () => {
85+
const propsWithoutRpcPrefs = {
86+
...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS,
87+
network: {
88+
...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS.network,
89+
rpcPrefs: {},
90+
},
91+
};
92+
93+
const { getByTestId } = render(
94+
<MultichainAddressRow {...propsWithoutRpcPrefs} />,
95+
);
96+
const networkIcon = getByTestId(
97+
MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID,
98+
);
99+
expect(networkIcon).toBeTruthy();
100+
});
101+
102+
it('should render with default testID when not provided', () => {
103+
const { getByTestId } = render(
104+
<MultichainAddressRow {...SAMPLE_MULTICHAIN_ADDRESS_ROW_PROPS} />,
105+
);
106+
const component = getByTestId(MULTICHAIN_ADDRESS_ROW_TEST_ID);
107+
expect(component).toBeTruthy();
108+
});
109+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useCallback } from 'react';
2+
import { View } from 'react-native';
3+
4+
import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar';
5+
import ButtonIcon, { ButtonIconSizes } from '../../Buttons/ButtonIcon';
6+
import Text, { TextVariant, TextColor } from '../../Texts/Text';
7+
import { IconName, IconColor } from '../../Icons/Icon';
8+
import { useStyles } from '../../../hooks';
9+
import { formatAddress } from '../../../../util/address';
10+
import { getNetworkImageSource } from '../../../../util/networks';
11+
12+
import styleSheet from './MultichainAddressRow.styles';
13+
import { MultichainAddressRowProps } from './MultichainAddressRow.types';
14+
import {
15+
MULTICHAIN_ADDRESS_ROW_TEST_ID,
16+
MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID,
17+
MULTICHAIN_ADDRESS_ROW_NETWORK_NAME_TEST_ID,
18+
MULTICHAIN_ADDRESS_ROW_ADDRESS_TEST_ID,
19+
MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID,
20+
MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID,
21+
} from './MultichainAddressRow.constants';
22+
import useCopyClipboard from '../../../../components/Views/Notifications/Details/hooks/useCopyClipboard';
23+
24+
const MultichainAddressRow = ({
25+
network,
26+
address,
27+
style,
28+
testID = MULTICHAIN_ADDRESS_ROW_TEST_ID,
29+
...props
30+
}: MultichainAddressRowProps) => {
31+
const { styles } = useStyles(styleSheet, { style });
32+
const copyToClipboard = useCopyClipboard();
33+
34+
const networkImageSource = getNetworkImageSource({
35+
chainId: network.chainId,
36+
networkType: network.type,
37+
});
38+
const truncatedAddress = formatAddress(address, 'short');
39+
40+
const handleCopyClick = useCallback(() => {
41+
copyToClipboard(address);
42+
}, [copyToClipboard, address]);
43+
44+
const handleQrClick = useCallback(() => {
45+
// TODO: Implement QR code functionality
46+
// QR code clicked for address: address
47+
}, []);
48+
49+
return (
50+
<View style={styles.base} testID={testID} {...props}>
51+
<Avatar
52+
variant={AvatarVariant.Network}
53+
size={AvatarSize.Md}
54+
name={network.nickname}
55+
imageSource={networkImageSource}
56+
testID={MULTICHAIN_ADDRESS_ROW_NETWORK_ICON_TEST_ID}
57+
/>
58+
59+
<View style={styles.content}>
60+
<Text
61+
variant={TextVariant.BodyMDMedium}
62+
color={TextColor.Default}
63+
testID={MULTICHAIN_ADDRESS_ROW_NETWORK_NAME_TEST_ID}
64+
>
65+
{network.nickname}
66+
</Text>
67+
<Text
68+
variant={TextVariant.BodySM}
69+
color={TextColor.Alternative}
70+
testID={MULTICHAIN_ADDRESS_ROW_ADDRESS_TEST_ID}
71+
>
72+
{truncatedAddress}
73+
</Text>
74+
</View>
75+
76+
<View style={styles.actions}>
77+
<ButtonIcon
78+
iconName={IconName.Copy}
79+
size={ButtonIconSizes.Md}
80+
onPress={handleCopyClick}
81+
iconColor={IconColor.Default}
82+
testID={MULTICHAIN_ADDRESS_ROW_COPY_BUTTON_TEST_ID}
83+
/>
84+
85+
<ButtonIcon
86+
iconName={IconName.QrCode}
87+
size={ButtonIconSizes.Md}
88+
onPress={handleQrClick}
89+
iconColor={IconColor.Default}
90+
testID={MULTICHAIN_ADDRESS_ROW_QR_BUTTON_TEST_ID}
91+
/>
92+
</View>
93+
</View>
94+
);
95+
};
96+
97+
export default MultichainAddressRow;

0 commit comments

Comments
 (0)