@greatapps/common 1.1.699 → 1.1.701

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.
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useActiveSubscription } from '../../modules/subscriptions/hooks/find-active-subscription.hook';
5
+ import { useAuth } from '../../providers/auth.provider';
6
+ import { useWhitelabel } from '../../providers/whitelabel.provider';
7
+ import { useModalManager } from '../../store/useModalManager';
8
+ import {
9
+ getHexaPromoSeenDay,
10
+ getHexaPromoSince,
11
+ hexaPromoDayKey,
12
+ setHexaPromoSeenDay,
13
+ setHexaPromoSince,
14
+ } from '../../utils/cookies/hexa-promo';
15
+
16
+ /** Janela de tempo online antes de abrir a modal (3 minutos). */
17
+ const ONLINE_DELAY_MS = 3 * 60 * 1000;
18
+
19
+ /**
20
+ * TESTE: quando `true`, a modal abre imediatamente ao carregar o app, ignorando
21
+ * trial / whitelabel / timer de 3 min / cookies. Manter `false` em produção.
22
+ */
23
+ const FORCE_OPEN_FOR_TEST = false;
24
+
25
+ /**
26
+ * Dispara a modal "Rumo ao Hexa" quando um usuário em **trial** da **whitelabel 1** fica
27
+ * 3 minutos online. A janela é cumulativa entre gapps e gpages (timestamp compartilhado em
28
+ * cookie no domínio pai) e a modal abre **uma única vez** — reabrindo só no dia seguinte ou
29
+ * após logout/login. Montar este componente uma vez no layout de cada app consumidor.
30
+ */
31
+ export default function HexaPromoModalGate() {
32
+ const { isAuthenticated } = useAuth();
33
+ const { whitelabel } = useWhitelabel();
34
+ const { activeModal, openModal } = useModalManager();
35
+ const { data } = useActiveSubscription();
36
+
37
+ const subscription = data?.data?.[0] ?? null;
38
+ const eligible =
39
+ isAuthenticated && whitelabel?.id === 1 && subscription?.type === 'trial';
40
+
41
+ const forcedRef = useRef(false);
42
+
43
+ useEffect(() => {
44
+ if (typeof window === 'undefined') return;
45
+
46
+ // TESTE: abre uma vez ao carregar, sem nenhuma condição.
47
+ if (FORCE_OPEN_FOR_TEST) {
48
+ if (forcedRef.current || activeModal) return;
49
+ forcedRef.current = true;
50
+ openModal('hexaPromoModal');
51
+ return;
52
+ }
53
+
54
+ if (!eligible) return;
55
+ // Outra modal aberta: não empilha em cima. Reavalia quando ela fechar (dep `activeModal`).
56
+ if (activeModal) return;
57
+
58
+ const todayKey = hexaPromoDayKey();
59
+ if (getHexaPromoSeenDay() === todayKey) return; // já exibida hoje/nesta sessão
60
+
61
+ // Início da janela compartilhado entre os apps; reinicia a cada novo dia.
62
+ let since = getHexaPromoSince();
63
+ if (!since || since.day !== todayKey) {
64
+ since = { day: todayKey, ms: Date.now() };
65
+ setHexaPromoSince(since.day, since.ms);
66
+ }
67
+
68
+ const remaining = Math.max(0, since.ms + ONLINE_DELAY_MS - Date.now());
69
+ const timer = setTimeout(() => {
70
+ // Recheca o cookie no disparo: se o outro app já abriu, não reabre aqui.
71
+ if (getHexaPromoSeenDay() === todayKey) return;
72
+ setHexaPromoSeenDay(todayKey);
73
+ openModal('hexaPromoModal');
74
+ }, remaining);
75
+
76
+ return () => clearTimeout(timer);
77
+ }, [eligible, activeModal, openModal]);
78
+
79
+ return null;
80
+ }
package/src/index.ts CHANGED
@@ -199,9 +199,12 @@ export {
199
199
  export { useBuyCreditsModal } from "./store/useBuyCreditsModal";
200
200
  export { useCreditsDisabledModal } from "./store/useCreditsDisabledModal";
201
201
  export { usePaidPlanRequiredModal } from "./store/usePaidPlanRequiredModal";
202
+ export { useHexaPromoModal } from "./store/useHexaPromoModal";
202
203
  export { default as BuyCreditsModal } from "./components/modals/BuyCreditsModal";
203
204
  export { default as CreditsDisabledModal } from "./components/modals/CreditsDisabledModal";
204
205
  export { default as PaidPlanRequiredModal } from "./components/modals/PaidPlanRequiredModal";
206
+ export { default as HexaPromoModal } from "./components/modals/HexaPromoModal";
207
+ export { default as HexaPromoModalGate } from "./components/modals/HexaPromoModalGate";
205
208
  export { default as AddCardModal } from "./components/modals/cards/AddCardModal";
206
209
  export { DeleteCardModal } from "./components/modals/cards/DeleteCardModal";
207
210
  export { CannotDeleteCardModal } from "./components/modals/cards/CannotDeleteCardModal";
@@ -46,6 +46,7 @@ import { useWhitelabelUrls, useWhitelabel } from "./whitelabel.provider";
46
46
  import { useWlQueryKey } from "../hooks/useAuthQueryKey";
47
47
  import { normalizeLocale } from "../i18n/normalize";
48
48
  import { setLocaleCookie } from "../utils/intl/locale-cookie";
49
+ import { clearHexaPromo } from "../utils/cookies/hexa-promo";
49
50
 
50
51
  interface AuthContextProps extends AuthState {
51
52
  login: (credentials: LoginRequest) => Promise<LoginResult>;
@@ -220,6 +221,8 @@ export function AuthProvider({ children, publicRoutes = [] }: AuthProviderProps)
220
221
  } catch {
221
222
  // cookie já removido no servidor mesmo se a API falhou
222
223
  } finally {
224
+ // Limpa o estado da promo "Rumo ao Hexa" para reabrir a modal no próximo login.
225
+ clearHexaPromo();
223
226
  queryClient.clear();
224
227
  window.location.assign(resolveLogoutRedirect());
225
228
  }
@@ -0,0 +1,13 @@
1
+ import { useModalManager } from './useModalManager';
2
+
3
+ const HEXA_PROMO_MODAL_KEY = 'hexaPromoModal';
4
+
5
+ export const useHexaPromoModal = () => {
6
+ const { activeModal, openModal, closeModal } = useModalManager();
7
+
8
+ return {
9
+ open: activeModal === HEXA_PROMO_MODAL_KEY,
10
+ openModal: () => openModal(HEXA_PROMO_MODAL_KEY),
11
+ closeModal,
12
+ };
13
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Estado do "Rumo ao Hexa" promo modal — persistido em **cookies no domínio registrável
3
+ * pai** (ex.: `accounts.greatpages.dev.br` → `.greatpages.dev.br`) para serem compartilhados
4
+ * entre os subdomínios dos apps (accounts/gapps ↔ pages/gpages). Assim a janela de "3 minutos
5
+ * online" é cumulativa entre os apps e a modal abre **uma única vez** por sessão/dia,
6
+ * independente de em qual app o usuário esteja.
7
+ *
8
+ * São dois cookies:
9
+ * - `gpromo-hexa-since` = `<dia>|<timestampMs>` — início da janela de 3 min (gravado uma vez).
10
+ * - `gpromo-hexa-seen` = `<dia>` — dia em que a modal já foi exibida.
11
+ *
12
+ * `<dia>` é a data local `YYYY-MM-DD`: vira automaticamente no dia seguinte, fazendo a modal
13
+ * reaparecer. No logout, ambos os cookies são limpos (ver `auth.provider`), então um novo
14
+ * login também faz a modal reaparecer.
15
+ *
16
+ * Em localhost (sem subdomínio) caem em cookies host-scoped — equivalente ao localStorage;
17
+ * cross-port não compartilha (limitação aceita no dev local).
18
+ */
19
+
20
+ const SINCE_COOKIE = 'gpromo-hexa-since';
21
+ const SEEN_COOKIE = 'gpromo-hexa-seen';
22
+ const MAX_AGE_SECONDS = 60 * 60 * 24 * 2; // 2 dias — folga sobre a janela de 1 dia.
23
+
24
+ function parentCookieDomain(): string | undefined {
25
+ if (typeof window === 'undefined') return undefined;
26
+ const host = window.location.hostname;
27
+ if (!host || host === 'localhost') return undefined;
28
+ // IP literal → sem atributo Domain (cookie host-scoped)
29
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return undefined;
30
+ const parts = host.split('.');
31
+ if (parts.length < 2) return undefined;
32
+ // Tira o subdomínio mais à esquerda — `accounts.greatpages.dev.br` → `.greatpages.dev.br`
33
+ return '.' + parts.slice(1).join('.');
34
+ }
35
+
36
+ function readCookie(name: string): string | null {
37
+ if (typeof document === 'undefined') return null;
38
+ const m = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
39
+ if (!m) return null;
40
+ try {
41
+ return decodeURIComponent(m[1]);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function writeCookie(name: string, value: string): void {
48
+ if (typeof document === 'undefined' || typeof window === 'undefined') return;
49
+ const domain = parentCookieDomain();
50
+ const parts = [
51
+ `${name}=${encodeURIComponent(value)}`,
52
+ 'Path=/',
53
+ `Max-Age=${MAX_AGE_SECONDS}`,
54
+ 'SameSite=Lax',
55
+ domain ? `Domain=${domain}` : '',
56
+ window.location.protocol === 'https:' ? 'Secure' : '',
57
+ ].filter(Boolean);
58
+ try {
59
+ document.cookie = parts.join('; ');
60
+ } catch {
61
+ // Se o navegador bloquear cookies, seguimos com a exibição única por sessão (timer no Gate).
62
+ }
63
+ }
64
+
65
+ function expireCookie(name: string): void {
66
+ if (typeof document === 'undefined' || typeof window === 'undefined') return;
67
+ const domain = parentCookieDomain();
68
+ const parts = [
69
+ `${name}=`,
70
+ 'Path=/',
71
+ 'Max-Age=0',
72
+ 'SameSite=Lax',
73
+ domain ? `Domain=${domain}` : '',
74
+ window.location.protocol === 'https:' ? 'Secure' : '',
75
+ ].filter(Boolean);
76
+ try {
77
+ document.cookie = parts.join('; ');
78
+ } catch {
79
+ // noop
80
+ }
81
+ }
82
+
83
+ /** Chave do dia local (`YYYY-MM-DD`) usada para reabrir a modal a cada novo dia. */
84
+ export function hexaPromoDayKey(): string {
85
+ const d = new Date();
86
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
87
+ }
88
+
89
+ export function getHexaPromoSeenDay(): string | null {
90
+ return readCookie(SEEN_COOKIE);
91
+ }
92
+
93
+ export function setHexaPromoSeenDay(day: string): void {
94
+ writeCookie(SEEN_COOKIE, day);
95
+ }
96
+
97
+ export function getHexaPromoSince(): { day: string; ms: number } | null {
98
+ const raw = readCookie(SINCE_COOKIE);
99
+ if (!raw) return null;
100
+ const [day, msRaw] = raw.split('|');
101
+ const ms = Number(msRaw);
102
+ if (!day || !Number.isFinite(ms)) return null;
103
+ return { day, ms };
104
+ }
105
+
106
+ export function setHexaPromoSince(day: string, ms: number): void {
107
+ writeCookie(SINCE_COOKIE, `${day}|${ms}`);
108
+ }
109
+
110
+ /** Limpa o estado da promo (chamado no logout para reabrir a modal no próximo login). */
111
+ export function clearHexaPromo(): void {
112
+ expireCookie(SINCE_COOKIE);
113
+ expireCookie(SEEN_COOKIE);
114
+ }