@greatapps/common 1.1.715 → 1.1.716
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.
- package/dist/components/modals/HexaPromoModal.mjs +169 -0
- package/dist/components/modals/HexaPromoModal.mjs.map +1 -0
- package/dist/components/modals/HexaPromoModalGate.mjs +61 -0
- package/dist/components/modals/HexaPromoModalGate.mjs.map +1 -0
- package/dist/index.mjs +38 -32
- package/dist/index.mjs.map +1 -1
- package/dist/modules/ia-credits/handlers/list-ia-credits.handler.mjs +1 -1
- package/dist/modules/ia-credits/handlers/list-ia-credits.handler.mjs.map +1 -1
- package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs +27 -34
- package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs.map +1 -1
- package/dist/modules/ia-credits/services/ia-credits.service.mjs +33 -11
- package/dist/modules/ia-credits/services/ia-credits.service.mjs.map +1 -1
- package/dist/modules/ia-credits/types.mjs +12 -1
- package/dist/modules/ia-credits/types.mjs.map +1 -1
- package/dist/providers/auth.provider.mjs +2 -0
- package/dist/providers/auth.provider.mjs.map +1 -1
- package/dist/store/useHexaPromoModal.mjs +14 -0
- package/dist/store/useHexaPromoModal.mjs.map +1 -0
- package/dist/utils/cookies/hexa-promo.mjs +88 -0
- package/dist/utils/cookies/hexa-promo.mjs.map +1 -0
- package/package.json +11 -12
- package/src/components/modals/HexaPromoModal.tsx +218 -0
- package/src/components/modals/HexaPromoModalGate.tsx +93 -0
- package/src/index.ts +5 -0
- package/src/modules/ia-credits/handlers/list-ia-credits.handler.ts +1 -1
- package/src/modules/ia-credits/hooks/ia-credits.hook.ts +33 -46
- package/src/modules/ia-credits/services/ia-credits.service.ts +73 -37
- package/src/modules/ia-credits/types.ts +30 -1
- package/src/providers/auth.provider.tsx +3 -0
- package/src/store/useHexaPromoModal.ts +13 -0
- package/src/utils/cookies/hexa-promo.ts +114 -0
|
@@ -1,37 +1,73 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
|
|
3
|
-
import { api } from '../../../infra/api/client';
|
|
4
|
-
import { ApiError, ApiPaginatedActionResult,
|
|
5
|
-
import { getUserContext } from '../../auth/utils/get-user-context';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { api } from '../../../infra/api/client';
|
|
4
|
+
import { ApiError, ApiPaginatedActionResult, SuccessResult } from '../../../infra/api/types';
|
|
5
|
+
import { getUserContext } from '../../auth/utils/get-user-context';
|
|
6
|
+
import {
|
|
7
|
+
IaCreditAddBatch,
|
|
8
|
+
IaCreditAddBatchSchema,
|
|
9
|
+
IaCreditOperation,
|
|
10
|
+
IaCreditsSummaryData,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
class IaCreditsService {
|
|
14
|
+
async getSummaryData(
|
|
15
|
+
subscriptionId: number | string
|
|
16
|
+
): Promise<SuccessResult<IaCreditsSummaryData>> {
|
|
17
|
+
const { id_account } = await getUserContext();
|
|
18
|
+
|
|
19
|
+
const [availableCredits, addBatches] = await Promise.all([
|
|
20
|
+
this.fetchCurrentBalance(id_account, subscriptionId),
|
|
21
|
+
this.fetchAddBatches(id_account, subscriptionId),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
return { success: true, data: { availableCredits, addBatches } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Saldo atual = balance_after_operation da última operação do extrato
|
|
28
|
+
// (sort id:DESC, limit=1). Já é o saldo do usuário, não precisa recalcular.
|
|
29
|
+
private async fetchCurrentBalance(
|
|
30
|
+
idAccount: number | string,
|
|
31
|
+
subscriptionId: number | string
|
|
32
|
+
): Promise<number> {
|
|
33
|
+
const endpoint = `/accounts/${idAccount}/subscriptions/${subscriptionId}/items/balance/list?page=1&limit=1&sort=id:DESC`;
|
|
34
|
+
|
|
35
|
+
const response = await api.apps.get<ApiPaginatedActionResult<IaCreditOperation>>(endpoint);
|
|
36
|
+
|
|
37
|
+
if (response.status === 0) {
|
|
38
|
+
throw new ApiError(
|
|
39
|
+
response.message || 'Erro ao consultar saldo de créditos de IA',
|
|
40
|
+
'GET_IA_CREDITS_BALANCE_FAILED',
|
|
41
|
+
400
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const balance = (response.data ?? [])[0]?.balance_after_operation;
|
|
46
|
+
return balance == null ? 0 : Number(balance);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Lotes de adição com added/consumed/remaining já agregados pelo backend.
|
|
50
|
+
// sort id:DESC garante que os lotes mais recentes (não expirados) entram
|
|
51
|
+
// primeiro no limite; lotes expirados são filtrados no hook.
|
|
52
|
+
private async fetchAddBatches(
|
|
53
|
+
idAccount: number | string,
|
|
54
|
+
subscriptionId: number | string
|
|
55
|
+
): Promise<IaCreditAddBatch[]> {
|
|
56
|
+
const endpoint = `/accounts/${idAccount}/subscriptions/${subscriptionId}/items/balance/list/add?page=1&limit=100&sort=id:DESC`;
|
|
57
|
+
|
|
58
|
+
const response = await api.apps.get<ApiPaginatedActionResult<IaCreditAddBatch>>(endpoint);
|
|
59
|
+
|
|
60
|
+
if (response.status === 0) {
|
|
61
|
+
throw new ApiError(
|
|
62
|
+
response.message || 'Erro ao listar lotes de créditos de IA',
|
|
63
|
+
'LIST_IA_CREDITS_ADDS_FAILED',
|
|
64
|
+
400
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const raw = response.data ?? [];
|
|
69
|
+
return raw.map((item) => IaCreditAddBatchSchema.parse(item));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const iaCreditsService = new IaCreditsService();
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
|
|
3
|
+
// has_expired pode vir null (ou ausente) da API — normaliza para boolean.
|
|
4
|
+
const hasExpiredSchema = z
|
|
5
|
+
.boolean()
|
|
6
|
+
.nullish()
|
|
7
|
+
.transform((v) => v ?? false);
|
|
8
|
+
|
|
3
9
|
export const IaCreditOperationSchema = z
|
|
4
10
|
.object({
|
|
5
11
|
id: z.union([z.number(), z.string()]),
|
|
@@ -15,7 +21,7 @@ export const IaCreditOperationSchema = z
|
|
|
15
21
|
operation_ref: z.string().nullable().optional(),
|
|
16
22
|
operation_origin: z.string().nullable().optional(),
|
|
17
23
|
expiration_time: z.string().nullable(),
|
|
18
|
-
has_expired:
|
|
24
|
+
has_expired: hasExpiredSchema,
|
|
19
25
|
consumed_from_add_id: z.union([z.number(), z.string()]).nullable(),
|
|
20
26
|
id_user_add: z.union([z.number(), z.string()]).nullable(),
|
|
21
27
|
user_name: z.string().nullable().optional(),
|
|
@@ -24,6 +30,29 @@ export const IaCreditOperationSchema = z
|
|
|
24
30
|
|
|
25
31
|
export type IaCreditOperation = z.infer<typeof IaCreditOperationSchema>;
|
|
26
32
|
|
|
33
|
+
// Lote de créditos adicionados — agregado pelo backend em
|
|
34
|
+
// /items/balance/list/add (added/consumed/remaining já calculados).
|
|
35
|
+
export const IaCreditAddBatchSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
id: z.union([z.number(), z.string()]),
|
|
38
|
+
added_credits: z.coerce.number(),
|
|
39
|
+
consumed_credits: z.coerce.number(),
|
|
40
|
+
remaining_credits: z.coerce.number(),
|
|
41
|
+
datetime_add: z.string().nullable(),
|
|
42
|
+
expiration_time: z.string().nullable(),
|
|
43
|
+
has_expired: hasExpiredSchema,
|
|
44
|
+
})
|
|
45
|
+
.passthrough();
|
|
46
|
+
|
|
47
|
+
export type IaCreditAddBatch = z.infer<typeof IaCreditAddBatchSchema>;
|
|
48
|
+
|
|
49
|
+
// Payload servido pelo route handler: saldo atual (balance_after_operation)
|
|
50
|
+
// + lotes de adição, de onde o hook deriva o restante do sumário.
|
|
51
|
+
export type IaCreditsSummaryData = {
|
|
52
|
+
availableCredits: number;
|
|
53
|
+
addBatches: IaCreditAddBatch[];
|
|
54
|
+
};
|
|
55
|
+
|
|
27
56
|
export type IaCreditsSummary = {
|
|
28
57
|
totalCredits: number;
|
|
29
58
|
usedCredits: number;
|
|
@@ -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
|
+
}
|