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.