1import { initializeApp, getApp, getApps } from 'https://www.gstatic.com/firebasejs/12.16.0/firebase-app.js'; 2import { 3 GoogleAuthProvider, 4 browserLocalPersistence, 5 getAuth, 6 onAuthStateChanged, 7 setPersistence, 8 signInWithPopup, 9 signOut 10} from 'https://www.gstatic.com/firebasejs/12.16.0/firebase-auth.js'; 11import { 12 doc, 13 getDoc, 14 getFirestore, 15 serverTimestamp, 16 setDoc 17} from 'https://www.gstatic.com/firebasejs/12.16.0/firebase-firestore.js'; 18 19const CONFIG_URL = '/api/public/firebase-config'; 20const SYNC_META_KEY = 'wradar_auth_local_updated_at'; 21const AUTH_INTERNAL_PREFIX = 'wradar_auth_'; 22const USER_SYNC_MARKER_PREFIX = `${AUTH_INTERNAL_PREFIX}device_seeded_`; 23const TERMS_VERSION = '2026-07-22'; 24const TERMS_ACCEPTANCE_KEY = `wradar_terms_acceptance_${TERMS_VERSION}`; 25const SYNC_DEBOUNCE_MS = 2000; 26const EXPLICIT_SYNC_KEYS = new Set([ 27 'theme', 28 'view', 29 'language', 30 'favorites', 31 'favoriteCompetitions', 32 'competitionCollapseState', 33 'allCompetitionsCollapsed', 34 'eventViewMode_v2', 35 'oddsFilters', 36 'timeFilter', 37 'wradar_show_favorites_only', 38 'wradar_tour_completed' 39]); 40const PREFIX_SYNC_KEYS = ['wradar_', 'svcr_']; 41 42const state = { 43 ready: false, 44 available: false, 45 busy: false, 46 user: null, 47 status: 'guest', 48 message: '', 49 lastSyncedAt: 0 50}; 51 52let firebaseConfig = null; 53let firestore = null; 54let auth = null; 55let googleProvider = null; 56let remoteCreatedAtMs = null; 57let syncTimer = null; 58let applyingRemoteSettings = false; 59let uiBound = false; 60let lastSyncedSignature = ''; 61 62function hasAcceptedTerms() { 63 return safeStorageGet(TERMS_ACCEPTANCE_KEY) === 'accepted'; 64} 65 66function storeTermsAcceptance() { 67 safeStorageSet(TERMS_ACCEPTANCE_KEY, 'accepted'); 68 safeStorageSet('wradar_terms_acceptance_version', TERMS_VERSION); 69 safeStorageSet('wradar_terms_acceptance_at', String(Date.now())); 70} 71 72function getTranslations() { 73 return window.translations || {}; 74} 75 76function getCurrentLanguage() { 77 return window.currentLanguage || document.documentElement.lang?.slice(0, 2) || 'pt'; 78} 79 80function t(key, fallback = '') { 81 const translations = getTranslations(); 82 const language = getCurrentLanguage(); 83 return translations[language]?.[key] 84 ?? translations.pt?.[key] 85 ?? translations.en?.[key] 86 ?? fallback; 87} 88 89function updateState(nextState = {}) { 90 Object.assign(state, nextState); 91 document.dispatchEvent(new CustomEvent('wradar:auth-state', { 92 detail: { ...state } 93 })); 94 renderAuthUi(); 95} 96 97function safeStorageGet(key) { 98 try { 99 return window.localStorage.getItem(key); 100 } catch (error) { 101 return null; 102 } 103} 104 105function safeStorageSet(key, value) { 106 try { 107 window.localStorage.setItem(key, value); 108 return true; 109 } catch (error) { 110 return false; 111 } 112} 113 114function readLocalUpdatedAt() { 115 const raw = safeStorageGet(SYNC_META_KEY); 116 const value = Number(raw || 0); 117 return Number.isFinite(value) ? value : 0; 118} 119 120function writeLocalUpdatedAt(timestamp = Date.now()) { 121 safeStorageSet(SYNC_META_KEY, String(timestamp)); 122 return timestamp; 123} 124 125function getUserSyncMarkerKey(uid) { 126 return `${USER_SYNC_MARKER_PREFIX}${uid}`; 127} 128 129function hasUserSyncMarker(uid) { 130 return safeStorageGet(getUserSyncMarkerKey(uid)) === '1'; 131} 132 133function markUserSyncMarker(uid) { 134 safeStorageSet(getUserSyncMarkerKey(uid), '1'); 135} 136 137function shouldSyncKey(key) { 138 if (!key || typeof key !== 'string') { 139 return false; 140 } 141 142 if (key === SYNC_META_KEY || key.startsWith(AUTH_INTERNAL_PREFIX)) { 143 return false; 144 } 145 146 if (EXPLICIT_SYNC_KEYS.has(key)) { 147 return true; 148 } 149 150 return PREFIX_SYNC_KEYS.some((prefix) => key.startsWith(prefix)); 151} 152 153function collectLocalSettings() { 154 const settings = {}; 155 156 try { 157 for (let index = 0; index < window.localStorage.length; index += 1) { 158 const key = window.localStorage.key(index); 159 if (!shouldSyncKey(key)) { 160 continue; 161 } 162 163 const value = window.localStorage.getItem(key); 164 if (typeof value === 'string') { 165 settings[key] = value; 166 } 167 } 168 } catch (error) {} 169 170 return settings; 171} 172 173function settingsDiffer(leftSettings = {}, rightSettings = {}) { 174 const leftKeys = Object.keys(leftSettings).sort(); 175 const rightKeys = Object.keys(rightSettings).sort(); 176 177 if (leftKeys.length !== rightKeys.length) { 178 return true; 179 } 180 181 return leftKeys.some((key, index) => { 182 if (key !== rightKeys[index]) { 183 return true; 184 } 185 return leftSettings[key] !== rightSettings[key]; 186 }); 187} 188 189function buildSettingsSignature(settings = {}) { 190 const normalizedSettings = {}; 191
192 Object.keys(settings).sort().forEach((key) => { 193 normalizedSettings[key] = settings[key]; 194 }); 195 196 return JSON.stringify(normalizedSettings); 197} 198 199function applySettingsToLocalStorage(settings = {}) { 200 const currentKeys = Object.keys(collectLocalSettings()); 201 const nextKeys = new Set(Object.keys(settings || {})); 202 203 applyingRemoteSettings = true; 204 205 try { 206 currentKeys.forEach((key) => { 207 if (!nextKeys.has(key)) { 208 window.localStorage.removeItem(key); 209 } 210 }); 211 212 nextKeys.forEach((key) => { 213 const value = settings[key]; 214 if (typeof value === 'string') { 215 window.localStorage.setItem(key, value); 216 } 217 }); 218 } finally { 219 applyingRemoteSettings = false; 220 } 221} 222 223function queueSync(reason = 'update') { 224 if (!state.available || !state.user || !firestore) { 225 return; 226 } 227 228 window.clearTimeout(syncTimer); 229 syncTimer = window.setTimeout(() => { 230 syncLocalState(reason).catch((error) => { 231 updateState({ 232 busy: false, 233 status: 'error', 234 message: error?.message || 'Não foi possÃvel sincronizar agora.' 235 }); 236 }); 237 }, SYNC_DEBOUNCE_MS); 238} 239 240function patchLocalStorageWrites() { 241 if (window.__wradarAuthStoragePatched) { 242 return; 243 } 244 245 window.__wradarAuthStoragePatched = true; 246 247 const storageProto = Object.getPrototypeOf(window.localStorage); 248 const originalSetItem = storageProto.setItem; 249 const originalRemoveItem = storageProto.removeItem; 250 251 storageProto.setItem = function patchedSetItem(key, value) { 252 const result = originalSetItem.call(this, key, value); 253 254 if (!applyingRemoteSettings && shouldSyncKey(key)) { 255 writeLocalUpdatedAt(); 256 queueSync(`set:${key}`); 257 } 258 259 return result; 260 }; 261 262 storageProto.removeItem = function patchedRemoveItem(key) { 263 const result = originalRemoveItem.call(this, key); 264 265 if (!applyingRemoteSettings && shouldSyncKey(key)) { 266 writeLocalUpdatedAt(); 267 queueSync(`remove:${key}`); 268 } 269 270 return result; 271 }; 272 273 window.addEventListener('storage', (event) => { 274 if (event.storageArea !== window.localStorage || !shouldSyncKey(event.key)) { 275 return; 276 } 277 278 writeLocalUpdatedAt(); 279 queueSync(`storage:${event.key}`); 280 }); 281} 282 283function getUserDocRef(uid) { 284 return doc(firestore, 'users', uid); 285} 286 287function buildUserPayload(user, settings) { 288 const timestamp = Date.now(); 289 return { 290 schemaVersion: 1, 291 createdAtMs: remoteCreatedAtMs || timestamp, 292 updatedAtMs: timestamp, 293 updatedAt: serverTimestamp(), 294 ownership: { 295 product: 'WRadar', 296 company: 'WWBusiness' 297 }, 298 profile: { 299 uid: user.uid, 300 displayName: user.displayName || '', 301 email: user.email || '', 302 photoURL: user.photoURL || '', 303 providerId: user.providerData?.[0]?.providerId || 'google.com' 304 }, 305 settings 306 }; 307} 308 309async function pushSettingsToRemote(reason = 'manual') { 310 if (!state.user || !firestore) { 311 return; 312 } 313 314 const settings = collectLocalSettings(); 315 const settingsSignature = buildSettingsSignature(settings); 316 317 if (settingsSignature === lastSyncedSignature) { 318 markUserSyncMarker(state.user.uid); 319 updateState({ 320 busy: false, 321 status: 'synced', 322 message: t('authStatusSaved', 'Preferências sincronizadas com sucesso.') 323 }); 324 return; 325 } 326 327 const payload = buildUserPayload(state.user, settings); 328 329 updateState({ 330 busy: true, 331 status: 'syncing', 332 message: t('authStatusSyncing', 'Sincronizando preferências...') 333 }); 334 335 await setDoc(getUserDocRef(state.user.uid), payload); 336 remoteCreatedAtMs = payload.createdAtMs; 337 lastSyncedSignature = settingsSignature; 338 markUserSyncMarker(state.user.uid); 339 340 writeLocalUpdatedAt(payload.updatedAtMs); 341 updateState({ 342 busy: false, 343 status: 'synced', 344 lastSyncedAt: payload.updatedAtMs, 345 message: reason === 'restore' 346 ? t('authStatusSaved', 'Preferências sincronizadas com sucesso.') 347 : t('authStatusSaved', 'Preferências sincronizadas com sucesso.') 348 }); 349} 350 351function reloadAfterRemoteRestore() { 352 window.setTimeout(() => { 353 window.location.reload(); 354 }, 120); 355} 356 357async function restoreOrSyncSettings(user) { 358 const userRef = getUserDocRef(user.uid); 359 const snapshot = await getDoc(userRef); 360 const deviceAlreadySeeded = hasUserSyncMarker(user.uid); 361 const localUpdatedAt = readLocalUpdatedAt(); 362 const localSettings = collectLocalSettings(); 363 const localSettingsSignature = buildSettingsSignature(localSettings); 364 365 if (!snapshot.exists()) { 366 remoteCreatedAtMs = Date.now(); 367 lastSyncedSignature = ''; 368 if (Object.keys(localSettings).length) { 369 await pushSettingsToRemote('first-login'); 370 } else { 371 updateState({ 372 busy: false, 373 status: 'connected', 374 message: t('authStatusConnected', 'Conta conectada.') 375 }); 376 } 377 return; 378 } 379 380 const remoteData = snapshot.data() || {}; 381 const remoteUpdatedAt = Number(remoteData.updatedAtMs || 0); 382 const remoteSettings = remoteData.settings || {}; 383 const remoteSettingsSignature = buildSettingsSignature(remoteSettings); 384 remoteCreatedAtMs = Number(remoteData.createdAtMs || Date.now()); 385 386 if (!deviceAlreadySeeded) { 387 lastSyncedSignature = remoteSettingsSignature; 388 markUserSyncMarker(user.uid); 389 390 if (settingsDiffer(localSettings, remoteSettings)) { 391 applySettingsToLocalStorage(remoteSettings);
392 writeLocalUpdatedAt(remoteUpdatedAt || Date.now()); 393 updateState({ 394 busy: false, 395 status: 'synced', 396 lastSyncedAt: remoteUpdatedAt, 397 message: t('authStatusRestored', 'Preferências restauradas da sua conta.') 398 }); 399 reloadAfterRemoteRestore(); 400 return; 401 } 402 403 if (remoteUpdatedAt) { 404 writeLocalUpdatedAt(remoteUpdatedAt); 405 } 406 updateState({ 407 busy: false, 408 status: 'synced', 409 lastSyncedAt: remoteUpdatedAt, 410 message: t('authStatusSaved', 'Preferências sincronizadas com sucesso.') 411 }); 412 return; 413 } 414 415 if (remoteUpdatedAt >= localUpdatedAt && settingsDiffer(localSettings, remoteSettings)) { 416 applySettingsToLocalStorage(remoteSettings); 417 writeLocalUpdatedAt(remoteUpdatedAt); 418 lastSyncedSignature = remoteSettingsSignature; 419 markUserSyncMarker(user.uid); 420 updateState({ 421 busy: false, 422 status: 'synced', 423 lastSyncedAt: remoteUpdatedAt, 424 message: t('authStatusRestored', 'Preferências restauradas da sua conta.') 425 }); 426 reloadAfterRemoteRestore(); 427 return; 428 } 429 430 if (localUpdatedAt > remoteUpdatedAt && localSettingsSignature !== remoteSettingsSignature) { 431 await pushSettingsToRemote('restore'); 432 return; 433 } 434 435 lastSyncedSignature = remoteSettingsSignature || localSettingsSignature; 436 markUserSyncMarker(user.uid); 437 if (remoteUpdatedAt) { 438 writeLocalUpdatedAt(remoteUpdatedAt); 439 } 440 updateState({ 441 busy: false, 442 status: 'synced', 443 lastSyncedAt: remoteUpdatedAt, 444 message: t('authStatusSaved', 'Preferências sincronizadas com sucesso.') 445 }); 446} 447 448async function syncLocalState(reason = 'manual') { 449 if (!state.user || !firestore) { 450 return; 451 } 452 453 await pushSettingsToRemote(reason); 454} 455 456async function configureFirebase() { 457 const response = await window.fetch(CONFIG_URL, { 458 credentials: 'same-origin', 459 cache: 'no-store' 460 }); 461 const payload = await response.json(); 462 463 firebaseConfig = payload?.enabled ? payload.config : null; 464 if (!firebaseConfig) { 465 updateState({ 466 ready: true, 467 available: false, 468 busy: false, 469 status: 'unavailable', 470 message: t('authStatusUnavailable', 'Login indisponÃvel no momento.') 471 }); 472 return; 473 } 474 475 const app = getApps().length ? getApp() : initializeApp(firebaseConfig); 476 auth = getAuth(app); 477 firestore = getFirestore(app); 478 googleProvider = new GoogleAuthProvider(); 479 googleProvider.setCustomParameters({ prompt: 'select_account' }); 480 481 try { 482 await setPersistence(auth, browserLocalPersistence); 483 } catch (error) {} 484 485 updateState({ 486 ready: true, 487 available: true, 488 busy: false, 489 status: 'guest', 490 message: t('authStatusOffline', 'Entre para sincronizar favoritos e preferências.') 491 }); 492 493 onAuthStateChanged(auth, async (user) => { 494 if (!user) { 495 remoteCreatedAtMs = null; 496 lastSyncedSignature = ''; 497 updateState({ 498 user: null, 499 busy: false, 500 status: 'guest', 501 message: state.available 502 ? t('authStatusOffline', 'Entre para sincronizar favoritos e preferências.') 503 : t('authStatusUnavailable', 'Login indisponÃvel no momento.') 504 }); 505 return; 506 } 507 508 updateState({ 509 user, 510 busy: true, 511 status: 'connected', 512 message: t('authStatusConnected', 'Conta conectada.') 513 }); 514 515 try { 516 await restoreOrSyncSettings(user); 517 } catch (error) { 518 updateState({ 519 busy: false, 520 status: 'error', 521 message: error?.message || 'Não foi possÃvel sincronizar a conta.' 522 }); 523 } 524 }); 525} 526 527function getInitials(name, email) { 528 const source = `${name || ''} ${email || ''}`.trim(); 529 if (!source) { 530 return 'W'; 531 } 532 533 const parts = source.split(/\s+/).filter(Boolean); 534 return parts.slice(0, 2).map((part) => part.charAt(0).toUpperCase()).join('') || 'W'; 535} 536 537function getAuthElements() { 538 return { 539 entryButton: document.getElementById('auth-entry-button'), 540 entryLabel: document.getElementById('auth-entry-label'), 541 entryAvatar: document.getElementById('auth-entry-avatar'), 542 modal: document.getElementById('auth-modal'), 543 closeButton: document.getElementById('auth-modal-close'), 544 dismissButton: document.getElementById('auth-modal-dismiss'), 545 googleButton: document.getElementById('auth-google-button'), 546 signOutButton: document.getElementById('auth-signout-button'), 547 statusMessage: document.getElementById('auth-status-message'), 548 termsOpenButton: document.getElementById('auth-open-terms-button'), 549 termsInlineButton: document.getElementById('auth-open-terms-inline'), 550 termsAcceptance: document.getElementById('auth-terms-acceptance'), 551 profile: document.getElementById('auth-profile'), 552 profileName: document.getElementById('auth-profile-name'), 553 profileEmail: document.getElementById('auth-profile-email'), 554 termsModal: document.getElementById('terms-modal'), 555 termsCloseButton: document.getElementById('terms-modal-close'), 556 termsDismissButton: document.getElementById('terms-modal-dismiss'), 557 termsAcceptCheckbox: document.getElementById('terms-accept-checkbox'), 558 termsAcceptButton: document.getElementById('terms-accept-button') 559 }; 560} 561 562function syncBodyModalState() { 563 const { modal, termsModal } = getAuthElements(); 564 const authOpen = modal?.classList.contains('is-visible'); 565 const termsOpen = termsModal?.classList.contains('is-visible'); 566 567 document.body.classList.toggle('modal-open', Boolean(authOpen || termsOpen)); 568} 569 570function openAuthModal() { 571 const { modal } = getAuthElements(); 572 if (!modal) { 573 return; 574 } 575 576 modal.classList.add('is-visible'); 577 modal.setAttribute('aria-hidden', 'false'); 578 syncBodyModalState(); 579} 580 581function closeAuthModal() { 582 const { modal } = getAuthElements(); 583 if (!modal) { 584 return; 585 } 586 587 modal.classList.remove('is-visible'); 588 modal.setAttribute('aria-hidden', 'true'); 589 syncBodyModalState(); 590} 591 592function openTermsModal() { 593 const { termsModal, termsAcceptCheckbox, termsAcceptButton } = getAuthElements(); 594 if (!termsModal) { 595 return; 596 } 597 598 if (termsAcceptCheckbox) { 599 termsAcceptCheckbox.checked = false;
600 } 601 602 if (termsAcceptButton) { 603 termsAcceptButton.disabled = true; 604 } 605 606 termsModal.classList.add('is-visible'); 607 termsModal.setAttribute('aria-hidden', 'false'); 608 syncBodyModalState(); 609} 610 611function closeTermsModal() { 612 const { termsModal } = getAuthElements(); 613 if (!termsModal) { 614 return; 615 } 616 617 termsModal.classList.remove('is-visible'); 618 termsModal.setAttribute('aria-hidden', 'true'); 619 syncBodyModalState(); 620} 621 622function acceptTerms() { 623 storeTermsAcceptance(); 624 closeTermsModal(); 625 renderAuthUi(); 626} 627 628function wait(milliseconds) { 629 return new Promise((resolve) => { 630 window.setTimeout(resolve, milliseconds); 631 }); 632} 633 634async function handleGoogleLogin() { 635 if (!auth || !googleProvider) { 636 updateState({ 637 busy: false, 638 status: 'unavailable', 639 message: t('authStatusUnavailable', 'Login indisponÃvel no momento.') 640 }); 641 return; 642 } 643 644 if (!hasAcceptedTerms()) { 645 updateState({ 646 busy: false, 647 status: 'guest', 648 message: t('authGoogleTermsRequired', 'Aceite o termo de adesão antes de entrar com Google.') 649 }); 650 openTermsModal(); 651 return; 652 } 653 654 updateState({ 655 busy: true, 656 status: 'syncing', 657 message: t('authStatusSigningIn', 'Abrindo Google para entrar...') 658 }); 659 660 try { 661 await signInWithPopup(auth, googleProvider); 662 closeAuthModal(); 663 } catch (error) { 664 if (error?.code === 'auth/popup-closed-by-user') { 665 await wait(1800); 666 667 if (auth.currentUser) { 668 closeAuthModal(); 669 return; 670 } 671 } 672 673 updateState({ 674 busy: false, 675 status: 'error', 676 message: error?.code === 'auth/popup-closed-by-user' 677 ? t('authStatusPopupClosed', 'O login foi interrompido antes da confirmação final.') 678 : error?.code === 'auth/cancelled-popup-request' 679 ? t('authStatusPopupCancelled', 'Já existe uma tentativa de login em andamento.') 680 : (error?.message || 'Não foi possÃvel entrar com o Google.') 681 }); 682 } 683} 684 685async function handleSignOut() { 686 if (!auth) { 687 return; 688 } 689 690 try { 691 await signOut(auth); 692 closeAuthModal(); 693 } catch (error) { 694 updateState({ 695 busy: false, 696 status: 'error', 697 message: error?.message || 'Não foi possÃvel sair da conta.' 698 }); 699 } 700} 701 702function renderAuthUi() { 703 const { 704 entryButton, 705 entryLabel, 706 entryAvatar, 707 googleButton, 708 signOutButton, 709 statusMessage, 710 termsOpenButton, 711 termsAcceptance, 712 profile, 713 profileName, 714 profileEmail 715 } = getAuthElements(); 716 717 const termsAccepted = hasAcceptedTerms(); 718 719 if (entryButton) { 720 entryButton.classList.toggle('is-connected', Boolean(state.user)); 721 } 722 723 if (entryLabel) { 724 entryLabel.textContent = state.user 725 ? t('accountButton', 'Conta') 726 : t('loginButton', 'Entrar'); 727 } 728 729 if (entryAvatar) { 730 entryAvatar.replaceChildren(); 731 if (state.user?.photoURL) { 732 const image = document.createElement('img'); 733 image.src = state.user.photoURL; 734 image.alt = ''; 735 entryAvatar.appendChild(image); 736 } else if (state.user) { 737 entryAvatar.textContent = getInitials(state.user.displayName, state.user.email); 738 } else { 739 entryAvatar.textContent = ''; 740 } 741 entryAvatar.classList.toggle('is-visible', Boolean(state.user)); 742 } 743 744 if (googleButton) { 745 googleButton.disabled = !state.available || state.busy || Boolean(state.user) || !termsAccepted; 746 } 747 748 if (signOutButton) { 749 signOutButton.hidden = !state.user; 750 signOutButton.disabled = state.busy; 751 } 752 753 if (profile && profileName && profileEmail) { 754 profile.hidden = !state.user; 755 profileName.textContent = state.user?.displayName || t('authGuestTitle', 'Visitante'); 756 profileEmail.textContent = state.user?.email || ''; 757 } 758 759 if (termsOpenButton) { 760 termsOpenButton.textContent = termsAccepted 761 ? t('authTermsReviewButton', 'Revisar termo aceito') 762 : t('authOpenTermsButton', 'Ler termo de adesão'); 763 } 764 765 if (termsAcceptance) { 766 termsAcceptance.dataset.accepted = termsAccepted ? 'true' : 'false'; 767 termsAcceptance.textContent = termsAccepted 768 ? t('authTermsAccepted', 'Termo de adesão aceito. Você pode entrar com Google.') 769 : t('authTermsPending', 'Leia e aceite o termo para liberar o login com Google.'); 770 } 771 772 if (statusMessage) { 773 statusMessage.textContent = state.message || ''; 774 statusMessage.dataset.status = state.status || 'guest'; 775 } 776} 777 778function bindAuthUi() { 779 if (uiBound) { 780 return; 781 } 782 783 const { 784 entryButton, 785 modal, 786 closeButton, 787 dismissButton, 788 googleButton, 789 signOutButton, 790 termsOpenButton, 791 termsInlineButton, 792 termsModal, 793 termsCloseButton, 794 termsDismissButton, 795 termsAcceptCheckbox, 796 termsAcceptButton 797 } = getAuthElements(); 798 799 if (!entryButton && !modal) { 800 return; 801 } 802 803 uiBound = true; 804 805 entryButton?.addEventListener('click', openAuthModal); 806 closeButton?.addEventListener('click', closeAuthModal); 807 dismissButton?.addEventListener('click', closeAuthModal); 808 googleButton?.addEventListener('click', handleGoogleLogin); 809 signOutButton?.addEventListener('click', handleSignOut); 810 termsOpenButton?.addEventListener('click', openTermsModal); 811 termsInlineButton?.addEventListener('click', openTermsModal); 812 termsCloseButton?.addEventListener('click', closeTermsModal);
813 termsDismissButton?.addEventListener('click', closeTermsModal); 814 termsAcceptCheckbox?.addEventListener('change', () => { 815 if (termsAcceptButton) { 816 termsAcceptButton.disabled = !termsAcceptCheckbox.checked; 817 } 818 }); 819 termsAcceptButton?.addEventListener('click', acceptTerms); 820 821 modal?.addEventListener('click', (event) => { 822 if (event.target === modal) { 823 closeAuthModal(); 824 } 825 }); 826 827 termsModal?.addEventListener('click', (event) => { 828 if (event.target === termsModal) { 829 closeTermsModal(); 830 } 831 }); 832 833 document.addEventListener('keydown', (event) => { 834 if (event.key === 'Escape') { 835 closeTermsModal(); 836 closeAuthModal(); 837 } 838 }); 839 840 document.addEventListener('wradar:language-changed', () => { 841 renderAuthUi(); 842 }); 843 844 renderAuthUi(); 845} 846 847function exposeAuthApi() { 848 window.WRadarAuth = { 849 open: openAuthModal, 850 close: closeAuthModal, 851 signIn: handleGoogleLogin, 852 signOut: handleSignOut, 853 syncNow: () => syncLocalState('manual'), 854 getState: () => ({ ...state }) 855 }; 856} 857 858patchLocalStorageWrites(); 859exposeAuthApi(); 860 861if (document.readyState === 'loading') { 862 document.addEventListener('DOMContentLoaded', bindAuthUi, { once: true }); 863} else { 864 bindAuthUi(); 865} 866 867configureFirebase().catch((error) => { 868 updateState({ 869 ready: true, 870 available: false, 871 busy: false, 872 status: 'error', 873 message: error?.message || 'Não foi possÃvel iniciar o login.' 874 }); 875});
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.