From 36ade373ff578bb1b777d9dd583b2cff0d81f431 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 18 Apr 2026 21:16:10 +0200 Subject: [PATCH] Tweak browser extension popup dismissed checks (#1931) --- .../src/entrypoints/content.ts | 7 +++ .../src/entrypoints/contentScript/Form.ts | 32 ++++++++++-- .../src/entrypoints/contentScript/Popup.ts | 50 ++++++++++++++++--- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/content.ts b/apps/browser-extension/src/entrypoints/content.ts index 746a77b8d..611954c70 100644 --- a/apps/browser-extension/src/entrypoints/content.ts +++ b/apps/browser-extension/src/entrypoints/content.ts @@ -656,6 +656,13 @@ export default defineContentScript({ }; if (authStatus.isVaultLocked) { + // Check if the user has dismissed the vault locked popup + const dismissUntil = await LocalPreferencesService.getVaultLockedDismissUntil(); + if (dismissUntil && Date.now() < dismissUntil) { + // User has dismissed the popup, don't show it again + return; + } + // Vault is locked, show vault locked popup const { createVaultLockedPopup } = await import('@/entrypoints/contentScript/Popup'); createVaultLockedPopup(inputElement, container); diff --git a/apps/browser-extension/src/entrypoints/contentScript/Form.ts b/apps/browser-extension/src/entrypoints/contentScript/Form.ts index daf4bfac2..4a3f00d2d 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Form.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Form.ts @@ -1,6 +1,6 @@ import { sendMessage } from 'webext-bridge/content-script'; -import { openAutofillPopup, openTotpPopup } from '@/entrypoints/contentScript/Popup'; +import { openAutofillPopup, openTotpPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup'; import { LOGO_MARK_SVG } from '@/utils/constants/logo'; import type { Item } from '@/utils/dist/core/models/vault'; @@ -290,6 +290,15 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement, fiel window.addEventListener('scroll', updateIconPosition, true); window.addEventListener('resize', updateIconPosition); + /* + * Prevent mousedown from propagating to document to avoid triggering + * the "click outside" handler that would close the popup immediately. + */ + icon.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }); + // Add click event to trigger the autofill popup and refocus the input icon.addEventListener('click', async (e: MouseEvent) => { e.preventDefault(); @@ -303,11 +312,24 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement, fiel setTimeout(() => actualInput.focus(), 0); - // Open the appropriate popup based on field type - if (fieldType === DetectedFieldType.Totp) { - openTotpPopup(actualInput, container); + /* + * Toggle the popup: if it's already open, close it; otherwise open it. + * Check if popup exists in the container. + */ + const existingPopup = container.querySelector('#aliasvault-credential-popup'); + if (existingPopup) { + // Popup is open, close it + removeExistingPopup(container); } else { - openAutofillPopup(actualInput, container); + /* + * Open the appropriate popup based on field type. + * Pass forceShow=true since this is a manual user action (icon click). + */ + if (fieldType === DetectedFieldType.Totp) { + openTotpPopup(actualInput, container, true); + } else { + openAutofillPopup(actualInput, container, true); + } } }); diff --git a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts index 0f431091d..a50140aa8 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -109,8 +109,11 @@ const createSuggestedNameSpan = (name: string): HTMLElement => { /** * Open (or refresh) the autofill popup including check if vault is locked. + * @param input - The input element that triggered the popup + * @param container - The container element + * @param forceShow - If true, always show the popup even if dismissed (for manual icon clicks) */ -export function openAutofillPopup(input: HTMLInputElement, container: HTMLElement) : void { +export function openAutofillPopup(input: HTMLInputElement, container: HTMLElement, forceShow: boolean = false) : void { createLoadingPopup(input, '', container); /** @@ -140,6 +143,16 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen if (response.success) { await createAutofillPopup(input, response.items, container); } else { + // Check if the user has dismissed the vault locked popup (only for auto-show, not manual clicks) + if (!forceShow) { + const dismissUntil = await LocalPreferencesService.getVaultLockedDismissUntil(); + if (dismissUntil && Date.now() < dismissUntil) { + // User has dismissed the popup, don't show it again + removeExistingPopup(container); + return; + } + } + await createVaultLockedPopup(input, container); } })(); @@ -148,8 +161,11 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen /** * Open (or refresh) the TOTP autofill popup for 2FA code fields. * Shows only items that have TOTP codes stored. + * @param input - The input element that triggered the popup + * @param container - The container element + * @param forceShow - If true, always show the popup even if dismissed (for manual icon clicks) */ -export function openTotpPopup(input: HTMLInputElement, container: HTMLElement) : void { +export function openTotpPopup(input: HTMLInputElement, container: HTMLElement, forceShow: boolean = false) : void { createLoadingPopup(input, '', container); /** @@ -176,6 +192,16 @@ export function openTotpPopup(input: HTMLInputElement, container: HTMLElement) : if (response.success) { await createTotpPopup(input, response.items, container); } else { + // Check if the user has dismissed the vault locked popup (only for auto-show, not manual clicks) + if (!forceShow) { + const dismissUntil = await LocalPreferencesService.getVaultLockedDismissUntil(); + if (dismissUntil && Date.now() < dismissUntil) { + // User has dismissed the popup, don't show it again + removeExistingPopup(container); + return; + } + } + await createVaultLockedPopup(input, container); } })(); @@ -349,8 +375,11 @@ async function createTotpPopup(input: HTMLInputElement, items: Item[] | undefine return; } + // Check if the click is on the AliasVault icon + const isIconClick = targetElement.closest('.av-input-icon') !== null; + // Check if the click is outside the popup and outside the shadow UI - if (popupElement && !popupElement.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') { + if (popupElement && !popupElement.contains(target) && !input.contains(target) && !isIconClick && targetElement.tagName !== 'ALIASVAULT-UI') { removeExistingPopup(rootContainer); } }; @@ -995,8 +1024,11 @@ export async function createAutofillPopup(input: HTMLInputElement, items: Item[] return; } + // Check if the click is on the AliasVault icon + const isIconClick = targetElement.closest('.av-input-icon') !== null; + // Check if the click is outside the popup and outside the shadow UI - if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') { + if (popup && !popup.contains(target) && !input.contains(target) && !isIconClick && targetElement.tagName !== 'ALIASVAULT-UI') { removeExistingPopup(rootContainer); } }; @@ -1083,8 +1115,11 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai const target = event.target as Node; const targetElement = event.target as HTMLElement; + // Check if the click is on the AliasVault icon + const isIconClick = targetElement.closest('.av-input-icon') !== null; + // Check if the click is outside the popup and outside the shadow UI - if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') { + if (popup && !popup.contains(target) && !input.contains(target) && !isIconClick && targetElement.tagName !== 'ALIASVAULT-UI') { removeExistingPopup(rootContainer); document.removeEventListener('mousedown', handleClickOutside); } @@ -2502,8 +2537,11 @@ export async function createUpgradeRequiredPopup(input: HTMLInputElement, rootCo const target = event.target as Node; const targetElement = event.target as HTMLElement; + // Check if the click is on the AliasVault icon + const isIconClick = targetElement.closest('.av-input-icon') !== null; + // Check if the click is outside the popup and outside the shadow UI - if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') { + if (popup && !popup.contains(target) && !input.contains(target) && !isIconClick && targetElement.tagName !== 'ALIASVAULT-UI') { removeExistingPopup(rootContainer); document.removeEventListener('mousedown', handleClickOutside); }