mems / Crédit Agricole login autofill

// ==UserScript==
// @name	Crédit Agricole login autofill
// @namespace	memmie.lenglet.name
// @author	mems <memmie@lenglet.name>
// @homepageURL https://github.com/mems/ca-login-autofill/
// @description	Enable password autofill on Crédit Agricole.
// @match	https://*.credit-agricole.fr/stb/entreeBam*
// @match	https://www.credit-agricole.fr/*/*/acceder-a-mes-comptes.html*
// @match	https://*.credit-agricole.fr/ephi/*
// @license	MIT
// @updateURL   https://openuserjs.org/meta/mems/Cr%C3%A9dit_Agricole_login_autofill.meta.js
// @downloadURL https://openuserjs.org/src/scripts/mems/Cr%C3%A9dit_Agricole_login_autofill.user.js
// @version	1.2.1
// @grant	none
// ==/UserScript==

// Fake password input that let password manager fill it and trigger pad
const passwordInput = document.createElement("input");
passwordInput.type = "password";
passwordInput.autocomplete = "current-password";
// Bitwarden is quite strict with form field visibility
// See https://github.com/bitwarden/clients/blob/d444143a651dc93172de8bbee3e88f4c2a22e82d/apps/browser/src/autofill/services/dom-element-visibility.service.ts#L16-L26
//passwordInput.style = "position: fixed; right: 100%; bottom: 100%; opacity: 0;";
passwordInput.style = "position: fixed; z-index:1; opacity: 0.1;";
let element;

// espace projet immobilier (www.*-esimulca-enligne.credit-agricole.fr)
if((element = document.querySelector("script#ui-field-gridpassword-templateBAM[type='text/html']")) && element.textContent.includes("onClickChar(")){
  // DOM generated by Angular doesn't exist yet, wait until the required element exist in the DOM
  new MutationObserver((mutations, observer) => {
  	if(!(element = document.querySelector("input[name=PASSWORD][data-bind*=oObfuscatedPassword]"))){
      return;
    }
    
    observer.disconnect();
    
    const removeLastCharButton = document.querySelector("button[data-bind*='onFixPassword(']");
    // Create a map from button to index
    const keymap = [...document.querySelectorAll("button[data-bind*='onClickChar(']")].reduce((map, button) => {
      const val = button.textContent.trim();
      return val !== "" ? map.set(val, button) : map;// there is holes in the keypad
    }, new Map());


    passwordInput.addEventListener("change", event => {
      // Clear current value
      while(element.value.length > 0){
        removeLastCharButton.dispatchEvent(new MouseEvent("click"));
      }

      for(const char of passwordInput.value){
        if(!keymap.has(char)){
          continue;
        }
        keymap.get(char).dispatchEvent(new MouseEvent("click"));
      }
    });

    element.before(passwordInput);
  }).observe(document.body, {childList: true, subtree: true});
}
// comptes en ligne 2020 version
else if((element = document.getElementById("Login-account"))){
	const keypad = document.getElementById("clavier_num");
	const loginButton = document.querySelector(".Login-button");
	let dirty = false;
	let clearPasswordButton;

	const clearPasswordClickHandler = ({isTrusted}) => {
		// Not user action
		if(!isTrusted){
			return;
		}

		passwordInput.value = "";
	};
	const update = () => {
		if(keypad.style.display === "none" || !dirty){
			return false;
		}

		dirty = false;

		clearPasswordButton.dispatchEvent(new MouseEvent("click"));
      
		// Create a map from button to index
		const keymap = [...keypad.querySelectorAll(".Login-key div")].reduce((map, div) => map.set(div.textContent.trim(), div.parentElement), new Map());
    
		for(const char of passwordInput.value){
			if(!keymap.has(char)){
				continue;
			}
			keymap.get(char).dispatchEvent(new MouseEvent("click"));
		}

		return true;
	};

	passwordInput.addEventListener("change", event => {
  	dirty = true;

		if(!update()){
			loginButton.dispatchEvent(new MouseEvent("click"));// async load keypad (other rest of inputs are changed synchronously)
		}
	});

	new MutationObserver(mutations => {
		// if keypad is loaded and password is not entered yet
		if(mutations.find(({attributeName}) => attributeName === "style")){
			clearPasswordButton = document.querySelector("#Login-password ~ .add-clear-x");// only exist after keypad been loaded
			clearPasswordButton.addEventListener("click", clearPasswordClickHandler);

			update();
		}
	}).observe(keypad, {
		attributes: true,
		attributeFilter: ["style"],
		// attributeOldValue: true,
	});

	element.after(passwordInput);
}
// comptes en ligne pre-2020 version
else if((element = document.querySelector("input[name=CCPTE]"))){
	// Create a map from button to index
	const keymap = [...document.querySelectorAll("#pave-saisie-code a")].reduce((map, anchor) => {
		const val = anchor.textContent.trim();
		return val !== "" ? map.set(val, anchor.parentElement) : map;// there is holes in the keypad
	}, new Map());
	const removeLastCharButton = [...document.getElementsByTagName("a")].find(anchor => anchor.textContent.trim() == "Corriger");
	const removeLastChar = (function(){with(this){eval(removeLastCharButton.href)}}).bind(unsafeWindow);// use page context, not sandbow context

	passwordInput.addEventListener("input", function(event){
		// Clear current value
		while(removeLastCharButton && document.formulaire.CCCRYC2.value.length > 0){
			// removeLastCharButton.dispatchEvent(new MouseEvent("click"));
			// but because href="javascript:..." is executed async, this cause an infinite loop here
	    try{
				removeLastChar();
	    }
	    catch(error){}
		}

		for(const char of this.value){
			if(!keymap.has(char)){
				continue;
			}
			keymap.get(char).dispatchEvent(new MouseEvent("click"));
		}
	});

	element.after(passwordInput);

	// Allow to remove the last char
	removeLastCharButton.addEventListener("click", event => {
		passwordInput.value = passwordInput.value.slice(0, -1);
	});
}