PageSourceSearch

https://www.clubtalleres.com.ar/wp-content/plugins/eon-firebase-auth/assets/frontend-login.js?ver=0.2.0

js clubtalleres.com.ar collected 2026-10-02 04:27:48 UTC 45,202 bytes, 1,318 lines download raw bytes

1/**
2 * Frontend Login Handler
3 * Maneja el formulario de login con Firebase Auth + WordPress
4 */
5
6let firebaseAuth = null;
7
8document.addEventListener('DOMContentLoaded', async function () {
9  console.log('DOM Content Loaded');
10
11  // Enganchar el formulario de login
12  const loginForm = document.getElementById('eonLoginFormElement');
13  if (loginForm) {
14    console.log('Login form found and listener attached');
15    loginForm.addEventListener('submit', handleLoginSubmit);
16  }
17
18  // Enganchar el formulario de registro
19  const registerForm = document.getElementById('eonRegisterFormElement');
20  if (registerForm) {
21    console.log('Register form found and listener attached');
22    registerForm.addEventListener('submit', handleRegisterSubmit);
23  }
24
25  // Enganchar el formulario de reset
26  const resetForm = document.getElementById('eonResetFormElement');
27  if (resetForm) {
28    console.log('Reset form found and listener attached');
29    resetForm.addEventListener('submit', handleResetSubmit);
30  }
31
32  // Destino único del reset: ?eon_action=reset abre el panel de recuperación
33  // directamente. Es a donde apuntan los filtros lostpassword_url y
34  // woocommerce_lost_password_url, y a donde redirige el guard del endpoint
35  // lost-password de WooCommerce.
36  try {
37    const eonAction = new URLSearchParams(window.location.search).get('eon_action');
38    if (eonAction === 'reset' && document.getElementById('eonResetForm')) {
39      flipCard('reset');
40      document.getElementById('eonResetForm').scrollIntoView({ behavior: 'smooth', block: 'center' });
41    }
42  } catch (e) {
43    console.warn('No se pudo procesar eon_action:', e);
44  }
45
46  // Enganchar el formulario OTP
47  const otpForm = document.getElementById('eonOtpFormElement');
48  if (otpForm) {
49    otpForm.addEventListener('submit', function(e) {
50      e.preventDefault();
51      submitOtpCode();
52    });
53  }
54
55  // Solo dígitos en input OTP
56  const otpCodeInput = document.getElementById('eon_otp_code');
57  if (otpCodeInput) {
58    otpCodeInput.addEventListener('input', function() {
59      this.value = this.value.replace(/\D/g, '').slice(0, 6);
60    });
61  }
62
63  // Inicializar Firebase después de los listeners
64  await initializeFirebase();
65
66  // Listener Google (si existe el botón)
67  const googleBtn = document.getElementById('eon_google_login');
68  if (googleBtn) {
69    console.log('Google button found and listener attached');
70    googleBtn.addEventListener('click', handleGoogleLogin);
71  }
72
73  // Listener Apple (si existe el botón)
74  const appleBtn = document.getElementById('eon_apple_login');
75  if (appleBtn) {
76    console.log('Apple button found and listener attached');
77    appleBtn.addEventListener('click', handleAppleLogin);
78  }
79
80  // Limpiar errores al escribir
81  const userInput = document.getElementById('eon_login_user');
82  const passwordInput = document.getElementById('eon_login_password');
83
84  if (userInput) userInput.addEventListener('input', () => clearError('user'));
85  if (passwordInput) passwordInput.addEventListener('input', () => clearError('password'));
86
87  // Limpiar errores de registro
88  document.querySelectorAll('#eonRegisterFormElement input').forEach((input) => {
89    input.addEventListener('input', () => clearError(input.name, 'register'));
90  });
91
92  // Limpiar errores de reset
93  document.querySelectorAll('#eonResetFormElement input').forEach((input) => {
94    input.addEventListener('input', () => clearError(input.name, 'reset'));
95  });
96
97  // --- Modal Ayuda Número de Socio ---
98  const helpTrigger = document.getElementById('eon_help_member_trigger');
99  const helpModal = document.getElementById('eon_help_modal');
100  const helpClose = document.getElementById('eon_help_modal_close');
101  const helpOk = document.getElementById('eon_help_modal_ok');
102
103  if (helpTrigger && helpModal) {
104    helpTrigger.addEventListener('click', () => {
105      helpModal.classList.add('active');
106      document.body.style.overflow = 'hidden';
107    });
108
109    const closeModal = () => {
110      helpModal.classList.remove('active');
111      document.body.style.overflow = '';
112    };
113
114    if (helpClose) helpClose.addEventListener('click', closeModal);
115    if (helpOk) helpOk.addEventListener('click', closeModal);
116
117    helpModal.addEventListener('click', (e) => {
118      if (e.target === helpModal) closeModal();
119    });
120  }
121
122  // --- Toggle para el login del Checkout ---
123  const checkoutTrigger = document.getElementById('eon_checkout_login_trigger');
124  const checkoutLoginForm = document.querySelector('.eon-login-wrapper.is-woocommerce-checkout');
125  if (checkoutTrigger && checkoutLoginForm) {
126    checkoutTrigger.addEventListener('click', (e) => {
127      e.preventDefault();
128      checkoutLoginForm.classList.toggle('active');
129    });
130  }
131});
132
133/**
134 * Inicializa Firebase con la configuración desde WordPress
135 */
136async function initializeFirebase() {
137  const rawDomain = (window.eonFbLogin?.firebaseDomain || window.eonFbLogin?.authDomain || '').trim();
138  const authDomain = rawDomain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
139
140  const config = {
141    projectId: window.eonFbLogin?.projectId || '',
142    apiKey: window.eonFbLogin?.apiKey || '',
143    authDomain,
144  };
145
146  if (!config.projectId || !config.apiKey || !config.authDomain) {
147    console.error('Firebase config incompleta en WordPress', config, window.eonFbLogin);
148    return;
149  }
150
151  try {
152    if (!window.firebase?.initializeApp) {
153      console.error('Firebase SDK no disponible todavía (firebase-app-compat no cargó).');
154      return;
155    }
156
157    if (!firebase.apps || !firebase.apps.length) {
158      firebase.initializeApp(config);
159    }
160
161    firebaseAuth = firebase.auth();
162    window.firebaseAuth = firebaseAuth; // exposed globally for WooCommerce inline scripts
163    console.log('FirebaseAuth inicializado OK');
164    checkAndRefreshEmailVerification();
165  } catch (error) {
166    console.error('Error inicializando Firebase:', error, { config });
167  }
168}
169
170/**
171 * Detects when the user has clicked the verification link and updates WP meta.
172 * Also corrects WP meta when Firebase says 'not verified' but WP still shows 'yes'.
173 * Called on every page load for logged-in unverified users.
174 */
175function checkAndRefreshEmailVerification() {
176  if (!firebaseAuth) return;
177
178  firebaseAuth.onAuthStateChanged(async function(user) {
179    if (!user) return;
180
181    try {
182      await user.reload();
183      const freshUser = firebaseAuth.currentUser;
184      if (!freshUser) return;
185
186      const wpVerified = window.eonFbLogin?.emailVerified === true;
187      const fbVerified = freshUser.emailVerified;
188
189      // No discrepancy — nothing to sync
190      if (wpVerified === fbVerified) return;
191
192      const idToken = await freshUser.getIdToken(true);
193
194      const response = await fetch(window.eonFbLogin.ajaxUrl, {
195        method: 'POST',
196        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
197        body: new URLSearchParams({
198          action: 'eon_refresh_email_verified',
199          nonce: window.eonFbLogin.nonce,
200          id_token: idToken,
201        }),
202      });
203
204      const result = await response.json();
205      // Only reload on upgrade (WP didn't know it was verified yet)
206      if (result.success && result.data?.verified === true && !wpVerified) {
207        window.location.reload();
208      }
209      // Downgrade (WP='yes', Firebase='no'): server already corrected the meta,
210      // UI updates on next normal navigation.
211    } catch (err) {
212      console.warn('[checkAndRefreshEmailVerification] Error:', err);
213    }
214  });
215}
216
217/**
218 * Detecta si reCAPTCHA v2 (checkbox) está presente en la página
219 * @param {string} containerType - 'login' o 'register'
220 */
221function isRecaptchaV2Present(containerType = 'login') {
222  // El caso 'reset' faltaba: caia al contenedor de login, que en el panel de
223  // recuperacion no existe, asi que la funcion devolvia false y el submit se mandaba
224  // sin token. Del otro lado, handle_password_reset() SI exige el token cuando el
225  // reCAPTCHA esta activo -> con reCAPTCHA prendido, nadie podia recuperar su contrasena.
226  const containerIds = {
227    register: 'eon_recaptcha_container_register',
228    reset: 'eon_recaptcha_container_reset',
229  };
230  const containerId = containerIds[containerType] ?? 'eon_recaptcha_container';
231  return document.getElementById(containerId) !== null;
232}
233
234/**
235 * Obtiene un token de reCAPTCHA
236 * Soporta v2 (checkbox) y v3 (invisible)
237 * @param {string} action - 'login' o 'register'
238 */
239async function getRecaptchaToken(action = 'login') {
240  if (!window.eonFbLogin?.recaptchaEnabled || !window.eonFbLogin?.recaptchaSiteKey) {
241    return null;
242  }
243
244  try {
245    // Si hay un container de reCAPTCHA v2, obtener el token del widget
246    if (isRecaptchaV2Present(action)) {
247      // En v2, el token está en el widget renderizado
248      // Usar el ID del widget almacenado en eonRecaptchaWidgets
249      const widgetId = window.eonRecaptchaWidgets?.[action];
250      
251      if (widgetId === undefined) {
252        console.warn(`reCAPTCHA v2 (${action}): Widget no encontrado. widgetId:`, widgetId);
253        return null;
254      }
255      
256      const token = window.grecaptcha?.getResponse?.(widgetId);
257      
258      if (!token) {
259        console.warn(`reCAPTCHA v2 (${action}): el usuario aún no ha completado el captcha (widgetId: ${widgetId})`);
260        return null;
261      }
262      
263      console.log(`reCAPTCHA v2 token obtenido (${action}, widgetId: ${widgetId})`);
264      return token;
265    }
266    
267    // Si no, asumir que es v3 (invisible/enterprise)
268    console.log('Usando reCAPTCHA v3 (invisible)');
269    return await new Promise((resolve) => {
270      grecaptcha.execute(window.eonFbLogin.recaptchaSiteKey, { action }).then((token) => resolve(token));
271    });
272  } catch (error) {
273    console.error('Error obteniendo reCAPTCHA token:', error);
274    return null;
275  }
276}
277
278function resetRecaptcha(containerType = 'login') {
279  if (window.eonFbLogin?.recaptchaEnabled && window.grecaptcha) {
280    // Para v2 y v3
281    if (isRecaptchaV2Present(containerType)) {
282      // v2: resetea el widget específico usando su ID
283      const widgetId = window.eonRecaptchaWidgets?.[containerType];
284      
285      if (widgetId !== undefined) {
286        try {
287          window.grecaptcha.reset(widgetId);
288          console.log(`reCAPTCHA widget ${containerType} (ID: ${widgetId}) reseteado`);
289        } catch (e) {
290          console.warn(`No se pudo resetear reCAPTCHA ${containerType} (widgetId: ${widgetId}):`, e);
291        }
292      } else {
293        console.warn(`Widget ID no encontrado para ${containerType}`);
294      }
295    } else {
296      // v3: no necesita reset explícito, pero podemos limpiar si existe
297      try {
298        window.grecaptcha.reset?.();
299      } catch (e) {
300        // v3 enterprise no tiene reset, ignorar
301      }
302    }
303  }
304}
305
306/**
307 * Maneja el envío del formulario de login
308 */
309async function handleLoginSubmit(e) {
310  e.preventDefault();
311
312  const user = document.getElementById('eon_login_user')?.value.trim() || '';
313  const password = document.getElementById('eon_login_password')?.value || '';
314  const remember = document.getElementById('eon_login_remember')?.checked || false;
315
316  if (!user) {
317    showError('user', 'Ingresa tu usuario o email.');
318    return;
319  }
320
321  if (!password) {
322    showError('password', 'La contraseña es requerida.');
323    return;
324  }
325
326  // Validar reCAPTCHA v2 si está presente
327  if (isRecaptchaV2Present('login')) {
328    const widgetId = window.eonRecaptchaWidgets?.login;
329    const recaptchaResponse = window.grecaptcha?.getResponse?.(widgetId);
330    if (!recaptchaResponse) {
331      showError('general', 'Por favor completa el reCAPTCHA.');
332      return;
333    }
334  }
335
336  showLoading(true);
337
338  try {
339    // PASO 1: Verificar si es personal del admin
340    const adminCheckResponse = await fetch(window.eonFbLogin.ajaxUrl, {
341      method: 'POST',
342      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
343      body: new URLSearchParams({
344        action: 'eon_check_is_admin',
345        nonce: window.eonFbLogin.nonce,
346        user,
347      }),
348    });
349
350    const adminCheckResult = await adminCheckResponse.json();
351
352    if (!adminCheckResult.success) {
353      showError('general', adminCheckResult.data?.message || 'Error al verificar usuario.');
354      return;
355    }
356
357    const { isAdmin, exists } = adminCheckResult.data;
358
359    // SI TIENE ACCESO AL ADMIN: Usar login de WordPress normal
360    if (exists && isAdmin) {
361      await loginAsWordPressAdmin(user, password, remember);
362      return;
363    }
364
365    // SI NO TIENE ACCESO AL ADMIN: Usar Firebase (cliente)
366    await loginWithFirebase(user, password, remember);
367  } catch (error) {
368    console.error('Error en login:', error);
369    showError('general', 'Error al acceder. Intenta nuevamente.');
370    resetRecaptcha();
371  } finally {
372    showLoading(false);
373  }
374}
375
376/**
377 * Login como administrador de WordPress (sin Firebase)
378 */
379async function loginAsWordPressAdmin(user, password, remember) {
380  showLoading(true);
381
382  try {
383    const recaptchaToken = await getRecaptchaToken('login');
384
385    const response = await fetch(window.eonFbLogin.ajaxUrl, {
386      method: 'POST',
387      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
388      body: new URLSearchParams({
389        action: 'eon_admin_wordpress_login',
390        nonce: window.eonFbLogin.nonce,
391        user,
392        password,
393        remember: remember ? 'true' : 'false',
394        g_recaptcha_response: recaptchaToken || '',
395        redirect_to: document.getElementById('eon_login_redirect')?.value || '',
396      }),
397    });
398
399    // Verificar que la respuesta sea JSON antes de parsear
400    const contentType = response.headers.get('content-type') || '';
401    if (!contentType.includes('application/json')) {
402      console.error('Respuesta no-JSON del servidor:', response.status, contentType);
403      showError('general', 'Error del servidor. Intenta nuevamente o recarga la página.');
404      resetRecaptcha();
405      return;
406    }
407
408    const result = await response.json();
409
410    if (result.success) {
411      // Verificar si requiere OTP/2FA
412      if (result.data?.requires_otp) {
413        showLoading(false);
414        showOtpStep(result.data.otp_token, result.data.message);
415        return;
416      }
417
418      showSuccess('Acceso exitoso. Redirigiendo...');
419      setTimeout(() => {
420        window.location.href = result.data?.redirect || window.location.href;
421      }, 800);
422    } else {
423      showError('general', result.data?.message || 'Error al acceder.');
424      if (result.data?.recaptcha_error) resetRecaptcha();
425    }
426  } catch (error) {
427    console.error('Error en login de admin:', error);
428    showError('general', 'Error al acceder .');
429    resetRecaptcha();
430  } finally {
431    showLoading(false);
432  }
433}
434
435/**
436 * NUEVO: chequea en WP si existe customer con email+password (para migrar a Firebase)
437 */
438async function checkWpCustomerCredentials(email, password) {
439  const recaptchaToken = await getRecaptchaToken('login');
440
441  const resp = await fetch(window.eonFbLogin.ajaxUrl, {
442    method: 'POST',
443    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
444    body: new URLSearchParams({
445      action: 'eon_check_wp_customer_credentials',
446      nonce: window.eonFbLogin.nonce,
447      email,
448      password,
449      g_recaptcha_response: recaptchaToken || '',
450    }),
451  });
452
453  const json = await resp.json();
454  return json;
455}
456
457/**
458 * Login con Firebase (para usuarios normales)
459 * - Si Firebase dice user-not-found:
460 *   - consultamos WP (customer + password ok)
461 *   - si ok: creamos usuario en Firebase y continuamos el login normal
462 */
463async function loginWithFirebase(email, password, remember) {
464  if (!firebaseAuth) {
465    showError('general', 'Login no está inicializado. Recarga la página.');
466    return;
467  }
468
469  showLoading(true);
470
471  try {
472    // 1) Intento login Firebase
473    const userCredential = await firebaseAuth.signInWithEmailAndPassword(email, password);
474    await finalizeWpLoginWithFirebaseCredential(userCredential, email, remember);
475  } catch (error) {
476    console.error('Error en login con Firebase:', error);
477
478    // Si no existe en Firebase => intento migración desde WP (Woo customer)
479    if (error?.code === 'auth/user-not-found') {
480      try {
481        const wpCheck = await checkWpCustomerCredentials(email, password);
482
483        if (!wpCheck.success) {
484          // WP tampoco lo validó => mensaje “creá cuenta” como antes
485          const msg = wpCheck.data?.message || 'Email no registrado. Creá una cuenta.';
486          showError('general', msg);
487          if (wpCheck.data?.recaptcha_error) resetRecaptcha();
488          return;
489        }
490
491        // WP OK => crear usuario en Firebase con ese email/pass
492        const displayName = wpCheck.data?.user?.displayName || '';
493
494        let createdUserCredential;
495        try {
496          createdUserCredential = await firebaseAuth.createUserWithEmailAndPassword(email, password);
497        } catch (eCreate) {
498          // Si justo ya existe (race) => intentamos login
499          if (eCreate?.code === 'auth/email-already-in-use') {
500            createdUserCredential = await firebaseAuth.signInWithEmailAndPassword(email, password);
501          } else {
502            throw eCreate;
503          }
504        }
505
506        // Set displayName si tenemos algo
507        try {
508          if (displayName && createdUserCredential?.user) {
509            await createdUserCredential.user.updateProfile({ displayName });
510          }
511        } catch (eProfile) {
512          console.warn('No se pudo setear displayName en Firebase:', eProfile);
513        }
514
515        // Listo: continuar flujo normal (token -> WP)
516        await finalizeWpLoginWithFirebaseCredential(createdUserCredential, email, remember);
517        return;
518      } catch (migrateErr) {
519        console.error('Migración WP->Firebase falló:', migrateErr);
520        showError('general', migrateErr?.message || 'No se pudo iniciar sesión. Probá porfavor creando una cuenta nueva.');
521        resetRecaptcha();
522        return;
523      }
524    }
525
526    // Errores normales de Firebase
527    let message = 'Error al acceder. Intenta nuevamente.';
528
529    if (error.code === 'auth/wrong-password') {
530      message = 'Contraseña incorrecta.';
531      showError('password', message);
532    } else if (error.code === 'auth/invalid-email') {
533      message = 'Email inválido.';
534      showError('email', message);
535    } else if (error.code === 'auth/invalid-login-credentials') {
536      message = 'Credenciales incorrectas. Verificá tu email y contraseña.';
537    } else if (error.code === 'auth/too-many-requests') {
538      message = 'Demasiados intentos fallidos. Intenta más tarde.';
539    } else {
540      message = error.message || message;
541    }
542
543    showError('general', message);
544    resetRecaptcha();
545  } finally {
546    showLoading(false);
547  }
548}
549
550/**
551 * Se encarga de:
552 * - obtener idToken
553 * - pegarle a WP eon_frontend_login
554 * - redirigir
555 * 
556 * @param {object} userCredential - Credencial de Firebase
557 * @param {string} email - Email del usuario
558 * @param {boolean} remember - Mantener sesión
559 * @param {boolean} skipRecaptcha - Si es true, no incluye validación de reCAPTCHA (para logins sociales)
560 */
561async function finalizeWpLoginWithFirebaseCredential(userCredential, email, remember, skipRecaptcha = false) {
562  const idToken = await userCredential.user.getIdToken();
563  const recaptchaToken = skipRecaptcha ? '' : await getRecaptchaToken('login');
564
565  const response = await fetch(window.eonFbLogin.ajaxUrl, {
566    method: 'POST',
567    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
568    body: new URLSearchParams({
569      action: 'eon_frontend_login',
570      nonce: window.eonFbLogin.nonce,
571      email,
572      id_token: idToken,
573      remember: remember ? 'true' : 'false',
574      g_recaptcha_response: recaptchaToken || '',
575      redirect_to: document.getElementById('eon_login_redirect')?.value || '',
576    }),
577  });
578
579  const result = await response.json();
580
581  if (result.success) {
582    showSuccess(result.data?.message || 'Acceso exitoso.');
583    setTimeout(() => {
584      window.location.href = result.data?.redirect || window.location.href;
585    }, 800);
586  } else {
587    showError('general', result.data?.message || 'Error en validación del servidor.');
588    if (result.data?.recaptcha_error) resetRecaptcha();
589  }
590}
591
592/**
593 * Google Sign-In (Popup) + logueo WP con ID token
594 * NO requiere reCAPTCHA porque Google ya tiene su propia verificación
595 */
596async function handleGoogleLogin(e) {
597  e.preventDefault();
598
599  if (!firebaseAuth) {
600    showError('general', 'Firebase no está inicializado. Recarga la página.');
601    return;
602  }
603
604  try {
605    showLoading(true);
606
607    const provider = new firebase.auth.GoogleAuthProvider();
608    const result = await firebaseAuth.signInWithPopup(provider);
609
610    const email = result?.user?.email || '';
611    // NO obtenemos reCAPTCHA aquí - Google ya valida
612
613    const response = await fetch(window.eonFbLogin.ajaxUrl, {
614      method: 'POST',
615      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
616      body: new URLSearchParams({
617        action: 'eon_frontend_login',
618        nonce: window.eonFbLogin.nonce,
619        email,
620        id_token: await result.user.getIdToken(),
621        remember: 'true',
622        g_recaptcha_response: '', // Vacío porque Google ya validó
623        redirect_to: document.getElementById('eon_login_redirect')?.value || '',
624      }),
625    });
626
627    const data = await response.json();
628
629    if (data.success) {
630      showSuccess(data.data?.message || 'Acceso exitoso. Redirigiendo...');
631      setTimeout(() => {
632        window.location.href = data.data?.redirect || window.location.href;
633      }, 800);
634    } else {
635      showError('general', data.data?.message || 'No se pudo validar el login con el servidor.');
636      if (data.data?.recaptcha_error) resetRecaptcha();
637    }
638  } catch (err) {
639    console.error('Google login error:', err);
640    showError('general', err?.message || 'Error al iniciar sesión con Google.');
641  } finally {
642    showLoading(false);
643  }
644}
645
646/**
647 * Apple Sign-In (Popup) + logueo WP con ID token
648 * NO requiere reCAPTCHA porque Apple ya tiene su propia verificación
649 */
650async function handleAppleLogin(e) {
651  e.preventDefault();
652
653  if (!firebaseAuth) {
654    showError('general', 'Firebase no está inicializado. Recarga la página.');
655    return;
656  }
657
658  try {
659    showLoading(true);
660
661    const provider = new firebase.auth.OAuthProvider('apple.com');
662    // Solicitar acceso al email (opcional pero recomendado)
663    provider.addScope('email');
664    provider.addScope('name');
665    
666    const result = await firebaseAuth.signInWithPopup(provider);
667
668    const email = result?.user?.email || '';
669    // NO obtenemos reCAPTCHA aquí - Apple ya valida
670
671    const response = await fetch(window.eonFbLogin.ajaxUrl, {
672      method: 'POST',
673      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
674      body: new URLSearchParams({
675        action: 'eon_frontend_login',
676        nonce: window.eonFbLogin.nonce,
677        email,
678        id_token: await result.user.getIdToken(),
679        remember: 'true',
680        g_recaptcha_response: '', // Vacío porque Apple ya validó
681        redirect_to: document.getElementById('eon_login_redirect')?.value || '',
682      }),
683    });
684
685    const data = await response.json();
686
687    if (data.success) {
688      showSuccess(data.data?.message || 'Acceso exitoso. Redirigiendo...');
689      setTimeout(() => {
690        window.location.href = data.data?.redirect || window.location.href;
691      }, 800);
692    } else {
693      showError('general', data.data?.message || 'No se pudo validar el login con el servidor.');
694      if (data.data?.recaptcha_error) resetRecaptcha();
695    }
696  } catch (err) {
697    console.error('Apple login error:', err);
698    
699    // Manejar errores específicos de Apple
700    let errorMessage = 'Error al iniciar sesión con Apple.';
701    if (err?.code === 'auth/popup-closed-by-user') {
702      errorMessage = 'Cerraste la ventana de login. Intenta nuevamente.';
703    } else if (err?.code === 'auth/popup-blocked') {
704      errorMessage = 'El popup fue bloqueado. Verifica la configuración del navegador.';
705    } else if (err?.message) {
706      errorMessage = err.message;
707    }
708    
709    showError('general', errorMessage);
710  } finally {
711    showLoading(false);
712  }
713}
714
715/**
716 * Muestra un error en el formulario
717 */
718function showError(field, message) {
719  if (field === 'general') {
720    clearError('email');
721    clearError('password');
722  }
723
724  const errorElement = document.getElementById(`eon_login_${field}_error`);
725  if (errorElement) {
726    errorElement.textContent = message;
727    errorElement.style.display = 'block';
728  }
729
730  const generalError = document.getElementById('eon_login_error');
731  if (field === 'general' && generalError) {
732    generalError.textContent = message;
733    generalError.style.display = 'block';
734  }
735}
736
737function clearError(field, form = 'login') {
738  const errorElement = document.getElementById(`eon_${form}_${field}_error`);
739  if (errorElement) {
740    errorElement.textContent = '';
741    errorElement.style.display = 'none';
742  }
743}
744
745function showLoading(show) {
746  const loadingElement = document.getElementById('eon_login_loading');
747  const submitButton = document.getElementById('eon_login_submit');
748
749  if (loadingElement) loadingElement.style.display = show ? 'flex' : 'none';
750
751  if (submitButton) {
752    submitButton.disabled = show;
753    if (show) {
754      submitButton.dataset.originalText = submitButton.textContent;
755      submitButton.textContent = 'Accediendo...';
756    } else if (submitButton.dataset.originalText) {
757      submitButton.textContent = submitButton.dataset.originalText;
758    }
759  }
760}
761
762function showSuccess(message) {
763  const successElement = document.getElementById('eon_login_success');
764  if (successElement) {
765    successElement.textContent = message;
766    successElement.style.display = 'block';
767  }
768
769  const errorElement = document.getElementById('eon_login_error');
770  if (errorElement) errorElement.style.display = 'none';
771}
772
773/**
774 * Muestra el paso de verificación OTP/2FA después del login de admin
775 */
776function showOtpStep(otpToken, message) {
777  // Ocultar el formulario de login y mostrar el paso OTP
778  const loginContainer = document.getElementById('eonLoginContainer');
779  const otpContainer = document.getElementById('eonOtpContainer');
780
781  if (loginContainer) loginContainer.classList.remove('active');
782
783  if (otpContainer) {
784    otpContainer.classList.add('active');
785    otpContainer.style.display = '';
786    otpContainer.dataset.otpToken = otpToken;
787
788    const otpInput = document.getElementById('eon_otp_code');
789    if (otpInput) {
790      otpInput.value = '';
791      otpInput.focus();
792    }
793
794    const otpMessage = document.getElementById('eon_otp_message');
795    if (otpMessage && message) {
796      otpMessage.textContent = message;
797    }
798  }
799}
800
801/**
802 * Envía el código OTP para verificación
803 */
804async function submitOtpCode() {
805  const otpContainer = document.getElementById('eonOtpContainer');
806  const otpInput = document.getElementById('eon_otp_code');
807  const otpError = document.getElementById('eon_otp_error');
808  const otpLoading = document.getElementById('eon_otp_loading');
809  const otpSubmitBtn = document.getElementById('eon_otp_submit');
810
811  const code = otpInput?.value.trim() || '';
812  const otpToken = otpContainer?.dataset.otpToken || '';
813
814  if (!/^\d{6}$/.test(code)) {
815    if (otpError) {
816      otpError.textContent = 'Ingresá un código de 6 dígitos.';
817      otpError.style.display = '';
818    }
819    return;
820  }
821
822  if (otpError) otpError.style.display = 'none';
823  if (otpLoading) otpLoading.style.display = '';
824  if (otpSubmitBtn) otpSubmitBtn.disabled = true;
825
826  try {
827    const response = await fetch(window.eonFbLogin.ajaxUrl, {
828      method: 'POST',
829      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
830      body: new URLSearchParams({
831        action: 'eon_verify_otp',
832        nonce: window.eonFbLogin.nonce,
833        otp_token: otpToken,
834        otp_code: code,
835      }),
836    });
837
838    const result = await response.json();
839
840    if (result.success) {
841      // Show success in OTP container
842      const otpSuccessEl = document.getElementById('eon_login_success_otp');
843      if (otpSuccessEl) {
844        otpSuccessEl.textContent = 'Acceso exitoso. Redirigiendo...';
845        otpSuccessEl.style.display = 'block';
846      }
847      setTimeout(() => {
848        window.location.href = result.data?.redirect || window.location.href;
849      }, 800);
850    } else {
851      if (result.data?.expired) {
852        // Token expirado — volver al login
853        if (otpError) {
854          otpError.textContent = result.data.message;
855          otpError.style.display = '';
856        }
857        setTimeout(() => {
858          if (otpContainer) {
859            otpContainer.classList.remove('active');
860            otpContainer.style.display = 'none';
861          }
862          const loginContainer = document.getElementById('eonLoginContainer');
863          if (loginContainer) loginContainer.classList.add('active');
864          showError('general', 'La sesión expiró. Volvé a iniciar sesión.');
865        }, 2000);
866      } else {
867        if (otpError) {
868          otpError.textContent = result.data?.message || 'Código incorrecto.';
869          otpError.style.display = '';
870        }
871        if (otpInput) {
872          otpInput.value = '';
873          otpInput.focus();
874        }
875      }
876    }
877  } catch (error) {
878    console.error('Error verificando OTP:', error);
879    if (otpError) {
880      otpError.textContent = 'Error de conexión. Intentá de nuevo.';
881      otpError.style.display = '';
882    }
883  } finally {
884    if (otpLoading) otpLoading.style.display = 'none';
885    if (otpSubmitBtn) otpSubmitBtn.disabled = false;
886  }
887}
888
889/**
890 * Flip Card - Transición entre formularios
891 */
892function flipCard(form) {
893  const loginForm = document.getElementById('eonLoginContainer');
894  const registerForm = document.getElementById('eonRegisterForm');
895  const resetForm = document.getElementById('eonResetForm');
896  const otpForm = document.getElementById('eonOtpContainer');
897
898  const allForms = [loginForm, registerForm, resetForm].filter(Boolean);
899
900  allForms.forEach((f) => {
901    const inputs = f.querySelectorAll('input');
902    inputs.forEach((input) => (input.value = ''));
903    const errors = f.querySelectorAll('.eon-error');
904    errors.forEach((error) => (error.textContent = ''));
905    const alertErrors = f.querySelectorAll('.eon-alert--error');
906    alertErrors.forEach((alert) => (alert.style.display = 'none'));
907    const alertSuccess = f.querySelectorAll('.eon-alert--success');
908    alertSuccess.forEach((alert) => (alert.style.display = 'none'));
909  });
910
911  allForms.forEach((f) => f.classList.remove('active'));
912
913  // Always hide OTP container when flipping
914  if (otpForm) {
915    otpForm.classList.remove('active');
916    otpForm.style.display = 'none';
917  }
918
919  if (form === 'login' && loginForm) loginForm.classList.add('active');
920  if (form === 'register' && registerForm) registerForm.classList.add('active');
921  if (form === 'reset' && resetForm) resetForm.classList.add('active');
922}
923
924/**
925 * Handler para registro (lo dejo como estaba en tu versión; no lo modifiqué acá)
926 * Si querés, lo pegamos completo con tus cambios actuales en otro mensaje.
927 */
928/**
929 * Handler para registro
930 * - Paso 1: valida en WP (eon_register_validate) -> puede devolver mode=register|resync
931 * - Paso 2: Firebase:
932 *    - register: createUserWithEmailAndPassword
933 *    - resync: signInWithEmailAndPassword; si no existe, createUserWithEmailAndPassword
934 * - Paso 3: finaliza en WP (eon_register)
935 */
936async function handleRegisterSubmit(e) {
937  e.preventDefault();
938
939  const name = document.getElementById('eon_register_name')?.value.trim() || '';
940  const lastName = document.getElementById('eon_register_last_name')?.value.trim() || '';
941  const dni = document.getElementById('eon_register_dni')?.value.trim() || '';
942  const memberNumber = document.getElementById('eon_register_member_number')?.value.trim() || '';
943  const birthdate = document.getElementById('eon_register_birthdate')?.value || '';
944  const email = document.getElementById('eon_register_email')?.value.trim() || '';
945  const password = document.getElementById('eon_register_password')?.value || '';
946  const passwordConfirm = document.getElementById('eon_register_password_confirm')?.value || '';
947
948  // (Opcional) Validaciones básicas frontend si querés (dejé tu backend como source of truth)
949  // Ejemplo mínimo:
950  if (!name || !lastName || !dni || !birthdate || !email || !password || !passwordConfirm) {
951    showRegisterError('general', 'Todos los campos obligatorios son requeridos.');
952    return;
953  }
954  if (!email.includes('@')) {
955    showRegisterError('general', 'Email inválido.');
956    return;
957  }
958  if (!/^\d{7,8}$/.test(dni)) {
959    showRegisterError('general', 'El DNI debe tener entre 7 y 8 números, sin puntos ni guiones.');
960    return;
961  }
962  // El minimo lo dicta el PHP (Credentials::MIN_PASSWORD) y viaja por wp_localize_script.
963  // El fallback existe solo por si el localize no llego; si el front validara un numero
964  // propio, aceptaria claves que el servidor despues rechaza.
965  const minPassword = window.eonFbLogin?.minPassword || 8;
966  if (password.length < minPassword) {
967    showRegisterError('general', `La contraseña debe tener mínimo ${minPassword} caracteres.`);
968    return;
969  }
970  if (password !== passwordConfirm) {
971    showRegisterError('general', 'Las contraseñas no coinciden.');
972    return;
973  }
974
975  showRegisterLoading(true);
976
977  let firebaseUser = null;
978  let createdInFirebase = false;
979
980  try {
981    const recaptchaToken = await getRecaptchaToken('register');
982
983    // 1) VALIDAR EN WP PRIMERO (no crea nada)
984    const validateResp = await fetch(window.eonFbLogin.ajaxUrl, {
985      method: 'POST',
986      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
987      body: new URLSearchParams({
988        action: 'eon_register_validate',
989        nonce: window.eonFbLogin.nonce,
990        name,
991        last_name: lastName,
992        dni,
993        member_number: memberNumber,
994        birthdate,
995        email,
996        password,
997        password_confirm: passwordConfirm,
998        g_recaptcha_response: recaptchaToken || '',
999      }),
1000    });
1001
1002    const validateJson = await validateResp.json();
1003
1004    if (!validateJson.success) {
1005      const msg = buildContactMessage(validateJson.data);
1006      showRegisterError('general', msg);
1007      if (validateJson.data?.recaptcha_error) resetRecaptcha('register');
1008      return;
1009    }
1010
1011    const mode = validateJson.data?.mode || 'register'; // register | resync
1012
1013    // 2) FIREBASE
1014    if (!firebaseAuth) throw new Error('Firebase no está inicializado. Recarga la página.');
1015
1016    let userCredential;
1017
1018    if (mode === 'resync') {
1019      // Intentar login; si no existe, crearlo
1020      try {
1021        userCredential = await firebaseAuth.signInWithEmailAndPassword(email, password);
1022        createdInFirebase = false;
1023      } catch (err) {
1024        if (err?.code === 'auth/user-not-found') {
1025          userCredential = await firebaseAuth.createUserWithEmailAndPassword(email, password);
1026          createdInFirebase = true;
1027        } else {
1028          throw err;
1029        }
1030      }
1031    } else {
1032      // Registro normal
1033      userCredential = await firebaseAuth.createUserWithEmailAndPassword(email, password);
1034      createdInFirebase = true;
1035    }
1036
1037    firebaseUser = userCredential.user;
1038
1039    // Actualizar displayName (si se puede)
1040    try {
1041      await firebaseUser.updateProfile({ displayName: name });
1042    } catch (e2) {
1043      console.warn('No se pudo actualizar displayName en Firebase:', e2);
1044    }
1045
1046    const idToken = await firebaseUser.getIdToken(true);
1047
1048    // 3) FINALIZAR EN WP (crear/linkear user + metas + login)
1049    // IMPORTANTE: NO enviamos g_recaptcha_response aquí porque ya fue validado en eon_register_validate
1050    // Reutilizar el mismo token causa error "timeout-or-duplicate" de Google
1051    const finalizeResp = await fetch(window.eonFbLogin.ajaxUrl, {
1052      method: 'POST',
1053      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1054      body: new URLSearchParams({
1055        action: 'eon_register',
1056        nonce: window.eonFbLogin.nonce,
1057        name,
1058        last_name: lastName,
1059        dni,
1060        member_number: memberNumber,
1061        birthdate,
1062        email,
1063        password,
1064        password_confirm: passwordConfirm,
1065        firebase_uid: firebaseUser.uid,
1066        firebase_id_token: idToken,
1067        mode,
1068      }),
1069    });
1070
1071    const data = await finalizeResp.json();
1072
1073    if (data.success) {
1074      showRegisterSuccess(data.data?.message || '¡Cuenta creada / sincronizada exitosamente! Redirigiendo...');
1075      setTimeout(() => {
1076        window.location.href = data.data?.redirect || window.location.href;
1077      }, 800);
1078    } else {
1079      showRegisterError('general', data.data?.message || 'Error al vincular cuenta con WordPress.');
1080
1081      // ROLLBACK solo si nosotros LO CREAMOS recién en Firebase
1082      if (firebaseUser && createdInFirebase) {
1083        try {
1084          await firebaseUser.delete();
1085        } catch (err) {
1086          console.warn('No se pudo borrar el usuario de Firebase:', err);
1087        }
1088      }
1089    }
1090  } catch (error) {
1091    console.error('Error en registro:', error);
1092
1093    // Mensajes más amigables por errores típicos
1094    let msg = error?.message || 'Error al registrar. Intenta nuevamente.';
1095    // Un SyntaxError acá significa que el servidor devolvio algo que no era JSON
1096    // (un fatal de PHP, un warning que ensucio la salida, un 502 del hosting).
1097    // Sin este caso el usuario veia el mensaje crudo del parser, que no le dice nada.
1098    if (error instanceof SyntaxError || error?.name === 'SyntaxError') {
1099      msg = 'Error en el servidor. Por favor intentá nuevamente o contactá soporte.';
1100    } else if (error?.code === 'auth/email-already-in-use') {
1101      msg = 'Ese email ya está registrado. Si ya tenías cuenta, iniciá sesión o recuperá tu contraseña.';
1102    } else if (error?.code === 'auth/wrong-password') {
1103      msg = 'La contraseña no coincide con la cuenta existente. Probá iniciar sesión o recuperá tu contraseña.';
1104    } else if (error?.code === 'auth/too-many-requests') {
1105      msg = 'Demasiados intentos. Intentá más tarde.';
1106    }
1107
1108    showRegisterError('general', msg);
1109    resetRecaptcha('register');
1110  } finally {
1111    showRegisterLoading(false);
1112  }
1113}
1114
1115/**
1116 * Handler para recuperación de contraseña
1117 */
1118async function handleResetSubmit(e) {
1119  e.preventDefault();
1120
1121  const email = document.getElementById('eon_reset_email')?.value.trim() || '';
1122
1123  if (!email || !email.includes('@')) {
1124    showResetError('email', 'Email inválido.');
1125    return;
1126  }
1127
1128  if (isRecaptchaV2Present('reset')) {
1129    const widgetId = window.eonRecaptchaWidgets?.reset;
1130    if (!window.grecaptcha?.getResponse?.(widgetId)) {
1131      showResetError('general', 'Por favor completa el reCAPTCHA.');
1132      return;
1133    }
1134  }
1135
1136  showResetLoading(true);
1137
1138  try {
1139    const recaptchaToken = await getRecaptchaToken('reset');
1140
1141    const response = await fetch(window.eonFbLogin.ajaxUrl, {
1142      method: 'POST',
1143      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1144      body: new URLSearchParams({
1145        action: 'eon_password_reset',
1146        nonce: window.eonFbLogin.nonce,
1147        email,
1148        g_recaptcha_response: recaptchaToken || '',
1149      }),
1150    });
1151
1152    const result = await response.json();
1153
1154    if (result.success) {
1155      // Si tu backend indica use_firebase, usamos sendPasswordResetEmail
1156      if (result.data?.use_firebase) {
1157        if (!firebaseAuth) throw new Error('Firebase Auth not initialized');
1158        await firebaseAuth.sendPasswordResetEmail(email);
1159        showResetSuccess('Te hemos enviado un enlace a tu correo para restablecer tu contraseña (vía Firebase).');
1160      } else {
1161        showResetSuccess(result.data?.message || 'Si el email existe, recibirás un enlace para recuperar tu contraseña.');
1162      }
1163
1164      setTimeout(() => flipCard('login'), 4000);
1165    } else {
1166      showResetError('general', result.data?.message || 'Error al enviar el email.');
1167      // Con argumento: sin el, reseteaba el widget de LOGIN y dejaba el del panel de
1168      // reset con su token ya consumido, asi que el segundo intento fallaba siempre.
1169      if (result.data?.recaptcha_error) resetRecaptcha('reset');
1170    }
1171  } catch (error) {
1172    console.error('Error en reset:', error);
1173    showResetError('general', 'Error al enviar el email. Intenta nuevamente.');
1174    resetRecaptcha('reset');
1175  } finally {
1176    showResetLoading(false);
1177  }
1178}
1179
1180
1181// Auxiliares registro/reset (los dejo iguales)
1182function showRegisterError(field, message) {
1183  const errorElement = document.getElementById(`eon_register_${field}_error`);
1184  if (errorElement && field !== 'general') {
1185    errorElement.textContent = message;
1186    errorElement.style.display = 'block';
1187  }
1188  const generalError = document.getElementById('eon_register_error');
1189  if (field === 'general' && generalError) {
1190    generalError.textContent = message;
1191    generalError.style.display = 'block';
1192  }
1193}
1194
1195function showRegisterLoading(show) {
1196  const loadingElement = document.getElementById('eon_register_loading');
1197  const submitButton = document.getElementById('eon_register_submit');
1198
1199  if (loadingElement) loadingElement.style.display = show ? 'flex' : 'none';
1200
1201  if (submitButton) {
1202    submitButton.disabled = show;
1203    if (show) {
1204      submitButton.dataset.originalText = submitButton.textContent;
1205      submitButton.textContent = 'Registrando...';
1206    } else if (submitButton.dataset.originalText) {
1207      submitButton.textContent = submitButton.dataset.originalText;
1208    }
1209  }
1210}
1211
1212function showRegisterSuccess(message) {
1213  const successElement = document.getElementById('eon_register_success');
1214  if (successElement) {
1215    successElement.textContent = message;
1216    successElement.style.display = 'block';
1217  }
1218
1219  const errorElement = document.getElementById('eon_register_error');
1220  if (errorElement) errorElement.style.display = 'none';
1221}
1222
1223function showResetError(field, message) {
1224  const errorElement = document.getElementById(`eon_reset_${field}_error`);
1225  if (errorElement && field !== 'general') {
1226    errorElement.textContent = message;
1227    errorElement.style.display = 'block';
1228  }
1229  const generalError = document.getElementById('eon_reset_error');
1230  if (field === 'general' && generalError) {
1231    generalError.textContent = message;
1232    generalError.style.display = 'block';
1233  }
1234}
1235
1236function showResetLoading(show) {
1237  const loadingElement = document.getElementById('eon_reset_loading');
1238  const submitButton = document.getElementById('eon_reset_submit');
1239
1240  if (loadingElement) loadingElement.style.display = show ? 'flex' : 'none';
1241
1242  if (submitButton) {
1243    submitButton.disabled = show;
1244    if (show) {
1245      submitButton.dataset.originalText = submitButton.textContent;
1246      submitButton.textContent = 'Enviando...';
1247    } else if (submitButton.dataset.originalText) {
1248      submitButton.textContent = submitButton.dataset.originalText;
1249    }
1250  }
1251}
1252
1253function showResetSuccess(message) {
1254  const successElement = document.getElementById('eon_reset_success');
1255  if (successElement) {
1256    successElement.textContent = message;
1257    successElement.style.display = 'block';
1258  }
1259
1260  const errorElement = document.getElementById('eon_reset_error');
1261  if (errorElement) errorElement.style.display = 'none';
1262}
1263
1264/**
1265 * Construye un mensaje de error con información de contacto si está disponible
1266 */
1267function buildContactMessage(data) {
1268  if (!data) return 'Error en la validación.';
1269  
1270  let msg = data.message || 'Error en la validación.';
1271  
1272  // Si hay información de contacto, agregarla al mensaje
1273  if (data.contact && typeof data.contact === 'object') {
1274    const contact = data.contact;
1275    if (contact.owner_email || contact.owner_phone) {
1276      msg += '\n\nContactá con: ';
1277      if (contact.owner_email) {
1278        msg += contact.owner_email;
1279      }
1280      if (contact.owner_phone) {
1281        msg += ` (${contact.owner_phone})`;
1282      }
1283    }
1284  }
1285  
1286  return msg;
1287}
1288
1289/**
1290 * Toggle Password Visibility
1291 */
1292const EYE_SVG = `
1293<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1294     fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1295  <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path>
1296  <circle cx="12" cy="12" r="3"></circle>
1297</svg>`;
1298
1299const EYE_OFF_SVG = `
1300<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1301     fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1302  <path d="M10.58 10.58A2 2 0 0 0 12 14a2 2 0 0 0 1.42-.58"></path>
1303  <path d="M9.88 4.24A10.94 10.94 0 0 1 12 4c6.5 0 10 8 10 8a19.2 19.2 0 0 1-3.17 4.57"></path>
1304  <path d="M6.61 6.61A19.1 19.1 0 0 0 2 12s3.5 8 10 8c1.5 0 2.9-.35 4.15-.96"></path>
1305  <line x1="2" y1="2" x2="22" y2="22"></line>
1306</svg>`;
1307
1308function togglePasswordVisibility(id) {
1309  const input = document.getElementById(id);
1310  const icon = document.getElementById(id + '_icon');
1311  if (!input || !icon) return;
1312
1313  const isHidden = input.type === 'password';
1314  input.type = isHidden ? 'text' : 'password';
1315
1316  icon.innerHTML = isHidden ? EYE_OFF_SVG : EYE_SVG;
1317  icon.setAttribute('aria-label', isHidden ? 'Ocultar contraseña' : 'Mostrar contraseña');
1318}

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.