Skip to content

fix: Display alerts on add network request if there are pending confirmations #30634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 7, 2025
Merged
7 changes: 7 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions app/_locales/en_GB/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion app/scripts/lib/approval/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ApprovalType } from '@metamask/controller-utils';
import { providerErrors } from '@metamask/rpc-errors';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../shared/constants/app';
import { rejectAllApprovals } from './utils';
import { rejectAllApprovals, rejectOriginApprovals } from './utils';

const ID_MOCK = '123';
const ID_MOCK_2 = '456';
Expand Down Expand Up @@ -100,4 +100,30 @@ describe('Approval Utils', () => {
expect(deleteInterface).toHaveBeenCalledWith(INTERFACE_ID_MOCK);
});
});

describe('rejectOriginApprovals', () => {
it('rejects approval requests from given origin', () => {
const origin = 'https://example.com';
const approvalController = createApprovalControllerMock([
{ id: ID_MOCK, origin, type: ApprovalType.Transaction },
{
id: ID_MOCK_2,
origin: 'www.test.com',
type: ApprovalType.EthSignTypedData,
},
]);

rejectOriginApprovals({
approvalController,
deleteInterface: () => undefined,
origin,
});

expect(approvalController.reject).toHaveBeenCalledTimes(1);
expect(approvalController.reject).toHaveBeenCalledWith(
ID_MOCK,
providerErrors.userRejectedRequest(),
);
});
});
});
119 changes: 78 additions & 41 deletions app/scripts/lib/approval/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { ApprovalController } from '@metamask/approval-controller';
import {
ApprovalController,
ApprovalRequest,
} from '@metamask/approval-controller';
import { ApprovalType } from '@metamask/controller-utils';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import { providerErrors } from '@metamask/rpc-errors';
import { createProjectLogger } from '@metamask/utils';
import { createProjectLogger, Json } from '@metamask/utils';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../shared/constants/app';
///: END:ONLY_INCLUDE_IF
Expand All @@ -20,44 +23,78 @@ export function rejectAllApprovals({
const approvalRequests = Object.values(approvalRequestsById);

for (const approvalRequest of approvalRequests) {
const { id, type } = approvalRequest;
const interfaceId = approvalRequest.requestData?.id as string;

switch (type) {
case ApprovalType.SnapDialogAlert:
case ApprovalType.SnapDialogPrompt:
case DIALOG_APPROVAL_TYPES.default:
log('Rejecting snap dialog', { id, interfaceId });
approvalController.accept(id, null);
deleteInterface?.(interfaceId);
break;

case ApprovalType.SnapDialogConfirmation:
log('Rejecting snap confirmation', { id, interfaceId });
approvalController.accept(id, false);
deleteInterface?.(interfaceId);
break;

///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation:
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountRemoval:
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showSnapAccountRedirect:
log('Rejecting snap account confirmation', { id });
approvalController.accept(id, false);
break;
///: END:ONLY_INCLUDE_IF

default:
log('Rejecting pending approval', { id });
approvalController.reject(
id,
providerErrors.userRejectedRequest({
data: {
cause: 'rejectAllApprovals',
},
}),
);
break;
}
rejectApproval({
approvalController,
approvalRequest,
deleteInterface,
});
}
}

export function rejectOriginApprovals({
approvalController,
deleteInterface,
origin,
}: {
approvalController: ApprovalController;
deleteInterface?: (id: string) => void;
origin: string;
}) {
const approvalRequestsById = approvalController.state.pendingApprovals;
const approvalRequests = Object.values(approvalRequestsById);

const originApprovalRequests = approvalRequests.filter(
(approvalRequest) => approvalRequest.origin === origin,
);

for (const approvalRequest of originApprovalRequests) {
rejectApproval({
approvalController,
approvalRequest,
deleteInterface,
});
}
}

function rejectApproval({
approvalController,
approvalRequest,
deleteInterface,
}: {
approvalController: ApprovalController;
approvalRequest: ApprovalRequest<Record<string, Json>>;
deleteInterface?: (id: string) => void;
}) {
const { id, type, origin } = approvalRequest;
const interfaceId = approvalRequest.requestData?.id as string;

switch (type) {
case ApprovalType.SnapDialogAlert:
case ApprovalType.SnapDialogPrompt:
case DIALOG_APPROVAL_TYPES.default:
log('Rejecting snap dialog', { id, interfaceId, origin, type });
approvalController.accept(id, null);
deleteInterface?.(interfaceId);
break;

case ApprovalType.SnapDialogConfirmation:
log('Rejecting snap confirmation', { id, interfaceId, origin, type });
approvalController.accept(id, false);
deleteInterface?.(interfaceId);
break;

///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation:
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountRemoval:
case SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showSnapAccountRedirect:
log('Rejecting snap account confirmation', { id, origin, type });
approvalController.accept(id, false);
break;
///: END:ONLY_INCLUDE_IF

default:
log('Rejecting pending approval', { id, origin, type });
approvalController.reject(id, providerErrors.userRejectedRequest());
break;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const addEthereumChain = {
getCaveat: true,
requestPermittedChainsPermissionForOrigin: true,
requestPermittedChainsPermissionIncrementalForOrigin: true,
rejectApprovalRequestsForOrigin: true,
},
};

Expand All @@ -42,6 +43,7 @@ async function addEthereumChainHandler(
getCaveat,
requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin,
rejectApprovalRequestsForOrigin,
},
) {
let validParams;
Expand Down Expand Up @@ -191,5 +193,6 @@ async function addEthereumChainHandler(
getCaveat,
requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin,
rejectApprovalRequestsForOrigin,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const createMockedHandler = () => {
}),
requestPermittedChainsPermissionForOrigin: jest.fn(),
requestPermittedChainsPermissionIncrementalForOrigin: jest.fn(),
rejectApprovalRequestsForOrigin: jest.fn(),
};
const response = {};
const handler = (request) =>
Expand Down Expand Up @@ -191,6 +192,7 @@ describe('addEthereumChainHandler', () => {
mocks.requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin:
mocks.requestPermittedChainsPermissionIncrementalForOrigin,
rejectApprovalRequestsForOrigin: mocks.rejectApprovalRequestsForOrigin,
},
);
});
Expand Down Expand Up @@ -258,6 +260,8 @@ describe('addEthereumChainHandler', () => {
mocks.requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin:
mocks.requestPermittedChainsPermissionIncrementalForOrigin,
rejectApprovalRequestsForOrigin:
mocks.rejectApprovalRequestsForOrigin,
},
);
});
Expand Down Expand Up @@ -305,6 +309,8 @@ describe('addEthereumChainHandler', () => {
mocks.requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin:
mocks.requestPermittedChainsPermissionIncrementalForOrigin,
rejectApprovalRequestsForOrigin:
mocks.rejectApprovalRequestsForOrigin,
},
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export function validateAddEthereumChainParams(params) {
* @param {Function} hooks.requestPermittedChainsPermissionForOrigin - The callback to request a new permittedChains-equivalent CAIP-25 permission.
* @param {Function} hooks.requestPermittedChainsPermissionIncrementalForOrigin - The callback to add a new chain to the permittedChains-equivalent CAIP-25 permission.
* @param {Function} hooks.setTokenNetworkFilter - The callback to set the token network filter.
* @param {Function} hooks.rejectApprovalRequestsForOrigin - The callback to reject all pending approval requests for the origin.
* @returns a null response on success or an error if user rejects an approval when autoApprove is false or on unexpected errors.
*/
export async function switchChain(
Expand All @@ -185,6 +186,7 @@ export async function switchChain(
requestPermittedChainsPermissionForOrigin,
requestPermittedChainsPermissionIncrementalForOrigin,
setTokenNetworkFilter,
rejectApprovalRequestsForOrigin,
},
) {
try {
Expand All @@ -209,6 +211,10 @@ export async function switchChain(
});
}

if (rejectApprovalRequestsForOrigin) {
rejectApprovalRequestsForOrigin();
}

await setActiveNetwork(networkClientId);
setTokenNetworkFilter(chainId);
response.result = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('Ethereum Chain Utils', () => {
requestPermittedChainsPermissionForOrigin: jest.fn(),
requestPermittedChainsPermissionIncrementalForOrigin: jest.fn(),
setTokenNetworkFilter: jest.fn(),
rejectApprovalRequestsForOrigin: jest.fn(),
};
const response: { result?: true } = {};
const switchChain = (chainId: Hex, networkClientId: string) =>
Expand All @@ -40,6 +41,13 @@ describe('Ethereum Chain Utils', () => {
});
});

it('calls rejectApprovalRequestsForOrigin if passed', async () => {
const { mocks, switchChain } = createMockedSwitchChain();
await switchChain('0x1', 'mainnet');

expect(mocks.rejectApprovalRequestsForOrigin).toHaveBeenCalledTimes(1);
});

describe('with no existing CAIP-25 permission', () => {
it('requests a switch chain approval without autoApprove if autoApprove: false', async () => {
const { mocks, switchChain } = createMockedSwitchChain();
Expand Down
Loading
Loading