Auto copy TOTP to clipboard, update settings (#1891)

This commit is contained in:
Leendert de Borst
2026-04-10 12:23:40 +02:00
committed by Leendert de Borst
parent 49cab65631
commit 6e33694b2c
4 changed files with 126 additions and 68 deletions

View File

@@ -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<boolean> {
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<boolean> {
// 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);
}

View File

@@ -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<voi
sendMessage('SET_RECENTLY_SELECTED', { itemId: item.Id, domain: window.location.hostname }, 'background').catch(() => {
// 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

View File

@@ -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<boolean> {
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
*/

View File

@@ -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<boolean> {
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<void> {
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<boolean> {
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<void> {
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).