PageSourceSearch

https://apps.mesepices.com/js/mesepices-email.js?v=2026092401

js mesepices.com collected 2026-10-02 18:49:33 UTC 10,923 bytes, 246 lines download raw bytes

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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                       '&current_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.