1/** 2 * Changement d'adresse e-mail â widget front (page compte). 3 * Sert sur apps.mesepices.com/js/mesepices-email.js, chargé via <script async> dans le thème. 4 * Vanilla ES5 (pas de jQuery, pas de dépendance au thème). Voir docs/EMAIL-CLIENT.md. 5 * 6 * Parcours : bouton « Modifier mon e-mail » -> modal (nouvelle adresse saisie deux 7 * fois + mot de passe actuel) -> succès : message puis déconnexion ; échec : message 8 * d'erreur, la modal reste ouverte. 9 * 10 * Markup attendu (dans le twig compte) : 11 * <button id="mes-em-open">â¦</button> 12 * <div id="mes-email" class="mes-em-modal" data-customerid="â¦" data-customeremail="â¦"> 13 * ⦠[data-mes-em-close] ⦠<form id="mes-em-form"> #mes-em-new #mes-em-new2 #mes-em-pass 14 * <div id="mes-em-error"></div> 15 * </div> 16 * 17 * â ï¸ Modal maison, PAS le `morphbutton` du thème : morph.js:106 fait écouter à 18 * chaque instance l'évènement `custom:morph` de TOUTES les autres, donc un 2e 19 * morphbutton sur la page compte ouvrirait aussi la modal « supprimer mon 20 * compte ». On reste autonome. 21 * 22 * â ï¸ Requête en application/x-www-form-urlencoded, PAS en JSON : requête « simple » 23 * au sens CORS, donc pas de préflight OPTIONS à gérer côté nginx. Et en POST, 24 * jamais en GET : un mot de passe en query string finirait dans les logs d'accès. 25 * 26 * â ï¸ Après un changement réussi, la session boutique continue d'afficher l'ANCIENNE 27 * adresse (constaté en conditions réelles le 2026-08-24) : le changement est bien 28 * en base mais la session en cours ne le reflète pas. D'où la déconnexion imposée â 29 * sans elle, le client croit que ça n'a pas marché. 30 */ 31(function () { 32 'use strict'; 33 34 var HOST = 'https://apps.mesepices.com/client-email.php'; 35 var LOGOUT = '/deco.php'; 36 var OPEN = 'mes-em-modal--open'; 37 38 function init() { 39 var root = document.getElementById('mes-email'); 40 if (!root) { return; } // pas sur la page compte 41 42 // Classe et non id : l'entree existe dans QUATRE menus (sidebar compte, 43 // nav mobile, dropdown du header, popup de connexion) et plusieurs 44 // peuvent etre rendus sur la meme page. Un id n'en aurait branche qu'un. 45 var openers = document.querySelectorAll('.js-mes-em-open'); 46 var form = document.getElementById('mes-em-form'); 47 var newEl = document.getElementById('mes-em-new'); 48 var new2El = document.getElementById('mes-em-new2'); 49 var passEl = document.getElementById('mes-em-pass'); 50 var errEl = document.getElementById('mes-em-error'); 51 if (!form || !newEl || !new2El || !passEl) { return; } 52 53 var customerid = root.getAttribute('data-customerid'); 54 var current = root.getAttribute('data-customeremail') || ''; 55 var busy = false; // requête en vol : verrouille la fermeture (cf. close()) 56 57 // Regex email strict : exige [email protected] (le type=email natif accepte "a@a"). 58 function emailValid(v) { 59 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v); 60 } 61 62 function esc(s) { 63 return String(s).replace(/[&<>"']/g, function (c) { 64 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 65 }); 66 } 67 68 function showMsg(html, ok) { 69 if (!errEl) { return; } 70 errEl.className = errEl.className.replace(/\s*mes-em-ok/g, '') + (ok ? ' mes-em-ok' : ''); 71 errEl.innerHTML = html; 72 } 73 74 /* ---------- ouverture / fermeture ---------- */ 75 76 function open() { 77 root.className = root.className.replace(/\s*mes-em-modal--open/g, '') + ' ' + OPEN; 78 document.addEventListener('keydown', onKey); 79 if (newEl.focus) { newEl.focus(); } 80 } 81 82 /** Vide le mot de passe ET retire sa puce de validation, qui resterait 83 verte sur un champ devenu vide. */ 84 function clearPassword() { 85 passEl.value = ''; 86 passEl.parentNode.className = passEl.parentNode.className.replace(/\s*mes-(ok|ko)/g, ''); 87 } 88 89 function close() { 90 // Pendant l'appel, la modal ne se ferme plus : ni fond, ni croix, ni Ãchap. 91 // La vérification prend plusieurs secondes ; un clic à côté fermerait la 92 // fenêtre et ferait perdre la saisie, alors que la requête, elle, part 93 // quand même. On rend l'attente non-annulable plutôt que trompeuse. 94 if (busy) { return; } 95 root.className = root.className.replace(/\s*mes-em-modal--open/g, ''); 96 document.removeEventListener('keydown', onKey); 97 clearPassword(); // jamais laisser le mot de passe dans le DOM 98 } 99 100 function onKey(e) { 101 if (e.keyCode === 27) { close(); } // Ãchap 102 } 103 104 // Le lien du menu pointe sur <page compte>#mes-email : depuis une page du 105 // compte qui ne rend PAS la modal (historique de commande), il navigue 106 // normalement et on ouvre à l'arrivée grâce au hash. Sur une page qui la 107 // rend, on intercepte le clic et on ouvre sur place. 108 for (var i = 0; i < openers.length; i++) { 109 openers[i].addEventListener('click', function (e) { e.preventDefault(); open(); }); 110 } 111 if (window.location.hash === '#mes-email') { open(); } 112 // Fond et croix de fermeture, en délégation : un seul handler. 113 root.addEventListener('click', function (e) { 114 var el = e.target; 115 while (el && el !== root) { 116 if (el.getAttribute && el.getAttribute('data-mes-em-close') !== null) { 117 e.preventDefault(); 118 close(); 119 return; 120 } 121 el = el.parentNode; 122 } 123 }); 124 125 /* ---------- validation visuelle (puce verte/rouge, comme le bloc parrainage) ---------- */ 126 127 function wireField(el, validator) { 128 var ctrl = el.parentNode; // .mes-em-control 129 function check() { 130 if (validator) { 131 var msg = validator(el.value); 132 el.setCustomValidity(msg); 133 } 134 if (el.value === '') { 135 ctrl.className = ctrl.className.replace(/\s*mes-(ok|ko)/g, ''); 136 return; 137 } 138 var ok = el.checkValidity(); 139 ctrl.className = ctrl.className.replace(/\s*mes-(ok|ko)/g, '') + (ok ? ' mes-ok' : ' mes-ko'); 140 } 141 el.addEventListener('input', check); 142 el.addEventListener('blur', check); 143 return check; 144 } 145 146 var checkNew2; 147 wireField(newEl, function (v) { 148 if (v !== '' && !emailValid(v)) { return 'Veuillez saisir une adresse e-mail valide.'; } 149 if (v !== '' && current && v.toLowerCase() === current.toLowerCase()) { 150 return 'Cette adresse est déjà celle de votre compte.'; 151 } 152 // La confirmation dépend de ce champ : on la revalide à chaque frappe, 153 // sinon elle reste marquée en erreur après correction de la 1re saisie. 154 if (checkNew2) { checkNew2(); } 155 return ''; 156 }); 157 checkNew2 = wireField(new2El, function (v) { 158 if (v !== '' && v !== newEl.value) { return 'Les deux adresses ne sont pas identiques.'; } 159 return ''; 160 }); 161 wireField(passEl, null); 162 163 /* ---------- envoi ---------- */ 164 165 form.addEventListener('submit', function (e) { 166 e.preventDefault(); 167 showMsg('', false); 168 169 var next = newEl.value.trim(); 170 var pw = passEl.value; 171 172 // checkValidity couvre required + type=email + les setCustomValidity ci-dessus 173 // (donc aussi « les deux adresses diffèrent » et « adresse déjà la vôtre »). 174 if (form.checkValidity && !form.checkValidity()) { 175 if (form.reportValidity) { form.reportValidity(); } 176 return; 177 } 178 179 // `.loading` = anneau partagé du site (mixin loading-ring, _account.scss). 180 // La vérification enchaîne trois requêtes vers la boutique : sans témoin, 181 // le bouton reste muet plusieurs secondes et le client reclique. 182 var btn = form.querySelector('[type="submit"]'); 183 if (btn) { 184 btn.disabled = true; 185 btn.className += ' loading'; 186 } 187 busy = true; 188 189 var body = 'customer_id=' + encodeURIComponent(customerid) + 190 '¤t_email=' + encodeURIComponent(current) + 191 '&new_email=' + encodeURIComponent(next) + 192 '&password=' + encodeURIComponent(pw); 193 194 var xhr = new XMLHttpRequest(); 195 xhr.open('POST', HOST, true); 196 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 197 xhr.onreadystatechange = function () { 198 if (xhr.readyState !== 4) { return; } 199 busy = false;
200 if (btn) { 201 btn.disabled = false; 202 btn.className = btn.className.replace(/\s*loading/g, ''); 203 } 204 clearPassword(); 205 206 var data = null; 207 try { data = JSON.parse(xhr.responseText); } catch (err) {} 208 209 if (!data) { 210 showMsg('Un problème technique est survenu.', false); 211 return; 212 } 213 if (!data.ok) { 214 showMsg(esc(data.error || 'Une erreur est survenue.'), false); 215 return; 216 } 217 done(next); 218 }; 219 xhr.send(body); 220 }); 221 222 /** 223 * Succès : on prévient AVANT de déconnecter. La session boutique garde 224 * l'ancienne adresse en mémoire, donc rester connecté donnerait au client 225 * l'impression que rien n'a changé. 226 */ 227 function done(next) { 228 form.style.display = 'none'; 229 document.removeEventListener('keydown', onKey); // plus de sortie par Ãchap : la déconnexion est acquise 230 showMsg( 231 '<strong>Votre adresse e-mail est modifiée.</strong><br>' 232 + 'Elle est désormais <strong>' + esc(next) + '</strong>.<br>' 233 + 'Vous allez être déconnecté : reconnectez-vous avec cette nouvelle ' 234 + 'adresse et votre mot de passe habituel.', 235 true 236 ); 237 window.setTimeout(function () { window.location.href = LOGOUT; }, 5000); 238 } 239 } 240 241 if (document.readyState === 'loading') { 242 document.addEventListener('DOMContentLoaded', init); 243 } else { 244 init(); 245 } 246})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.