Add TOTP autofill popup and field scaffolding (#1634)

This commit is contained in:
Leendert de Borst
2026-02-18 10:24:04 +01:00
committed by Leendert de Borst
parent 0cdfe5f4d9
commit c858bc4e8a
4 changed files with 73 additions and 17 deletions

View File

@@ -6,11 +6,12 @@ import '@/entrypoints/contentScript/style.css';
import { onMessage, sendMessage } from "webext-bridge/content-script";
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
import { isAutoShowPopupEnabled, openAutofillPopup, openTotpPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
import { showSavePrompt, isSavePromptVisible, updateSavePromptLogin, getPersistedSavePromptState, restoreSavePromptFromState } from '@/entrypoints/contentScript/SavePrompt';
import { initializeWebAuthnInterceptor } from '@/entrypoints/contentScript/WebAuthnInterceptor';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { DetectedFieldType } from '@/utils/formDetector/types/FormFields';
import { LoginDetector } from '@/utils/loginDetector';
import type { CapturedLogin } from '@/utils/loginDetector';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
@@ -407,7 +408,7 @@ export default defineContentScript({
// Only show popup if debounce time has passed
if (popupDebounceTimeHasPassed()) {
await showPopupWithAuthCheck(inputElement, container);
await showPopupWithAuthCheck(inputElement, container, detectedFieldType);
}
}
}
@@ -468,6 +469,8 @@ export default defineContentScript({
return;
}
const detectedFieldType = formDetector.getDetectedFieldType();
/**
* By default we check if the popup is not disabled (for current site) and if the field is autofill-triggerable
* but if forceShow is true, we show the popup regardless.
@@ -476,14 +479,17 @@ export default defineContentScript({
if (canShowPopup) {
injectIcon(inputElement, container);
await showPopupWithAuthCheck(inputElement, container);
await showPopupWithAuthCheck(inputElement, container, detectedFieldType ?? undefined);
}
}
/**
* Show popup with auth check.
* @param inputElement - The input element to show the popup for.
* @param container - The container element.
* @param fieldType - The detected field type (optional, defaults to regular autofill).
*/
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement) : Promise<void> {
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement, fieldType?: DetectedFieldType) : Promise<void> {
try {
// Check auth status and pending migrations in a single call
const { sendMessage } = await import('webext-bridge/content-script');
@@ -513,8 +519,12 @@ export default defineContentScript({
return;
}
// No upgrade required, show normal autofill popup
openAutofillPopup(inputElement, container);
// Show appropriate popup based on field type
if (fieldType === DetectedFieldType.Totp) {
openTotpPopup(inputElement, container);
} else {
openAutofillPopup(inputElement, container);
}
} catch (error) {
console.error('[AliasVault] Error checking vault status:', error);
// Fall back to normal autofill popup if check fails

View File

@@ -272,6 +272,41 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
actualInput.addEventListener('keydown', handleKeyPress);
}
/**
* Fill TOTP code into the input field.
* Generates the code via background script and fills it.
*
* @param itemId - The item ID to generate TOTP code for.
* @param input - The input element to fill the TOTP code into.
*/
export async function fillTotpCode(itemId: string, input: HTMLInputElement): Promise<void> {
// Set debounce time to 300ms to prevent the popup from being shown again within 300ms because of autofill events.
hidePopupFor(300);
// Reset auto-lock timer when autofilling
sendMessage('RESET_AUTO_LOCK_TIMER', {}, 'background').catch(() => {
// Ignore errors as background script might not be ready
});
// Generate TOTP code via background
const response = await sendMessage('GENERATE_TOTP_CODE', { itemId }, 'background') as {
success: boolean;
code?: string;
error?: string;
};
if (!response.success || !response.code) {
console.error('Failed to generate TOTP code:', response.error);
return;
}
// Fill the TOTP field
input.value = response.code;
// Trigger input events for form validation
triggerInputEvents(input);
}
/**
* Trigger input events for an element to trigger form validation
* which some websites require before the "continue" button is enabled.

View File

@@ -1,5 +1,5 @@
import { CombinedFieldPatterns, CombinedGenderOptionPatterns, CombinedStopWords } from "./FieldPatterns";
import { FormFields } from "./types/FormFields";
import { DetectedFieldType, FormFields } from "./types/FormFields";
/**
* Form detector.
@@ -971,19 +971,19 @@ export class FormDetector {
/**
* Get the detected field type for the clicked element.
* Returns 'username', 'password', or 'email' if detected, null otherwise.
* Returns a DetectedFieldType enum value if detected, null otherwise.
* First checks for our custom data-av-field-type attribute (set on previous interactions),
* then falls back to full field detection.
*/
public getDetectedFieldType(): string | null {
public getDetectedFieldType(): DetectedFieldType | null {
if (!this.clickedElement) {
return null;
}
// First check if we already detected and stored the field type
const storedFieldType = this.clickedElement.getAttribute('data-av-field-type');
if (storedFieldType) {
return storedFieldType;
if (storedFieldType && Object.values(DetectedFieldType).includes(storedFieldType as DetectedFieldType)) {
return storedFieldType as DetectedFieldType;
}
// Get the actual input element (handles shadow DOM)
@@ -992,8 +992,8 @@ export class FormDetector {
// Also check the actual element for stored field type
if (actualElement !== this.clickedElement) {
const actualStoredFieldType = actualElement.getAttribute('data-av-field-type');
if (actualStoredFieldType) {
return actualStoredFieldType;
if (actualStoredFieldType && Object.values(DetectedFieldType).includes(actualStoredFieldType as DetectedFieldType)) {
return actualStoredFieldType as DetectedFieldType;
}
}
@@ -1008,26 +1008,26 @@ export class FormDetector {
// Check if any of the elements is a username field
const usernameFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text']);
if (usernameFields.some(input => elementsToCheck.includes(input))) {
return 'username';
return DetectedFieldType.Username;
}
// Check if any of the elements is a password field
const passwordField = this.findPasswordField(formWrapper as HTMLFormElement | null);
if ((passwordField.primary && elementsToCheck.includes(passwordField.primary)) ||
(passwordField.confirm && elementsToCheck.includes(passwordField.confirm))) {
return 'password';
return DetectedFieldType.Password;
}
// Check if any of the elements is an email field
const emailFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.email, ['text', 'email']);
if (emailFields.some(input => elementsToCheck.includes(input))) {
return 'email';
return DetectedFieldType.Email;
}
// Check if any of the elements is a TOTP field
const totpField = this.findTotpField(formWrapper as HTMLFormElement | null);
if (totpField && elementsToCheck.includes(totpField)) {
return 'totp';
return DetectedFieldType.Totp;
}
return null;

View File

@@ -1,3 +1,14 @@
/**
* Detected field type for autofill trigger fields.
* Used to determine which popup to show and how to handle the field.
*/
export enum DetectedFieldType {
Username = 'username',
Password = 'password',
Email = 'email',
Totp = 'totp'
}
export type FormFields = {
form: HTMLFormElement | null;
emailField: HTMLInputElement | null;