@kaikybrofc/omnizap-system 2.2.2 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { React, createRoot, useMemo, useState, useEffect, useRef } from '../runtime/react-runtime.js?v=20260226-googlefix1';
1
+ import { React, createRoot, useMemo, useState, useEffect, useCallback } from '../runtime/react-runtime.js?v=20260228-runtime-usecallback1';
2
2
  import htm from 'https://esm.sh/htm@3.1.1';
3
3
 
4
4
  const html = htm.bind(React.createElement);
@@ -9,7 +9,6 @@ const GOOGLE_AUTH_CACHE_KEY = 'omnizap_google_web_auth_cache_v1';
9
9
  const GOOGLE_AUTH_CACHE_MAX_STALE_MS = 8 * 24 * 60 * 60 * 1000;
10
10
  const MAX_MANUAL_TAGS = 8;
11
11
  const DEFAULT_SUGGESTED_TAGS = ['anime', 'meme', 'game', 'texto', 'nsfw', 'dark', 'cartoon', 'foto-real', 'cyberpunk'];
12
- const GOOGLE_GSI_SCRIPT_SRC = 'https://accounts.google.com/gsi/client';
13
12
  const PACK_STATUS_PUBLISHED = 'published';
14
13
  const FIXED_UPLOAD_QUEUE_CONCURRENCY = 3;
15
14
  const UPLOAD_AUTO_RETRY_ATTEMPTS = 2;
@@ -51,11 +50,6 @@ const sanitizePackName = (value, maxLength = 120) =>
51
50
  .slice(0, maxLength);
52
51
 
53
52
  const toBytesLabel = (bytes) => `${Math.round(Number(bytes || 0) / 1024)} KB`;
54
- const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
55
- const isValidPhone = (value) => {
56
- const digits = normalizePhoneDigits(value);
57
- return digits.length >= 10 && digits.length <= 15;
58
- };
59
53
  const normalizeTag = (value) =>
60
54
  String(value || '')
61
55
  .trim()
@@ -80,19 +74,6 @@ const mergeTags = (...groups) => {
80
74
  return ordered;
81
75
  };
82
76
 
83
- const decodeJwtPayload = (jwt) => {
84
- const parts = String(jwt || '').split('.');
85
- if (parts.length < 2) return null;
86
- try {
87
- const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
88
- const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
89
- const decoded = atob(padded);
90
- return JSON.parse(decoded);
91
- } catch {
92
- return null;
93
- }
94
- };
95
-
96
77
  const normalizeGoogleAuthState = (value) => {
97
78
  const user = value?.user && typeof value.user === 'object' ? value.user : null;
98
79
  const sub = String(user?.sub || '').trim();
@@ -163,31 +144,6 @@ const clearGoogleAuthCache = () => {
163
144
  }
164
145
  };
165
146
 
166
- const loadScript = (src) =>
167
- new Promise((resolve, reject) => {
168
- const existing = document.querySelector(`script[src="${src}"]`);
169
- if (existing) {
170
- if (existing.dataset.loaded === '1') {
171
- resolve();
172
- return;
173
- }
174
- existing.addEventListener('load', () => resolve(), { once: true });
175
- existing.addEventListener('error', () => reject(new Error(`Falha ao carregar script: ${src}`)), { once: true });
176
- return;
177
- }
178
-
179
- const script = document.createElement('script');
180
- script.src = src;
181
- script.async = true;
182
- script.defer = true;
183
- script.addEventListener('load', () => {
184
- script.dataset.loaded = '1';
185
- resolve();
186
- });
187
- script.addEventListener('error', () => reject(new Error(`Falha ao carregar script: ${src}`)));
188
- document.head.appendChild(script);
189
- });
190
-
191
147
  const fetchJson = async (url, options = {}) => {
192
148
  const response = await fetch(url, { credentials: 'same-origin', ...options });
193
149
  const payload = await response.json().catch(() => ({}));
@@ -481,6 +437,7 @@ function CreatePackApp() {
481
437
  const root = document.getElementById('create-pack-react-root');
482
438
  const apiBasePath = root?.dataset?.apiBasePath || '/api/sticker-packs';
483
439
  const webPath = root?.dataset?.webPath || '/stickers';
440
+ const loginPath = root?.dataset?.loginPath || '/login';
484
441
  const googleSessionApiPath = `${apiBasePath}/auth/google/session`;
485
442
 
486
443
  const [step, setStep] = useState(1);
@@ -492,13 +449,9 @@ function CreatePackApp() {
492
449
  const [tags, setTags] = useState([]);
493
450
  const [tagInput, setTagInput] = useState('');
494
451
  const [suggestedTags, setSuggestedTags] = useState(DEFAULT_SUGGESTED_TAGS);
495
- const [accountId, setAccountId] = useState('');
496
- const [googleAuthConfig, setGoogleAuthConfig] = useState({ enabled: false, required: false, clientId: '' });
497
452
  const [googleAuth, setGoogleAuth] = useState(() => readGoogleAuthCache() || { user: null, expiresAt: '' });
498
- const [googleAuthUiReady, setGoogleAuthUiReady] = useState(false);
499
- const [googleAuthError, setGoogleAuthError] = useState('');
500
- const [googleAuthBusy, setGoogleAuthBusy] = useState(false);
501
453
  const [googleSessionChecked, setGoogleSessionChecked] = useState(false);
454
+ const [authRedirecting, setAuthRedirecting] = useState(false);
502
455
  const [files, setFiles] = useState([]);
503
456
  const [coverId, setCoverId] = useState('');
504
457
  const [dragActive, setDragActive] = useState(false);
@@ -514,17 +467,31 @@ function CreatePackApp() {
514
467
  const [backendPublishState, setBackendPublishState] = useState(null);
515
468
  const [draftLoaded, setDraftLoaded] = useState(false);
516
469
  const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false);
517
- const googleButtonRef = useRef(null);
518
- const googleLoginEnabled = Boolean(googleAuthConfig.enabled && googleAuthConfig.clientId);
519
- const googleLoginRequired = Boolean(googleAuthConfig.required);
520
470
  const hasGoogleLogin = Boolean(googleAuth.user?.sub);
521
- const shouldRenderGoogleButton = googleLoginEnabled && !hasGoogleLogin && googleSessionChecked && !googleAuthBusy;
471
+ const buildLoginRedirectUrl = useCallback(() => {
472
+ const nextPath = `${window.location.pathname || '/'}${window.location.search || ''}`;
473
+ const loginUrl = new URL(loginPath, window.location.origin);
474
+ loginUrl.searchParams.set('next', nextPath);
475
+ return `${loginUrl.pathname}${loginUrl.search}`;
476
+ }, [loginPath]);
477
+ const redirectToLogin = useCallback(
478
+ (reason = 'Sessão ausente. Redirecionando para login...') => {
479
+ if (authRedirecting) return;
480
+ setAuthRedirecting(true);
481
+ setStatus(reason);
482
+ setError('');
483
+ window.location.assign(buildLoginRedirectUrl());
484
+ },
485
+ [authRedirecting, buildLoginRedirectUrl],
486
+ );
522
487
 
523
488
  const canStep2 = useMemo(
524
489
  () =>
525
490
  sanitizePackName(name, limits.pack_name_max_length).length > 0 &&
526
- (googleLoginRequired ? hasGoogleLogin : googleLoginEnabled ? hasGoogleLogin || isValidPhone(accountId) : isValidPhone(accountId)),
527
- [name, accountId, limits.pack_name_max_length, googleLoginRequired, googleLoginEnabled, hasGoogleLogin],
491
+ hasGoogleLogin &&
492
+ googleSessionChecked &&
493
+ !authRedirecting,
494
+ [name, limits.pack_name_max_length, hasGoogleLogin, googleSessionChecked, authRedirecting],
528
495
  );
529
496
  const canStep3 = useMemo(() => files.length > 0, [files.length]);
530
497
  const publishReady = canStep2 && canStep3 && !busy;
@@ -632,16 +599,10 @@ function CreatePackApp() {
632
599
  const payload = await fetchJson(`${apiBasePath}/create-config`);
633
600
  const apiLimits = payload?.data?.limits || {};
634
601
  const apiSuggestions = payload?.data?.rules?.suggested_tags;
635
- const apiGoogleAuth = payload?.data?.auth?.google || {};
636
602
  setLimits((prev) => ({ ...prev, ...apiLimits }));
637
603
  if (Array.isArray(apiSuggestions) && apiSuggestions.length) {
638
604
  setSuggestedTags(mergeTags(apiSuggestions).slice(0, 20));
639
605
  }
640
- setGoogleAuthConfig({
641
- enabled: Boolean(apiGoogleAuth?.enabled),
642
- required: Boolean(apiGoogleAuth?.required),
643
- clientId: String(apiGoogleAuth?.client_id || '').trim(),
644
- });
645
606
  } catch {
646
607
  // keep default
647
608
  }
@@ -650,10 +611,6 @@ function CreatePackApp() {
650
611
  }, [apiBasePath]);
651
612
 
652
613
  useEffect(() => {
653
- if (!googleLoginEnabled) {
654
- setGoogleSessionChecked(true);
655
- return;
656
- }
657
614
  let cancelled = false;
658
615
  setGoogleSessionChecked(false);
659
616
 
@@ -661,9 +618,10 @@ function CreatePackApp() {
661
618
  .then((payload) => {
662
619
  if (cancelled) return;
663
620
  const sessionData = payload?.data || {};
664
- if (!sessionData?.authenticated || !sessionData?.user?.sub) {
621
+ if (!sessionData?.authenticated || !sessionData?.user?.sub || !sessionData?.owner_jid) {
665
622
  setGoogleAuth({ user: null, expiresAt: '' });
666
623
  clearGoogleAuthCache();
624
+ redirectToLogin('Sessão inválida ou expirada. Redirecionando para login...');
667
625
  return;
668
626
  }
669
627
  const nextAuth = {
@@ -677,10 +635,10 @@ function CreatePackApp() {
677
635
  };
678
636
  setGoogleAuth(nextAuth);
679
637
  writeGoogleAuthCache(nextAuth);
680
- setGoogleAuthError('');
681
638
  })
682
639
  .catch(() => {
683
- // silent: endpoint may be unavailable in some setups
640
+ if (cancelled) return;
641
+ setError('Não foi possível validar sua sessão agora. Recarregue a página.');
684
642
  })
685
643
  .finally(() => {
686
644
  if (cancelled) return;
@@ -690,110 +648,7 @@ function CreatePackApp() {
690
648
  return () => {
691
649
  cancelled = true;
692
650
  };
693
- }, [googleLoginEnabled, googleSessionApiPath]);
694
-
695
- useEffect(() => {
696
- const clearGoogleButton = () => {
697
- if (googleButtonRef.current) googleButtonRef.current.innerHTML = '';
698
- try {
699
- window.google?.accounts?.id?.cancel?.();
700
- } catch {
701
- // ignore sdk errors
702
- }
703
- };
704
-
705
- if (!shouldRenderGoogleButton) {
706
- clearGoogleButton();
707
- return;
708
- }
709
- if (!googleButtonRef.current) return;
710
-
711
- let cancelled = false;
712
- setGoogleAuthUiReady(false);
713
- setGoogleAuthError('');
714
-
715
- loadScript(GOOGLE_GSI_SCRIPT_SRC)
716
- .then(() => {
717
- if (cancelled) return;
718
- const accounts = window.google?.accounts?.id;
719
- if (!accounts) throw new Error('SDK do Google não disponível.');
720
-
721
- accounts.initialize({
722
- client_id: googleAuthConfig.clientId,
723
- callback: (response) => {
724
- const credential = String(response?.credential || '').trim();
725
- const claims = decodeJwtPayload(credential);
726
- if (!credential || !claims?.sub) {
727
- setGoogleAuthError('Falha ao concluir login Google.');
728
- return;
729
- }
730
- setGoogleAuthBusy(true);
731
- fetchJson(googleSessionApiPath, {
732
- method: 'POST',
733
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
734
- body: JSON.stringify({ google_id_token: credential }),
735
- })
736
- .then((sessionPayload) => {
737
- const sessionData = sessionPayload?.data || {};
738
- if (!sessionData?.authenticated || !sessionData?.user?.sub) {
739
- throw new Error('Sessão Google não foi criada.');
740
- }
741
- setGoogleAuth({
742
- user: {
743
- sub: String(sessionData.user.sub || claims.sub || ''),
744
- email: String(sessionData.user.email || claims.email || ''),
745
- name: String(sessionData.user.name || claims.name || claims.given_name || 'Conta Google'),
746
- picture: String(sessionData.user.picture || claims.picture || ''),
747
- },
748
- expiresAt: String(sessionData.expires_at || ''),
749
- });
750
- writeGoogleAuthCache({
751
- user: {
752
- sub: String(sessionData.user.sub || claims.sub || ''),
753
- email: String(sessionData.user.email || claims.email || ''),
754
- name: String(sessionData.user.name || claims.name || claims.given_name || 'Conta Google'),
755
- picture: String(sessionData.user.picture || claims.picture || ''),
756
- },
757
- expiresAt: String(sessionData.expires_at || ''),
758
- });
759
- setGoogleAuthError('');
760
- setError('');
761
- })
762
- .catch((sessionError) => {
763
- setGoogleAuthError(sessionError?.message || 'Falha ao salvar sessão Google.');
764
- })
765
- .finally(() => setGoogleAuthBusy(false));
766
- },
767
- auto_select: false,
768
- cancel_on_tap_outside: true,
769
- });
770
-
771
- if (googleButtonRef.current) {
772
- googleButtonRef.current.innerHTML = '';
773
- const measuredWidth = Math.floor(Number(googleButtonRef.current.clientWidth || 0));
774
- const buttonWidth = Math.max(180, Math.min(320, measuredWidth || 320));
775
- accounts.renderButton(googleButtonRef.current, {
776
- type: 'standard',
777
- theme: 'filled_black',
778
- size: 'large',
779
- text: 'signin_with',
780
- shape: 'pill',
781
- logo_alignment: 'left',
782
- width: buttonWidth,
783
- });
784
- }
785
- setGoogleAuthUiReady(true);
786
- })
787
- .catch((sdkError) => {
788
- if (cancelled) return;
789
- setGoogleAuthError(sdkError?.message || 'Falha ao carregar login Google.');
790
- });
791
-
792
- return () => {
793
- cancelled = true;
794
- clearGoogleButton();
795
- };
796
- }, [shouldRenderGoogleButton, googleAuthConfig.clientId, googleSessionApiPath]);
651
+ }, [googleSessionApiPath, redirectToLogin]);
797
652
 
798
653
  useEffect(() => {
799
654
  try {
@@ -814,7 +669,6 @@ function CreatePackApp() {
814
669
  if (typeof parsed.description === 'string') setDescription(parsed.description);
815
670
  if (typeof parsed.publisher === 'string') setPublisher(parsed.publisher);
816
671
  if (typeof parsed.visibility === 'string') setVisibility(parsed.visibility);
817
- if (typeof parsed.accountId === 'string') setAccountId(parsed.accountId);
818
672
  if (Array.isArray(parsed.tags)) setTags(mergeTags(parsed.tags).slice(0, MAX_MANUAL_TAGS));
819
673
  const parsedStep = Number.isFinite(Number(parsed.step)) ? Math.max(1, Math.min(3, Number(parsed.step))) : 1;
820
674
 
@@ -885,7 +739,6 @@ function CreatePackApp() {
885
739
  description,
886
740
  publisher,
887
741
  visibility,
888
- accountId,
889
742
  tags,
890
743
  coverId,
891
744
  activeSession: activeSession?.packKey && activeSession?.editToken ? activeSession : null,
@@ -911,7 +764,7 @@ function CreatePackApp() {
911
764
  } catch {
912
765
  // ignore storage errors
913
766
  }
914
- }, [draftLoaded, busy, step, name, description, publisher, visibility, accountId, tags, coverId, files, activeSession]);
767
+ }, [draftLoaded, busy, step, name, description, publisher, visibility, tags, coverId, files, activeSession]);
915
768
 
916
769
  useEffect(() => {
917
770
  if (!draftLoaded) return;
@@ -1137,13 +990,8 @@ function CreatePackApp() {
1137
990
  setStep(1);
1138
991
  return;
1139
992
  }
1140
- if (googleLoginRequired && !hasGoogleLogin) {
1141
- setError('Faça login com Google para publicar packs.');
1142
- setStep(1);
1143
- return;
1144
- }
1145
- if (!googleLoginRequired && !googleLoginEnabled && !isValidPhone(accountId)) {
1146
- setError('Informe seu número de celular com DDD para publicar.');
993
+ if (!hasGoogleLogin) {
994
+ redirectToLogin('Sua sessão expirou. Redirecionando para login...');
1147
995
  setStep(1);
1148
996
  return;
1149
997
  }
@@ -1186,11 +1034,13 @@ function CreatePackApp() {
1186
1034
  description: finalDescription,
1187
1035
  tags,
1188
1036
  visibility,
1189
- owner_jid: hasGoogleLogin ? '' : clampText(accountId, 64),
1190
1037
  }),
1191
1038
  });
1192
1039
 
1193
1040
  const createPayload = await createResponse.json().catch(() => ({}));
1041
+ if (createResponse.status === 401 || createResponse.status === 403) {
1042
+ redirectToLogin('Sua sessão expirou. Redirecionando para login...');
1043
+ }
1194
1044
  if (!createResponse.ok) throw new Error(createPayload?.error || 'Não foi possível criar o pack.');
1195
1045
 
1196
1046
  const created = createPayload?.data || {};
@@ -1466,13 +1316,11 @@ function CreatePackApp() {
1466
1316
  setError('Defina um nome para avançar.');
1467
1317
  return;
1468
1318
  }
1469
- setError(
1470
- googleLoginRequired
1471
- ? 'Faça login com Google para avançar.'
1472
- : googleLoginEnabled
1473
- ? 'Faça login com Google ou informe seu número de celular para avançar.'
1474
- : 'Informe seu número de celular com DDD para avançar.',
1475
- );
1319
+ if (!googleSessionChecked) {
1320
+ setError('Validando sua sessão. Aguarde alguns segundos...');
1321
+ return;
1322
+ }
1323
+ redirectToLogin('Sessão não encontrada. Redirecionando para login...');
1476
1324
  return;
1477
1325
  }
1478
1326
  if (step === 2 && !canStep3) {
@@ -1488,23 +1336,6 @@ function CreatePackApp() {
1488
1336
  setStep((prev) => Math.max(1, prev - 1));
1489
1337
  };
1490
1338
 
1491
- const disconnectGoogleLogin = () => {
1492
- try {
1493
- window.google?.accounts?.id?.disableAutoSelect?.();
1494
- } catch {
1495
- // ignore sdk errors
1496
- }
1497
- setGoogleAuthBusy(true);
1498
- fetchJson(googleSessionApiPath, { method: 'DELETE' })
1499
- .catch(() => null)
1500
- .finally(() => {
1501
- clearGoogleAuthCache();
1502
- setGoogleAuth({ user: null, expiresAt: '' });
1503
- setGoogleAuthBusy(false);
1504
- });
1505
- setGoogleAuthError('');
1506
- };
1507
-
1508
1339
  const restartCreateFlow = () => {
1509
1340
  if (busy) return;
1510
1341
  const confirmed = window.confirm('Recomeçar a criação? Isso vai limpar o rascunho local e o progresso salvo neste dispositivo.');
@@ -1518,7 +1349,6 @@ function CreatePackApp() {
1518
1349
  setVisibility('public');
1519
1350
  setTags([]);
1520
1351
  setTagInput('');
1521
- setAccountId('');
1522
1352
  setFiles([]);
1523
1353
  setCoverId('');
1524
1354
  setDragActive(false);
@@ -1671,69 +1501,26 @@ function CreatePackApp() {
1671
1501
  hint="Como seu nome será exibido no catálogo."
1672
1502
  onChange=${(e) => setPublisher(clampInputText(e.target.value, limits.publisher_max_length))}
1673
1503
  />
1674
- ${googleLoginEnabled
1675
- ? html`
1676
- <div className="rounded-2xl border border-line/70 bg-panel/70 p-3 md:p-4">
1677
- <div className="flex items-start justify-between gap-3">
1678
- <div>
1679
- <p className="text-xs font-semibold uppercase tracking-[.08em] text-slate-400">
1680
- ${googleLoginRequired ? 'Login obrigatório' : 'Login Google'}
1681
- </p>
1682
- <p className="mt-1 text-sm font-semibold text-slate-100">Entrar com Google</p>
1683
- <p className="mt-1 text-xs text-slate-400">
1684
- ${googleLoginRequired
1685
- ? 'Somente contas logadas podem criar packs nesta página.'
1686
- : 'Faça login para vincular o pack à sua conta Google.'}
1687
- </p>
1688
- </div>
1689
- ${hasGoogleLogin
1690
- ? html`<span className="rounded-full border border-emerald-400/40 bg-emerald-400/10 px-2.5 py-1 text-[11px] font-semibold text-emerald-300">Conectado</span>`
1691
- : null}
1692
- </div>
1693
-
1694
- ${hasGoogleLogin
1695
- ? html`
1696
- <div className="mt-3 flex items-center justify-between gap-2 rounded-xl border border-line/70 bg-panelSoft/80 p-2.5 md:gap-3 md:p-3">
1697
- <div className="min-w-0">
1698
- <p className="truncate text-sm font-semibold text-slate-100">${googleAuth.user?.name || 'Conta Google'}</p>
1699
- <p className="truncate text-xs text-slate-400">${googleAuth.user?.email || ''}</p>
1700
- </div>
1701
- <button type="button" onClick=${disconnectGoogleLogin} className="h-10 rounded-lg border border-line/70 px-3 text-xs font-semibold text-slate-200">
1702
- Trocar conta
1703
- </button>
1704
- </div>
1705
- `
1706
- : html`
1707
- <div className="mt-3">
1708
- <div ref=${googleButtonRef} className="min-h-[42px] w-full max-w-full overflow-hidden"></div>
1709
- ${!googleSessionChecked
1710
- ? html`<p className="mt-2 text-xs text-slate-400">Verificando sessão Google...</p>`
1711
- : googleAuthBusy
1712
- ? html`<p className="mt-2 text-xs text-slate-400">Conectando conta Google...</p>`
1713
- : !googleAuthUiReady && !googleAuthError
1714
- ? html`<p className="mt-2 text-xs text-slate-400">Carregando login Google...</p>`
1715
- : null}
1716
- ${googleSessionChecked && !googleAuthBusy && !shouldRenderGoogleButton && !googleAuthError
1717
- ? html`<p className="mt-2 text-xs text-slate-400">Login Google indisponível no momento. Tente recarregar a página.</p>`
1718
- : null}
1719
- </div>
1720
- `}
1721
-
1722
- ${googleAuthError ? html`<p className="mt-2 text-xs text-rose-300">${googleAuthError}</p>` : null}
1723
- </div>
1724
- `
1725
- : html`
1726
- <${FloatingField}
1727
- label="Celular (WhatsApp)"
1728
- value=${accountId}
1729
- maxLength=${32}
1730
- hint="Obrigatório para vincular o pack ao criador. Ex: 5511999998888"
1731
- onChange=${(e) => setAccountId(String(e.target.value || '').replace(/[^\d+()\-\s]/g, '').slice(0, 32))}
1732
- />
1733
- ${accountId && !isValidPhone(accountId)
1734
- ? html`<p className="text-xs text-rose-300">Informe um número válido com DDD (10 a 15 dígitos).</p>`
1735
- : null}
1736
- `}
1504
+ <div className="rounded-2xl border border-line/70 bg-panel/70 p-3 md:p-4">
1505
+ <div className="flex items-start justify-between gap-3">
1506
+ <div>
1507
+ <p className="text-xs font-semibold uppercase tracking-[.08em] text-slate-400">Sessão da conta</p>
1508
+ <p className="mt-1 text-sm font-semibold text-slate-100">Conta Google vinculada</p>
1509
+ <p className="mt-1 text-xs text-slate-400">
1510
+ ${googleSessionChecked
1511
+ ? 'Sua sessão foi validada para criar packs.'
1512
+ : 'Validando sua sessão para liberar a criação.'}
1513
+ </p>
1514
+ </div>
1515
+ ${hasGoogleLogin
1516
+ ? html`<span className="rounded-full border border-emerald-400/40 bg-emerald-400/10 px-2.5 py-1 text-[11px] font-semibold text-emerald-300">Conectado</span>`
1517
+ : html`<span className="rounded-full border border-amber-400/40 bg-amber-400/10 px-2.5 py-1 text-[11px] font-semibold text-amber-300">Validando</span>`}
1518
+ </div>
1519
+ <div className="mt-3 rounded-xl border border-line/70 bg-panelSoft/80 p-2.5 md:p-3">
1520
+ <p className="truncate text-sm font-semibold text-slate-100">${googleAuth.user?.name || 'Conta Google'}</p>
1521
+ <p className="truncate text-xs text-slate-400">${googleAuth.user?.email || 'Sessão será validada automaticamente.'}</p>
1522
+ </div>
1523
+ </div>
1737
1524
  <label className="block">
1738
1525
  <span className="mb-2 inline-block text-xs font-semibold text-slate-300">Tags do pack</span>
1739
1526
  <div className="rounded-2xl border border-line/70 bg-panelSoft/80 px-3 py-3">
@@ -231,6 +231,112 @@ function HomeEffects() {
231
231
  return () => toggle.removeEventListener('click', handleClick);
232
232
  }, []);
233
233
 
234
+ useEffect(() => {
235
+ const authLink = document.getElementById('nav-auth-link');
236
+ const schedulerLink = document.getElementById('nav-scheduler-link');
237
+ const heroLoginCta = document.getElementById('hero-login-cta');
238
+ if (!authLink) return;
239
+
240
+ const fallbackAvatar = 'https://iili.io/FC3FABe.jpg';
241
+ const clearChildren = (node) => {
242
+ while (node.firstChild) {
243
+ node.removeChild(node.firstChild);
244
+ }
245
+ };
246
+
247
+ const showLoginButton = () => {
248
+ document.body.classList.remove('home-authenticated');
249
+ authLink.classList.remove('nav-user-chip');
250
+ authLink.href = '/login/';
251
+ authLink.removeAttribute('title');
252
+ authLink.removeAttribute('aria-label');
253
+ clearChildren(authLink);
254
+
255
+ if (schedulerLink) {
256
+ schedulerLink.hidden = false;
257
+ schedulerLink.removeAttribute('aria-hidden');
258
+ }
259
+
260
+ if (heroLoginCta) {
261
+ heroLoginCta.hidden = false;
262
+ heroLoginCta.removeAttribute('aria-hidden');
263
+ }
264
+
265
+ const icon = document.createElement('i');
266
+ icon.className = 'fa-solid fa-right-to-bracket icon-inline';
267
+ icon.setAttribute('aria-hidden', 'true');
268
+ authLink.append(icon, document.createTextNode('Login'));
269
+ };
270
+
271
+ const showLoggedUser = (sessionData) => {
272
+ const profile = sessionData?.user || {};
273
+ const resolvedName = String(profile?.name || profile?.email || 'Conta Google').trim() || 'Conta Google';
274
+ const resolvedPhoto = String(profile?.picture || '').trim() || fallbackAvatar;
275
+
276
+ document.body.classList.add('home-authenticated');
277
+ authLink.classList.add('nav-user-chip');
278
+ authLink.href = '/user/';
279
+ authLink.title = `${resolvedName} (sessão ativa)`;
280
+ authLink.setAttribute('aria-label', `Sessão ativa de ${resolvedName}`);
281
+ clearChildren(authLink);
282
+
283
+ if (schedulerLink) {
284
+ schedulerLink.hidden = true;
285
+ schedulerLink.setAttribute('aria-hidden', 'true');
286
+ }
287
+
288
+ if (heroLoginCta) {
289
+ heroLoginCta.hidden = true;
290
+ heroLoginCta.setAttribute('aria-hidden', 'true');
291
+ }
292
+
293
+ const avatarBubble = document.createElement('span');
294
+ avatarBubble.className = 'nav-user-avatar-bubble';
295
+
296
+ const photo = document.createElement('img');
297
+ photo.className = 'nav-user-photo';
298
+ photo.src = resolvedPhoto;
299
+ photo.alt = `Foto de ${resolvedName}`;
300
+ photo.loading = 'lazy';
301
+ photo.decoding = 'async';
302
+ photo.onerror = () => {
303
+ photo.src = fallbackAvatar;
304
+ };
305
+ avatarBubble.appendChild(photo);
306
+
307
+ const nameBubble = document.createElement('span');
308
+ nameBubble.className = 'nav-user-name-bubble';
309
+
310
+ const icon = document.createElement('i');
311
+ icon.className = 'fa-solid fa-user nav-user-icon';
312
+ icon.setAttribute('aria-hidden', 'true');
313
+
314
+ const name = document.createElement('span');
315
+ name.className = 'nav-user-name';
316
+ name.textContent = resolvedName;
317
+
318
+ nameBubble.append(icon, name);
319
+ authLink.append(avatarBubble, nameBubble);
320
+ };
321
+
322
+ fetch('/api/sticker-packs/auth/google/session', { credentials: 'include' })
323
+ .then((response) => {
324
+ if (!response.ok) throw new Error('Sessão indisponível');
325
+ return response.json();
326
+ })
327
+ .then((payload) => {
328
+ const sessionData = payload?.data || {};
329
+ if (!sessionData?.authenticated || !sessionData?.user?.sub) {
330
+ showLoginButton();
331
+ return;
332
+ }
333
+ showLoggedUser(sessionData);
334
+ })
335
+ .catch(() => {
336
+ showLoginButton();
337
+ });
338
+ }, []);
339
+
234
340
  useEffect(() => {
235
341
  const wppButton = document.getElementById('wpp-float');
236
342
  if (!wppButton) return;