fill.ts

3.7 KB · raw

1import { isVisible } from "./detect";2import { extractDomain } from "../shared/domain";34function getRegistrableDomain(hostname: string): string {5  return extractDomain(hostname) ?? hostname;6}78function checkFormAction(form: HTMLFormElement | null, expectedDomain: string): boolean {9  if (!form?.action) return true;10  try {11    const actionUrl = new URL(form.action);12    const actionDomain = getRegistrableDomain(actionUrl.hostname);13    if (actionDomain !== expectedDomain) {14      return false;15    }16  } catch {}17  return true;18}1920export function fillFields(21  usernameEl: HTMLInputElement | null,22  passwordEl: HTMLInputElement | null,23  username: string,24  password: string,25  form?: HTMLFormElement | null,26  expectedDomain?: string27): void {28  if (expectedDomain && form && !checkFormAction(form, expectedDomain)) {29    return;30  }31  // Fill username regardless of visibility — on SPA step-by-step login flows32  // the username input may be visually hidden but still tracked by the form.33  if (usernameEl && username) {34    setNativeValue(usernameEl, username);35  }36  if (passwordEl && isVisible(passwordEl)) {37    setNativeValue(passwordEl, password);38  }39}4041// React tracks input values via a synthetic event system that does not see42// direct `.value =` assignment. We must call the native HTMLInputElement43// value setter (pulled off the prototype) and then dispatch the real input44// and change events so React re-reads the new value. Without this, the45// field reads as empty to the form's controlled-input state.46export function setNativeValue(el: HTMLInputElement, value: string): void {47  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(48    window.HTMLInputElement.prototype,49    "value"50  )?.set;51  nativeInputValueSetter?.call(el, value);52  el.dispatchEvent(new Event("input", { bubbles: true }));53  el.dispatchEvent(new Event("change", { bubbles: true }));54}5556export interface CardFillFields {57  number: HTMLInputElement | null;58  expMonth: HTMLInputElement | null;59  expYear: HTMLInputElement | null;60  expiryCombo: HTMLInputElement | null;61  cvc: HTMLInputElement | null;62  name: HTMLInputElement | null;63}6465export function fillCardForm(fields: CardFillFields, card: { cardNumber: string; expMonth: string; expYear: string; cvc: string; cardholderName: string }): void {66  const raw = card.cardNumber.replace(/\s/g, '');67  if (fields.number) setNativeValue(fields.number, formatCardNumber(raw));68  if (fields.expiryCombo) {69    setNativeValue(fields.expiryCombo, `${card.expMonth}/${card.expYear.slice(-2)}`);70  } else {71    if (fields.expMonth) setNativeValue(fields.expMonth, card.expMonth);72    if (fields.expYear) setNativeValue(fields.expYear, card.expYear);73  }74  if (fields.cvc) setNativeValue(fields.cvc, card.cvc);75  if (fields.name) setNativeValue(fields.name, card.cardholderName);76}7778export function formatCardNumber(raw: string): string {79  if (/^3[47]/.test(raw)) return raw.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');80  return raw.replace(/(\d{4})(?=\d)/g, '$1 ').trim();81}8283export function detectCardNetwork(number: string): import('../shared/types').CardNetwork {84  const n = number.replace(/\s/g, '');85  if (/^4/.test(n)) return 'visa';86  if (/^5[1-5]|^2[2-7]/.test(n)) return 'mastercard';87  if (/^3[47]/.test(n)) return 'amex';88  if (/^6(?:011|5)/.test(n)) return 'discover';89  return 'unknown';90}9192export function passesLuhn(number: string): boolean {93  const digits = number.replace(/\s/g, '').split('').map(Number);94  let sum = 0, isEven = false;95  for (let i = digits.length - 1; i >= 0; i--) {96    let d = digits[i]!;97    if (isEven) { d *= 2; if (d > 9) d -= 9; }98    sum += d;99    isEven = !isEven;100  }101  return sum % 10 === 0;102}103104export function extractLast4(number: string): string {105  return number.replace(/\s/g, '').slice(-4);106}