Skip to content

Commit eeb085f

Browse files
authored
fix: fix ConnectPage when a non-EVM account is selected (#28436)
## **Description** The currently selected non-EVM account was part of the requested account on during the account connection flow. The `ConnectPage` was allowing to "confirm" the request even if the UI was showing 0 account. To fix this, we no longer uses the currently selected account if this account is not EVM-compatible. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28436?quickstart=1) ## **Related issues** Fixes: MetaMask/accounts-planning#670 ## **Manual testing steps** 1. `yarn start:flask` 2. Enable Bitcoin support 3. Create a Bitcoin account 4. Select this account 5. Go to: https://metamask.github.io/test-dapp/ 6. Try to connect your accounts - None accounts should be selected by default - You should not be able to "confirm" the dialog ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-11-13 at 14 57 11](https://github.com/user-attachments/assets/0b1b3de2-e968-408a-9aea-c9ea4006a1b1) If you try to connect, you will get: ![Screenshot 2024-11-13 at 14 57 44](https://github.com/user-attachments/assets/a0c8e507-7530-4de7-a73f-5414ba1a2656) ### **After** ![Screenshot 2024-11-13 at 15 01 39](https://github.com/user-attachments/assets/170f4ec3-01a9-422e-a32c-66ad81d5568e) ## **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/develop/.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/develop/.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 2e2edb5 commit eeb085f

File tree

4 files changed

+145
-47
lines changed

4 files changed

+145
-47
lines changed

test/jest/mocks.ts

+64
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BtcMethod,
55
BtcAccountType,
66
InternalAccount,
7+
isEvmAccountType,
78
} from '@metamask/keyring-api';
89
import { KeyringTypes } from '@metamask/keyring-controller';
910
import { v4 as uuidv4 } from 'uuid';
@@ -14,6 +15,9 @@ import {
1415
initialState,
1516
} from '../../ui/ducks/send';
1617
import { MetaMaskReduxState } from '../../ui/store/store';
18+
import mockState from '../data/mock-state.json';
19+
20+
export type MockState = typeof mockState;
1721

1822
export const MOCK_DEFAULT_ADDRESS =
1923
'0xd5e099c71b797516c10ed0f0d895f429c2781111';
@@ -245,3 +249,63 @@ export const getSelectedInternalAccountFromMockState = (
245249
state.metamask.internalAccounts.selectedAccount
246250
];
247251
};
252+
253+
export function overrideAccountsFromMockState<
254+
MockMetaMaskState extends MockState['metamask'],
255+
>(
256+
state: { metamask: MockMetaMaskState },
257+
accounts: InternalAccount[],
258+
selectedAccountId?: string,
259+
): { metamask: MockMetaMaskState } {
260+
// First, re-create the accounts mapping and the currently selected account.
261+
const [{ id: newFirstAccountId }] = accounts;
262+
const newSelectedAccount = selectedAccountId ?? newFirstAccountId ?? '';
263+
const newInternalAccounts = accounts.reduce(
264+
(
265+
acc: MetaMaskReduxState['metamask']['internalAccounts']['accounts'],
266+
account,
267+
) => {
268+
acc[account.id] = account;
269+
return acc;
270+
},
271+
{},
272+
);
273+
274+
// Re-create the keyring mapping too, since some selectors are using their internal
275+
// account list.
276+
const newKeyrings: MetaMaskReduxState['metamask']['keyrings'] = [];
277+
for (const keyring of state.metamask.keyrings) {
278+
const newAccountsForKeyring = [];
279+
for (const account of accounts) {
280+
if (account.metadata.keyring.type === keyring.type) {
281+
newAccountsForKeyring.push(account.address);
282+
}
283+
}
284+
newKeyrings.push({
285+
type: keyring.type,
286+
accounts: newAccountsForKeyring,
287+
});
288+
}
289+
290+
// Compute balances for EVM addresses:
291+
// FIXME: Looks like there's no `balances` type in `MetaMaskReduxState`.
292+
const newBalances: Record<string, string> = {};
293+
for (const account of accounts) {
294+
if (isEvmAccountType(account.type)) {
295+
newBalances[account.address] = '0x0';
296+
}
297+
}
298+
299+
return {
300+
...state,
301+
metamask: {
302+
...state.metamask,
303+
internalAccounts: {
304+
accounts: newInternalAccounts,
305+
selectedAccount: newSelectedAccount,
306+
},
307+
keyrings: newKeyrings,
308+
balances: newBalances,
309+
},
310+
};
311+
}

ui/pages/permissions-connect/connect-page/connect-page.test.tsx

+74-43
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,42 @@ import {
77
EndowmentTypes,
88
RestrictedMethods,
99
} from '../../../../shared/constants/permissions';
10-
import { ConnectPage, ConnectPageRequest } from './connect-page';
10+
import { overrideAccountsFromMockState } from '../../../../test/jest/mocks';
11+
import {
12+
MOCK_ACCOUNT_BIP122_P2WPKH,
13+
MOCK_ACCOUNT_EOA,
14+
} from '../../../../test/data/mock-accounts';
15+
import { ConnectPage, ConnectPageProps } from './connect-page';
16+
17+
const mockTestDappUrl = 'https://test.dapp';
1118

1219
const render = (
13-
props: {
14-
request: ConnectPageRequest;
15-
permissionsRequestId: string;
16-
rejectPermissionsRequest: (id: string) => void;
17-
approveConnection: (request: ConnectPageRequest) => void;
18-
activeTabOrigin: string;
19-
} = {
20-
request: {
21-
id: '1',
22-
origin: 'https://test.dapp',
23-
},
24-
permissionsRequestId: '1',
25-
rejectPermissionsRequest: jest.fn(),
26-
approveConnection: jest.fn(),
27-
activeTabOrigin: 'https://test.dapp',
28-
},
29-
state = {},
20+
options: {
21+
props?: ConnectPageProps;
22+
state?: object;
23+
} = {},
3024
) => {
25+
const {
26+
props = {
27+
request: {
28+
id: '1',
29+
origin: mockTestDappUrl,
30+
},
31+
permissionsRequestId: '1',
32+
rejectPermissionsRequest: jest.fn(),
33+
approveConnection: jest.fn(),
34+
activeTabOrigin: mockTestDappUrl,
35+
},
36+
state,
37+
} = options;
38+
3139
const store = configureStore({
3240
...mockState,
3341
metamask: {
3442
...mockState.metamask,
3543
...state,
3644
permissionHistory: {
37-
'https://test.dapp': {
45+
mockTestDappUrl: {
3846
eth_accounts: {
3947
accounts: {
4048
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1709225290848,
@@ -44,7 +52,7 @@ const render = (
4452
},
4553
},
4654
activeTab: {
47-
origin: 'https://test.dapp',
55+
origin: mockTestDappUrl,
4856
},
4957
});
5058
return renderWithProvider(<ConnectPage {...props} />, store);
@@ -82,33 +90,56 @@ describe('ConnectPage', () => {
8290

8391
it('should render with defaults from the requested permissions', () => {
8492
const { container } = render({
85-
request: {
86-
id: '1',
87-
origin: 'https://test.dapp',
88-
permissions: {
89-
[RestrictedMethods.eth_accounts]: {
90-
caveats: [
91-
{
92-
type: CaveatTypes.restrictReturnedAccounts,
93-
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
94-
},
95-
],
96-
},
97-
[EndowmentTypes.permittedChains]: {
98-
caveats: [
99-
{
100-
type: CaveatTypes.restrictNetworkSwitching,
101-
value: ['0x1'],
102-
},
103-
],
93+
props: {
94+
request: {
95+
id: '1',
96+
origin: mockTestDappUrl,
97+
permissions: {
98+
[RestrictedMethods.eth_accounts]: {
99+
caveats: [
100+
{
101+
type: CaveatTypes.restrictReturnedAccounts,
102+
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
103+
},
104+
],
105+
},
106+
[EndowmentTypes.permittedChains]: {
107+
caveats: [
108+
{
109+
type: CaveatTypes.restrictNetworkSwitching,
110+
value: ['0x1'],
111+
},
112+
],
113+
},
104114
},
105115
},
116+
permissionsRequestId: '1',
117+
rejectPermissionsRequest: jest.fn(),
118+
approveConnection: jest.fn(),
119+
activeTabOrigin: mockTestDappUrl,
106120
},
107-
permissionsRequestId: '1',
108-
rejectPermissionsRequest: jest.fn(),
109-
approveConnection: jest.fn(),
110-
activeTabOrigin: 'https://test.dapp',
111121
});
112122
expect(container).toMatchSnapshot();
113123
});
124+
125+
it('should render a disabled confirm if current account is a non-EVM account', () => {
126+
// NOTE: We select the non-EVM account by default here!
127+
const mockSelectedAccountId = MOCK_ACCOUNT_BIP122_P2WPKH.id;
128+
const mockAccounts = [MOCK_ACCOUNT_EOA, MOCK_ACCOUNT_BIP122_P2WPKH];
129+
const mockAccountsState = overrideAccountsFromMockState(
130+
mockState,
131+
mockAccounts,
132+
mockSelectedAccountId,
133+
);
134+
135+
const { getByText } = render({
136+
state: mockAccountsState.metamask,
137+
});
138+
const confirmButton = getByText('Connect');
139+
const cancelButton = getByText('Cancel');
140+
// The currently selected account is a Bitcoin account, the "connecting account list" would be
141+
// empty by default and thus, we cannot confirm without explictly select an EVM account.
142+
expect(confirmButton).toBeDisabled();
143+
expect(cancelButton).toBeDefined();
144+
});
114145
});

ui/pages/permissions-connect/connect-page/connect-page.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type ConnectPageRequest = {
5050
>;
5151
};
5252

53-
type ConnectPageProps = {
53+
export type ConnectPageProps = {
5454
request: ConnectPageRequest;
5555
permissionsRequestId: string;
5656
rejectPermissionsRequest: (id: string) => void;
@@ -124,10 +124,11 @@ export const ConnectPage: React.FC<ConnectPageProps> = ({
124124
}, [accounts, internalAccounts]);
125125

126126
const currentAccount = useSelector(getSelectedInternalAccount);
127+
const currentAccountAddress = isEvmAccountType(currentAccount.type)
128+
? [currentAccount.address]
129+
: []; // We do not support non-EVM accounts connections
127130
const defaultAccountsAddresses =
128-
requestedAccounts.length > 0
129-
? requestedAccounts
130-
: [currentAccount?.address];
131+
requestedAccounts.length > 0 ? requestedAccounts : currentAccountAddress;
131132
const [selectedAccountAddresses, setSelectedAccountAddresses] = useState(
132133
defaultAccountsAddresses,
133134
);

ui/pages/permissions-connect/permissions-connect.component.js

+2
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ function getDefaultSelectedAccounts(currentAddress, permissionsRequest) {
4141
return new Set(
4242
requestedAccounts
4343
.map((address) => address.toLowerCase())
44+
// We only consider EVM accounts here (used for `eth_requestAccounts` or `eth_accounts`)
4445
.filter(isEthAddress),
4546
);
4647
}
4748

49+
// We only consider EVM accounts here (used for `eth_requestAccounts` or `eth_accounts`)
4850
return new Set(isEthAddress(currentAddress) ? [currentAddress] : []);
4951
}
5052

0 commit comments

Comments
 (0)