diff --git a/apps/browser-extension/src/entrypoints/content.ts b/apps/browser-extension/src/entrypoints/content.ts index 7df2fd941..fda669a78 100644 --- a/apps/browser-extension/src/entrypoints/content.ts +++ b/apps/browser-extension/src/entrypoints/content.ts @@ -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 { + async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement, fieldType?: DetectedFieldType) : Promise { 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 diff --git a/apps/browser-extension/src/entrypoints/contentScript/Form.ts b/apps/browser-extension/src/entrypoints/contentScript/Form.ts index 0991c691c..59880e933 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Form.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Form.ts @@ -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 { + // 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. diff --git a/apps/browser-extension/src/utils/formDetector/FormDetector.ts b/apps/browser-extension/src/utils/formDetector/FormDetector.ts index 73c2c6a34..002a593dd 100644 --- a/apps/browser-extension/src/utils/formDetector/FormDetector.ts +++ b/apps/browser-extension/src/utils/formDetector/FormDetector.ts @@ -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; diff --git a/apps/browser-extension/src/utils/formDetector/types/FormFields.ts b/apps/browser-extension/src/utils/formDetector/types/FormFields.ts index 7f8301b27..0ffa684e1 100644 --- a/apps/browser-extension/src/utils/formDetector/types/FormFields.ts +++ b/apps/browser-extension/src/utils/formDetector/types/FormFields.ts @@ -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;