AutoComplete

dummy data and fill the trulioo form

// ==UserScript==
// @name         AutoComplete
// @namespace    https://trulioo.com/
// @version      1.0.0
// @description  dummy data and fill the trulioo form
// @author       You
// @match        *://*/eidv/personMatch*
// @match        *://*/verification*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      auto-completed.sitienbmt.workers.dev
// ==/UserScript==

(function () {
  'use strict';

  // Config
  const BACKEND_ENDPOINT = 'https://auto-completed.sitienbmt.workers.dev';
  const CLICK_DELAY_MS = 600;

  // State
  let countrySelection = null;
  let isVerification = false;
  let isKyb = false;
  let isSearchFieldExisting = false;

  // Helpers (ported)
  const dobMonthOpts = [
    { text: 'January', value: 1 },
    { text: 'February', value: 2 },
    { text: 'March', value: 3 },
    { text: 'April', value: 4 },
    { text: 'May', value: 5 },
    { text: 'June', value: 6 },
    { text: 'July', value: 7 },
    { text: 'August', value: 8 },
    { text: 'September', value: 9 },
    { text: 'October', value: 10 },
    { text: 'November', value: 11 },
    { text: 'December', value: 12 },
  ];

  const genderMap = new Map([
    ['M', 'MALE'],
    ['MALE', 'MALE'],
    ['F', 'FEMALE'],
    ['FEMALE', 'FEMALE'],
  ]);

  function normalizeInput(input) {
    return String(input).toLowerCase().replace(/[()]/g, '').trim();
  }

  function normalizeMonthOfBirth(input) {
    input = String(input).trim();
    const numericInput = parseInt(input);
    if (!isNaN(numericInput) && numericInput >= 1 && numericInput <= 12) {
      const month = dobMonthOpts.find((m) => m.value === numericInput);
      return `${month.text} (${month.value})`;
    }
    const normalizedInput = normalizeInput(input);
    const monthByText = dobMonthOpts.find(
      (m) => normalizeInput(m.text) === normalizedInput
    );
    if (monthByText) return `${monthByText.text} (${monthByText.value})`;

    const complexMatch = normalizedInput.match(/([a-z]+)\s*(?:\()?(\d+)(?:\))?/);
    if (complexMatch) {
      const [, monthText, monthValue] = complexMatch;
      const month = dobMonthOpts.find(
        (m) =>
          normalizeInput(m.text) === monthText.trim() ||
          m.value === parseInt(monthValue)
      );
      if (month) return `${month.text} (${month.value})`;
    }
    return 'Invalid month input';
  }

  function compareGender(a, b) {
    if (!a || !b) return false;
    const norm = (x) => x.trim().toUpperCase();
    const extractParen = (s) => (s.match(/\(([^)]+)\)/) || [])[1];
    const stripParen = (s) => s.replace(/\s*\([^)]*\)\s*/g, '').trim();

    const n1 = norm(a), n2 = norm(b);
    const c1 = stripParen(n1), c2 = stripParen(n2);
    const p1 = extractParen(n1), p2 = extractParen(n2);

    const s1 = genderMap.get(c1) || genderMap.get(p1) || c1;
    const s2 = genderMap.get(c2) || genderMap.get(p2) || c2;
    return s1 === s2;
  }

  function compareStrings(a, b) {
    if (!a || !b) return false;
    a = a.trim().toUpperCase();
    b = b.trim().toUpperCase();
    if (a === b) return true;

    const abbr = (s) => (s.match(/\(([^)]+)\)/) || [])[1];
    const strip = (s) => s.replace(/\s*\([^)]*\)\s*/g, '').trim();

    const aAbbr = abbr(a), bAbbr = abbr(b);
    const aClean = strip(a), bClean = strip(b);

    return (
      (aAbbr && bClean === aAbbr) ||
      (bAbbr && aClean === bAbbr) ||
      aClean === bClean ||
      (aAbbr && bAbbr && aAbbr === bAbbr)
    );
  }

  // Environment detection
  function detectContext() {
    isVerification = window.location.href.endsWith('verification');
    const kybRootId = 'KYBMFComponent';
    isKyb = isVerification && !!document.getElementById(kybRootId);

    if (isVerification) {
      // Old UI country text
      const el = isKyb
        ? document.getElementById(kybRootId)
        : document.querySelector('td.country-name');
      if (el) {
        const text = isKyb
          ? document
              .querySelector('[data-testid=country-selection-inputbox] #search')
              ?.value
          : el.textContent;
        countrySelection = text ? text.toUpperCase() : null;
      }
    } else {
      // New UI country is in search input as "Country (code)"
      const searchInput = document.querySelector('input[name="search"]');
      if (searchInput) {
        const m = searchInput.value.match(/^(.+?) \(/);
        if (m) countrySelection = m[1].toUpperCase();
      }
    }
  }

  // Field discovery (ported)
  function getOldUIFields() {
    const selectors = [
      '.mat-input',
      '[id^=number-range-field-]',
      '[id^=option-field-]',
      '[id^=number-range-picker]',
      'input.form-control',
    ];
    const fields = [
      ...new Set(
        selectors.flatMap((selector) =>
          Array.from(document.querySelectorAll(selector)).map((item) =>
            (item.getAttribute('id') || '').split('-').pop()
          )
        )
      ),
    ].filter(Boolean);

    const refIndex = fields.indexOf('Customer Reference ID');
    if (refIndex !== -1) fields.splice(refIndex, 1);
    return fields;
  }

  function getNewUIFields() {
    const fields = [
      ...new Set(
        Array.from(document.querySelectorAll('.form-control'))
          .map((item) => item.getAttribute('name'))
          .slice(1)
      ),
    ].filter(Boolean);

    const searchIndex = fields.indexOf('search');
    if (searchIndex !== -1) {
      isSearchFieldExisting = true;
      fields.splice(searchIndex, 1);

      const searchFields = document.querySelectorAll('[data-testid$="-search-field"]');
      searchFields.forEach((sf) => {
        const field = sf.getAttribute('data-testid').split('-').shift().trim();
        fields.push(field);
      });
    }
    return fields;
  }

  function getFormFields() {
    if (isVerification) return getOldUIFields();
    return getNewUIFields();
  }

  // Parsing and filling (ported)
  function parseKeyValueLines(text) {
    const parsed = {};
    const invalid = new Set([
      'null', 'none', 'na', 'n/a', '', 'undefined',
      'unspecified', 'unknown', 'not applicable', 'not available', 'not provided',
    ]);
    text.split('\n').forEach((line) => {
      const [k, v] = line.split('=').map((p) => p?.trim());
      if (k && v && !invalid.has(v.toLowerCase())) parsed[k] = v;
    });
    return parsed;
  }

  function handleSelectField(selectEl, responseMap) {
    const options = selectEl.querySelectorAll('option');
    let optionIndex = -1;
    const field = (selectEl.getAttribute('id') || '').split('-').pop();
    const value = responseMap[field];

    switch (field) {
      case 'MonthOfBirth': {
        const month = normalizeMonthOfBirth(value);
        optionIndex = Array.from(options).findIndex(
          (o) => normalizeMonthOfBirth(o.getAttribute('value')) === month
        );
        break;
      }
      case 'Gender':
        optionIndex = Array.from(options).findIndex((o) =>
          compareGender(o.getAttribute('value'), value)
        );
        break;
      default:
        optionIndex = Array.from(options).findIndex((o) =>
          compareStrings(o.getAttribute('value'), value)
        );
        break;
    }
    if (optionIndex >= 0) {
      options[optionIndex].selected = true;
    }
    else {
      const randomIndex = Math.floor(Math.random() * options.length);
      options[randomIndex].selected = true;
    }
  }

  function rejectSearchFields() {
    if (!isSearchFieldExisting) return;
    const icons = document.querySelectorAll('[data-icon=xmark]');
    if (icons.length > 1) {
      for (let i = 1; i < icons.length; i++) {
        icons[i].dispatchEvent(new MouseEvent('click', { bubbles: true, cancellable: true }));
      }
    }
  }

  function handleSearchField(responseMap) {
    if (!isSearchFieldExisting) return;
    document.querySelectorAll('input[name=search]').forEach((searchInput) => {
      searchInput.click();
      document.querySelectorAll('[id$=-dropdown-menu]').forEach((dropDown) => {
        const options = dropDown.querySelectorAll('[data-testid*=-dropdown-row-]');
        const field = dropDown.getAttribute('id').split('-').shift().trim();
        const value = responseMap[field];
        let option = null;

        switch (field) {
          case 'countrySelection':
            break;
          case 'MonthOfBirth': {
            const month = normalizeMonthOfBirth(value);
            option = Array.from(options).find(
              (opt) => normalizeMonthOfBirth(opt.textContent) === month
            );
            break;
          }
          case 'Gender':
            option = Array.from(options).find((opt) =>
              compareGender(opt.textContent, value)
            );
            break;
          default:
            option = Array.from(options).find((opt) =>
              compareStrings(opt.textContent, value)
            );
            break;
        }
        if (option) {
          option.dispatchEvent(
            new MouseEvent('click', { bubbles: true, cancellable: true })
          );
        }
      });
    });
  }

  function resetFormFields() {
    const fields = getFormFields();
    fields.forEach((field) => {
      const control = document.querySelector(`[name="${field}"], [id$="${field}"]`);
      if (!control) return;
      if (control.tagName.toLowerCase() === 'select') {
        control.selectedIndex = 0;
      } else {
        control.value = '';
      }
      control.dispatchEvent(new Event('change', { bubbles: true }));
    });
    rejectSearchFields();
  }

  function clickTestTransactionCheckboxes() {
    if (isVerification) {
      // Old UI
      const consentText = 'I agree T&C*';
      document.querySelectorAll('div:has(+ label input[type="checkbox"])').forEach((div) => {
        if (!div.textContent.includes(consentText)) return;
        const checkbox = div.nextElementSibling?.querySelector('input[type="checkbox"]');
        if (checkbox?.checked === false) checkbox.click();
      });

      const TEST_TRANSACTION_TEXT = 'Run A Test Transaction';
      document.querySelectorAll('input[type="checkbox"] ~ span').forEach((span) => {
        if (!span.textContent.includes(TEST_TRANSACTION_TEXT)) return;
        const checkbox = span.previousElementSibling?.previousElementSibling;
        if (checkbox?.type === 'checkbox' && !checkbox.checked) checkbox.click();
      });
    } else {
      // New UI
      const TEST_TRANSACTION_TEXT = 'Run a Test Transaction';
      setTimeout(() => {
        document.querySelectorAll('input[type="checkbox"] + p').forEach((p) => {
          if (!p.textContent.includes(TEST_TRANSACTION_TEXT)) return;
          const checkbox = p.previousElementSibling;
          if (checkbox?.type === 'checkbox' && !checkbox.checked) checkbox.click();
        });
      }, CLICK_DELAY_MS);
    }
  }

  // Enhanced form control handling
  function setNativeValue(el, value) {
    const prototype = Object.getPrototypeOf(el);
    const desc = Object.getOwnPropertyDescriptor(prototype, 'value');
    if (desc && desc.set) {
      desc.set.call(el, value);
    } else {
      el.value = value;
    }
  }

  function commitInput(el) {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
  }

  function setControlValue(control, value, responseMap) {
    const tag = control.tagName.toLowerCase();
    if (tag === 'select') {
      handleSelectField(control, responseMap);
      commitInput(control);
      return;
    }

    if (tag === 'input') {
      const type = (control.getAttribute('type') || '').toLowerCase();
      if (type === 'checkbox') {
        const desired = value === true || String(value).toLowerCase() === 'true';
        if (control.checked !== desired) {
          control.click(); // click toggles and triggers events in most frameworks
        }
        return;
      }
      if (type === 'radio') {
        const name = control.getAttribute('name');
        const radios = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
        const match = Array.from(radios).find(r => compareStrings(r.value, value) || compareStrings(r.id, value) || compareStrings(r.getAttribute('data-value') || '', value));
        if (match && !match.checked) {
          match.click();
        }
        return;
      }
    }

    // Default: text-like controls
    control.focus();
    setNativeValue(control, value);
    commitInput(control);
    control.blur();
  }

  function fillFormFields(responseMap) {
    resetFormFields();
    handleSearchField(responseMap);

    if (isVerification) {
      if (responseMap.MonthOfBirth) {
        const idx = dobMonthOpts.findIndex(
          (m) => normalizeMonthOfBirth(m.text) === normalizeMonthOfBirth(responseMap.MonthOfBirth)
        );
        if (idx !== -1) {
          responseMap.MonthOfBirth = dobMonthOpts[idx].value;
        }
      }
    }
    else {
      // new UI already handled by clickTestTransactionCheckboxes()
    }

    for (const [key, value] of Object.entries(responseMap)) {
      const control = document.querySelector(`[name="${key}"], [id$="${key}"]`);
      if (!control) continue;

      setControlValue(control, value, responseMap);
    }
  }

  // Network
  function postJson(url, body) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'POST',
        url,
        headers: { 'Content-Type': 'application/json' },
        data: JSON.stringify(body),
        onload: (res) => resolve(res),
        onerror: (err) => reject(err),
        ontimeout: () => reject(new Error('Timeout')),
      });
    });
  }

  async function fetchExtractedDataFromClipboard(fields) {
    const clipboardText = await navigator.clipboard.readText();
    if (!clipboardText || !clipboardText.trim()) {
      throw new Error('Clipboard is empty');
    }
    const payload = {
      fields: fields.join(','),
      unstructuredData: clipboardText,
    };
    const res = await postJson(`${BACKEND_ENDPOINT}/api/extracting-data`, payload);
    const json = JSON.parse(res.responseText);
    if (!json.success) throw new Error(json.message || 'Error extracting data');
    return json.result; // expected key=value lines
  }

  async function fetchDummyData(fields, countrySelection) {
    // Optional: country-specific rules from TM storage (if you store them)
    const rules = GM_getValue(
      `autocompleted-countrySelectionRules_${countrySelection}`,
      ''
    );
    const payload = {
      country: countrySelection,
      fields: fields.join(','),
      rule: rules || '',
    };
    const res = await postJson(`${BACKEND_ENDPOINT}/api/dummy-data`, payload);
    const json = JSON.parse(res.responseText);
    if (!json.success) throw new Error(json.message || 'Error fetching data');
    return json.result; // server returns key=value lines
  }

  // Wait for fields to be available
  async function waitForFields(timeoutMs = 5000) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
      const fields = getFormFields();
      if (fields && fields.length > 0) return fields;
      await new Promise(r => setTimeout(r, 150));
    }
    return getFormFields();
  }

  // UI trigger
  function addFloatingButton() {
    const btn = document.createElement('button');
    btn.textContent = 'Auto Fill';
    btn.style.cssText = `
      position: fixed;
      z-index: 999999;
      bottom: 20px;
      right: 20px;
      background: #1f6feb;
      color: #fff;
      border: none;
      border-radius: 6px;
      padding: 10px 14px;
      cursor: pointer;
      box-shadow: 0 4px 10px rgba(0,0,0,0.15);
      font-size: 14px;
    `;
    btn.addEventListener('click', async () => {
      let success = false;
      // Disable during processing
      btn.disabled = true;
      btn.style.opacity = '0.6';
      btn.style.cursor = 'not-allowed';
      btn.textContent = 'Auto Fill…';
      try {
        detectContext();
        const fields = await waitForFields();
        if (!fields || fields.length === 0) {
          throw new Error('No fields detected.');
        }
        if (!countrySelection) {
          throw new Error('Country selection not found.');
        }
        clickTestTransactionCheckboxes();

        const textContent = await fetchDummyData(fields, countrySelection);
        const parsed = parseKeyValueLines(textContent);
        fillFormFields(parsed);
        
        // Retry search fields after a delay to let dropdowns mount
        await new Promise(r => setTimeout(r, 150));
        handleSearchField(parsed);
        success = true;
      } catch (e) {
        console.error(e);
        alert('Auto Fill failed: ' + (e?.message || e));
      } finally {
        if (success) {
          // Re-enable on success
          btn.disabled = false;
          btn.style.opacity = '';
          btn.style.cursor = 'pointer';
          btn.textContent = 'Auto Fill';
        } else {
          // Keep disabled on failure
          btn.disabled = true;
          btn.style.opacity = '0.6';
          btn.style.cursor = 'not-allowed';
          btn.textContent = 'Auto Fill (disabled)';
        }
      }
    });
    document.body.appendChild(btn);
  }

  function addPasteAndFillButton() {
    const btn = document.createElement('button');
    btn.textContent = 'Paste & Fill';
    btn.style.cssText = `
      position: fixed;
      z-index: 999999;
      bottom: 20px;
      right: 120px;
      background: #6e40c9;
      color: #fff;
      border: none;
      border-radius: 6px;
      padding: 10px 14px;
      cursor: pointer;
      box-shadow: 0 4px 10px rgba(0,0,0,0.15);
      font-size: 14px;
    `;
    btn.addEventListener('click', async () => {
      let success = false;
      btn.disabled = true;
      btn.style.opacity = '0.6';
      btn.style.cursor = 'not-allowed';
      btn.textContent = 'Paste & Fill…';
      try {
        detectContext();
        const fields = await waitForFields();
        if (!fields || fields.length === 0) {
          throw new Error('No fields detected.');
        }
        clickTestTransactionCheckboxes();

        const textContent = await fetchExtractedDataFromClipboard(fields);
        const parsed = parseKeyValueLines(textContent);
        fillFormFields(parsed);

        await new Promise(r => setTimeout(r, 150));
        handleSearchField(parsed);
        success = true;
      } catch (e) {
        console.error(e);
        alert('Paste & Fill failed: ' + (e?.message || e));
      } finally {
        if (success) {
          btn.disabled = false;
          btn.style.opacity = '';
          btn.style.cursor = 'pointer';
          btn.textContent = 'Paste & Fill';
        } else {
          btn.disabled = true;
          btn.style.opacity = '0.6';
          btn.style.cursor = 'not-allowed';
          btn.textContent = 'Paste & Fill (disabled)';
        }
      }
    });
    document.body.appendChild(btn);
  }

  // Init
  window.addEventListener('load', () => {
    detectContext();
    addFloatingButton();
    addPasteAndFillButton();
  });
})();