From 6e33694b2cdbf872661088c00698a24f005f4e85 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 10 Apr 2026 12:23:40 +0200 Subject: [PATCH] Auto copy TOTP to clipboard, update settings (#1891) --- .../src/entrypoints/content.ts | 84 +++++++++++++++---- .../src/entrypoints/contentScript/Form.ts | 42 +++++++++- .../src/entrypoints/contentScript/Popup.ts | 34 -------- .../src/utils/LocalPreferencesService.ts | 34 ++++---- 4 files changed, 126 insertions(+), 68 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/content.ts b/apps/browser-extension/src/entrypoints/content.ts index 35be97658..746a77b8d 100644 --- a/apps/browser-extension/src/entrypoints/content.ts +++ b/apps/browser-extension/src/entrypoints/content.ts @@ -6,12 +6,13 @@ import '@/entrypoints/contentScript/style.css'; import { onMessage, sendMessage } from "webext-bridge/content-script"; import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form'; -import { isAutoShowPopupEnabled, openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup'; +import { openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup'; import { showSavePrompt, showAddUrlPrompt, isSavePromptVisible, updateSavePromptLogin, getPersistedSavePromptState, restoreSavePromptFromState, restoreAddUrlPromptFromState } from '@/entrypoints/contentScript/SavePrompt'; import { initializeWebAuthnInterceptor } from '@/entrypoints/contentScript/WebAuthnInterceptor'; import { FormDetector } from '@/utils/formDetector/FormDetector'; import { DetectedFieldType } from '@/utils/formDetector/types/FormFields'; +import { LocalPreferencesService } from '@/utils/LocalPreferencesService'; import { LoginDetector } from '@/utils/loginDetector'; import type { CapturedLogin, LastAutofilledCredential } from '@/utils/loginDetector'; import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse'; @@ -503,17 +504,24 @@ export default defineContentScript({ return; } - // Only inject icon and show popup if autofill popup is enabled - if (await isAutoShowPopupEnabled()) { - // Store our detected field type for subsequent clicks - inputElement.setAttribute('data-av-field-type', detectedFieldType); + // Check if site allows autofill (site-specific disabled sites) + if (!await isSiteAllowed()) { + return; + } - injectIcon(inputElement, container); + // Check if we should show autofill UI for this field type + if (!await shouldShowAutofillUi(detectedFieldType)) { + return; + } - // Only show popup if debounce time has passed - if (popupDebounceTimeHasPassed()) { - await showPopupWithAuthCheck(inputElement, container, detectedFieldType); - } + // Store our detected field type for subsequent clicks + inputElement.setAttribute('data-av-field-type', detectedFieldType); + + injectIcon(inputElement, container, detectedFieldType); + + // Only show popup if debounce time has passed + if (popupDebounceTimeHasPassed()) { + await showPopupWithAuthCheck(inputElement, container, detectedFieldType); } } }; @@ -558,6 +566,44 @@ export default defineContentScript({ return { success: true }; }); + /** + * Check if autofill is disabled for the current site (site-specific settings only). + * @returns True if site allows autofill, false if site has disabled it + */ + async function isSiteAllowed(): Promise { + const disabledSites = await LocalPreferencesService.getDisabledSites(); + const temporaryDisabledSites = await LocalPreferencesService.getTemporaryDisabledSites(); + const currentHostname = window.location.hostname; + + if (disabledSites.includes(currentHostname)) { + return false; + } + + const temporaryDisabledUntil = temporaryDisabledSites[currentHostname]; + if (temporaryDisabledUntil && Date.now() < temporaryDisabledUntil) { + return false; + } + + return true; + } + + /** + * Check if we should show autofill UI (icon/popup) for a given field type. + * Checks the appropriate toggle setting based on field type. + * + * @param fieldType - The detected field type + * @returns True if we should show UI for this field type, false otherwise + */ + async function shouldShowAutofillUi(fieldType: DetectedFieldType | null): Promise { + // For TOTP fields, check TOTP autofill toggle + if (fieldType === DetectedFieldType.Totp) { + return await LocalPreferencesService.getTotpAutofillEnabled(); + } + + // For credential fields, check credential autofill toggle + return await LocalPreferencesService.getGlobalAutofillPopupEnabled(); + } + /** * Show popup for element. */ @@ -576,13 +622,18 @@ export default defineContentScript({ const detectedFieldType = formDetector.getDetectedFieldType(); /** - * By default we check if the popup is not disabled (for current site) and if the field is autofill-triggerable + * By default we check if the site allows autofill and if the field is autofill-triggerable * but if forceShow is true, we show the popup regardless. */ - const canShowPopup = forceShow || (await isAutoShowPopupEnabled() && formDetector.isAutofillTriggerableField()); + const canShowPopup = forceShow || (await isSiteAllowed() && formDetector.isAutofillTriggerableField()); if (canShowPopup) { - injectIcon(inputElement, container); + // Check field-type-specific settings (credential vs TOTP toggles) + if (!await shouldShowAutofillUi(detectedFieldType)) { + return; + } + + injectIcon(inputElement, container, detectedFieldType ?? undefined); await showPopupWithAuthCheck(inputElement, container, detectedFieldType ?? undefined); } } @@ -625,7 +676,12 @@ export default defineContentScript({ // Show appropriate popup based on field type if (fieldType === DetectedFieldType.Totp) { - openTotpPopup(inputElement, container); + // Check if TOTP autofill is enabled + const totpAutofillEnabled = await LocalPreferencesService.getTotpAutofillEnabled(); + if (totpAutofillEnabled) { + openTotpPopup(inputElement, container); + } + // If disabled, don't show any popup (user can rely on clipboard auto-copy) } else { openAutofillPopup(inputElement, container); } diff --git a/apps/browser-extension/src/entrypoints/contentScript/Form.ts b/apps/browser-extension/src/entrypoints/contentScript/Form.ts index 2c05b18c8..daf4bfac2 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Form.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Form.ts @@ -1,12 +1,14 @@ import { sendMessage } from 'webext-bridge/content-script'; -import { openAutofillPopup } from '@/entrypoints/contentScript/Popup'; +import { openAutofillPopup, openTotpPopup } from '@/entrypoints/contentScript/Popup'; import { LOGO_MARK_SVG } from '@/utils/constants/logo'; import type { Item } from '@/utils/dist/core/models/vault'; import { itemToCredential, FieldKey } from '@/utils/dist/core/models/vault'; import { FormDetector } from '@/utils/formDetector/FormDetector'; import { FormFiller } from '@/utils/formDetector/FormFiller'; +import { DetectedFieldType } from '@/utils/formDetector/types/FormFields'; +import { LocalPreferencesService } from '@/utils/LocalPreferencesService'; import type { LastAutofilledCredential } from '@/utils/loginDetector'; import { ClickValidator } from '@/utils/security/ClickValidator'; import { SqliteClient } from '@/utils/SqliteClient'; @@ -143,6 +145,31 @@ export async function fillItem(item: Item, input: HTMLInputElement): Promise { // Ignore errors as background script might not be ready }); + + // Auto-copy TOTP to clipboard if enabled and item has TOTP after autofill. + const autoCopyTotpEnabled = await LocalPreferencesService.getAutoCopyTotpOnAutofill(); + if (autoCopyTotpEnabled) { + try { + // Generate TOTP code via background + const response = await sendMessage('GENERATE_TOTP_CODE', { itemId: item.Id }, 'background') as { + success: boolean; + code?: string; + error?: string; + }; + + if (response.success && response.code) { + // Copy TOTP code to clipboard + await navigator.clipboard.writeText(response.code); + + // Notify background script that clipboard was copied to start countdown + sendMessage('CLIPBOARD_COPIED', { value: response.code }, 'background').catch(() => { + // Ignore errors as background script might not be ready + }); + } + } catch { + // Silently fail in case TOTP code is not available which is possible. + } + } } /** @@ -189,8 +216,11 @@ function findActualInput(element: HTMLElement): HTMLInputElement { /** * Inject icon for a focused input element + * @param input - The input element to inject icon for + * @param container - The container element + * @param fieldType - The detected field type (optional, defaults to regular autofill) */ -export function injectIcon(input: HTMLInputElement, container: HTMLElement): void { +export function injectIcon(input: HTMLInputElement, container: HTMLElement, fieldType?: DetectedFieldType): void { // Find the actual input element to use for positioning const actualInput = findActualInput(input); @@ -272,7 +302,13 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi } setTimeout(() => actualInput.focus(), 0); - openAutofillPopup(actualInput, container); + + // Open the appropriate popup based on field type + if (fieldType === DetectedFieldType.Totp) { + openTotpPopup(actualInput, container); + } else { + openAutofillPopup(actualInput, container); + } }); // Append the icon to the overlay container diff --git a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts index 67624394a..0f431091d 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -1277,40 +1277,6 @@ function createItemList(items: Item[], input: HTMLInputElement, rootContainer: H /** * Check if auto-popup is disabled for current site */ -export async function isAutoShowPopupEnabled(): Promise { - const disabledSites = await LocalPreferencesService.getDisabledSites(); - const temporaryDisabledSites = await LocalPreferencesService.getTemporaryDisabledSites(); - const globalPopupEnabled = await LocalPreferencesService.getGlobalAutofillPopupEnabled(); - - const currentHostname = window.location.hostname; - - if (!globalPopupEnabled) { - // Popup is disabled for all sites. - return false; - } - - if (disabledSites.includes(currentHostname)) { - // Popup is permanently disabled for current site. - return false; - } - - // Check temporary disable - const temporaryDisabledUntil = temporaryDisabledSites[currentHostname]; - if (temporaryDisabledUntil && Date.now() < temporaryDisabledUntil) { - // Popup is temporarily disabled for current site. - return false; - } - - // Check time-based dismissal - const dismissUntil = await LocalPreferencesService.getVaultLockedDismissUntil(); - if (dismissUntil && Date.now() < dismissUntil) { - // Popup is dismissed for a certain amount of time. - return false; - } - - return true; -} - /** * Disable auto-popup for current site */ diff --git a/apps/browser-extension/src/utils/LocalPreferencesService.ts b/apps/browser-extension/src/utils/LocalPreferencesService.ts index a29282727..ef709d7cc 100644 --- a/apps/browser-extension/src/utils/LocalPreferencesService.ts +++ b/apps/browser-extension/src/utils/LocalPreferencesService.ts @@ -11,7 +11,7 @@ const KEYS = { PASSKEY_DISABLED_SITES: 'local:aliasvault_passkey_disabled_sites', // Global toggles - GLOBAL_AUTOFILL_POPUP_ENABLED: 'local:aliasvault_global_autofill_popup_enabled', + CREDENTIAL_AUTOFILL_POPUP_ENABLED: 'local:aliasvault_global_autofill_popup_enabled', GLOBAL_CONTEXT_MENU_ENABLED: 'local:aliasvault_global_context_menu_enabled', PASSKEY_PROVIDER_ENABLED: 'local:aliasvault_passkey_provider_enabled', @@ -94,22 +94,6 @@ export const LocalPreferencesService = { await storage.setItem(KEYS.AUTO_CLOSE_UNLOCK_POPUP, enabled); }, - /** - * Get whether the global autofill popup is enabled. - * @returns Whether autofill popup is globally enabled. Defaults to true. - */ - async getGlobalAutofillPopupEnabled(): Promise { - const value = await storage.getItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED) as boolean | null; - return value !== false; - }, - - /** - * Set whether the global autofill popup is enabled. - */ - async setGlobalAutofillPopupEnabled(enabled: boolean): Promise { - await storage.setItem(KEYS.GLOBAL_AUTOFILL_POPUP_ENABLED, enabled); - }, - /** * Get the autofill matching mode. * @returns The matching mode. Defaults to DEFAULT. @@ -456,6 +440,22 @@ export const LocalPreferencesService = { * ============================================ */ + /** + * Get whether the global autofill popup is enabled. + * @returns Whether autofill popup is globally enabled. Defaults to true. + */ + async getGlobalAutofillPopupEnabled(): Promise { + const value = await storage.getItem(KEYS.CREDENTIAL_AUTOFILL_POPUP_ENABLED) as boolean | null; + return value !== false; + }, + + /** + * Set whether the global autofill popup is enabled. + */ + async setGlobalAutofillPopupEnabled(enabled: boolean): Promise { + await storage.setItem(KEYS.CREDENTIAL_AUTOFILL_POPUP_ENABLED, enabled); + }, + /** * Get whether TOTP autofill is enabled. * @returns Whether TOTP autofill is enabled. Defaults to true (enabled by default).