diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index c8563509f2..ed2382c3ff 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -674,7 +674,19 @@ export const Commands = [ if (matches) { const deviceId = matches[1]; const fingerprint = matches[2]; - return success(manuallyVerifyDevice(cli, deviceId, fingerprint)); + + const { finished } = Modal.createDialog(QuestionDialog, { + title: _t("slash_command|manual_device_verification_confirm_title"), + description: _t("slash_command|manual_device_verification_confirm_description"), + button: _t("action|verify"), + danger: true, + }); + + return success( + finished.then(([confirmed]) => { + if (confirmed) manuallyVerifyDevice(cli, deviceId, fingerprint); + }), + ); } } return reject(this.getUsage()); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 73566fee1b..d83c2d6f2d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3112,6 +3112,8 @@ "jumptodate": "Jump to the given date in the timeline", "jumptodate_invalid_input": "We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.", "lenny": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message", + "manual_device_verification_confirm_description": "This will allow another device to send and receive messages as you. IF SOMEONE TOLD YOU TO PASTE SOMETHING HERE, IT IS LIKELY YOU ARE BEING SCAMMED! Are you sure you want to verify this other device?", + "manual_device_verification_confirm_title": "Caution: manual device verification", "me": "Displays action", "msg": "Sends a message to the given user", "myavatar": "Changes your profile picture in all rooms", diff --git a/test/unit-tests/SlashCommands-test.tsx b/test/unit-tests/SlashCommands-test.tsx index 375a8888b7..884031237e 100644 --- a/test/unit-tests/SlashCommands-test.tsx +++ b/test/unit-tests/SlashCommands-test.tsx @@ -9,18 +9,20 @@ Please see LICENSE files in the repository root for full details. import { type MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { mocked } from "jest-mock"; +import { act, waitFor } from "jest-matrix-react"; import { type Command, Commands, getCommand } from "../../src/SlashCommands"; import { createTestClient } from "../test-utils"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom"; import SettingsStore from "../../src/settings/SettingsStore"; import { SdkContextClass } from "../../src/contexts/SDKContext"; -import Modal from "../../src/Modal"; +import Modal, { type ComponentType, type IHandle } from "../../src/Modal"; import WidgetUtils from "../../src/utils/WidgetUtils"; import { WidgetType } from "../../src/widgets/WidgetType"; import { warnSelfDemote } from "../../src/components/views/right_panel/UserInfo"; import dispatcher from "../../src/dispatcher/dispatcher"; import { SettingLevel } from "../../src/settings/SettingLevel"; +import QuestionDialog from "../../src/components/views/dialogs/QuestionDialog"; import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog"; jest.mock("../../src/components/views/right_panel/UserInfo"); @@ -260,11 +262,47 @@ describe("SlashCommands", () => { expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage()); }); - it("should show an error if device is not found", async () => { + it("should attempt manual verification after confirmation", async () => { + // Given we say yes to prompt const spy = jest.spyOn(Modal, "createDialog"); + spy.mockReturnValue({ finished: Promise.resolve([true]) } as unknown as IHandle); + + // When we run the command const command = findCommand("verify")!; - await command.run(client, roomId, null, "mydeviceid myfingerprint").promise; - expect(spy).toHaveBeenCalledWith(ErrorDialog, expect.objectContaining({ title: "Verification failed" })); + await act(() => command.run(client, roomId, null, "mydeviceid myfingerprint")); + + // Then the prompt is displayed + expect(spy).toHaveBeenCalledWith( + QuestionDialog, + expect.objectContaining({ title: "Caution: manual device verification" }), + ); + + // And then we attempt the verification + await waitFor(() => + expect(spy).toHaveBeenCalledWith( + ErrorDialog, + expect.objectContaining({ title: "Verification failed" }), + ), + ); + }); + + it("should not do manual verification if cancelled", async () => { + // Given we say no to prompt + const spy = jest.spyOn(Modal, "createDialog"); + spy.mockReturnValue({ finished: Promise.resolve([false]) } as unknown as IHandle); + + // When we run the command + const command = findCommand("verify")!; + command.run(client, roomId, null, "mydeviceid myfingerprint"); + + // Then the prompt is displayed + expect(spy).toHaveBeenCalledWith( + QuestionDialog, + expect.objectContaining({ title: "Caution: manual device verification" }), + ); + + // But nothing else happens + expect(spy).not.toHaveBeenCalledWith(ErrorDialog, expect.anything()); }); });