PageSourceSearch

https://yomera.ai/wp-content/plugins/automation-web-platform/assets/js/wawp-otp-frontend.js?ver=5.6.13

js yomera.ai collected 2026-10-05 13:23:25 UTC 64,645 bytes, 1,370 lines download raw bytes

1jQuery(document).ready(function ($) {
2    window.wawpOtpAjax = window.wawpOtpAjax || {};
3    const wawpOtpAjax = window.wawpOtpAjax;
4
5    /* ------------------------------------------------------------
6     * Per-instance branding helpers (scoped to .wawp-otp-login-form)
7     * ---------------------------------------------------------- */
8    function hexToRgbA(hex, alpha = 1) {
9        let c;
10        if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
11            c = hex.substring(1).split('');
12            if (c.length === 3) {
13                c = [c[0], c[0], c[1], c[1], c[2], c[2]];
14            }
15            c = '0x' + c.join('');
16            return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + alpha + ')';
17        }
18        return hex;
19    }
20
21    function updateBranding($wrapper, type, identifier, isMagicLink = false) {
22        const $branding = $wrapper.find('.wawp-login-branding');
23        if (!$branding.length) return;
24
25        const l10n = window.wawpOtpL10n || {};
26
27        if (!type) {
28            // Restore original stored on the wrapper
29            const orig = $wrapper.data('wawp-branding-orig');
30            if (orig) {
31                $branding.find('.wawp-logo-container').html(orig.logo);
32                $branding.find('.wawp-login-title').text(orig.title);
33                $branding.find('.wawp-login-description').html(orig.desc);
34            }
35            return;
36        }
37
38        let iconClass = '', titleText = '', descHtml = '', themeClass = '', customColor = '';
39
40        if (type === 'whatsapp') {
41            iconClass = 'ri-whatsapp-line'; titleText = l10n.checkWhatsApp || 'Check your WhatsApp';
42            descHtml = (l10n.sentTo || 'We\u2019ve sent you a 6-digit code to') + ' <br><b>' + identifier + '</b>';
43            themeClass = 'wawp-otp-theme-whatsapp';
44            customColor = (window.wawpOtpAjax && window.wawpOtpAjax.button_colors) ? window.wawpOtpAjax.button_colors.whatsapp : '#25d366';
45        } else if (type === 'firebase') {
46            iconClass = 'ri-message-3-line'; titleText = l10n.checkSMS || 'Check your SMS';
47            descHtml = (l10n.sentTo || 'We\u2019ve sent you a 6-digit code to') + ' <br><b>' + identifier + '</b>';
48            themeClass = 'wawp-otp-theme-sms';
49            customColor = (window.wawpOtpAjax && window.wawpOtpAjax.button_colors) ? window.wawpOtpAjax.button_colors.firebase : '#3b82f6';
50        } else if (type === 'email') {
51            iconClass = 'ri-mail-line'; titleText = l10n.checkEmail || 'Check your Email';
52            descHtml = isMagicLink
53                ? ((l10n.magicLinkSent || 'We\u2019ve sent you a secure login link to') + ' <br><b>' + identifier + '</b>')
54                : ((l10n.sentTo || 'We\u2019ve sent you a 6-digit code to') + ' <br><b>' + identifier + '</b>');
55            themeClass = 'wawp-otp-theme-email';
56            customColor = (window.wawpOtpAjax && window.wawpOtpAjax.button_colors) ? window.wawpOtpAjax.button_colors.email_otp : '#ef4444';
57        }
58
59        $branding.find('.wawp-logo-container').html('<div class="wawp-icon-wrapper ' + themeClass + '"><i class="wawp-otp-icon ' + iconClass + '"></i></div>');
60        if (customColor) {
61            setTimeout(function () {
62                const $ic = $branding.find('.wawp-otp-icon');
63                if ($ic.length) {
64                    $ic.css({ 'background-color': customColor, 'color': '#ffffff',
65                        'box-shadow': '0 0 0 10px ' + hexToRgbA(customColor, 0.15),
66                        'animation': 'wawp-pulse-scale 2s infinite ease-in-out' });
67                }
68            }, 50);
69        }
70        $branding.find('.wawp-login-title').text(titleText);
71        $branding.find('.wawp-login-description').html(descHtml);
72    }
73
74    // Store original branding per wrapper on DOM ready
75    $('.wawp-otp-login-form').each(function () {
76        const $w = $(this);
77        const $b = $w.find('.wawp-login-branding');
78        $w.data('wawp-branding-orig', {
79            logo: $b.find('.wawp-logo-container').html(),
80            title: $b.find('.wawp-login-title').text(),
81            desc: $b.find('.wawp-login-description').html()
82        });
83    });
84
85    /* ------------------------------------------------------------
86     * Email Autocomplete
87     * ---------------------------------------------------------- */
88    const emailDomains = wawpOtpAjax.email_domains || ['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com'];
89
90    function setupEmailAutocomplete($input) {
91        const $wrapper = $('<div class="wawp-email-autocomplete-wrapper"></div>');
92        $input.wrap($wrapper);
93        const $list = $('<ul class="wawp-email-suggestions"></ul>');
94        $input.after($list);
95
96        $input.on('input', function () {
97            const val = $(this).val();
98            $list.empty().hide();
99            if (!val || !val.includes('@')) return;
100
101            const [user, domainPart] = val.split('@');
102            if (!user) return;
103
104            const matches = emailDomains.filter(d => d.startsWith(domainPart));
105            if (matches.length > 0) {
106                matches.forEach(m => {
107                    const $li = $('<li>' + user + '@' + m + '</li>');
108                    $li.on('click', function () {
109                        $input.val($(this).text()).trigger('input');
110                        $list.hide();
111                    });
112                    $list.append($li);
113                });
114                $list.show();
115            }
116        });
117
118        $(document).on('click', function (e) {
119            if (!$(e.target).closest('.wawp-email-autocomplete-wrapper').length) {
120                $list.hide();
121            }
122        });
123    }
124
125    $('.js-wawp-email-input').each(function () { setupEmailAutocomplete($(this)); });
126
127    /* ------------------------------------------------------------
128     * Cooldown Guard (Persistent & Incremental)
129     * ---------------------------------------------------------- */
130    const STORAGE_KEY = 'wawp_otp_cooldown';
131    let cooldownState = { phone: { attempts: 0, expires: 0 }, email: { attempts: 0, expires: 0 } };
132    try {
133        const stored = localStorage.getItem(STORAGE_KEY);
134        if (stored) cooldownState = JSON.parse(stored);
135    } catch (e) {
136        console.error('AWP OTP: Corrupt storage reset', e);
137        localStorage.removeItem(STORAGE_KEY);
138    }
139
140    function saveCooldownState() {
141        localStorage.setItem(STORAGE_KEY, JSON.stringify(cooldownState));
142    }
143
144    function runTimer(method, $btn, expiryTime) {
145        const $container = $btn.closest('.wawp-action-buttons');
146        let $timer = $container.find('.wawp-resend-timer');
147
148        if (!$timer.length) {
149            $timer = $('<div class="wawp-resend-timer" aria-live="polite" style="margin-top:10px;font-size:13px;opacity:0.9;text-align:center;width:100%;"></div>');
150            $container.append($timer);
151        }
152
153        function label(sec) {
154            return (window.wawpOtpL10n && wawpOtpL10n.resendIn) ? wawpOtpL10n.resendIn.replace('%s', sec) : 'Resend in ' + sec + 's';
155        }
156
157        $btn.prop('disabled', true);
158
159        function update() {
160            const now = Date.now();
161            const remaining = Math.ceil((expiryTime - now) / 1000);
162
163            if (remaining <= 0) {
164                clearInterval($timer.data('interval'));
165                $btn.prop('disabled', false);
166                $timer.remove();
167                cooldownState[method].expires = 0;
168                saveCooldownState();
169            } else {
170                $timer.text(label(remaining));
171            }
172        }
173
174        update(); // Run immediately
175
176        // Clear any existing interval on this element
177        if ($timer.data('interval')) clearInterval($timer.data('interval'));
178
179        const interval = setInterval(update, 1000);
180        $timer.data('interval', interval);
181    }
182
183    function checkCooldown(method, $btn) {
184        const state = cooldownState[method];
185        if (!state || !state.expires) return;
186
187        // If no specific button passed, try to find the standard ones (for page load)
188        if (!$btn || !$btn.length) {
189            if (method === 'phone') {
190                $btn = $('.wawp-request-btn[data-method="whatsapp"], .wawp-request-btn[data-method="firebase"], .wawp-resend-otp-phone');
191            } else if (method === 'email') {
192                $btn = $('.js-wawp-request-otp-email, .js-wawp-resend-otp-email');
193            }
194        }
195
196        const now = Date.now();
197        if (state.expires > now) {
198            $btn.each(function () {
199                runTimer(method, $(this), state.expires);
200            });
201        } else {
202            // Reset attempts if expired long ago (e.g. 5 mins)
203            if (now > state.expires + 300000) {
204                state.attempts = 0;
205                saveCooldownState();
206            }
207        }
208    }
209
210    function startCooldown(method, $btn) {
211        const state = cooldownState[method];
212        state.attempts = (state.attempts || 0) + 1;
213
214        // Incremental: 30s, 60s, 120s, ... max 5 mins
215        const initial = parseInt(wawpOtpAjax.initial_cooldown) || 30;
216        const max = parseInt(wawpOtpAjax.max_cooldown) || 300;
217        const seconds = Math.min(initial * Math.pow(2, state.attempts - 1), max);
218
219        state.expires = Date.now() + (seconds * 1000);
220        saveCooldownState();
221
222        runTimer(method, $btn, state.expires);
223    }
224
225
226    /* ------------------------------------------------------------
227     * Helpers
228     * ---------------------------------------------------------- */
229    function withSpinner($btn, fn) {
230        const originalHtml = $btn.html();
231        $btn.data('original-html', originalHtml).prop('disabled', true).addClass('is-loading').html('<i class="ri-loader-5-line"></i>');
232
233        // Start progress timeline in the message box area
234        const $form = $btn.closest('form');
235        const $msgBox = $form.find('.wawp-login-message-phone, .js-wawp-msg-box-email');
236
237        return fn(() => {
238            $btn.prop('disabled', false).removeClass('is-loading').html($btn.data('original-html'));
239        });
240    }
241
242    function setMessage($el, text, ok = true) {
243        if (!$el || !$el.length) return;
244        $el.html('<p class="' + (ok ? 'success' : 'error') + '">
244' + text + '</p>');
245    }
246
247    async function wawpRest(op, data = {}) {
248        if (!wawpOtpAjax.rest_url) {
249            return $.post(wawpOtpAjax.ajax_url, {
250                action: 'wawp_request_otp',
251                nonce: wawpOtpAjax.nonce,
252                ...data,
253                op: op
254            }).promise();
255        }
256
257        const headers = { 'Content-Type': 'application/json' };
258        if (wawpOtpAjax.nonce) {
259            headers['X-WP-Nonce'] = wawpOtpAjax.nonce;
260        }
261
262        try {
263            let response = await fetch(wawpOtpAjax.rest_url + '/' + op, {
264                method: 'POST',
265                headers: headers,
266                body: JSON.stringify(data)
267            });
268
269            let result = await response.json().catch(() => null);
270
271            // 🛡️ Auto-Retry on Stale Cache / Nonce Cookie Check Failure
272            if (!response.ok && result && (result.code === 'rest_cookie_invalid_nonce' || result.message === 'Cookie check failed' || response.status === 403)) {
273                const retryResponse = await fetch(wawpOtpAjax.rest_url + '/' + op, {
274                    method: 'POST',
275                    headers: { 'Content-Type': 'application/json' },
276                    body: JSON.stringify(data)
277                });
278                const retryResult = await retryResponse.json().catch(() => null);
279                if (retryResponse.ok && retryResult) {
280                    return retryResult;
281                }
282
283                if (wawpOtpAjax.ajax_url) {
284                    return $.post(wawpOtpAjax.ajax_url, {
285                        action: 'wawp_request_otp',
286                        nonce: wawpOtpAjax.nonce,
287                        ...data,
288                        op: op
289                    }).promise();
290                }
291            }
292
293            if (!response.ok || !result) {
294                const err = new Error(result?.message || 'REST Request Failed');
295                err.data = result?.data || result;
296                throw err;
297            }
298            return result;
299        } catch (fetchErr) {
300            if (wawpOtpAjax.ajax_url && (fetchErr.message?.includes('Cookie check failed') || fetchErr.message?.includes('REST Request Failed'))) {
301                return $.post(wawpOtpAjax.ajax_url, {
302                    action: 'wawp_request_otp',
303                    nonce: wawpOtpAjax.nonce,
304                    ...data,
305                    op: op
306                }).promise();
307            }
308            throw fetchErr;
309        }
310    }
311
312    /* ------------------------------------------------------------
313     * Firebase SDK Integration
314     * ---------------------------------------------------------- */
315    let firebaseLoading = false;
316    let confirmationResult = null;
317
318    async function resetFirebaseVerifier(force = false) {
319        if (window.recaptchaVerifier) {
320            try {
321                window.recaptchaVerifier.clear();
322            } catch (e) {
323                console.warn('Error clearing verifier:', e);
324            }
325            window.recaptchaVerifier = null;
326        }
327
328        const container = document.getElementById('wawp-firebase-recaptcha-phone');
329        if (container) {
330            const parent = container.parentNode;
331            if (parent) {
332                const newContainer = document.createElement('div');
333                newContainer.id = 'wawp-firebase-recaptcha-phone';
334                newContainer.className = container.className;
335                parent.replaceChild(newContainer, container);
336
337                $(newContainer).removeClass('wawp-hide').css({
338                    'position': 'absolute',
339                    'width': '1px',
340                    'height': '1px',
341                    'padding': '0',
342                    'margin': '-1px',
343                    'overflow': 'hidden',
344                    'clip': 'rect(0, 0, 0, 0)',
345                    'border': '0',
346                    'opacity': '0',
347                    'pointer-events': 'none'
348                });
349            } else {
350                container.innerHTML = '';
351            }
352        }
353
354        await initFirebaseVerifier(force);
355    }
356
357    async function ensureFirebase(force = false) {
358        if (typeof firebase !== 'undefined' && firebase.auth) {
359            if (!window.recaptchaVerifier) {
360                await resetFirebaseVerifier(force);
361            }
362            return true;
363        }
364
365        // Fallback: Dynamic loading if not enqueued (should happen rarely now)
366        if (firebaseLoading) return new Promise((resolve, reject) => {
367            const i = setInterval(() => {
368                if (typeof firebase !== 'undefined' && firebase.auth) {
369                    clearInterval(i);
370                    resolve(true);
371                }
372            }, 100);
373            setTimeout(() => {
374                clearInterval(i);
375                reject(new Error('Firebase SDK initialization timed out.'));
376            }, 15000);
377        });
378        firebaseLoading = true;
379
380        const scripts = [
381            'https://www.gstatic.com/firebasejs/9.1.1/firebase-app-compat.js',
382            'https://www.gstatic.com/firebasejs/9.1.1/firebase-auth-compat.js'
383        ];
384
385        for (const src of scripts) {
386            await new Promise((resolve, reject) => {
387                const s = document.createElement('script');
388                s.src = src;
389                s.onload = resolve;
390                s.onerror = () => reject(new Error('Failed to load Firebase SDK from Google servers. Please check your network connection.'));
391                document.head.appendChild(s);
392            });
393        }
394
395        await resetFirebaseVerifier(force);
396        return true;
397    }
398
399    async function initFirebaseVerifier(force = false) {
400        // Guard: If the container doesn't exist (e.g. page without login form), skip init
401        const container = document.getElementById('wawp-firebase-recaptcha-phone');
402        if (!container) return;
403
404        // Skip initialization if the parent element is hidden (e.g. inside an inactive tab) unless forced
405        if (!force && $(container).parent().closest(':hidden').length > 0) {
406            return;
407        }
408
409        const config = wawpOtpAjax.firebase_config;
410        if (!config) { console.error('Firebase config missing'); return; }
411
412        // Mobile fallback: map api_key to apiKey if missing
413        if (!config.apiKey && config.api_key) {
414            config.apiKey = config.api_key;
415        }
416
417        if (!config.apiKey && !config.api_key) {
418            console.error('AWP OTP: Firebase API Key missing in localized config object. Please check your Firebase Sender settings and ensure a project is set as Active.');
419            return;
420        }
421
422        // Clean the config object to only include valid Firebase parameters
423        const firebaseConfig = {
424            apiKey: config.apiKey || config.api_key,
425            authDomain: config.authDomain,
426            projectId: config.projectId,
427            storageBucket: config.storageBucket,
428            messagingSenderId: config.messagingSenderId,
429            appId: config.appId,
430            measurementId: config.measurementId
431        };
432
433        // Use a NAMED app to avoid conflicts with other plugins (like Google One Tap)
434        let app = null;
435        try {
436            app = firebase.app('wawp_firebase_app');
437        } catch (e) {
438            // App not initialized yet
439            app = firebase.initializeApp(firebaseConfig, 'wawp_firebase_app');
440        }
441
442        // Always ensure the container styles are properly set up (visible to DOM but screen-reader hidden)
443        if (container) {
444            $(container).removeClass('wawp-hide').css({
445                'position': 'absolute',
446                'width': '1px',
447                'height': '1px',
448                'padding': '0',
449                'margin': '-1px',
450                'overflow': 'hidden',
451                'clip': 'rect(0, 0, 0, 0)',
452                'border': '0',
453                'opacity': '0',
454                'pointer-events': 'none'
455            });
456        }
457
458        if (!window.recaptchaVerifier) {
459            try {
460                // Pass the specific app instance
461                window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('wawp-firebase-recaptcha-phone', {
462                    'size': 'invisible',
463                    'callback': (response) => { /* reCAPTCHA solved */ }
464                }, app);
465
466                // Render it immediately to warm up
467                await window.recaptchaVerifier.render();
468            } catch (e) { console.warn('Recaptcha init error', e); }
469        }
470    }
471
472    // Highly-optimized async pre-loading: warm up and pre-load Firebase scripts and verifier immediately on page load in the background
473    if (wawpOtpAjax.firebase_enabled) {
474        ensureFirebase().catch(e => console.warn('Pre-loading Firebase failed asynchronously', e));
475    }
476
477    /* ------------------------------------------------------------
478     * ReCAPTCHA (V2/V3)
479     * ---------------------------------------------------------- */
480    var recaptchaWidgets = {};
481    let recaptchaInitAttempts = 0;
482    function initRecaptcha(callback) {
483        if (!wawpOtpAjax.recaptcha_enabled || wawpOtpAjax.recaptcha_enabled === '0') {
484            if (typeof callback === 'function') callback();
485            return;
486        }
487
488        if (typeof grecaptcha === 'undefined' || typeof grecaptcha.render !== 'function') {
489            recaptchaInitAttempts++;
490            if (recaptchaInitAttempts > 10) {
491                console.warn('AWP OTP: Google ReCAPTCHA script not found. Proceeding without it.');
492                if (typeof callback === 'function') callback();
493                return;
494            }
495            setTimeout(() => initRecaptcha(callback), 300);
496            return;
497        }
498
499        grecaptcha.ready(function () {
500            $('.wawp-recaptcha-container').each(function () {
501                var id = $(this).attr('id');
502                if (id === 'wawp-recaptcha-signup') return; // Handled by wawp-signup.js
503
504                if (id && recaptchaWidgets[id] === undefined) {
505                    var isCheckbox = wawpOtpAjax.recaptcha_type === 'v2_checkbox';
506                    var params = {
507                        'sitekey': wawpOtpAjax.recaptcha_site_key,
508                        'size': isCheckbox ? 'normal' : 'invisible',
509                        'badge': wawpOtpAjax.is_rtl ? 'bottomleft' : 'bottomright',
510                        'callback': function (token) {
511                            var cb = $('#' + id).data('awp-recaptcha-cb');
512                            if (cb) {
513                                cb(token);
514                                $('#' + id).data('awp-recaptcha-cb', null);
515                            }
516                        }
517                    };
518                    try {
519                        recaptchaWidgets[id] = grecaptcha.render(id, params);
520                    } catch (e) {
521                        console.warn('AWP OTP: ReCAPTCHA render error for', id, e);
522                    }
523                }
524            });
525            if (typeof callback === 'function') callback();
526        });
527    }
528
529    // Try init on ready and load
530    initRecaptcha();
531    $(window).on('load', function () { initRecaptcha(); });
532    $(document).on('wawp_recaptcha_init', function () { initRecaptcha(); });
533
534    function withRecaptcha(action, onSuccess, onError, $contextForm) {
535        if (!wawpOtpAjax.recaptcha_enabled || !wawpOtpAjax.recaptcha_site_key || wawpOtpAjax.recaptcha_enabled === '0') return onSuccess(null);
536
537        let called = false;
538        const onceSuccess = (token) => {
539            if (called) return;
540            called = true;
541            clearTimeout(safetyTimeout);
542            onSuccess(token);
543        };
544
545        const safetyTimeout = setTimeout(() => {
546            console.warn('AWP OTP: reCAPTCHA timed out for ' + action);
547            onceSuccess(null);
548        }, 6000);
549
550        initRecaptcha(function () {
551            // Try the new unified container first
552            var containerId = 'wawp-recaptcha-login';
553            var widgetId = recaptchaWidgets[containerId];
554
555            // Fallback or override by context form
556            if (widgetId === undefined && $contextForm && $contextForm.length) {
557                var $c = $contextForm.find('.wawp-recaptcha-container');
558                if ($c.length) {
559                    var cid = $c.attr('id');
560                    if (cid && recaptchaWidgets[cid] !== undefined) {
561                        widgetId = recaptchaWidgets[cid];
562                        containerId = cid;
563                    }
564                }
565            }
566
567            if (widgetId !== undefined) {
568                var isCheckbox = wawpOtpAjax.recaptcha_type === 'v2_checkbox';
569                var response = grecaptcha.getResponse(widgetId);
570
571                try {
572                    if (isCheckbox) {
573                        if (response) {
574                            onceSuccess(response);
575                        } else {
576                            setMessage($contextForm.find('.wawp-login-message-phone, .js-wawp-msg-box-email'), window.wawpOtpL10n.pleaseSolveRecaptcha || 'Please solve the reCAPTCHA.', false);
577                            // We don't call onceSuccess(null) here yet, let user solve it and click again?
578                            // Actually, withRecaptcha is called on button click.
579                        }
580                    } else {
581                        // Invisible
582                        if (response) {
583                            grecaptcha.reset(widgetId);
584                        }
585                        $('#' + containerId).data('awp-recaptcha-cb', onceSuccess);
586                        grecaptcha.execute(widgetId);
587                    }
588                } catch (e) {
589                    console.error('AWP OTP: reCAPTCHA execute error', e);
590                    onceSuccess(null);
591                }
592            } else {
593                console.warn('AWP OTP: reCAPTCHA widget NOT rendered for', action);
594                onceSuccess(null);
595            }
596        });
597    }
598
599    /* ------------------------------------------------------------
600     * Tabs & UI Navigation (instance-scoped)
601     * ---------------------------------------------------------- */
602    $(document).on('click', '.wawp-tab', function () {
603        const $tab = $(this);
604        const tab = $tab.data('tab');
605        const $wrapper = $tab.closest('.wawp-otp-login-form');
606        $wrapper.find('.wawp-tab, .wawp-tab-pane').removeClass('active');
607        $tab.addClass('active');
608        $wrapper.find('#' + tab).addClass('active');
609
610        if (tab.indexOf('phone_login') === 0 && wawpOtpAjax.firebase_enabled) {
611            ensureFirebase().catch(e => console.warn('Pre-loading Firebase on tab switch failed', e));
612        }
613    });
614
615    /* ------------------------------------------------------------
616     * Phone Login Flow (instance-scoped)
617     * ---------------------------------------------------------- */
618    $(document).on('click', '.wawp-request-btn', function () {
619        const $btn = $(this);
620        const $form = $btn.closest('form');
621        const $wrapper = $btn.closest('.wawp-otp-login-form');
622        const method = $btn.data('method');
623        const phone = $form.find('[name="phone"]').val().trim();
624        const $msgBox = $form.find('.wawp-login-message-phone');
625
626        if (!phone) return setMessage($msgBox, wawpOtpAjax.emptyFields, false);
627
628        // Cooldown check
629        const cooldownMethod = 'phone';
630        const timerState = cooldownState[cooldownMethod];
631        if (timerState && timerState.expires > Date.now()) {
632            // Highlight the timer if active
633            const $container = $btn.closest('.wawp-action-buttons');
634            const $timer = $container.find('.wawp-resend-timer');
635            if ($timer.length) {
636                $timer.removeClass('wawp-timer-alert');
637                void $timer[0].offsetWidth; // trigger reflow
638                $timer.addClass('wawp-timer-alert');
639            }
640            return;
641        }
642
643        withSpinner($btn, async (finish) => {
644            $msgBox.html('');
645
646            if (method === 'firebase') {
647                // Pre-check if user exists in WordPress before calling Firebase
648                wawpRest('request', {
649                    login_method: 'phone_firebase',
650                    phone: phone
651                }).then(async resp => {
652                    if (resp.success) {
653                        await ensureFirebase(true);
654                        const $phoneInput = $form.find('[name="phone"]')[0];
655                        const iti = ($phoneInput && window.intlTelInputGlobals && window.intlTelInputGlobals.getInstance) ? window.intlTelInputGlobals.getInstance($phoneInput) : null;
656                        let fullPhone = '';
657                        if (iti) {
658                            const cd = iti.getSelectedCountryData() || {};
659                            const dial = cd.dialCode ? String(cd.dialCode) : '';
660                            const digits = phone.replace(/\D+/g, '');
661                            const looksInternational = dial && digits.indexOf(dial) === 0;
662                            let fullDigits = looksInternational ? digits : (dial + digits);
663
664                            // Auto-Correction for Benin (+229) 10-digit upgrade (October 2024: adds "01" after dial)
665                            if (dial === '229') {
666                                const numPart = fullDigits.startsWith('229') ? fullDigits.substring(3) : fullDigits;
667                                if (numPart.length === 8) {
668                                    fullDigits = '22901' + numPart;
669                                }
670                            }
671
672                            fullPhone = '+' + fullDigits;
673                        } else {
674                            fullPhone = phone.startsWith('+') ? phone : '+' + phone;
675                        }
676
677                        // Bulletproof Auto-Correction: If number is Benin (+229) and length is 12 (+229 + 8 digits), add "01"
678                        if (fullPhone.startsWith('+229') && fullPhone.length === 12) {
679                            const numberPart = fullPhone.substring(4); // Skip "+229"
680                            fullPhone = '+22901' + numberPart;
681                        }
682
683                        // Use the NAMED app instance
684                        const app = firebase.app('wawp_firebase_app');
685
686                        app.auth().signInWithPhoneNumber(fullPhone, window.recaptchaVerifier)
687                            .then((result) => {
688                                confirmationResult = result;
689                                // Log the request to history
690                                wawpRest('log_firebase', { phone: phone });
691                                finish();
692                                updateBranding($wrapper, 'firebase', phone);
693                                showOtpStep($wrapper, $form, 'phone', phone);
694                                startCooldown('phone', $form.find('.wawp-resend-otp-phone'));
695                            }).catch(async (error) => {
696                                finish();
697                                let userMsg = error.message;
698                                if (error.code === 'auth/network-request-failed' || String(error).includes('network-request-failed')) {
699                                    userMsg = (window.wawpOtpL10n && window.wawpOtpL10n.firebaseNetworkError)
700                                        ? window.wawpOtpL10n.firebaseNetworkError
701                                        : 'A network error occurred. If you are using Safari or Brave, please disable tracking protection (Shields) or try another browser.';
702                                }
703                                setMessage($msgBox, userMsg, false);
704                                await resetFirebaseVerifier();
705                                wawpRest('log_auth_failure', {
706                                    phone: phone,
707                                    method: 'firebase',
708                                    error: error.message || String(error)
709                                }).catch(e => console.warn('Logging failure error', e));
710                            });
711                    } else {
712                        finish();
713                        const errorMsg = (resp.data && resp.data.message) ? resp.data.message : (resp.data || (typeof resp === 'string' ? resp : 'Unknown error'));
714                        if (resp.data && resp.data.redirect_to_signup) {
715                            window.location.href = resp.data.redirect_to_signup;
716                        } else {
717                            setMessage($msgBox, errorMsg, false);
718                        }
719                    }
720                }).catch((err) => {
721                    finish();
722                    if (err.data && err.data.redirect_to_signup) {
723                        window.location.href = err.data.redirect_to_signup;
724                        return;
725                    }
726                    setMessage($msgBox, err.message || 'Server error. Please try again later.', false);
727                    wawpRest('log_auth_failure', {
728                        phone: phone,
729                        method: 'firebase',
730                        error: err.message || String(err)
731                    }).catch(e => console.warn('Logging failure error', e));
732                });
733            } else {
734                // WhatsApp
735                withRecaptcha('phone_' + method, (token) => {
736                    wawpRest('request', {
737                        login_method: 'phone_' + method,
738                        phone: phone,
739                        'g-recaptcha-response': token
740                    }).then(resp => {
741                        finish();
742                        if (resp && resp.success) {
743                            const authType = resp.data.whatsapp_auth_type || 'otp';
744
745                            if (authType === 'list') {
746                                // 1. Hide messy elements
747                                $wrapper.find('.wawp-tabs').hide();
748                                $wrapper.find('.wawp-remember-me').addClass('wawp-hide-force');
749                                $wrapper.find('.wawp-login-branding').hide();
750                                $form.find('.wawp-phone-group, .wawp-action-buttons, .wawp-otp-group-phone, .wawp-verify-row-phone, .wawp-separator, .wawp-switch-form').addClass('wawp-hide-force');
751                                $msgBox.empty();
752
753                                // 2. Inject Premium Waiting UI
754                                const waitingHtml = `
755                                    <div class="wawp-list-waiting-container">
756                                        <div class="wawp-list-waiting-icon">
757                                            <i class="ri-shield-check-line"></i>
758                                        </div>
759                                        <h4 class="wawp-list-waiting-title">${window.wawpOtpL10n.verifyYourIdentity || 'Verify Your Identity'}</h4>
760                                        <p class="wawp-list-waiting-text">${window.wawpOtpL10n.weSentSecureRequest || 'We sent a secure verification request to'} <b>${phone}</b>.<br>${window.wawpOtpL10n.pleaseCheckDevice || 'Please check your device and approve it to continue.'}</p>
761                                        <div style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
762                                            <a href="#" class="js-wawp-back-to-input" style="font-size: 12px; color: #718096; text-decoration: underline;">${window.wawpOtpL10n.editNumberOrMethod || 'Edit number or use another method'}</a>
763                                        </div>
764                                    </div>
765                                `;
766                                $msgBox.after(waitingHtml);
767
768                                // 3. Scroll to form if needed
769                                $('html, body').animate({ scrollTop: $form.offset().top - 100 }, 500);
770
771                                startListPolling(phone, resp.data.otp_transient, $msgBox);
772                            } else {
773                                setMessage($msgBox, resp.data.message || 'Verification code sent successfully.', true);
774                                showOtpStep($wrapper, $form, 'phone', phone);
775                                updateBranding($wrapper, 'whatsapp', phone);
776                                $form.find('.wawp-verify-row-phone').removeClass('wawp-hide').css('display', 'flex').show();
777                                $btn.hide();
778                                $form.find('.wawp-remember-me').addClass('wawp-hide-force');
779                                if (resp.data && resp.data.otp_transient) {
780                                    $form.find('.wawp-resend-otp-phone').data('transient', resp.data.otp_transient);
781                                    $form.find('.wawp-verify-otp-phone').data('transient', resp.data.otp_transient);
782                                }
783                                startCooldown('phone', $form.find('.wawp-resend-otp-phone'));
784                            }
785                        } else {
786                            const errorMsg = (resp && resp.data && resp.data.message) ? resp.data.message : (resp && resp.data ? resp.data : (typeof resp === 'string' ? resp : 'Request failed.'));
787                            if (resp && resp.data && resp.data.redirect_to_signup) {
788                                window.location.href = resp.data.redirect_to_signup;
789                            } else {
790                                setMessage($msgBox, errorMsg, false);
791                            }
792                        }
793                    }).catch((err) => {
794                        finish();
795                        if (err.data && err.data.redirect_to_signup) {
796                            window.location.href = err.data.redirect_to_signup;
797                            return;
798                        }
799                        setMessage($msgBox, 'Server error: ' + (err.message || 'unknown'), false);
800                    });
801                }, (err) => { finish(); setMessage($msgBox, err, false); }, $form);
802            }
803        });
804    });
805
806    // Resend Phone OTP (instance-scoped)
807    $(document).on('click', '.wawp-resend-otp-phone', function () {
808        const $btn = $(this);
809        const $form = $btn.closest('form');
810        const phone = $form.find('[name="phone"]').val().trim();
811        const $msgBox = $form.find('.wawp-login-message-phone');
812        setMessage($msgBox, window.wawpOtpL10n.sendingCode || 'Sending code...', true);
813
814        wawpRest('request', {
815            login_method: 'phone_whatsapp',
816            phone: phone
817        }).then(resp => {
818            if (resp.success) {
819                setMessage($msgBox, window.wawpOtpL10n.codeResent || 'Code resent!');
820                startCooldown('phone', $btn);
821            } else {
822                const errorMsg = (resp.data && resp.data.message) ? resp.data.message : (resp.data || (typeof resp === 'string' ? resp : 'Unknown error'));
823                setMessage($msgBox, errorMsg, false);
824            }
825        }).catch((err) => {
826            setMessage($msgBox, err.message || 'Failed to resend. Server error.', false);
827        });
828    });
829
830    function showOtpStep($wrapper, $form, tab, identifier) {
831        $wrapper.find('.wawp-tabs').hide();
832        $form.find('.wawp-remember-me').addClass('wawp-hide-force');
833        if (tab === 'phone') {
834            $form.find('.wawp-phone-group, .wawp-request-btn').hide();
835            $form.find('.wawp-otp-group-phone, .wawp-otp-sent-message-phone').removeClass('wawp-hide').show();
836            $form.find('.wawp-verify-row-phone').removeClass('wawp-hide').css('display', 'flex');
837
838            // Focus 1st digit
839            setTimeout(() => {
840                const $otpInputs = $form.find('.wawp-otp-group-phone .wawp-otp-inputs input');
841                $otpInputs.val('').removeClass('filled');
842                $otpInputs.first().focus();
843            }, 100);
844            checkCooldown('phone', $form.find('.wawp-resend-otp-phone'));
845        } else if (tab === 'email') {
846            $form.find('.js-wawp-email-group').hide();
847            $form.find('.js-wawp-request-otp-email, .js-wawp-show-password-btn').hide();
848            $form.find('.js-wawp-otp-group-email, .js-wawp-email-back-link').removeClass('wawp-hide').show();
849            $form.find('.js-wawp-verify-row-email').removeClass('wawp-hide').css('display', 'flex');
850
851            // Focus 1st digit
852            setTimeout(() => {
853                const $otpInputs = $form.find('.js-wawp-otp-group-email .wawp-otp-inputs input');
854                $otpInputs.val('').removeClass('filled');
855                $otpInputs.first().focus();
856            }, 100);
857            checkCooldown('email', $form.find('.js-wawp-resend-otp-email'));
858        }
859    }
860
861    // Edit phone (instance-scoped)
862    $(document).on('click', '.wawp-edit-phone', function () {
863        const $form = $(this).closest('form');
864        const $wrapper = $(this).closest('.wawp-otp-login-form');
865        $wrapper.find('.wawp-tabs').show();
866        $form.find('.wawp-phone-group, .wawp-request-btn').show();
867        $form.find('.wawp-remember-me').removeClass('wawp-hide-force');
868        $form.find('.wawp-otp-group-phone, .wawp-otp-sent-message-phone').hide();
869        $form.find('.wawp-verify-row-phone').hide();
870        $form.find('.wawp-login-message-phone').html('');
871        updateBranding($wrapper, null);
872        if (typeof firebase !== 'undefined' && firebase.auth) {
873            resetFirebaseVerifier();
874        }
875    });
876
877    // Verify Phone OTP (instance-scoped)
878    $(document).on('click', '.wawp-verify-otp-phone', function () {
879        const $btn = $(this);
880        const $form = $btn.closest('form');
881        const $wrapper = $btn.closest('.wawp-otp-login-form');
882        const otp = $form.find('[name="otp"]').val().trim();
883        const phone = $form.find('[name="phone"]').val().trim();
884        const $msgBox = $form.find('.wawp-login-message-phone');
885
886        if (!otp) return setMessage($msgBox, window.wawpOtpL10n.pleaseEnterCode || 'Please enter the code.', false);
887
888        withSpinner($btn, (finish) => {
889            if (confirmationResult) {
890                // Firebase Verification
891                confirmationResult.confirm(otp).then((result) => {
892                    result.user.getIdToken().then(idToken => {
893                        wawpRest('verify_firebase', {
894                            idToken: idToken,
895                            remember: $form.find('.wawp-remember-phone-cb').is(':checked') ? 1 : 0
896                        }).then(resp => {
897                            finish();
898                            if (resp && resp.success) window.location.href = resp.data.redirect_url;
899                            else {
900                                const errorMsg = (resp && resp.data && resp.data.message) ? resp.data.message : (resp && resp.data ? resp.data : (typeof resp === 'string' ? resp : 'Verification failed.'));
901                                setMessage($msgBox, errorMsg, false);
902                            }
903                        }).catch((err) => {
904                            finish();
905                            setMessage($msgBox, 'Server error: ' + (err.message || 'unknown'), false);
906                        });
907                    });
908                }).catch(error => { finish(); setMessage($msgBox, error.message, false); });
909            } else {
910                // WhatsApp Verification
911                wawpRest('verify', {
912                    login_method: 'whatsapp_otp',
913                    phone: phone,
914                    otp: otp,
915                    otp_transient: $form.find('.wawp-verify-otp-phone').data('transient') || $form.find('.wawp-resend-otp-phone').data('transient') || '',
916                    remember: $form.find('.wawp-remember-phone-cb').is(':checked') ? 1 : 0
917                }).then(resp => {
918                    finish();
919                    if (resp && resp.success) window.location.href = resp.data.redirect_url;
920                    else {
921                        const errorMsg = (resp && resp.data && resp.data.message) ? resp.data.message : (resp && resp.data ? resp.data : (typeof resp === 'string' ? resp : '
921Login failed. Please try again.'));
922                        setMessage($msgBox, errorMsg, false);
923                    }
924                }).catch((err) => {
925                    finish();
926                    setMessage($msgBox, 'Server error: ' + (err.message || 'unknown'), false);
927                });
928            }
929        });
930    });
931
932    /* ------------------------------------------------------------
933     * Email Login Flow
934     * ---------------------------------------------------------- */
935    /* ------------------------------------------------------------
936     * Email Login Flow (Scoped)
937     * ---------------------------------------------------------- */
938    $(document).on('click', '.js-wawp-show-password-btn', function () {
939        const $form = $(this).closest('.wawp-merged-form');
940        $form.find('.js-wawp-password-group, .js-wawp-login-submit-password, .js-wawp-email-back-link').removeClass('wawp-hide').show();
941        $form.find('.js-wawp-show-password-btn, .js-wawp-request-otp-email').hide();
942    });
943
944    $(document).on('click', '.js-wawp-back-to-email-options', function (e) {
945        e.preventDefault();
946        const $form = $(this).closest('.wawp-merged-form');
947        const $wrapper = $(this).closest('.wawp-otp-login-form');
948        $form.find('.js-wawp-password-group, .js-wawp-login-submit-password, .js-wawp-otp-group-email, .js-wawp-email-back-link').hide();
949        $form.find('.js-wawp-verify-row-email').hide();
950        $form.find('.js-wawp-show-password-btn, .js-wawp-request-otp-email').show();
951        $form.find('.js-wawp-email-group').show();
952        $form.find('.wawp-remember-me').removeClass('wawp-hide-force');
953        $wrapper.find('.wawp-tabs').show();
954        updateBranding($wrapper, null);
955    });
956
957    // Email Resend Handler (instance-scoped)
958    $(document).on('click', '.js-wawp-resend-otp-email', function () {
959        const $btn = $(this);
960        const $form = $btn.closest('.wawp-merged-form');
961        const email = $form.find('.js-wawp-email-input').val();
962        const $msgBox = $form.find('.js-wawp-msg-box-email');
963
964        withSpinner($btn, (finish) => {
965            wawpRest('request', {
966                login_method: 'email_otp',
967                email: email
968            }).then(resp => {
969                finish();
970                if (resp.success) {
971                    setMessage($msgBox, window.wawpOtpL10n.codeResent || 'Code resent!');
972                    startCooldown('email', $btn);
973                } else setMessage($msgBox, resp.data, false);
974            }).catch((err) => {
975                finish();
976                setMessage($msgBox, err.message || 'Server error', false);
977            });
978        });
979    });
980
981    // Email Password Submission (instance-scoped)
982    $(document).on('click', '.js-wawp-login-submit-password', function () {
983        const $btn = $(this);
984        const $form = $btn.closest('.wawp-merged-form');
985        const login = $form.find('.js-wawp-email-input').val();
986        const password = $form.find('.js-wawp-password-input').val();
987        const $msgBox = $form.find('.js-wawp-msg-box-email');
988
989        if (!login || !password) return setMessage($msgBox, wawpOtpAjax.emptyFields, false);
990
991        withSpinner($btn, (finish) => {
992            withRecaptcha('email_password', (token) => {
993                wawpRest('login_password', {
994                    login: login,
995                    password: password,
996                    remember: $form.find('.wawp-remember-email-cb').is(':checked') ? 1 : 0,
997                    'g-recaptcha-response': token
998                }).then(resp => {
999                    finish();
1000                    if (resp && resp.success) window.location.href = resp.data.redirect_url;
1001                    else {
1002                        const errorMsg = (resp && resp.data && resp.data.message) ? resp.data.message : (resp && resp.data ? resp.data : (typeof resp === 'string' ? resp : 'Login failed. Please try again.'));
1003                        setMessage($msgBox, errorMsg, false);
1004                        const cId = 'wawp-recaptcha-email-merged';
1005                        if (recaptchaWidgets[cId] !== undefined) grecaptcha.reset(recaptchaWidgets[cId]);
1006                    }
1007                }).catch((err) => {
1008                    finish();
1009                    setMessage($msgBox, 'Server error: ' + (err.message || 'unknown'), false);
1010                });
1011            }, (err) => { finish(); setMessage($msgBox, err, false); }, $form);
1012        });
1013    });
1014
1015    // Email OTP Submission (instance-scoped)
1016    $(document).on('click', '.js-wawp-request-otp-email', function () {
1017        const $btn = $(this);
1018        const $form = $btn.closest('.wawp-merged-form');
1019        const email = $form.find('.js-wawp-email-input').val();
1020        const $msgBox = $form.find('.js-wawp-msg-box-email');
1021
1022        if (!email) return setMessage($msgBox, wawpOtpAjax.emptyFields, false);
1023
1024        withSpinner($btn, (finish) => {
1025            withRecaptcha('email_otp', (token) => {
1026                wawpRest('request', {
1027                    login_method: 'email_otp',
1028                    email: email,
1029                    'g-recaptcha-response': token
1030                }).then(resp => {
1031                    finish();
1032                    if (resp && resp.success) {
1033                        setMessage($msgBox, resp.data.message || 'Magic link sent! Please check your inbox.', true);
1034                        const $wrapper2 = $btn.closest('.wawp-otp-login-form');
1035                        if (resp.data && resp.data.is_magic_link) {
1036                            $wrapper2.find('.wawp-tabs').hide();
1037                            $form.find('.wawp-remember-me').addClass('wawp-hide-force');
1038                            $wrapper2.find('.wawp-login-branding').hide();
1039                            $form.find('.js-wawp-email-group, .js-wawp-request-otp-email, .js-wawp-show-password-btn, .wawp-switch-form').hide();
1040                            $msgBox.empty();
1041                            const waitingHtml = `
1042                                <div class="wawp-list-waiting-container">
1043                                    <div class="wawp-list-waiting-icon">
1044                                        <i class="ri-shield-check-line"></i>
1045                                    </div>
1046                                    <h4 class="wawp-list-waiting-title">${window.wawpOtpL10n.verifyYourIdentity || 'Verify Your Identity'}</h4>
1047                                    <p class="wawp-list-waiting-text">${window.wawpOtpL10n.weSentSecureRequest || 'We sent a secure verification request to'} <b>${email}</b>.<br>${window.wawpOtpL10n.pleaseCheckInbox || 'Please check your inbox and click the link to continue.'}</p>
1048                                    <div style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
1049                                        <a href="#" onclick="window.location.reload();" class="js-wawp-back-to-input" style="font-size: 12px; color: #718096; text-decoration: underline;">${window.wawpOtpL10n.editEmailOrMethod || 'Edit email or use another method'}</a>
1050                                    </div>
1051                                </div>
1052                            `;
1053                            $msgBox.after(waitingHtml);
1054                            startListPolling(email, resp.data.otp_transient, $msgBox);
1055                        } else {
1056                            updateBranding($wrapper2, 'email', email, false);
1057                            showOtpStep($wrapper2, $form, 'email', email);
1058                            if (resp.data && resp.data.otp_transient) {
1059                                $form.find('.js-wawp-resend-otp-email').data('transient', resp.data.otp_transient);
1060                                $form.find('.js-wawp-verify-otp-email').data('transient', resp.data.otp_transient);
1061                            }
1062                            startCooldown('email', $form.find('.js-wawp-resend-otp-email'));
1063                        }
1064                    } else {
1065                        const errorMsg = (resp && resp.data && resp.data.message) ? resp.data.message : (resp && resp.data ? resp.data : (typeof resp === 'string' ? resp : 'Request failed.'));
1066                        setMessage($msgBox, errorMsg, false);
1067                        const cId = 'wawp-recaptcha-email-merged';
1068                        if (recaptchaWidgets[cId] !== undefined) grecaptcha.reset(recaptchaWidgets[cId]);
1069                    }
1070                }).catch((err) => {
1071                    finish();
1072                    if (err.data && err.data.redirect_to_signup) {
1073                        window.location.href = err.data.redirect_to_signup;
1074                        return;
1075                    }
1076                    setMessage($msgBox, 'Server error: ' + (err.message || 'unknown'), false);
1077                });
1078            }, (err) => { finish(); setMessage($msgBox, err, false); }, $form);
1079        });
1080    });
1081
1082    $(document).on('click', '.js-wawp-verify-otp-email', function () {
1083        const $btn = $(this);
1084        const $form = $btn.closest('.wawp-merged-form');
1085        const email = $form.find('.js-wawp-email-input').val();
1086        const otp = $form.find('.js-wawp-otp-input-email').val();
1087        const $msgBox = $form.find('.js-wawp-msg-box-email');
1088
1089        withSpinner($btn, (finish) => {
1090            wawpRest('verify', {
1091                login_method: 'email_otp',
1092                email: email,
1093                otp: otp,
1094                otp_transient: $btn.data('transient') || $form.find('.js-wawp-resend-otp-email').data('transient') || '',
1095                remember: $form.find('.wawp-remember-email-cb').is(':checked') ? 1 : 0
1096            }).then(resp => {
1097                finish();
1098                if (resp.success) window.location.href = resp.data.redirect_url;
1099                else {
1100                    const errorMsg = (resp.data && resp.data.message) ? resp.data.message : (resp.data || (typeof resp === 'string' ? resp : 'Unknown error'));
1101                    setMessage($msgBox, errorMsg, false);
1102                }
1103            }).catch(err => {
1104                finish();
1105                setMessage($msgBox, err.message || 'Error occurred.', false);
1106            });
1107        });
1108    });
1109
1110    // Password Toggle (instance-scoped)
1111    $(document).on('click', '.js-wawp-password-toggle', function () {
1112        const $form = $(this).closest('.wawp-merged-form');
1113        const input = $form.find('.js-wawp-password-input');
1114        const type = input.attr('type') === 'password' ? 'text' : 'password';
1115        input.attr('type', type);
1116        $(this).toggleClass('ri-eye-line ri-eye-off-line');
1117    });
1118
1119    // Check cooldowns on load
1120    checkCooldown('phone');
1121    checkCooldown('email');
1122
1123    /* ------------------------------------------------------------
1124     * Enter Key Support
1125     * ---------------------------------------------------------- */
1126    function handleEnter(inputSel, btnSel) {
1127        $(document).on('keypress', inputSel, function (e) {
1128            if (e.which === 13) {
1129                e.preventDefault();
1130                $(btnSel).first().trigger('click');
1131            }
1132        });
1133    }
1134
1135    // Phone Input -> Submit first visible request button (instance-scoped)
1136    $(document).on('keypress', '.wawp-phone-form [name="phone"]', function (e) {
1137        if (e.which === 13) {
1138            e.preventDefault();
1139            $(this).blur();
1140            $(this).closest('form').find('.wawp-request-btn:visible').first().trigger('click');
1141        }
1142    });
1143
1144    // Phone OTP split inputs -> Verify on Enter
1145    $(document).on('keypress', '.wawp-otp-group-phone .wawp-otp-inputs input', function (e) {
1146        if (e.which === 13) {
1147            e.preventDefault();
1148            $(this).closest('form').find('.wawp-verify-otp-phone:visible').first().trigger('click');
1149        }
1150    });
1151
1152    // Email Input -> Check context (Password login or OTP request) (instance-scoped)
1153    $(document).on('keypress', '.js-wawp-email-input', function (e) {
1154        if (e.which === 13) {
1155            e.preventDefault();
1156            $(this).blur();
1157            const $form = $(this).closest('.wawp-merged-form');
1158            const $passBtn = $form.find('.js-wawp-login-submit-password:visible');
1159            if ($passBtn.length) {
1160                $passBtn.trigger('click');
1161            } else {
1162                $form.find('.js-wawp-request-otp-email:visible').trigger('click');
1163            }
1164        }
1165    });
1166
1167    // Password -> Login (instance-scoped)
1168    $(document).on('keypress', '.js-wawp-password-input', function (e) {
1169        if (e.which === 13) {
1170            e.preventDefault();
1171            $(this).closest('form').find('.js-wawp-login-submit-password:visible').trigger('click');
1172        }
1173    });
1174
1175    // Email OTP split inputs -> Verify on Enter (instance-scoped)
1176    $(document).on('keypress', '.js-wawp-otp-group-email .wawp-otp-inputs input', function (e) {
1177        if (e.which === 13) {
1178            e.preventDefault();
1179            $(this).closest('form').find('.js-wawp-verify-otp-email:visible').first().trigger('click');
1180        }
1181    });
1182
1183    // Fast Login Toggles (instance-scoped) - Re-init recaptcha just in case
1184    $(document).on('click', '.wawp-fast-login-signup-toggle, .wawp-fast-login-login-toggle', function () {
1185        setTimeout(function () {
1186            $(document).trigger('wawp_recaptcha_init');
1187        }, 100);
1188    });
1189
1190    /* ------------------------------------------------------------
1191     * Legacy Client Fixes (Adapted for modern classes)
1192     * ---------------------------------------------------------- */
1193    // 1. Force enable buttons stuck in disabled state (e.g. from bfcache or browser bugs)
1194    $(document).on('mouseenter', '.wawp-btn, .wawp-request-btn', function () {
1195        var $btn = $(this);
1196        // Do not force enable if explicitly in a loading or active cooldown state
1197        if ($btn.hasClass('is-loading') || $btn.closest('.wawp-action-buttons').find('.wawp-resend-timer').length > 0) {
1198            return;
1199        }
1200        $btn.css('pointer-events', 'auto');
1201        $btn.removeAttr('disabled');
1202        $btn.removeClass('disabled loading blockUI');
1203    });
1204
1205    // 2. Formatting Sync: Force focus to the request button 1 second after typing
1206    var typingTimer;
1207    var doneTypingInterval = 1000;
1208    $(document).on('keyup', '.wawp-phone-form [name="phone"]', function () {
1209        const $inp = $(this);
1210        clearTimeout(typingTimer);
1211        if ($inp.val()) {
1212            typingTimer = setTimeout(function () {
1213                var valLength = $inp.val().replace(/\D/g, '').length;
1214                if (valLength >= 7) {
1215                    var $whatsappBtn = $inp.closest('form').find('.wawp-request-btn[data-method="whatsapp"]:visible, .wawp-request-btn:visible').first();
1216                    if ($whatsappBtn.length) { $whatsappBtn.focus(); }
1217                }
1218            }, doneTypingInterval);
1219        }
1220    });
1221    $(document).on('keydown', '.wawp-phone-form [name="phone"]', function () {
1222        clearTimeout(typingTimer);
1223    });
1224
1225    // 3. Modern Split OTP Input Logic
1226    $(document).on('input', '.wawp-otp-inputs input', function () {
1227        const $this = $(this);
1228        const $inputs = $this.parent().find('input');
1229        const targetId = $this.parent().data('target');
1230        const $hiddenInput = $(targetId);
1231
1232        // Move forward if filled
1233        if ($this.val().length === 1) {
1234            $this.addClass('filled');
1235            const nextIdx = $inputs.index($this) + 1;
1236            if (nextIdx < $inputs.length) {
1237                $inputs.eq(nextIdx).focus();
1238            }
1239        } else {
1240            $this.removeClass('filled');
1241        }
1242
1243        // Update hidden input
1244        let fullValue = '';
1245        $inputs.each(function () {
1246            fullValue += $(this).val();
1247        });
1248        $hiddenInput.val(fullValue);
1249    });
1250
1251    $(document).on('keydown', '.wawp-otp-inputs input', function (e) {
1252        const $this = $(this);
1253        const $inputs = $this.parent().find('input');
1254
1255        // Move backward on backspace if empty
1256        if (e.key === 'Backspace' && $this.val().length === 0) {
1257            const prevIdx = $inputs.index($this) - 1;
1258            if (prevIdx >= 0) {
1259                $inputs.eq(prevIdx).focus();
1260            }
1261        }
1262    });
1263
1264    // Handle Paste for Split OTP
1265    $(document).on('paste', '.wawp-otp-inputs input', function (e) {
1266        e.preventDefault();
1267        const $this = $(this);
1268        const $inputs = $this.parent().find('input');
1269        const targetId = $this.parent().data('target');
1270        const $hiddenInput = $(targetId);
1271
1272        const pasteData = (e.originalEvent || e).clipboardData.getData('text').slice(0, 6).split('');
1273
1274        pasteData.forEach((char, i) => {
1275            if ($inputs.eq(i).length) {
1276                $inputs.eq(i).val(char).addClass('filled');
1277            }
1278        });
1279
1280        $hiddenInput.val(pasteData.join(''));
1281        if ($inputs.eq(pasteData.length - 1).length) {
1282            $inputs.eq(pasteData.length - 1).focus();
1283        } else {
1284            $inputs.last().focus();
1285        }
1286    });
1287
1288    $(document).on('click', '.js-wawp-back-to-input', function (e) {
1289        e.preventDefault();
1290        if (pollInterval) clearInterval(pollInterval);
1291        const $form = $(this).closest('form');
1292        const $wrapper = $(this).closest('.wawp-otp-login-form');
1293        $form.find('.wawp-list-waiting-container').remove();
1294        $wrapper.find('.wawp-tabs').show();
1295        $form.find('.wawp-remember-me').removeClass('wawp-hide-force');
1296        $wrapper.find('.wawp-login-branding').show();
1297        if ($form.hasClass('js-wawp-email-form')) {
1298            $form.find('.js-wawp-email-group, .js-wawp-request-otp-email, .js-wawp-show-password-btn, .wawp-switch-form').show();
1299        } else {
1300            $form.find('.wawp-phone-group, .wawp-action-buttons, .wawp-separator, .wawp-switch-form').removeClass('wawp-hide-force');
1301            $form.find('.wawp-login-message-phone').empty();
1302        }
1303        if (typeof firebase !== 'undefined' && firebase.auth) {
1304            resetFirebaseVerifier();
1305        }
1306    });
1307
1308    let pollInterval = null;
1309    function startListPolling(phone, authId, $msgBox) {
1310        if (pollInterval) clearInterval(pollInterval);
1311
1312        let attempts = 0;
1313        const maxAttempts = 60; // 3 minutes (3s * 60)
1314        const isEmail = $msgBox.hasClass('js-wawp-msg-box-email');
1315        const $form = $msgBox.closest('form');
1316
1317        pollInterval = setInterval(() => {
1318            attempts++;
1319            if (attempts > maxAttempts) {
1320                clearInterval(pollInterval);
1321                const $wrapperP = $form.closest('.wawp-otp-login-form');
1322                $form.find('.wawp-list-waiting-container').remove();
1323                $wrapperP.find('.wawp-tabs').show();
1324                $form.find('.wawp-remember-me').removeClass('wawp-hide-force');
1325                $wrapperP.find('.wawp-login-branding').show();
1326                setMessage($msgBox, 'Authentication timeout. Please try again.', false);
1327                if (isEmail) {
1328                    $form.find('.js-wawp-email-group, .js-wawp-request-otp-email, .js-wawp-show-password-btn, .wawp-switch-form').show();
1329                } else {
1330                    $form.find('.wawp-phone-group, .wawp-action-buttons, .wawp-separator, .wawp-switch-form').removeClass('wawp-hide-force');
1331                }
1332                return;
1333            }
1334
1335            wawpRest('status', {
1336                otp_transient: authId,
1337                whatsapp: phone,
1338                remember: isEmail ? ($form.find('.wawp-remember-email-cb').is(':checked') ? 1 : 0) : ($form.find('.wawp-remember-phone-cb').is(':checked') ? 1 : 0)
1339            }).then(resp => {
1340                if (resp.success && resp.data.verified) {
1341                    clearInterval(pollInterval);
1342                    $('.wawp-list-waiting-container').html('<div class="wawp-list-waiting-icon" style="background:#15803d"><i class="ri-check-line"></i></div><h4 class="wawp-list-waiting-title">' + (window.wawpOtpL10n.verified || 'Verified!') + '</h4><p class="wawp-list-waiting-text">' + (window.wawpOtpL10n.redirectingDashboard || 'Redirecting you to dashboard...') + '</p>');
1343
1344                    setTimeout(() => {
1345                        if (resp.data.redirect_url) {
1346                            window.location.href = resp.data.redirect_url;
1347                        } else {
1348                            window.location.reload();
1349                        }
1350                    }, 1000);
1351                } else if (resp.success && resp.data.status === 'rejected') {
1352                    clearInterval(pollInterval);
1353                    const $wrapperR = $form.closest('.wawp-otp-login-form');
1354                    $form.find('.wawp-list-waiting-container').remove();
1355                    $wrapperR.find('.wawp-tabs').show();
1356                    $form.find('.wawp-remember-me').removeClass('wawp-hide-force');
1357                    $wrapperR.find('.wawp-login-branding').show();
1358                    setMessage($msgBox, isEmail ? (window.wawpOtpL10n.loginRejectedEmail || 'Login request was rejected.') : (window.wawpOtpL10n.loginRejectedPhone || 'Login request was rejected from your phone.'), false);
1359                    if (isEmail) {
1360                        $form.find('.js-wawp-email-group, .js-wawp-request-otp-email, .js-wawp-show-password-btn, .wawp-switch-form').show();
1361                    } else {
1362                        $form.find('.wawp-phone-group, .wawp-action-buttons, .wawp-separator, .wawp-switch-form').removeClass('wawp-hide-force');
1363                    }
1364                }
1365            }).catch(() => {
1366                // Silently ignore polling errors to keep trying
1367            });
1368        }, parseInt(wawpOtpAjax.polling_interval) || 1500);
1369    }
1370});

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.