diff --git a/applications/pass-extension/manifest-chrome.json b/applications/pass-extension/manifest-chrome.json index f0852aa5dcb..59a5a468c2c 100644 --- a/applications/pass-extension/manifest-chrome.json +++ b/applications/pass-extension/manifest-chrome.json @@ -78,6 +78,12 @@ "default": "Ctrl+Shift+L" }, "description": "Open Proton Pass in a larger window" + }, + "autofill": { + "suggested_key": { + "default": "Ctrl+Shift+U" + }, + "description": "Autofill login credentials" } }, "icons": { diff --git a/applications/pass-extension/manifest-firefox.json b/applications/pass-extension/manifest-firefox.json index 94d121f16df..7a34a3f2b77 100644 --- a/applications/pass-extension/manifest-firefox.json +++ b/applications/pass-extension/manifest-firefox.json @@ -76,6 +76,12 @@ "default": "Ctrl+Shift+L" }, "description": "Open Proton Pass in a larger window" + }, + "autofill": { + "suggested_key": { + "default": "Ctrl+Shift+U" + }, + "description": "Autofill login credentials" } }, "icons": { diff --git a/applications/pass-extension/src/app/content/services/autofill/autofill.service.ts b/applications/pass-extension/src/app/content/services/autofill/autofill.service.ts index 5623827a0b1..cb096639f7a 100644 --- a/applications/pass-extension/src/app/content/services/autofill/autofill.service.ts +++ b/applications/pass-extension/src/app/content/services/autofill/autofill.service.ts @@ -319,7 +319,25 @@ export const createAutofillService = ({ controller }: ContentScriptContextFactor } ); + const onAutofillTrigger = withContext((ctx) => { + const fields = ctx?.service.formManager.getFields(); + const loginField = fields?.find( + (field) => field.action?.type === DropdownAction.AUTOFILL_LOGIN + ); + + if (loginField) { + ctx?.service.inline.dropdown.toggle({ + type: 'field', + action: DropdownAction.AUTOFILL_LOGIN, + autofocused: false, + autofilled: loginField.autofilled !== null, + field: loginField, + }); + } + }); + controller.channel.register(WorkerMessageType.AUTOFILL_SEQUENCE, onAutofillRequest); + controller.channel.register(WorkerMessageType.AUTOFILL_TRIGGER, onAutofillTrigger); return { get processing() { @@ -338,6 +356,7 @@ export const createAutofillService = ({ controller }: ContentScriptContextFactor sync, destroy: () => { controller.channel.unregister(WorkerMessageType.AUTOFILL_SEQUENCE, onAutofillRequest); + controller.channel.unregister(WorkerMessageType.AUTOFILL_TRIGGER, onAutofillTrigger); }, }; }; diff --git a/applications/pass-extension/src/lib/extension/commands.spec.ts b/applications/pass-extension/src/lib/extension/commands.spec.ts new file mode 100644 index 00000000000..8e07aa533ab --- /dev/null +++ b/applications/pass-extension/src/lib/extension/commands.spec.ts @@ -0,0 +1,58 @@ +import { handleExtensionCommand } from './commands'; +import browser from '@proton/pass/lib/globals/browser'; +import { WorkerMessageType } from 'proton-pass-extension/types/messages'; + +jest.mock('@proton/pass/lib/globals/browser', () => ({ + tabs: { + create: jest.fn(() => Promise.resolve()), + query: jest.fn(() => Promise.resolve([{ id: 42 }])), + sendMessage: jest.fn(() => Promise.resolve()), + }, + runtime: { + getURL: jest.fn((path: string) => `chrome-extension://abc/${path}`), + }, + commands: { + getAll: jest.fn(() => Promise.resolve([])), + }, +})); + +describe('handleExtensionCommand', () => { + beforeEach(() => jest.clearAllMocks()); + + it('should open larger window for open-larger-window command', async () => { + await handleExtensionCommand('open-larger-window'); + expect(browser.tabs.create).toHaveBeenCalledWith({ + url: 'chrome-extension://abc/popup.html#', + }); + }); + + it('should send AUTOFILL_TRIGGER to active tab for autofill command', async () => { + await handleExtensionCommand('autofill'); + expect(browser.tabs.query).toHaveBeenCalledWith({ active: true, currentWindow: true }); + expect(browser.tabs.sendMessage).toHaveBeenCalledWith( + 42, + expect.objectContaining({ type: WorkerMessageType.AUTOFILL_TRIGGER }) + ); + }); + + it('should send message when tab id is 0', async () => { + (browser.tabs.query as jest.Mock).mockResolvedValueOnce([{ id: 0 }]); + await handleExtensionCommand('autofill'); + expect(browser.tabs.sendMessage).toHaveBeenCalledWith( + 0, + expect.objectContaining({ type: WorkerMessageType.AUTOFILL_TRIGGER }) + ); + }); + + it('should not send message when no active tab', async () => { + (browser.tabs.query as jest.Mock).mockResolvedValueOnce([]); + await handleExtensionCommand('autofill'); + expect(browser.tabs.sendMessage).not.toHaveBeenCalled(); + }); + + it('should do nothing for unknown commands', async () => { + await handleExtensionCommand('unknown-command'); + expect(browser.tabs.create).not.toHaveBeenCalled(); + expect(browser.tabs.sendMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/applications/pass-extension/src/lib/extension/commands.ts b/applications/pass-extension/src/lib/extension/commands.ts index fce78a34622..068963638f1 100644 --- a/applications/pass-extension/src/lib/extension/commands.ts +++ b/applications/pass-extension/src/lib/extension/commands.ts @@ -1,6 +1,9 @@ import browser from '@proton/pass/lib/globals/browser'; import noop from '@proton/utils/noop'; +import { backgroundMessage } from 'proton-pass-extension/lib/message/send-message'; +import { WorkerMessageType } from 'proton-pass-extension/types/messages'; + export type Shortcut = { name: string; description: string; shortcut: string }; type BrowserCommand = Awaited>[number]; @@ -10,7 +13,7 @@ export const resolveShortcuts = (commands: BrowserCommand[], supported: Record Boolean(cmd.name && cmd.name in supported)) .map(({ name, shortcut }) => ({ name, shortcut: shortcut ?? '', description: supported[name] })); -export const handleExtensionCommand = (command: string) => { +export const handleExtensionCommand = async (command: string) => { if (command === 'open-larger-window') { browser.tabs .create({ @@ -18,4 +21,11 @@ export const handleExtensionCommand = (command: string) => { }) .catch(noop); } + + if (command === 'autofill') { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (tab?.id != null) { + browser.tabs.sendMessage(tab.id, backgroundMessage({ type: WorkerMessageType.AUTOFILL_TRIGGER })).catch(noop); + } + } }; diff --git a/applications/pass-extension/src/types/messages.ts b/applications/pass-extension/src/types/messages.ts index 449b8a5deec..82881df9076 100644 --- a/applications/pass-extension/src/types/messages.ts +++ b/applications/pass-extension/src/types/messages.ts @@ -110,6 +110,7 @@ export enum WorkerMessageType { AUTOFILL_OTP_CHECK = 'AUTOFILL_OTP_CHECK', AUTOFILL_SEQUENCE = 'AUTOFILL_SEQUENCE', AUTOFILL_SYNC = 'AUTOFILL_SYNC', + AUTOFILL_TRIGGER = 'AUTOFILL_TRIGGER', AUTOSAVE_REQUEST = 'AUTOSAVE_REQUEST', AUTOSUGGEST_ALIAS = 'AUTOSUGGEST_ALIAS', @@ -217,6 +218,7 @@ export type AutofillOTPCheckMessage = { type: WorkerMessageType.AUTOFILL_OTP_CHE export type AutofillPasswordOptionsMessage = { type: WorkerMessageType.AUTOSUGGEST_PASSWORD }; export type AutofillSequenceMessage = WithPayload; export type AutofillSyncMessage = { type: WorkerMessageType.AUTOFILL_SYNC }; +export type AutofillTriggerMessage = { type: WorkerMessageType.AUTOFILL_TRIGGER }; export type AutoSaveRequestMessage = WithPayload; export type AutosuggestAliasMessage = { type: WorkerMessageType.AUTOSUGGEST_ALIAS }; @@ -320,6 +322,7 @@ export type WorkerMessage = | AutofillPasswordOptionsMessage | AutofillSequenceMessage | AutofillSyncMessage + | AutofillTriggerMessage | AutoSaveRequestMessage | AutosuggestAliasMessage | B2BEventMessage