import { isVisible } from "./detect"; import { extractDomain } from "../shared/domain"; function getRegistrableDomain(hostname: string): string { return extractDomain(hostname) ?? hostname; } function checkFormAction(form: HTMLFormElement | null, expectedDomain: string): boolean { if (!form?.action) return true; try { const actionUrl = new URL(form.action); const actionDomain = getRegistrableDomain(actionUrl.hostname); if (actionDomain !== expectedDomain) { return false; } } catch {} return true; } export function fillFields( usernameEl: HTMLInputElement | null, passwordEl: HTMLInputElement | null, username: string, password: string, form?: HTMLFormElement | null, expectedDomain?: string ): void { if (expectedDomain && form && !checkFormAction(form, expectedDomain)) { return; } // Fill username regardless of visibility — on SPA step-by-step login flows // the username input may be visually hidden but still tracked by the form. if (usernameEl && username) { setNativeValue(usernameEl, username); } if (passwordEl && isVisible(passwordEl)) { setNativeValue(passwordEl, password); } } // React tracks input values via a synthetic event system that does not see // direct `.value =` assignment. We must call the native HTMLInputElement // value setter (pulled off the prototype) and then dispatch the real input // and change events so React re-reads the new value. Without this, the // field reads as empty to the form's controlled-input state. function setNativeValue(el: HTMLInputElement, value: string): void { const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, "value" )?.set; nativeInputValueSetter?.call(el, value); el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } export interface CardFillFields { number: HTMLInputElement | null; expMonth: HTMLInputElement | null; expYear: HTMLInputElement | null; expiryCombo: HTMLInputElement | null; cvc: HTMLInputElement | null; name: HTMLInputElement | null; } export function fillCardForm(fields: CardFillFields, card: { cardNumber: string; expMonth: string; expYear: string; cvc: string; cardholderName: string }): void { const raw = card.cardNumber.replace(/\s/g, ''); if (fields.number) setNativeValue(fields.number, formatCardNumber(raw)); if (fields.expiryCombo) { setNativeValue(fields.expiryCombo, `${card.expMonth}/${card.expYear.slice(-2)}`); } else { if (fields.expMonth) setNativeValue(fields.expMonth, card.expMonth); if (fields.expYear) setNativeValue(fields.expYear, card.expYear); } if (fields.cvc) setNativeValue(fields.cvc, card.cvc); if (fields.name) setNativeValue(fields.name, card.cardholderName); } export function formatCardNumber(raw: string): string { if (/^3[47]/.test(raw)) return raw.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3'); return raw.replace(/(\d{4})(?=\d)/g, '$1 ').trim(); } export function detectCardNetwork(number: string): import('../shared/types').CardNetwork { const n = number.replace(/\s/g, ''); if (/^4/.test(n)) return 'visa'; if (/^5[1-5]|^2[2-7]/.test(n)) return 'mastercard'; if (/^3[47]/.test(n)) return 'amex'; if (/^6(?:011|5)/.test(n)) return 'discover'; return 'unknown'; } export function passesLuhn(number: string): boolean { const digits = number.replace(/\s/g, '').split('').map(Number); let sum = 0, isEven = false; for (let i = digits.length - 1; i >= 0; i--) { let d = digits[i]!; if (isEven) { d *= 2; if (d > 9) d -= 9; } sum += d; isEven = !isEven; } return sum % 10 === 0; } export function extractLast4(number: string): string { return number.replace(/\s/g, '').slice(-4); }