PageSourceSearch

https://members.whmcsglobalservices.com/modules/addons/passkeyauth…?v=20260925_mojeid_logo_v3

js whmcsglobalservices.com collected 2026-09-26 11:06:36 UTC 69,028 bytes, 1,714 lines download raw bytes

1(function () {
2    if (!window.PasskeyAuthLogin) {
3        return;
4    }
5
6    var EMAIL_STORAGE_KEY = 'passkeyauth_client_login_email';
7    var PASSKEY_DEVICE_STORAGE_KEY = 'passkeyauth_client_device_passkeys';
8    var cfg = window.PasskeyAuthLogin;
9    var hasWebAuthn = !!(window.PublicKeyCredential);
10    var passkeyUiEnabled = hasWebAuthn && cfg.passkeyEnabled !== false && cfg.authMode !== 'password_only';
11    var availabilityTimers = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
12
13    function isPasskeyOnlyMode() {
14        return !!(cfg && cfg.authMode === 'passkey_only');
15    }
16
17    function allowsUsernameless() {
18        if (!cfg) {
19            return false;
20        }
21        if (cfg.allowUsernameless === true || cfg.allowUsernameless === 1 || cfg.allowUsernameless === '1') {
22            return true;
23        }
24        return cfg.residentKey === 'required';
25    }
26
27    function b64urlToBuffer(base64url) {
28        var padding = '='.repeat((4 - (base64url.length % 4)) % 4);
29        var base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
30        var raw = atob(base64);
31        var out = new Uint8Array(raw.length);
32        for (var i = 0; i < raw.length; i++) {
33            out[i] = raw.charCodeAt(i);
34        }
35
36        return out.buffer;
37    }
38
39    function bufferToB64url(buffer) {
40        var bytes = new Uint8Array(buffer);
41        var str = '';
42        for (var i = 0; i < bytes.byteLength; i++) {
43            str += String.fromCharCode(bytes[i]);
44        }
45
46        return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
47    }
48
49    function showLoginError(message) {
50        var text = message || (cfg && cfg.errorMessage) || 'Passkey sign-in failed.';
51        window.alert(text);
52    }
53
54    function showLoginFlash(form) {
55        if (!cfg || !cfg.flash || !cfg.flash.message) {
56            return;
57        }
58        var existing = document.getElementById('passkeyauth-login-flash');
59        if (existing && existing.parentNode) {
60            existing.parentNode.removeChild(existing);
61        }
62
63        var type = String(cfg.flash.type || 'warning').toLowerCase();
64        if (type === 'danger' || type === 'error') {
65            type = 'danger';
66        } else if (type === 'success') {
67            type = 'success';
68        } else if (type === 'info') {
69            type = 'info';
70        } else {
71            type = 'warning';
72        }
73
74        var alert = document.createElement('div');
75        alert.id = 'passkeyauth-login-flash';
76        alert.className = 'alert alert-' + type + ' text-center';
77        alert.setAttribute('role', 'alert');
78        alert.style.cssText = 'margin: 0 0 16px 0; width: 100%; box-sizing: border-box;';
79        alert.textContent = String(cfg.flash.message);
80
81        // 1. Checkout existing user signin container
82        var existingBox = document.getElementById('containerExistingUserSignin');
83        if (existingBox) {
84            existingBox.insertBefore(alert, existingBox.firstChild);
85            finishFlash(alert);
86            return;
87        }
88
89        // 2. Insert inside the login card directly before the login form
90        var passwordInput = document.querySelector(
91            'form input[type="password"], form input[name="password"], form input[name="loginpw"]'
92        );
93        var loginForm = form
94            || (passwordInput && passwordInput.form)
95            || document.querySelector('form#frmLogin, form[action*="dologin"], form[action*="login"]');
96
97        if (loginForm && loginForm.parentNode) {
98            loginForm.parentNode.insertBefore(alert, loginForm);
99        } else if (loginForm) {
100            loginForm.insertBefore(alert, loginForm.firstChild);
101        } else {
102            var loginContainer = document.querySelector('.login-container .card-body, .login-container, .logincontainer, #main-body .card-body, #main-body .card');
103            if (loginContainer) {
104                loginContainer.insertBefore(alert, loginContainer.firstChild);
105            } else {
106                var main = document.querySelector('#main-body, .main-content, main');
107                if (main) {
108                    main.insertBefore(alert, main.firstChild);
109                } else {
110                    document.body.appendChild(alert);
111                }
112            }
113        }
114
115        finishFlash(alert);
116    }
117
118    function finishFlash(alert) {
119        if (alert && alert.scrollIntoView) {
120            try {
121                alert.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
122            } catch (err) {
123                // Ignore scroll failures.
124            }
125        }
126
127        // Show once per page load.
128        cfg.flash = null;
129    }
130
131    function getFormCsrfToken(form) {
132        if (form) {
133            var input = form.querySelector('input[name="token"], input[name="csrfToken"], input[name="csrf_token"]');
134            if (input && input.value) {
135                return input.value;
136            }
137        }
138        var docInput = document.querySelector('input[name="token"], input[name="csrfToken"], input[name="csrf_token"]');
139        if (docInput && docInput.value) {
140            return docInput.value;
141        }
142        return (cfg && cfg.csrfToken) || '';
143    }
144
145    function postJson(action, payload) {
146        var endpoint = cfg.endpoint || '';
147        var url = endpoint + (endpoint.indexOf('?') >= 0 ? '&' : '?') + 'action=' + encodeURIComponent(action);
148        payload = payload || {};
149        var token = getFormCsrfToken() || cfg.csrfToken || payload.token || payload.csrfToken || '';
150        if (token) {
151            payload.token = token;
152            payload.csrfToken = token;
153        }
154
155        return fetch(url, {
156            method: 'POST',
157            credentials: 'same-origin',
158            headers: { 'Content-Type': 'application/json' },
159            body: JSON.stringify(payload)
160        }).then(function (res) {
161            return res.text().then(function (text) {
162                var data = null;
163                try {
164                    data = text ? JSON.parse(text) : null;
165                } catch (e) {
166                    throw new Error('Passkey login failed: invali
166d server response.');
167                }
168                if (!data || typeof data !== 'object') {
169                    throw new Error('Passkey login failed: empty server response.');
170                }
171                if (!res.ok && !data.message) {
172                    data.message = cfg.errorMessage || 'Passkey sign-in failed.';
173                    data.ok = false;
174                }
175                if (data.token || data.csrfToken) {
176                    cfg.csrfToken = data.token || data.csrfToken;
177                }
178                return data;
179            });
180        });
181    }
182
183    function findEmailInput(form) {
184        if (!form) {
185            return null;
186        }
187        var scope = checkoutFieldScope(form);
188        var preferred = scope.querySelector(
189            'input[name="loginemail"], input#inputLoginEmail, input#loginemail, input[name="loginusername"]'
190        );
191        if (preferred) {
192            return preferred;
193        }
194        var input = scope.querySelector(
195            'input[type="email"], input[name="username"], input[name="loginemail"], input[name="email"], input[name="loginusername"], input[name="login"], input#inputEmail, input#username, input#loginemail'
196        );
197        if (input) {
198            return input;
199        }
200        var textInputs = scope.querySelectorAll('input[type="text"], input:not([type])');
201        for (var i = 0; i < textInputs.length; i++) {
202            var el = textInputs[i];
203            var id = String(el.id || '').toLowerCase();
204            var name = String(el.getAttribute('name') || '').toLowerCase();
205            if (name.indexOf('email') !== -1 || name.indexOf('user') !== -1 || name.indexOf('login') !== -1 || id.indexOf('email') !== -1 || id.indexOf('user') !== -1) {
206                return el;
207            }
208        }
209        return textInputs.length > 0 ? textInputs[0] : null;
210    }
211
212    function findPasswordInput(form) {
213        if (!form) {
214            return null;
215        }
216        var scope = checkoutFieldScope(form);
217        var preferred = scope.querySelector('input[name="loginpw"], input#inputLoginPassword, input#loginpw');
218        if (preferred) {
219            return preferred;
220        }
221        return scope.querySelector('input[type="password"], input[name="password"]');
222    }
223
224    function findNativeSubmit(form) {
225        if (!form) {
226            return null;
227        }
228
229        var existingLogin = form.querySelector('#btnExistingLogin')
230            || document.getElementById('btnExistingLogin');
231        if (existingLogin && !isCompleteOrderControl(existingLogin)) {
232            return existingLogin;
233        }
234
235        var candidates = form.querySelectorAll(
236            'button[type="submit"], input[type="submit"], button#btnLogin, input#btnLogin, button#btnExistingLogin, button[name="login"], input[name="login"], button.btn, input[type="button"]'
237        );
238        var fallback = null;
239
240        for (var i = 0; i < candidates.length; i++) {
241            var el = candidates[i];
242            if (isCompleteOrderControl(el)) {
243                continue;
244            }
245            if (el.classList && (
246                el.classList.contains('passkeyauth-signin-btn')
247                || el.classList.contains('passkeyauth-mode-btn')
248            )) {
249                continue;
250            }
251
252            var id = String(el.id || '').toLowerCase();
253            var name = String(el.getAttribute('name') || '').toLowerCase();
254            var text = String((el.value || '') + ' ' + (el.textContent || '')).replace(/\s+/g, ' ').trim().toLowerCase();
255
256            if (
257                id === 'btnlogin'
258                || id === 'btnexistinglogin'
259                || id.indexOf('btnlogin') !== -1
260                || id === 'login'
261                || name === 'login'
262                || name === 'loginsubmit'
263                || text === 'login'
264                || text === 'log in'
265                || text === 'sign in'
266                || text.indexOf('login') === 0
267            ) {
268                return el;
269            }
270
271            if (!fallback && (el.matches('button[type="submit"], input[type="submit"], button.btn-primary, button.btn'))) {
272                fallback = el;
273            }
274        }
275
276        return fallback || form.querySelector('button[type="submit"], input[type="submit"], button');
277    }
278
279    function isCompleteOrderControl(el) {
280        if (!el) {
281            return false;
282        }
283
284        var id = String(el.id || '').toLowerCase();
285        var name = String(el.getAttribute('name') || '').toLowerCase();
286        var cls = String(el.className || '').toLowerCase();
287        var text = String((el.value || '') + ' ' + (el.textContent || '')).replace(/\s+/g, ' ').trim().toLowerCase();
288
289        if (
290            id.indexOf('completeorder') !== -1
291            || id.indexOf('btncomplete') !== -1
292            || name.indexOf('completeorder') !== -1
293            || cls.indexOf('btn-checkout') !== -1
294            || cls.indexOf('complete-order') !== -1
295        ) {
296            return true;
297        }
298
299        if (text.indexOf('complete order') !== -1 || text.indexOf('place order') !== -1) {
300            return true;
301        }
302
303        return false;
304    }
305
306    function getCheckoutSsoRedirectPath() {
307        var path = String(window.location.pathname || '');
308        var search = String(window.location.search || '');
309        var file = path.split('/').pop() || '';
310
311        if (file.toLowerCase().indexOf('cart.php') !== -1 || search.toLowerCase().indexOf('a=checkout') !== -1) {
312            return 'cart.php?a=checkout';
313        }
314
315        if (!file) {
316            return '';
317        }
318
319        return file + search;
320    }
321
322    function hasCheckoutExistingLogin(form) {
323        if (!form) {
324            return false;
325        }
326        if (String(form.id || '').toLowerCase() === 'frmcheckout') {
327            return true;
328        }
329        return !!form.querySelector(
330            '#containerExistingUserSignin, input[name="loginemail"], input[name="loginpw"], #btnExistingLogin, #existingLoginButton'
331        );
332    }
333
334    function checkoutFieldScope(form) {
335        if (!form) {
336            return form;
337        }
338        if (!hasCheckoutExistingLogin(form) && !isCheckoutLoginForm(form)) {
339            return form;
340        }
341        var box = form.querySelector('#containerExistingUserSignin')
342            || document.getElementById('containerExistingUserSignin');
343        if (box && form.contains(box)) {
344            return box;
345        }
346        return form;
347    }
348
349    function isRegistrationForm(form) {
350        if (!form) {
351            return false;
352        }
353
354        // cart.php checkout form has both existing-customer login and new-customer fields.
355        if (hasCheckoutExistingLogin(form) || isCheckoutLoginForm(form)) {
356            return false;
357        }
358
359        var path = (window.location.pathname || '').toLowerCase();
360        var search = (window.location.search || '').toLowerCase() + (window.location.hash || '').toLowerCase();
361        var href = (window.location.href || '').toLowerCase();
362
363        if (
364            path.indexOf('register') !== -1
365            || search.indexOf('register') !== -1
366            || href.indexOf('register.php') !== -1
367            || search.indexOf('rp=/register') !== -1
368            || search.indexOf('rp=%2fregister') !== -1
369        ) {
370            return true;
371        }
372
373        var formId = String(form.id || '').toLowerCase();
374        if (formId.indexOf('register') !== -1 || formId.indexOf('frmregister') !== -1) {
375            return true;
376        }
377
378        var formClass = String(form.className || '').toLowerCase();
379        if (formClass.indexOf('register') !== -1) {
380            return true;
381        }
382
383        if (form.closest && form.closest('#registration, .register-form, .page-register, #frmRegister, #clientRegistrationForm, .register')) {
384            return true;
385        }
386
387        var action = String(form.getAttribute('action') || '').toLowerCase();
388        if (
389            action.indexOf('register.php') !== -1
390            || action.indexOf('rp=/register') !== -1
391            || action.indexOf('rp=%2fregister') !== -1
392        ) {
393            return true;
394        }
395
396        if (form.querySelector(
397            'input[name="firstname"], input[name="lastname"], input[name="accepttos"], input[name="customfield"], input[name="clientareaaction"][value="register"]'
398        )) {
399            return true;
400        }
401
402        return false;
403    }
404
405    function isStandaloneLoginForm(form) {
406        if (!form) {
407            return false;
408        }
409        if (form.classList.contains('login-form')) {
410            return true;
411        }
412        if (form.closest('.logincontainer, .login-form, #login, .page-login')) {
413            return true;
414        }
415        var path = (window.location.pathname || '').toLowerCase();
416        var search = (window.location.search || '').toLowerCase() + (window.location.hash || '').toLowerCase();
417        if (path.indexOf('login') !== -1 || search.indexOf('rp=/login') !== -1 || search.indexOf('rp=%2flogin') !== -1) {
418            return true;
419        }
420        return false;
421    }
422
423    function isCheckoutLoginForm(form) {
424        if (!form || isStandaloneLoginForm(form)) {
425            return false;
426        }
427
428        var path = (window.location.pathname || '').toLowerCase();
429        var search = (window.location.search || '').toLowerCase();
430        if (
431            path.indexOf('cart.php') !== -1
432            || path.indexOf('/cart/') !== -1
433            || path.slice(-5) === '/cart'
434            || search.indexOf('a=checkout') !== -1
435            || search.indexOf('rp=/cart') !== -1
436            || search.indexOf('rp=%2fcart') !== -1
437        ) {
438            return true;
439        }
440
441        if (form.closest(
442            '#frmCheckout, #containerExistingUserSignin, .already-registered, .secondary-cart-body, #checkout, .wgs-checkout, .cart-body'
443        )) {
444            return true;
445        }
446
447        var action = (form.getAttribute('action') || '').toLowerCase();
448        if (action.indexOf('cart.php') !== -1) {
449            return true;
450        }
451
452        return false;
453    }
454
455    function passwordGroup(form) {
456        var password = findPasswordInput(form);
457        if (!password) {
458            return null;
459        }
460
461        var group = password.closest('.form-group') || password.parentElement;
462        if (!group || group === form) {
463            return null;
464        }
465
466        // wrapper also has the email field — leave it.
467        if (group.querySelector('input[type="email"], input[name="username"], input[name="loginemail"], input[name="email"]')) {
468            return null;
469        }
470
471        return group;
472    }
473
474    function wrapNativeSubmit(form) {
475        var submit = findNativeSubmit(form);
476        if (!submit || isCompleteOrderControl(submit)) {
477            return null;
478        }
479
480        // Complete Order on checkout is off-limits.
481        submit.classList.remove('passkeyauth-native-submit');
482        if (submit.closest('#btnCompleteOrder, .btn-checkout, [id*="CompleteOrder"]')) {
483            return null;
484        }
485
486        submit.classList.add('passkeyauth-native-submit');
487
488        var wrap = submit.closest('.passkeyauth-native-submit-wrap');
489        if (wrap) {
490            return wrap;
491        }
492
493        var parent = submit.parentElement;
494        if (parent && parent !== form) {
495            // parent also has Complete Order
496            if (parent.querySelector && (
497                parent.querySelector('#btnCompleteOrder, [id*="CompleteOrder"]')
498                || isCompleteOrderControl(parent.querySelector('button[type="submit"], input[type="submit"]'))
499            )) {
500                // wrap the Login control only
501                wrap = document.createElement('div');
502                wrap.className = 'passkeyauth-native-submit-wrap';
503                if (submit.parentNode) {
504                    submit.parentNode.insertBefore(wrap, submit);
505                    wrap.appendChild(submit);
506                }
507                return wrap;
508            }
509
510            var onlySubmitChild = true;
511            var children = parent.children;
512            for (var i = 0; i < children.length; i++) {
513                var child = children[i];
514                if (child === submit) {
515                    continue;
516                }
517                if (child.matches && child.matches('input[type="hidden"]')) {
518                    continue;
519                }
520                onlySubmitChild = false;
521                break;
522            }
523            if (onlySubmitChild) {
524                parent.classList.add('passkeyauth-native-submit-wrap');
525                return parent;
526            }
527        }
528
529        wrap = document.createElement('div');
530        wrap.className = 'passkeyauth-native-submit-wrap';
531        if (submit.parentNode) {
532            submit.parentNode.insertBefore(wrap, submit);
533            wrap.appendChild(submit);
534        }
535        return wrap;
536    }
537
538    function normalizeCheckoutLayout(form) {
539        if (!form || form.dataset.passkeyCheckoutNormalized === '1') {
540            return;
541        }
542        form.dataset.passkeyCheckoutNormalized = '1';
543        form.classList.add('passkeyauth-checkout-form');
544
545        var scope = checkoutFieldScope(form) || form;
546        var rows = scope.querySelectorAll('.row');
547        for (var r = 0; r < rows.length; r++) {
548            var row = rows[r];
549            var cols = [];
550            try {
551                cols = row.querySelectorAll(':scope > [class*="col-"]');
552            } catch (e) {
553                cols = [];
554            }
555            if (!cols.length) {
556                cols = [];
557                for (var c = 0; c < row.children.length; c++) {
558                    var child = row.children[c];
559                    if (child.className && String(child.className).indexOf('col-') !== -1) {
560                        cols.push(child);
561                    }
562                }
563            }
564            var touchesLoginFields = false;
565            for (var i = 0; i < cols.length; i++) {
566                if (cols[i].querySelector(
567                    'input[type="email"], input[name="username"], input[name="loginemail"], input[name="email"], input[type="password"], input[name="password"]'
568                )) {
569                    touchesLoginFields = true;
570                    break;
571                }
572            }
573            if (!touchesLoginFields) {
574                continue;
575            }
576            row.classList.add('passkeyauth-checkout-stack-row');
577            for (var j = 0; j < cols.length; j++) {
578                cols[j].classList.add('passkeyauth-checkout-stack-col');
579            }
580        }
581
582        var emailInput = findEmailInput(form);
583        if (emailInput) {
584            var emailGroup = emailInput.closest('.form-group') || emailInput.parentElement;
585            if (emailGroup && emailGroup !== form) {
586                emailGroup.classList.add('passkeyauth-checkout-email-group');
587            }
588        }
589
590        wrapNativeSubmit(form);
591    }
592
593    function reorderCheckoutFields(form) {
594        if (!form || !form.classList.contains('passkeyauth-checkout-form')) {
595            return;
596        }
597
598        var emailInput = findEmailInput(form);
599        var emailGroup = emailInput
600            ? (emailInput.closest('.passkeyauth-checkout-email-group, .form-group') || emailInput.parentElement)
601            : null;
602        var choiceWrap = form.querySelector('.passkeyauth-auth-method-wrap');
603        var passwordWrap = form.querySelector('.passkeyauth-password-group') || passwordGroup(form);
604        var passkeySection = form.querySelector('.passkeyauth-passkey-section');
605        var submitWrap = form.querySelector('.passkeyauth-native-submit-wrap') || wrapNativeSubmit(form);
606        var rememberInput = form.querySelector(
607            'input[name="rememberme"], input[name="remember"], input#rememberme, input#inputRememberMe'
608        );
609        var rememberGroup = nu
609ll;
610        if (rememberInput) {
611            rememberGroup = rememberInput.closest('.form-check, .checkbox, .custom-control, .form-group, label')
612                || rememberInput.parentElement;
613            if (rememberGroup === form || (submitWrap && rememberGroup === submitWrap)) {
614                rememberGroup = rememberInput.parentElement;
615            }
616            if (rememberGroup === form) {
617                rememberGroup = null;
618            }
619        }
620
621        var sequence = [];
622        if (emailGroup) {
623            sequence.push(emailGroup);
624        }
625        if (choiceWrap) {
626            sequence.push(choiceWrap);
627        }
628        if (passwordWrap) {
629            sequence.push(passwordWrap);
630        }
631        if (passkeySection) {
632            sequence.push(passkeySection);
633        }
634        if (rememberGroup && sequence.indexOf(rememberGroup) === -1) {
635            sequence.push(rememberGroup);
636        }
637        if (submitWrap) {
638            sequence.push(submitWrap);
639        }
640        if (sequence.length < 2) {
641            return;
642        }
643
644        var stack = form.querySelector('.passkeyauth-checkout-fields');
645        if (!stack) {
646            stack = document.createElement('div');
647            stack.className = 'passkeyauth-checkout-fields';
648
649            var stackRow = form.querySelector('.passkeyauth-checkout-stack-row');
650            if (stackRow && stackRow.parentNode) {
651                stackRow.parentNode.insertBefore(stack, stackRow);
652            } else if (emailGroup && emailGroup.parentNode) {
653                emailGroup.parentNode.insertBefore(stack, emailGroup);
654            } else {
655                form.insertBefore(stack, form.firstChild);
656            }
657        }
658
659        for (var i = 0; i < sequence.length; i++) {
660            stack.appendChild(sequence[i]);
661        }
662
663        // leftover empty col/row after we moved the button
664        var rows = form.querySelectorAll('.passkeyauth-checkout-stack-row');
665        for (var r = 0; r < rows.length; r++) {
666            var row = rows[r];
667            if (!row.querySelector('input, button, select, textarea, .passkeyauth-auth-method-wrap, .passkeyauth-passkey-section')) {
668                row.classList.add('passkeyauth-checkout-row-empty');
669            }
670        }
671    }
672
673    function rememberEmail(value) {
674        try {
675            if (value) {
676                window.localStorage.setItem(EMAIL_STORAGE_KEY, value);
677            }
678        } catch (e) {
679            // private mode / blocked storage
680        }
681    }
682
683    function restoreRememberedEmail(form) {
684        var emailInput = findEmailInput(form);
685        if (!emailInput || emailInput.value) {
686            return;
687        }
688
689        try {
690            var saved = window.localStorage.getItem(EMAIL_STORAGE_KEY);
691            if (saved) {
692                emailInput.value = saved;
693            }
694        } catch (e) {
695            // Ignore storage failures.
696        }
697    }
698
699    function updateToggleState(form, mode) {
700        var choice = form.querySelector('.passkeyauth-login-choice');
701        if (!choice) {
702            return;
703        }
704
705        var passwordBtn = choice.querySelector('.passkeyauth-mode-password');
706        var passkeyBtn = choice.querySelector('.passkeyauth-mode-passkey');
707        var isPasskey = mode === 'passkey';
708
709        if (passwordBtn) {
710            passwordBtn.classList.toggle('is-active', !isPasskey);
711            passwordBtn.setAttribute('aria-pressed', isPasskey ? 'false' : 'true');
712        }
713        if (passkeyBtn) {
714            passkeyBtn.classList.toggle('is-active', isPasskey);
715            passkeyBtn.setAttribute('aria-pressed', isPasskey ? 'true' : 'false');
716        }
717
718        refreshModeButtonStyles(form);
719    }
720
721    function normalizeStyleName(name) {
722        name = name || 'blue-gradient';
723        return name === 'gradient' ? 'blue-gradient' : name;
724    }
725
726    function clearButtonStyle(btn) {
727        if (!btn) {
728            return;
729        }
730
731        var classes = (btn.className || '').split(/\s+/);
732        var filtered = [];
733        for (var i = 0; i < classes.length; i++) {
734            var className = classes[i];
735            if (className && className.indexOf('passkeyauth-btn-style-') !== 0) {
736                filtered.push(className);
737            }
738        }
739        btn.className = filtered.join(' ');
740        btn.style.removeProperty('background-color');
741        btn.style.removeProperty('background-image');
742        btn.style.removeProperty('color');
743        btn.style.removeProperty('border-color');
744        btn.style.removeProperty('box-shadow');
745    }
746
747    function applyModeButtonStyle(btn, styleName, customColors, isActive) {
748        if (!btn) {
749            return;
750        }
751
752        clearButtonStyle(btn);
753        if (!isActive) {
754            return;
755        }
756
757        styleName = normalizeStyleName(styleName);
758        btn.classList.add('passkeyauth-btn-style-' + styleName);
759        var isPassword = btn.classList.contains('passkeyauth-mode-password');
760        var scopeClass = isPassword
761            ? (cfg.passwordModeScopeClass || 'passkeyauth-btn-custom-client-password')
762            : (cfg.passkeyModeScopeClass || 'passkeyauth-btn-custom-client-passkey');
763        if (scopeClass) {
764            btn.classList.add(scopeClass);
765        }
766        var extraClass = isPassword ? cfg.passwordModeCustomClass : cfg.passkeyModeCustomClass;
767        String(extraClass || '').split(/\s+/).forEach(function (c) {
768            if (/^[A-Za-z_][A-Za-z0-9_-]{0,63}$/.test(c)) {
769                btn.classList.add(c);
770            }
771        });
772        if (styleName === 'custom' && customColors) {
773            btn.style.setProperty('background-color', customColors.bg || '#2563eb', 'important');
774            btn.style.setProperty('background-image', 'none', 'important');
775            btn.style.setProperty('color', customColors.text || '#ffffff', 'important');
776            btn.style.setProperty('border-color', customColors.border || '#2563eb', 'important');
777        }
778    }
779
780    function refreshModeButtonStyles(form) {
781        var choice = form.querySelector('.passkeyauth-login-choice');
782        if (!choice) {
783            return;
784        }
785
786        var passwordBtn = choice.querySelector('.passkeyauth-mode-password');
787        var passkeyBtn = choice.querySelector('.passkeyauth-mode-passkey');
788        applyModeButtonStyle(
789            passwordBtn,
790            cfg.passwordModeBtnStyle,
791            cfg.passwordModeCustomColors,
792            !!(passwordBtn && passwordBtn.classList.contains('is-active'))
793        );
794        applyModeButtonStyle(
795            passkeyBtn,
796            cfg.passkeyModeBtnStyle,
797            cfg.passkeyModeCustomColors,
798            !!(passkeyBtn && passkeyBtn.classList.contains('is-active'))
799        );
800    }
801
802    function setMode(form, mode) {
803        if (isPasskeyOnlyMode()) {
804            mode = 'passkey';
805        }
806        var isPasskey = mode === 'passkey';
807        var isCheckout = form.classList.contains('passkeyauth-checkout-form') || isCheckoutLoginForm(form);
808        form.classList.toggle('passkeyauth-passkey-only', isPasskeyOnlyMode());
809
810        if (isPasskey) {
811            form.classList.add('passkeyauth-password-hidden');
812            form.classList.remove('passkeyauth-password-visible');
813            form.dataset.passkeyauthMode = 'passkey';
814            if (isCheckout) {
815                document.body.classList.add('passkeyauth-checkout-passkey-mode');
816            }
817        } else {
818            form.classList.remove('passkeyauth-password-hidden');
819            form.classList.add('passkeyauth-password-visible');
820            form.dataset.passkeyauthMode = 'password';
821            if (isCheckout) {
822                document.body.classList.remove('passkeyauth-checkout-passkey-mode');
823            }
824        }
825
826        updateToggleState(form, mode);
827        syncCheckoutLoginButtonVisibility(form, isPasskey);
828
829        if (!isPasskey) {
830            var password = findPasswordInput(form);
831            if (password) {
832                password.focus();
833            }
834        } else {
835            var emailInput = findEmailInput(form);
836            if (emailInput) {
837                emailInput.focus();
838            }
839        }
840    }
841
842    function isPasskeyUiControl(el) {
843        if (!el) {
844            return false;
845        }
846        if (el.classList && (
847            el.classList.contains('passkeyauth-signin-btn')
848            || el.classList.contains('passkeyauth-client-signin-btn')
849            || el.classList.contains('passkeyauth-admin-signin-btn')
850            || el.classList.contains('passkeyauth-mode-btn')
851        )) {
852            return true;
853        }
854        return !!(el.closest
855            && el.closest('.passkeyauth-client-passkey-block, .passkeyauth-admin-passkey-block, .passkeyauth-passkey-section, .passkeyauth-login-choice, .passkeyauth-auth-method-wrap'));
856    }
857
858    function looksLikeLoginControl(el) {
859        if (!el || isCompleteOrderControl(el) || isPasskeyUiControl(el)) {
860            return false;
861        }
862
863        var id = String(el.id || '').toLowerCase();
864        var name = String(el.getAttribute('name') || '').toLowerCase();
865        var text = String((el.value || '') + ' ' + (el.textContent || '')).replace(/\s+/g, ' ').trim().toLowerCase();
866
867        if (
868            id === 'btnlogin'
869            || id === 'btnexistinglogin'
870            || id.indexOf('btnlogin') !== -1
871            || id === 'login'
872            || id === 'existingloginbutton'
873            || name === 'login'
874            || name === 'loginsubmit'
875            || text === 'login'
876            || text === 'log in'
877            || text === 'sign in'
878            || text.indexOf('login') === 0
879        ) {
880            return true;
881        }
882
883        if (el.classList && el.classList.contains('passkeyauth-native-submit')) {
884            return true;
885        }
886
887        return !!(el.matches && el.matches(
888            'button#btnExistingLogin, #btnExistingLogin, button[type="submit"], input[type="submit"], button#btnLogin, input#btnLogin, button[name="login"], input[name="login"]'
889        ));
890    }
891
892    function collectCheckoutLoginButtons(form) {
893        var list = [];
894        var seen = [];
895        var stack = form.querySelector('.passkeyauth-checkout-fields');
896
897        function add(el) {
898            if (!el || seen.indexOf(el) !== -1 || isPasskeyUiControl(el) || isCompleteOrderControl(el)) {
899                return;
900            }
901
902            // if they passed the inner span, use the actual button
903            if (el.id === 'existingLoginButton' && el.closest) {
904                var parentBtn = el.closest('#btnExistingLogin, button, .btn');
905                if (parentBtn) {
906                    el = parentBtn;
907                }
908            }
909
910            if (seen.indexOf(el) !== -1) {
911                return;
912            }
913
914            var inStack = !!(stack && stack.contains(el));
915            var isAction = !!(el.matches && el.matches(
916                'button, input[type="submit"], input[type="button"], a.btn, #btnExistingLogin'
917            ));
918
919            // leftover action control in the checkout login stack is native Login
920            if (!looksLikeLoginControl(el) && !(inStack && isAction)) {
921                return;
922            }
923
924            seen.push(el);
925            list.push(el);
926        }
927
928        var existingLoginBtn = form.querySelector('#btnExistingLogin')
929            || document.getElementById('btnExistingLogin');
930        if (existingLoginBtn) {
931            add(existingLoginBtn);
932        }
933
934        var existingLoginLabel = form.querySelector('#existingLoginButton')
935            || document.getElementById('existingLoginButton');
936        if (existingLoginLabel) {
937            add(existingLoginLabel);
938        }
939
940        var primary = findNativeSubmit(form);
941        if (primary) {
942            add(primary);
943        }
944
945        if (form.id) {
946            var external = document.querySelectorAll(
947                'button[form="' + form.id + '"], input[form="' + form.id + '"]'
948            );
949            for (var e = 0; e < external.length; e++) {
950                add(external[e]);
951            }
952        }
953
954        var roots = stack ? [stack, form] : [form];
955        var existingBox = form.closest('#containerExistingUserSignin, .already-registered');
956        if (existingBox) {
957            roots.push(existingBox);
958        }
959
960        for (var r = 0; r < roots.length; r++) {
961            var candidates = roots[r].querySelectorAll(
962                'button, input[type="submit"], input[type="button"], a.btn, #btnExistingLogin, #existingLoginButton'
963            );
964            for (var i = 0; i < candidates.length; i++) {
965                add(candidates[i]);
966            }
967        }
968
969        return list;
970    }
971
972    function syncCheckoutLoginButtonVisibility(form, isPasskey) {
973        if (!form || !form.classList.contains('passkeyauth-checkout-form')) {
974            return;
975        }
976
977        wrapNativeSubmit(form);
978
979        var loginBtns = collectCheckoutLoginButtons(form);
980        for (var i = 0; i < loginBtns.length; i++) {
981            var loginBtn = loginBtns[i];
982            if (!loginBtn || isCompleteOrderControl(loginBtn)) {
983                continue;
984            }
985
986            loginBtn.classList.add('passkeyauth-native-submit');
987            var wrap = loginBtn.closest('.passkeyauth-native-submit-wrap');
988
989            if (isPasskey) {
990                loginBtn.style.setProperty('display', 'none', 'important');
991                loginBtn.style.setProperty('visibility', 'hidden', 'important');
992                loginBtn.setAttribute('aria-hidden', 'true');
993                loginBtn.setAttribute('tabindex', '-1');
994                if (wrap) {
995                    wrap.style.setProperty('display', 'none', 'important');
996                    wrap.style.setProperty('visibility', 'hidden', 'important');
997                }
998            } else {
999                loginBtn.style.removeProperty('display');
1000                loginBtn.style.removeProperty('visibility');
1001                loginBtn.removeAttribute('aria-hidden');
1002                loginBtn.removeAttribute('tabindex');
1003                if (wrap) {
1004                    wrap.style.removeProperty('display');
1005                    wrap.style.removeProperty('visibility');
1006                }
1007            }
1008        }
1009
1010        // Complete Order stays visible even if it got our class by accident
1011        var completeBtns = form.querySelectorAll(
1012            '#btnCompleteOrder, button[id*="CompleteOrder"], input[id*="CompleteOrder"], .btn-checkout'
1013        );
1014        for (var c = 0; c < completeBtns.length; c++) {
1015            var btn = completeBtns[c];
1016            btn.classList.remove('passkeyauth-native-submit');
1017            btn.style.removeProperty('display');
1018            btn.style.removeProperty('visibility');
1019            btn.removeAttribute('aria-hidden');
1020            btn.removeAttribute('tabindex');
1021            var cWrap = btn.closest('.passkeyauth-native-submit-wrap');
1022            if (cWrap && cWrap.querySelector('#btnCompleteOrder, [id*="CompleteOrder"], .btn-checkout')) {
1023                cWrap.classList.remove('passkeyauth-native-submit-wrap');
1024                cWrap.style.removeProperty('display');
1025                cWrap.style.removeProperty('visibility');
1026            }
1027        }
1028    }
1029
1030    function runPasskeyLogin(form, button) {
1031        var emailInput = findEmailInput(form);
1032        var identifier = emailInput && emailInput.value ? emailInput.value.trim() : '';
1033        if (!identifier && !allowsUsernameless()) {
1034            identifier = window.prompt(cfg.emailPrompt, '') || '';
1035            if (identifier && emailInput) {
1036                emailInput.value = identifier;
1037            }
1038        }
1039        if (!identifier && !allowsUsernameless()) {
1040            return;
1041        }
1042
1043        if (identifier) {
1044            rememberEmail(identifier);
1045        }
1046        button.disabled = true;
1047
1048        var finishPayload = {
1049            csrfToken: cfg.csrfToken,
1050            challengeToken: null,
1051            credential: null
1052        };
1053
1054        postJson('api_login_begin', {
1055            csrfToken: cfg.csrfToken,
1056            identifier: identifier || ''
1057        }).then(function (begin) {
1058            if (!begin.ok) {
1059                throw new Error(begin.message || cfg.errorMessage);
1060            }
1061
1062            var options = begin.publicKey || {};
1063            options.challenge = b64urlToBuffer(options.challenge);
1064            options.allowCredentials = (options.allowCredentials || []).map(function (item) {
1065                item.id = b64urlToBuffer(item.id);
1066                return item;
1067            });
1068
1069            var choose = window.PasskeyAuthWebAuthn && window.PasskeyAuthWebAuthn.choosePasskey
1070                ? window.PasskeyAuthWebAuthn.choosePasskey
1071                : function (passkeys) {
1072                    return Promise.resolve(passkeys && passkeys[0] ? passkeys[0].id : '');
1073                };
1074
1075            return choose(begin.passkeys || [], cfg).then(function (selectedId) {
1076                if (window.PasskeyAuthWebAuthn && window.PasskeyAuthWebAuthn.restrictAllowCredentials) {
1077                    options = window.PasskeyAuthWebAuthn.restrictAllowCredentials(options, selectedId);
1078                } else if (selectedId) {
1079                    options.allowCredentials = [{
1080                        type: 'public-key',
1081                        id: b64urlToBuffer(selectedId),
1082                        transports: ['internal', 'hybrid', 'usb', 'nfc', 'ble']
1083                    }];
1084                } else {
1085                    options.allowCredentials = [];
1086                }
1087
1088                return navigator.credentials.get({ publicKey: options });
1089            }).then(function (cred) {
1090                if (!cred) {
1091                    throw new Error('Passkey authentication was cancelled.');
1092                }
1093
1094                if (window.PasskeyAuthWebAuthn && typeof window.PasskeyAuthWebAuthn.showLoader === 'function') {
1095                    window.PasskeyAuthWebAuthn.showLoader('Verifying Passkey...');
1096                }
1097
1098                finishPayload.challengeToken = begin.challengeToken;
1099                finishPayload.credential = {
1100                    id: cred.id,
1101                    rawId: bufferToB64url(cred.rawId),
1102                    type: cred.type,
1103                    response: {
1104                        clientDataJSON: bufferToB64url(cred.response.clientDataJSON),
1105                        authenticatorData: bufferToB64url(cred.response.authenticatorData),
1106                        signature: bufferToB64url(cred.response.signature),
1107                        userHandle: cred.response.userHandle ? bufferToB64url(cred.response.userHandle) : ''
1108                    }
1109                };
1110
1111                // checkout: send them back to cart after SSO
1112                if (form.classList.contains('passkeyauth-checkout-form') || isCheckoutLoginForm(form)) {
1113                    var redirectPath = getCheckoutSsoRedirectPath();
1114                    if (redirectPath) {
1115                        finishPayload.sso_redirect_path = redirectPath;
1116                    }
1117                }
1118
1119                return postJson('api_login_finish', finishPayload);
1120            });
1121        }).then(function (finish) {
1122            if (!finish || !finish.ok) {
1123                if (window.PasskeyAuthWebAuthn && typeof window.PasskeyAuthWebAuthn.hideLoader === 'function') {
1124                    window.PasskeyAuthWebAuthn.hideLoader();
1125                }
1126                throw new Error((finish && finish.message) || cfg.errorMessage);
1127            }
1128            setLocalPasskeyHint(identifier);
1129            // SSO token is one-shot — created on the complete page, not in this JSON.
1130            var completeUrl = finish.completeUrl;
1131            if (typeof completeUrl !== 'string' || completeUrl.indexOf('complete_login') < 0) {
1132                var endpoint = cfg.endpoint || 'index.php?m=passkeyauth';
1133                completeUrl = endpoint + (endpoint.indexOf('?') >= 0 ? '&' : '?') + 'action=complete_login';
1134            }
1135            window.location.replace(completeUrl);
1136        }).catch(function (error) {
1137            if (window.PasskeyAuthWebAuthn && typeof window.PasskeyAuthWebAuthn.hideLoader === 'function') {
1138                window.PasskeyAuthWebAuthn.hideLoader();
1139            }
1140            button.disabled = false;
1141            // user hit cancel on the WebAuthn prompt
1142            if (error && (error.name === 'NotAllowedError' || error.
1142name === 'AbortError')) {
1143                return;
1144            }
1145            showLoginError((error && error.message) ? error.message : '');
1146        });
1147    }
1148
1149    var KEY_ICON =
1150        '<svg class="passkeyauth-mode-icon passkeyauth-btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
1151            '<circle cx="7.5" cy="15.5" r="4.5"></circle>' +
1152            '<path d="M10.7 12.3L19 4"></path>' +
1153            '<path d="M15.5 7.5L18 10"></path>' +
1154            '<path d="M13 10L15 12"></path>' +
1155        '</svg>';
1156
1157    var LOCK_ICON = KEY_ICON;
1158
1159    var MICROSOFT_ICON =
1160        '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" style="flex-shrink:0;vertical-align:middle;display:inline-block;" aria-hidden="true">' +
1161            '<path fill="#f25022" d="M1 1h10v10H1z"/>' +
1162            '<path fill="#7fba00" d="M13 1h10v10H13z"/>' +
1163            '<path fill="#00a4ef" d="M1 13h10v10H1z"/>' +
1164            '<path fill="#ffb900" d="M13 13h10v10H13z"/>' +
1165        '</svg>';
1166
1167    var MOJEID_ICON =
1168        '<svg width="18" height="14" viewBox="0 0 48 37" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0;vertical-align:middle;display:inline-block;" aria-hidden="true">' +
1169            '<path d="M31.2911 1.09375H17.6641V20.4158C19.8387 18.8503 22.1551 17.4921 24.5831 16.3587V8.01075H31.2911V8.02375C33.8842 8.02375 36.3712 9.05388 38.2048 10.8875C40.0384 12.7212 41.0686 15.2081 41.0686 17.8013C41.0686 20.3944 40.0384 22.8813 38.2048 24.715C36.3712 26.5486 33.8842 27.5788 31.2911 27.5788V27.5847H24.5831V21.5478C24.5831 21.3748 24.4261 21.3767 24.3471 21.4347C21.8306 23.186 19.5797 25.291 17.6641 27.6847V34.5097H31.2911C35.7222 34.5097 39.9718 32.7495 43.105 29.6162C46.2383 26.483 47.9986 22.2334 47.9986 17.8022C47.9986 13.3711 46.2383 9.12153 43.105 5.98826C39.9718 2.855 35.7222 1.09475 31.2911 1.09475V1.09375Z" fill="currentColor"/>' +
1170            '<path d="M24.5831 16.3587L17.6641 20.4158V1.09375L24.5831 8.01075V16.3587Z" fill="url(#mojeid-g0)"/>' +
1171            '<path d="M10.9699 11.3828H4.33594V34.4838H10.9699V11.3828Z" fill="url(#mojeid-g1)"/>' +
1172            '<path d="M38.1881 13.2576C38.1833 13.0709 38.1125 12.8919 37.9881 12.7526C37.8759 12.6588 37.7432 12.5927 37.6008 12.5595C37.4584 12.5264 37.3102 12.5271 37.1681 12.5616C30.325 13.2722 23.7581 15.6399 18.036 19.4596C12.3138 23.2794 7.60877 28.4362 4.32812 34.4836H10.9621C17.9991 22.0916 28.3311 16.4936 37.6531 13.8746C38.0691 13.7366 38.1971 13.4836 38.1881 13.2576Z" fill="url(#mojeid-g2)"/>' +
1173            '<path d="M7.64869 0.683594C8.43373 0.683594 9.20113 0.916397 9.85385 1.35256C10.5066 1.78872 11.0153 2.40866 11.3157 3.13395C11.616 3.85925 11.6946 4.65734 11.5414 5.42728C11.3881 6.19721 11.01 6.90442 10.4548 7.45945C9.89967 8.01449 9.19236 8.39242 8.42239 8.54545C7.65241 8.69848 6.85435 8.61974 6.12913 8.31918C5.40391 8.01863 4.7841 7.50975 4.3481 6.85692C3.9121 6.20409 3.67949 5.43663 3.67969 4.65159C3.68048 3.59929 4.09893 2.59032 4.84312 1.84632C5.58731 1.10232 6.59638 0.684123 7.64869 0.683594Z" fill="#FFCB00"/>' +
1174            '<defs>' +
1175                '<linearGradient id="mojeid-g0" x1="22.88" y1="17.98" x2="15.48" y2="2.11" gradientUnits="userSpaceOnUse"><stop offset="0.006" stop-color="#222"/><stop offset="0.393" stop-color="#888"/></linearGradient>' +
1176                '<linearGradient id="mojeid-g1" x1="14.71" y1="31.34" x2="0.59" y2="14.52" gradientUnits="userSpaceOnUse"><stop stop-color="#DF8200"/><stop offset="0.392" stop-color="#F0A800"/><stop offset="0.713" stop-color="#FFCB00"/><stop offset="1" stop-color="#FFCB00"/></linearGradient>' +
1177                '<linearGradient id="mojeid-g2" x1="4.33" y1="23.52" x2="38.19" y2="23.52" gradientUnits="userSpaceOnUse"><stop stop-color="#FCC200"/><stop offset="1" stop-color="#FFCB00"/></linearGradient>' +
1178            '</defs>' +
1179        '</svg>';
1180
1181    function emailFieldGroup(form) {
1182        var emailInput = findEmailInput(form);
1183        if (!emailInput) {
1184            return null;
1185        }
1186
1187        return emailInput.closest('.passkeyauth-checkout-email-group, .form-group') || emailInput.parentElement;
1188    }
1189
1190    function insertAfterEmailGroup(form, element) {
1191        var emailGroup = emailFieldGroup(form);
1192        if (emailGroup && emailGroup.parentNode) {
1193            if (emailGroup.nextSibling) {
1194                emailGroup.parentNode.insertBefore(element, emailGroup.nextSibling);
1195            } else {
1196                emailGroup.parentNode.appendChild(element);
1197            }
1198            return;
1199        }
1200
1201        form.appendChild(element);
1202    }
1203
1204    function normalizeEmail(value) {
1205        return String(value || '').trim().toLowerCase();
1206    }
1207
1208    function getLocalPasskeyHint(email) {
1209        var normalized = normalizeEmail(email);
1210        if (!normalized) {
1211            return false;
1212        }
1213
1214        try {
1215            var map = JSON.parse(window.localStorage.getItem(PASSKEY_DEVICE_STORAGE_KEY) || '{}');
1216            return !!map[normalized];
1217        } catch (e) {
1218            return false;
1219        }
1220    }
1221
1222    function setLocalPasskeyHint(email) {
1223        var normalized = normalizeEmail(email);
1224        if (!normalized) {
1225            return;
1226        }
1227
1228        try {
1229            var map = JSON.parse(window.localStorage.getItem(PASSKEY_DEVICE_STORAGE_KEY) || '{}');
1230            map[normalized] = true;
1231            window.localStorage.setItem(PASSKEY_DEVICE_STORAGE_KEY, JSON.stringify(ma
1231p));
1232        } catch (e) {
1233            // Ignore storage failures.
1234        }
1235    }
1236
1237    function updateAuthMethodVisibility(form, visible) {
1238        var wrap = form.querySelector('.passkeyauth-auth-method-wrap');
1239        if (!wrap) {
1240            return;
1241        }
1242
1243        wrap.classList.add('passkeyauth-auth-method-wrap');
1244        wrap.classList.remove('passkeyauth-auth-method-hidden');
1245        form.dataset.passkeyauthPasskeyAvailable = '1';
1246    }
1247
1248    function hasDevicePasskeyForAccount(email) {
1249        return getLocalPasskeyHint(email);
1250    }
1251
1252    function getUserModeChoice(form, email) {
1253        var choiceEmail = form.dataset.passkeyauthUserModeEmail || '';
1254        if (choiceEmail !== normalizeEmail(email)) {
1255            return null;
1256        }
1257        var mode = form.dataset.passkeyauthUserMode || '';
1258        return mode === 'passkey' || mode === 'password' ? mode : null;
1259    }
1260
1261    function setUserModeChoice(form, mode, email) {
1262        form.dataset.passkeyauthUserMode = mode;
1263        form.dataset.passkeyauthUserModeEmail = normalizeEmail(email);
1264    }
1265
1266    function clearUserModeChoice(form) {
1267        delete form.dataset.passkeyauthUserMode;
1268        delete form.dataset.passkeyauthUserModeEmail;
1269    }
1270
1271    function applyPasskeyAvailability(form, hasPasskeys, email) {
1272        if (isPasskeyOnlyMode()) {
1273            updateAuthMethodVisibility(form, true);
1274            setMode(form, 'passkey');
1275            form.dataset.passkeyauthCheckedEmail = normalizeEmail(email);
1276            return;
1277        }
1278
1279        updateAuthMethodVisibility(form, hasPasskeys || allowsUsernameless());
1280
1281        var normalizedEmail = normalizeEmail(email);
1282        var previousEmail = form.dataset.passkeyauthCheckedEmail || '';
1283        if (normalizedEmail !== previousEmail) {
1284            clearUserModeChoice(form);
1285        }
1286
1287        var userChoice = getUserModeChoice(form, email);
1288        if (userChoice) {
1289            setMode(form, userChoice);
1290        } else {
1291            var preferPasskey = hasPasskeys && (
1292                cfg.authMode === 'passkey_only'
1293                || cfg.authMode === 'passkey_preferred'
1294                || hasDevicePasskeyForAccount(email)
1295            );
1296            setMode(form, preferPasskey ? 'passkey' : 'password');
1297        }
1298
1299        form.dataset.passkeyauthCheckedEmail = normalizedEmail;
1300    }
1301
1302    function checkPasskeyAvailability(form, email) {
1303        email = normalizeEmail(email);
1304        if (!email || email.indexOf('@') < 1) {
1305            applyPasskeyAvailability(form, false, '');
1306            return Promise.resolve(false);
1307        }
1308
1309        if (form.dataset.passkeyauthChecking === email) {
1310            return Promise.resolve(form.dataset.passkeyauthPasskeyAvailable === '1');
1311        }
1312
1313        form.dataset.passkeyauthChecking = email;
1314
1315        return postJson('api_login_passkey_available', {
1316            csrfToken: cfg.csrfToken,
1317            identifier: email
1318        }).then(function (res) {
1319            var hasPasskeys = !!(res && res.ok && res.hasPasskeys);
1320            applyPasskeyAvailability(form, hasPasskeys, email);
1321            return hasPasskeys;
1322        }).catch(function () {
1323            applyPasskeyAvailability(form, false, email);
1324            return false;
1325        }).then(function (result) {
1326            delete form.dataset.passkeyauthChecking;
1327            return result;
1328        });
1329    }
1330
1331    function schedulePasskeyAvailabilityCheck(form) {
1332        var emailInput = findEmailInput(form);
1333        if (!emailInput) {
1334            return;
1335        }
1336
1337        var run = function () {
1338            checkPasskeyAvailability(form, emailInput.value || '');
1339        };
1340
1341        if (availabilityTimers && availabilityTimers.has(form)) {
1342            clearTimeout(availabilityTimers.get(form));
1343        }
1344
1345        var timer = setTimeout(run, 350);
1346        if (availabilityTimers) {
1347            availabilityTimers.set(form, timer);
1348        }
1349    }
1350
1351    function buildAuthMethodWrap(form) {
1352        if (form.querySelector('.passkeyauth-auth-method-wrap')) {
1353            return form.querySelector('.passkeyauth-auth-method-wrap');
1354        }
1355
1356        var wrap = document.createElement('div');
1357        wrap.className = 'passkeyauth-auth-method-wrap';
1358
1359        var choice = document.createElement('div');
1360        choice.className = 'passkeyauth-login-choice';
1361        choice.setAttribute('role', 'group');
1362        choice.setAttribute('aria-label', cfg.authMethodLabel || 'Authentication method');
1363
1364        var passwordBtn = document.createElement('button');
1365        passwordBtn.type = 'button';
1366        passwordBtn.className = 'btn btn-block passkeyauth-mode-btn passkeyauth-mode-password';
1367        passwordBtn.innerHTML = LOCK_ICON + '<span>' + (cfg.passwordModeLabel || 'Login with Password') + '</span>';
1368        passwordBtn.addEventListener('click', function () {
1369            if (isPasskeyOnlyMode()) {
1370                return;
1371            }
1372            var emailInput = findEmailInput(form);
1373            var email = emailInput && emailInput.value ? emailInput.value.trim() : '';
1374            setUserModeChoice(form, 'password', email);
1375            setMode(form, 'password');
1376        });
1377
1378        var passkeyBtn = document.createElement('button');
1379        passkeyBtn.type = 'button';
1380        passkeyBtn.className = 'btn btn-block passkeyauth-mode-btn passkeyauth-mode-passkey';
1381        passkeyBtn.innerHTML = KEY_ICON + '<span>' + (cfg.passkeyModeLabel || 'Sign in with Passkey') + '</span>';
1382        passkeyBtn.addEventListener('click', function () {
1383            var emailInput = findEmailInput(form);
1384            var email = emailInput && emailInput.value ? emailInput.value.trim() : '';
1385            if (!email && !allowsUsernameless()) {
1386                if (emailInput) {
1387                    emailInput.focus();
1388                }
1389                return;
1390            }
1391
1392            setUserModeChoice(form, 'passkey', email);
1393            setMode(form, 'passkey');
1394            runPasskeyLogin(form, passkeyBtn);
1395        });
1396
1397        if (!isPasskeyOnlyMode()) {
1398            choice.appendChild(passwordBtn);
1399        }
1400        if (passkeyUiEnabled) {
1401            choice.appendChild(passkeyBtn);
1402        }
1403        if (choice.childNodes.length) {
1404            wrap.appendChild(choice);
1405        }
1406
1407        var hasMs = !!(cfg.microsoftSsoEnabled && cfg.microsoftAuthUrl);
1408        var hasMoje = !!(cfg.mojeidSsoEnabled && cfg.mojeidAuthUrl);
1409
1410        if (hasMs || hasMoje) {
1411            var isBothSso = hasMs && hasMoje;
1412            var ssoRow = document.createElement('div');
1413            ssoRow.className = 'passkeyauth-sso-row' + (isBothSso ? ' passkeyauth-sso-dual' : ' passkeyauth-sso-single');
1414            ssoRow.style.marginTop = '10px';
1415            ssoRow.style.width = '100%';
1416            ssoRow.style.boxSizing = 'border-box';
1417            if (isBothSso) {
1418                ssoRow.style.display = 'flex';
1419                ssoRow.style.gap = '8px';
1420                ssoRow.style.alignItems = 'stretch';
1421            }
1422
1423            if (hasMs) {
1424                var msWrap = document.createElement('div');
1425                msWrap.className = 'passkeyauth-ms-login-wrap';
1426                msWrap.style.boxSizing = 'border-box';
1427                if (isBothSso) {
1428                    msWrap.style.flex = '1 1 0';
1429                    msWrap.style.width = '50%';
1430                    msWrap.style.minWidth = '0';
1431                    msWrap.style.margin = '0';
1432                } else {
1433                    msWrap.style.width = '100%';
1434                    msWrap.style.margin = '0';
1435                }
1436
1437                var msBtn = document.createElement('a');
1438
1439                function getMicrosoftAuthTargetUrl() {
1440                    var baseAuthUrl = cfg.microsoftAuthUrl;
1441                    if (baseAuthUrl.indexOf('redirect=') !== -1) {
1442                        return baseAuthUrl;
1443                    }
1444                    var target = '';
1445                    if (form.classList.contains('passkeyauth-checkout-form') || isCheckoutLoginForm(form) || window.location.href.indexOf('cart.php') !== -1 || window.location.search.indexOf('a=checkout') !== -1) {
1446                        target = getCheckoutSsoRedirectPath() || 'cart.php?a=checkout';
1447                    } else {
1448                        var search = window.location.search || '';
1449                        var match = search.match(/[?&](?:redirect|returnurl|rp)=([^&]+)/i);
1450                        if (match && match[1]) {
1451                            target = decodeURIComponent(match[1]);
1452                        }
1453                    }
1454                    if (target) {
1455                        return baseAuthUrl + (baseAuthUrl.indexOf('?') >= 0 ? '&' : '?') + 'redirect=' + encodeURIComponent(target);
1456                    }
1457                    return baseAuthUrl;
1458                }
1459
1460                msBtn.href = getMicrosoftAuthTargetUrl();
1461                msBtn.addEventListener('click', function () {
1462                    msBtn.href = getMicrosoftAuthTargetUrl();
1463                });
1464                msBtn.className = 'btn btn-block passkeyauth-ms-btn';
1465                msBtn.title = cfg.microsoftButtonLabel || 'Sign in with Microsoft';
1466                msBtn.style.display = 'flex';
1467                msBtn.style.alignItems = 'center';
1468                msBtn.style.justifyContent = 'center';
1469                msBtn.style.gap = '8px';
1470                msBtn.style.background = '#2f2f2f';
1471                msBtn.style.color = '#ffffff';
1472                msBtn.style.fontWeight = '600';
1473                msBtn.style.fontSize = '13px';
1474                msBtn.style.padding = '8px 14px';
1475                msBtn.style.borderRadius = '6px';
1476                msBtn.style.textDecoration = 'none';
1477                msBtn.style.border = '1px solid #1f1f1f';
1478                msBtn.style.boxSizing = 'border-box';
1479                msBtn.style.width = '100%';
1480                msBtn.style.height = '100%';
1481                msBtn.style.margin = '0';
1482                var msLabel = isBothSso ? 'Microsoft' : (cfg.microsoftButtonLabel || 'Sign in with Microsoft');
1483                msBtn.innerHTML = MICROSOFT_ICON + '<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + msLabel + '</span>';
1484                msWrap.appendChild(msBtn);
1485                ssoRow.appendChild(msWrap);
1486            }
1487
1488            if (hasMoje) {
1489                var mojeWrap = document.createElement('div');
1490                mojeWrap.className = 'passkeyauth-mojeid-login-wrap';
1491                mojeWrap.style.boxSizing = 'border-box';
1492                if (isBothSso) {
1493                    mojeWrap.style.flex = '1 1 0';
1494                    mojeWrap.style.width = '50%';
1495                    mojeWrap.style.minWidth = '0';
1496                    mojeWrap.style.margin = '0';
1497                } else {
1498                    mojeWrap.style.width = '100%';
1499                    mojeWrap.style.margin = '0';
1500                }
1501
1502                var mojeBtn = document.createElement('a');
1503
1504                function getMojeIdAuthTargetUrl() {
1505                    var baseAuthUrl = cfg.mojeidAuthUrl;
1506                    if (baseAuthUrl.indexOf('redirect=') !== -1) {
1507                        return baseAuthUrl;
1508                    }
1509                    var target = '';
1510                    if (form.classList.contains('passkeyauth-checkout-form') || isCheckoutLoginForm(form) || window.location.href.indexOf('cart.php') !== -1 || window.location.search.indexOf('a=checkout') !== -1) {
1511                        target = getCheckoutSsoRedirectPath() || 'cart.php?a=checkout';
1512                    } else {
1513                        var search = window.location.search || '';
1514                        var match = search.match(/[?&](?:redirect|returnurl|rp)=([^&]+)/i);
1515                        if (match && match[1]) {
1516                            target = decodeURIComponent(match[1]);
1517                        }
1518                    }
1519                    if (target) {
1520                        return baseAuthUrl + (baseAuthUrl.indexOf('?') >= 0 ? '&' : '?') + 'redirect=' + encodeURIComponent(target);
1521                    }
1522                    return baseAuthUrl;
1523                }
1524
1525                mojeBtn.href = getMojeIdAuthTargetUrl();
1526                mojeBtn.addEventListener('click', function () {
1527                    mojeBtn.href = getMojeIdAuthTargetUrl();
1528                });
1529                mojeBtn.className = 'btn btn-block passkeyauth-mojeid-btn passkeyauth-btn-style-' + (cfg.mojeidButtonStyle || 'dark');
1530                mojeBtn.title = cfg.mojeidButtonLabel || 'Sign in with MojeID';
1531                mojeBtn.style.display = 'flex';
1532                mojeBtn.style.alignItems = 'center';
1533                mojeBtn.style.justifyContent = 'center';
1534                mojeBtn.style.gap = '8px';
1535                mojeBtn.style.fontWeight = '600';
1536                mojeBtn.style.fontSize = '13px';
1537                mojeBtn.style.padding = '8px 14px';
1538                mojeBtn.style.borderRadius = '6px';
1539                mojeBtn.style.textDecoration = 'none';
1540                mojeBtn.style.boxSizing = 'border-box';
1541                mojeBtn.style.width = '100%';
1542                mojeBtn.style.height = '100%';
1543                mojeBtn.style.margin = '0';
1544                if (!cfg.mojeidButtonStyle || cfg.mojeidButtonStyle === 'dark') {
1545                    mojeBtn.style.background = '#c8102e';
1546                    mojeBtn.style.color = '#ffffff';
1547                    mojeBtn.style.border = '1px solid #a00d25';
1548                }
1549
1550                var mojeLabel = isBothSso ? 'MojeID' : (cfg.mojeidButtonLabel || 'Sign in with MojeID');
1551                mojeBtn.innerHTML = MOJEID_ICON + '<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + mojeLabel + '</span>';
1552                mojeWrap.appendChild(mojeBtn);
1553                ssoRow.appendChild(mojeWrap);
1554            }
1555
1556            wrap.appendChild(ssoRow);
1557        }
1558
1559        if (isPasskeyOnlyMode()) {
1560            wrap.classList.add('passkeyauth-passkey-only');
1561            wrap.classList.remove('passkeyauth-auth-method-hidden');
1562        } else if (allowsUsernameless()) {
1563            wrap.classList.remove('passkeyauth-auth-method-hidden');
1564        }
1565        insertAfterEmailGroup(form, wrap);
1566        refreshModeButtonStyles(form);
1567
1568        return wrap;
1569    }
1570
1571    function bindEmailRemember(form) {
1572        var emailInput = findEmailInput(form);
1573        if (!emailInput || emailInput.dataset.passkeyauthEmailBound === '1') {
1574            return;
1575        }
1576        emailInput.dataset.passkeyauthEmailBound = '1';
1577
1578        emailInput.addEventListener('change', function () {
1579            rememberEmail((emailInput.value || '').trim());
1580        });
1581        emailInput.addEventListener('blur', function () {
1582            rememberEmail((emailInput.value || '').trim());
1583        });
1584    }
1585
1586    function setupForm(form) {
1587        if (!form || form.dataset.passkeyInjected === '1') {
1588            return;
1589        }
1590        if (isRegistrationForm(form) && !hasCheckoutExistingLogin(form) && !isCheckoutLoginForm(form)) {
1591            return;
1592        }
1593        form.dataset.passkeyInjected = '1';
1594
1595        var checkoutForm = isCheckoutLoginForm(form);
1596        if (checkoutForm) {
1597            document.body.classList.add('passkeyauth-checkout-login-active');
1598            normalizeCheckoutLayout(form);
1599        }
1600
1601        var group = passwordGroup(form);
1602        if (group) {
1603            group.classList.add('passkeyauth-password-group');
1604        }
1605
1606        if (checkoutForm) {
1607            wrapNativeSubmit(form);
1608        } else {
1609            var submit = findNativeSubmit(form);
1610            if (submit) {
1611                submit.classList.add('passkeyauth-native-submit');
1612            }
1613        }
1614
1615        restoreRememberedEmail(form);
1616        bindEmailRemember(form);
1617
1618        var formToken = getFormCsrfToken(form);
1619        if (formToken && (!cfg.csrfToken || cfg.csrfToken !== formToken)) {
1620            cfg.csrfToken = formToken;
1621        }
1622
1623        buildAuthMethodWrap(form);
1624        if (passkeyUiEnabled && isPasskeyOnlyMode()) {
1625            form.classList.add('passkeyauth-passkey-only');
1626            setMode(form, 'passkey');
1627            if (form.dataset.passkeyauthPasskeyOnlySubmit !== '1') {
1628                form.dataset.passkeyauthPasskeyOnlySubmit = '1';
1629                form.addEventListener('submit', function (e) {
1630                    e.preventDefault();
1631                    e.stopPropagation();
1632                    runPasskeyLogin(form, form.querySelector('.passkeyauth-mode-passkey'));
1633                }, true);
1634            }
1635        } else {
1636            setMode(form, 'password');
1637        }
1638
1639        if (checkoutForm) {
1640            reorderCheckoutFields(form);
1641        }
1642    }
1643
1644    function injectButtons() {
1645        if (document.body) {
1646            document.body.classList.remove('passkeyauth-admin-login-active');
1647            document.body.classList.add('passkeyauth-client-login-active');
1648        }
1649
1650        var path = (window.location.pathname || '').toLowerCase();
1651        var search = (window.location.search || '').toLowerCase();
1652        var isCheckoutPage = path.indexOf('cart.php') !== -1
1653            || path.indexOf('/cart/') !== -1
1654            || path.slice(-5) === '/cart'
1655            || search.indexOf('a=checkout') !== -1
1656            || search.indexOf('rp=/cart') !== -1
1657            || search.indexOf('rp=%2fcart') !== -1
1658            || !!document.getElementById('frmCheckout')
1659            || !!document.getElementById('containerExistingUserSignin');
1660        if (!isCheckoutPage && (path.indexOf('register') !== -1 || search.indexOf('register') !== -1 || search.indexOf('rp=/register') !== -1)) {
1661            return;
1662        }
1663
1664        var forms = document.querySelectorAll('form');
1665        var firstLoginForm = null;
1666        for (var i = 0; i < forms.length; i++) {
1667            var form = forms[i];
1668            if (isRegistrationForm(form) && !hasCheckoutExistingLogin(form) && !isCheckoutLoginForm(form)) {
1669                continue;
1670            }
1671            var hasPassword = form.querySelector('input[type="password"], input[name="loginpw"]');
1672            if (!hasPassword) {
1673                continue;
1674            }
1675            if (!findEmailInput(form)) {
1676                continue;
1677            }
1678            if (!firstLoginForm) {
1679                firstLoginForm = form;
1680            }
1681            setupForm(form);
1682        }
1683
1684        var existingBox = document.getElementById('containerExistingUserSignin');
1685        if (existingBox) {
1686            var hostForm = existingBox.closest('form');
1687            if (hostForm) {
1688                if (!firstLoginForm) {
1689                    firstLoginForm = hostForm;
1690                }
1691                setupForm(hostForm);
1692            }
1693        }
1694
1695        showLoginFlash(firstLoginForm);
1696    }
1697
1698    if (document.readyState === 'loading') {
1699        document.addEventListener('DOMContentLoaded', injectButtons);
1700    } else {
1701        injectButtons();
1702    }
1703
1704    if (window.MutationObserver) {
1705        var observer = new MutationObserver(function () {
1706            injectButtons();
1707        });
1708        observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
1709    }
1710
1711    window.PasskeyAuthDeviceHint = {
1712        mark: setLocalPasskeyHint
1713    };
1714})();

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.