Skip to content

Commit dc35c6e

Browse files
authored
feat: add address list (BIP-44) (MetaMask#18325)
<!-- 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 adds a a new view to see all addresses spread across all available networks. ## **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: Adds address list for multichain accounts. ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUL-549 ## **Manual testing steps** ```gherkin Feature: Multichain account address list Scenario: user shares address Given an user that has multichain account When the user presses the copy icon in the header Then navigates to a list of all available address in the account ``` **Multichain accounts feature flag should be enabled.** ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** https://github.com/user-attachments/assets/188dc5f2-ee21-497a-8fca-3da966c4b63c ### **After** https://github.com/user-attachments/assets/aebf605c-b2fe-47cf-b1e2-65313b994c19 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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 3b0d0fa commit dc35c6e

14 files changed

Lines changed: 459 additions & 27 deletions

File tree

app/components/Nav/App/App.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ import SolanaNewFeatureContent from '../../UI/SolanaNewFeatureContent';
157157
import { DeepLinkModal } from '../../UI/DeepLinkModal';
158158
import { checkForDeeplink } from '../../../actions/user';
159159
import { WalletDetails } from '../../Views/MultichainAccounts/WalletDetails/WalletDetails';
160+
import { AddressList as AccountAddressList } from '../../Views/MultichainAccounts/AddressList';
160161
import MultichainAccountActions from '../../Views/MultichainAccounts/sheets/MultichainAccountActions/MultichainAccountActions';
161162
import useInterval from '../../hooks/useInterval';
162163
import { Duration } from '@metamask/utils';
@@ -712,6 +713,25 @@ const MultichainWalletDetails = () => {
712713
);
713714
};
714715

716+
const MultichainAddressList = () => {
717+
const route = useRoute();
718+
719+
return (
720+
<Stack.Navigator
721+
screenOptions={{
722+
headerShown: false,
723+
animationEnabled: false,
724+
}}
725+
>
726+
<Stack.Screen
727+
name={Routes.MULTICHAIN_ACCOUNTS.ADDRESS_LIST}
728+
component={AccountAddressList}
729+
initialParams={route?.params}
730+
/>
731+
</Stack.Navigator>
732+
);
733+
};
734+
715735
const ModalConfirmationRequest = () => (
716736
<Stack.Navigator
717737
screenOptions={{
@@ -849,6 +869,10 @@ const AppFlow = () => {
849869
name={Routes.MULTICHAIN_ACCOUNTS.WALLET_DETAILS}
850870
component={MultichainWalletDetails}
851871
/>
872+
<Stack.Screen
873+
name={Routes.MULTICHAIN_ACCOUNTS.ADDRESS_LIST}
874+
component={MultichainAddressList}
875+
/>
852876
<Stack.Screen
853877
name={Routes.SOLANA_NEW_FEATURE_CONTENT}
854878
component={SolanaNewFeatureContentView}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { InternalAccount } from '@metamask/keyring-internal-api';
3+
4+
import AddressCopy from './AddressCopy';
5+
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
6+
import renderWithProvider from '../../../util/test/renderWithProvider';
7+
import { createMockInternalAccount } from '../../../util/test/accountsControllerTestUtils';
8+
9+
// Mock navigation before importing renderWithProvider
10+
jest.mock('@react-navigation/native', () => ({
11+
...jest.requireActual('@react-navigation/native'),
12+
useNavigation: () => ({
13+
navigate: jest.fn(),
14+
}),
15+
}));
16+
17+
const renderWithAddressCopy = (account: InternalAccount) =>
18+
renderWithProvider(<AddressCopy account={account} />);
19+
20+
describe('AddressCopy', () => {
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
it('renders correctly the component', () => {
26+
const component = renderWithAddressCopy(
27+
createMockInternalAccount('0xaddress', 'Account 1'),
28+
);
29+
expect(component).toBeDefined();
30+
expect(
31+
component.getByTestId(WalletViewSelectorsIDs.ACCOUNT_COPY_BUTTON),
32+
).toBeDefined();
33+
});
34+
});

app/components/UI/AddressCopy/AddressCopy.tsx

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,66 @@
11
// Third parties dependencies
2-
import React from 'react';
3-
import { useDispatch } from 'react-redux';
2+
import React, { useCallback } from 'react';
3+
import { useSelector, useDispatch } from 'react-redux';
4+
import { View } from 'react-native';
5+
import { useNavigation } from '@react-navigation/native';
6+
import { AccountGroupId } from '@metamask/account-api';
47

58
// External dependencies
6-
import { View } from 'react-native';
79
import {
810
ButtonIcon,
911
ButtonIconSize,
1012
IconName,
11-
IconColor,
1213
} from '@metamask/design-system-react-native';
1314
import ClipboardManager from '../../../core/ClipboardManager';
1415
import { showAlert } from '../../../actions/alert';
1516
import { protectWalletModalVisible } from '../../../actions/user';
17+
1618
import { strings } from '../../../../locales/i18n';
1719
import { MetaMetricsEvents } from '../../../core/Analytics';
1820
import { useStyles } from '../../../component-library/hooks';
1921
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
20-
import { InternalAccount } from '@metamask/keyring-internal-api';
22+
import { selectMultichainAccountsState2Enabled } from '../../../selectors/featureFlagController/multichainAccounts/enabledMultichainAccounts';
23+
import { selectSelectedAccountGroupId } from '../../../selectors/multichainAccounts/accountTreeController';
24+
import { createAddressListNavigationDetails } from '../../Views/MultichainAccounts/AddressList';
2125

2226
// Internal dependencies
2327
import styleSheet from './AddressCopy.styles';
2428
import { useMetrics } from '../../../components/hooks/useMetrics';
2529
import { getFormattedAddressFromInternalAccount } from '../../../core/Multichain/utils';
26-
27-
interface AddressCopyProps {
28-
account: InternalAccount;
29-
iconColor?: IconColor;
30-
hitSlop?: {
31-
top?: number;
32-
bottom?: number;
33-
left?: number;
34-
right?: number;
35-
};
36-
}
30+
import type { AddressCopyProps } from './AddressCopy.types';
3731

3832
const AddressCopy = ({ account, iconColor, hitSlop }: AddressCopyProps) => {
3933
const { styles } = useStyles(styleSheet, {});
34+
const { navigate } = useNavigation();
4035

4136
const dispatch = useDispatch();
4237
const { trackEvent, createEventBuilder } = useMetrics();
4338

44-
const handleShowAlert = (config: {
45-
isVisible: boolean;
46-
autodismiss: number;
47-
content: string;
48-
data: { msg: string };
49-
}) => dispatch(showAlert(config));
39+
const isMultichainAccountsState2Enabled = useSelector(
40+
selectMultichainAccountsState2Enabled,
41+
);
42+
const selectedAccountGroupId = useSelector(selectSelectedAccountGroupId);
5043

51-
const handleProtectWalletModalVisible = () =>
52-
dispatch(protectWalletModalVisible());
44+
const handleShowAlert = useCallback(
45+
(config: {
46+
isVisible: boolean;
47+
autodismiss: number;
48+
content: string;
49+
data: { msg: string };
50+
}) => dispatch(showAlert(config)),
51+
[dispatch],
52+
);
53+
54+
const handleProtectWalletModalVisible = useCallback(
55+
() => dispatch(protectWalletModalVisible()),
56+
[dispatch],
57+
);
5358

5459
/**
5560
* A string that represents the selected address
5661
*/
5762

58-
const copyAccountToClipboard = async () => {
63+
const copyAccountToClipboard = useCallback(async () => {
5964
await ClipboardManager.setString(
6065
getFormattedAddressFromInternalAccount(account),
6166
);
@@ -70,15 +75,44 @@ const AddressCopy = ({ account, iconColor, hitSlop }: AddressCopyProps) => {
7075
trackEvent(
7176
createEventBuilder(MetaMetricsEvents.WALLET_COPIED_ADDRESS).build(),
7277
);
73-
};
78+
}, [
79+
account,
80+
createEventBuilder,
81+
handleProtectWalletModalVisible,
82+
handleShowAlert,
83+
trackEvent,
84+
]);
85+
86+
const navigateToAddressList = useCallback(() => {
87+
navigate(
88+
...createAddressListNavigationDetails({
89+
groupId: selectedAccountGroupId as AccountGroupId,
90+
title: `${strings(
91+
'multichain_accounts.address_list.receiving_address',
92+
)}`,
93+
}),
94+
);
95+
}, [navigate, selectedAccountGroupId]);
96+
97+
const handleOnPress = useCallback(() => {
98+
if (isMultichainAccountsState2Enabled) {
99+
navigateToAddressList();
100+
} else {
101+
copyAccountToClipboard();
102+
}
103+
}, [
104+
copyAccountToClipboard,
105+
isMultichainAccountsState2Enabled,
106+
navigateToAddressList,
107+
]);
74108

75109
return (
76110
<View style={styles.address}>
77111
<ButtonIcon
78112
iconName={IconName.Copy}
79113
size={ButtonIconSize.Lg}
80114
iconProps={iconColor && { color: iconColor }}
81-
onPress={copyAccountToClipboard}
115+
onPress={handleOnPress}
82116
testID={WalletViewSelectorsIDs.ACCOUNT_COPY_BUTTON}
83117
hitSlop={hitSlop}
84118
/>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { InternalAccount } from '@metamask/keyring-internal-api';
2+
import { IconColor } from '@metamask/design-system-react-native';
3+
4+
export interface AddressCopyProps {
5+
account: InternalAccount;
6+
iconColor?: IconColor;
7+
hitSlop?: {
8+
top?: number;
9+
bottom?: number;
10+
left?: number;
11+
right?: number;
12+
};
13+
}

app/components/Views/MultichainAccounts/AccountDetails/components/AccountInfo/AccountInfo.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import renderWithProvider from '../../../../../../util/test/renderWithProvider';
1010
import { InternalAccount } from '@metamask/keyring-internal-api';
1111
import { formatAddress } from '../../../../../../util/address';
1212

13+
// Mock navigation before importing renderWithProvider
14+
jest.mock('@react-navigation/native', () => ({
15+
...jest.requireActual('@react-navigation/native'),
16+
useNavigation: () => ({
17+
navigate: jest.fn(),
18+
}),
19+
}));
20+
1321
const mockAddress = '0x67B2fAf7959fB61eb9746571041476Bbd0672569';
1422
const mockAccount = createMockInternalAccount(
1523
mockAddress,

0 commit comments

Comments
 (0)