PageSourceSearch

https://wradar.com.br/js/wradar-auth-sync.js?v=71862

js wradar.com.br collected 2026-10-05 19:53:21 UTC 22,819 bytes, 875 lines download raw bytes

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.