@greatapps/common 1.1.715 → 1.1.717
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 +21 -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 +28 -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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/cookies/hexa-promo.ts"],"sourcesContent":["/**\n * Estado do \"Rumo ao Hexa\" promo modal — persistido em **cookies no domínio registrável\n * pai** (ex.: `accounts.greatpages.dev.br` → `.greatpages.dev.br`) para serem compartilhados\n * entre os subdomínios dos apps (accounts/gapps ↔ pages/gpages). Assim a janela de \"3 minutos\n * online\" é cumulativa entre os apps e a modal abre **uma única vez** por sessão/dia,\n * independente de em qual app o usuário esteja.\n *\n * São dois cookies:\n * - `gpromo-hexa-since` = `<dia>|<timestampMs>` — início da janela de 3 min (gravado uma vez).\n * - `gpromo-hexa-seen` = `<dia>` — dia em que a modal já foi exibida.\n *\n * `<dia>` é a data local `YYYY-MM-DD`: vira automaticamente no dia seguinte, fazendo a modal\n * reaparecer. No logout, ambos os cookies são limpos (ver `auth.provider`), então um novo\n * login também faz a modal reaparecer.\n *\n * Em localhost (sem subdomínio) caem em cookies host-scoped — equivalente ao localStorage;\n * cross-port não compartilha (limitação aceita no dev local).\n */\n\nconst SINCE_COOKIE = 'gpromo-hexa-since';\nconst SEEN_COOKIE = 'gpromo-hexa-seen';\nconst MAX_AGE_SECONDS = 60 * 60 * 24 * 2; // 2 dias — folga sobre a janela de 1 dia.\n\nfunction parentCookieDomain(): string | undefined {\n if (typeof window === 'undefined') return undefined;\n const host = window.location.hostname;\n if (!host || host === 'localhost') return undefined;\n // IP literal → sem atributo Domain (cookie host-scoped)\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host)) return undefined;\n const parts = host.split('.');\n if (parts.length < 2) return undefined;\n // Tira o subdomínio mais à esquerda — `accounts.greatpages.dev.br` → `.greatpages.dev.br`\n return '.' + parts.slice(1).join('.');\n}\n\nfunction readCookie(name: string): string | null {\n if (typeof document === 'undefined') return null;\n const m = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]+)`));\n if (!m) return null;\n try {\n return decodeURIComponent(m[1]);\n } catch {\n return null;\n }\n}\n\nfunction writeCookie(name: string, value: string): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n const domain = parentCookieDomain();\n const parts = [\n `${name}=${encodeURIComponent(value)}`,\n 'Path=/',\n `Max-Age=${MAX_AGE_SECONDS}`,\n 'SameSite=Lax',\n domain ? `Domain=${domain}` : '',\n window.location.protocol === 'https:' ? 'Secure' : '',\n ].filter(Boolean);\n try {\n document.cookie = parts.join('; ');\n } catch {\n // Se o navegador bloquear cookies, seguimos com a exibição única por sessão (timer no Gate).\n }\n}\n\nfunction expireCookie(name: string): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n const domain = parentCookieDomain();\n const parts = [\n `${name}=`,\n 'Path=/',\n 'Max-Age=0',\n 'SameSite=Lax',\n domain ? `Domain=${domain}` : '',\n window.location.protocol === 'https:' ? 'Secure' : '',\n ].filter(Boolean);\n try {\n document.cookie = parts.join('; ');\n } catch {\n // noop\n }\n}\n\n/** Chave do dia local (`YYYY-MM-DD`) usada para reabrir a modal a cada novo dia. */\nexport function hexaPromoDayKey(): string {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n\nexport function getHexaPromoSeenDay(): string | null {\n return readCookie(SEEN_COOKIE);\n}\n\nexport function setHexaPromoSeenDay(day: string): void {\n writeCookie(SEEN_COOKIE, day);\n}\n\nexport function getHexaPromoSince(): { day: string; ms: number } | null {\n const raw = readCookie(SINCE_COOKIE);\n if (!raw) return null;\n const [day, msRaw] = raw.split('|');\n const ms = Number(msRaw);\n if (!day || !Number.isFinite(ms)) return null;\n return { day, ms };\n}\n\nexport function setHexaPromoSince(day: string, ms: number): void {\n writeCookie(SINCE_COOKIE, `${day}|${ms}`);\n}\n\n/** Limpa o estado da promo (chamado no logout para reabrir a modal no próximo login). */\nexport function clearHexaPromo(): void {\n expireCookie(SINCE_COOKIE);\n expireCookie(SEEN_COOKIE);\n}\n"],"mappings":"AAmBA,MAAM,eAAe;AACrB,MAAM,cAAc;AACpB,MAAM,kBAAkB,KAAK,KAAK,KAAK;AAEvC,SAAS,qBAAyC;AAChD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,OAAO,OAAO,SAAS;AAC7B,MAAI,CAAC,QAAQ,SAAS,YAAa,QAAO;AAE1C,MAAI,uBAAuB,KAAK,IAAI,EAAG,QAAO;AAC9C,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,SAAO,MAAM,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AACtC;AAEA,SAAS,WAAW,MAA6B;AAC/C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,IAAI,SAAS,OAAO,MAAM,IAAI,OAAO,cAAc,IAAI,UAAU,CAAC;AACxE,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,mBAAmB,EAAE,CAAC,CAAC;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAY,MAAc,OAAqB;AACtD,MAAI,OAAO,aAAa,eAAe,OAAO,WAAW,YAAa;AACtE,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ;AAAA,IACZ,GAAG,IAAI,IAAI,mBAAmB,KAAK,CAAC;AAAA,IACpC;AAAA,IACA,WAAW,eAAe;AAAA,IAC1B;AAAA,IACA,SAAS,UAAU,MAAM,KAAK;AAAA,IAC9B,OAAO,SAAS,aAAa,WAAW,WAAW;AAAA,EACrD,EAAE,OAAO,OAAO;AAChB,MAAI;AACF,aAAS,SAAS,MAAM,KAAK,IAAI;AAAA,EACnC,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,aAAa,MAAoB;AACxC,MAAI,OAAO,aAAa,eAAe,OAAO,WAAW,YAAa;AACtE,QAAM,SAAS,mBAAmB;AAClC,QAAM,QAAQ;AAAA,IACZ,GAAG,IAAI;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,UAAU,MAAM,KAAK;AAAA,IAC9B,OAAO,SAAS,aAAa,WAAW,WAAW;AAAA,EACrD,EAAE,OAAO,OAAO;AAChB,MAAI;AACF,aAAS,SAAS,MAAM,KAAK,IAAI;AAAA,EACnC,QAAQ;AAAA,EAER;AACF;AAGO,SAAS,kBAA0B;AACxC,QAAM,IAAI,oBAAI,KAAK;AACnB,SAAO,GAAG,EAAE,YAAY,CAAC,IAAI,OAAO,EAAE,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAChH;AAEO,SAAS,sBAAqC;AACnD,SAAO,WAAW,WAAW;AAC/B;AAEO,SAAS,oBAAoB,KAAmB;AACrD,cAAY,aAAa,GAAG;AAC9B;AAEO,SAAS,oBAAwD;AACtE,QAAM,MAAM,WAAW,YAAY;AACnC,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,GAAG;AAClC,QAAM,KAAK,OAAO,KAAK;AACvB,MAAI,CAAC,OAAO,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACzC,SAAO,EAAE,KAAK,GAAG;AACnB;AAEO,SAAS,kBAAkB,KAAa,IAAkB;AAC/D,cAAY,cAAc,GAAG,GAAG,IAAI,EAAE,EAAE;AAC1C;AAGO,SAAS,iBAAuB;AACrC,eAAa,YAAY;AACzB,eAAa,WAAW;AAC1B;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greatapps/common",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.717",
|
|
4
4
|
"description": "Shared library for GreatApps frontend applications",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -46,12 +46,6 @@
|
|
|
46
46
|
"dist",
|
|
47
47
|
"src"
|
|
48
48
|
],
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsup",
|
|
51
|
-
"dev": "tsup --watch",
|
|
52
|
-
"typecheck": "tsc --noEmit",
|
|
53
|
-
"prepublishOnly": "npm run build"
|
|
54
|
-
},
|
|
55
49
|
"keywords": [],
|
|
56
50
|
"author": "",
|
|
57
51
|
"license": "ISC",
|
|
@@ -87,8 +81,8 @@
|
|
|
87
81
|
"tailwind-merge": "3.5.0"
|
|
88
82
|
},
|
|
89
83
|
"devDependencies": {
|
|
84
|
+
"@faker-js/faker": "9.9.0",
|
|
90
85
|
"@hookform/resolvers": "5.2.2",
|
|
91
|
-
"zod": "4.4.3",
|
|
92
86
|
"@tanstack/query-broadcast-client-experimental": "5.95.2",
|
|
93
87
|
"@tanstack/react-query": "5.95.2",
|
|
94
88
|
"@tanstack/react-query-devtools": "5.95.2",
|
|
@@ -98,7 +92,6 @@
|
|
|
98
92
|
"@types/react-dom": "19.2.3",
|
|
99
93
|
"glob": "13.0.6",
|
|
100
94
|
"msw": "2.7.0",
|
|
101
|
-
"@faker-js/faker": "9.9.0",
|
|
102
95
|
"next": "16.2.1",
|
|
103
96
|
"next-intl": "4.12.0",
|
|
104
97
|
"react": "19.1.4",
|
|
@@ -106,17 +99,18 @@
|
|
|
106
99
|
"react-hook-form": "7.72.0",
|
|
107
100
|
"tsup": "8.5.1",
|
|
108
101
|
"typescript": "5.9.3",
|
|
102
|
+
"zod": "4.4.3",
|
|
109
103
|
"zustand": "5.0.12"
|
|
110
104
|
},
|
|
111
105
|
"peerDependencies": {
|
|
106
|
+
"@faker-js/faker": ">=9.0.0",
|
|
112
107
|
"@hookform/resolvers": ">=3.0.0",
|
|
113
108
|
"@tanstack/query-broadcast-client-experimental": ">=5.0.0",
|
|
114
109
|
"@tanstack/react-query": ">=5.0.0",
|
|
115
110
|
"@tanstack/react-query-persist-client": ">=5.0.0",
|
|
111
|
+
"msw": ">=2.0.0",
|
|
116
112
|
"next": ">=16.0.0",
|
|
117
113
|
"next-intl": ">=4.0.0",
|
|
118
|
-
"@faker-js/faker": ">=9.0.0",
|
|
119
|
-
"msw": ">=2.0.0",
|
|
120
114
|
"react": ">=19.0.0",
|
|
121
115
|
"react-dom": ">=19.0.0",
|
|
122
116
|
"react-hook-form": ">=7.0.0",
|
|
@@ -131,5 +125,10 @@
|
|
|
131
125
|
"@faker-js/faker": {
|
|
132
126
|
"optional": true
|
|
133
127
|
}
|
|
128
|
+
},
|
|
129
|
+
"scripts": {
|
|
130
|
+
"build": "tsup",
|
|
131
|
+
"dev": "tsup --watch",
|
|
132
|
+
"typecheck": "tsc --noEmit"
|
|
134
133
|
}
|
|
135
|
-
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import {
|
|
5
|
+
IconCheck,
|
|
6
|
+
IconCopy,
|
|
7
|
+
IconInfoCircle,
|
|
8
|
+
IconStar,
|
|
9
|
+
IconTicket,
|
|
10
|
+
IconX,
|
|
11
|
+
} from '@tabler/icons-react';
|
|
12
|
+
import { Dialog, DialogClose, DialogContent, DialogTitle } from '../ui/overlay/Dialog';
|
|
13
|
+
import { Toast } from '../ui/feedback/Toast';
|
|
14
|
+
import { Button } from '../ui/buttons/Button';
|
|
15
|
+
import useCopyToClipboard from '../../hooks/copy-to-clipboard.hook';
|
|
16
|
+
import { useModalManager } from '../../store/useModalManager';
|
|
17
|
+
import { useWhitelabelUrls } from '../../providers/whitelabel.provider';
|
|
18
|
+
import { cn } from '../../infra/utils/clsx';
|
|
19
|
+
|
|
20
|
+
type ComboPeriod = 'monthly' | 'semiannual' | 'annual';
|
|
21
|
+
|
|
22
|
+
interface HexaCombo {
|
|
23
|
+
coupon: string;
|
|
24
|
+
name: string;
|
|
25
|
+
features: string[];
|
|
26
|
+
highlighted: boolean;
|
|
27
|
+
period: ComboPeriod;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Campanha "Rumo ao Hexa" — exclusiva da whitelabel 1 (público pt-br). O texto é fixo de
|
|
31
|
+
// propósito: a modal precisa renderizar também no gapps, que não tem provider de next-intl.
|
|
32
|
+
const COPY = {
|
|
33
|
+
title: 'Rumo ao Hexa!',
|
|
34
|
+
subtitlePrefix: 'Ofertas exclusivas até ',
|
|
35
|
+
subtitleDate: '24/06',
|
|
36
|
+
mostChosen: 'Mais escolhido',
|
|
37
|
+
couponLabel: 'Cupom',
|
|
38
|
+
choosePlan: 'Escolher plano',
|
|
39
|
+
couponCopied: 'Cupom copiado!',
|
|
40
|
+
close: 'Fechar',
|
|
41
|
+
footerPrefix: 'Válido para a ',
|
|
42
|
+
footerBold: 'primeira contratação',
|
|
43
|
+
footerSuffix: ' dos planos Essencial, Negócio ou Agência',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const COMBOS: HexaCombo[] = [
|
|
47
|
+
{
|
|
48
|
+
coupon: 'MENSAL10',
|
|
49
|
+
name: 'Combo convocação',
|
|
50
|
+
highlighted: false,
|
|
51
|
+
period: 'monthly',
|
|
52
|
+
features: ['10% OFF no plano mensal', 'Webinars exclusivos'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
coupon: 'SEMESTRAL10',
|
|
56
|
+
name: 'Combo Hexa',
|
|
57
|
+
highlighted: true,
|
|
58
|
+
period: 'semiannual',
|
|
59
|
+
features: ['10% OFF no plano semestral', '3 templates exclusivos', 'Webinars exclusivos'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
coupon: 'ANUAL10',
|
|
63
|
+
name: 'Combo escalação',
|
|
64
|
+
highlighted: false,
|
|
65
|
+
period: 'annual',
|
|
66
|
+
features: ['10% OFF no plano anual', '3 templates exclusivos'],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Cores promocionais (verde/amarelo do Brasil) — específicas desta campanha, fora do design
|
|
71
|
+
// system, por isso ficam hardcoded apenas aqui.
|
|
72
|
+
const GRADIENT = 'linear-gradient(180deg, #08c44f 0%, #7bd237 46.6%, #ffe31b 100%)';
|
|
73
|
+
|
|
74
|
+
export default function HexaPromoModal() {
|
|
75
|
+
const { activeModal, closeModal } = useModalManager();
|
|
76
|
+
const { accountsUrl } = useWhitelabelUrls();
|
|
77
|
+
const isOpen = activeModal === 'hexaPromoModal';
|
|
78
|
+
|
|
79
|
+
const goToPlans = (coupon: string, period: ComboPeriod) => {
|
|
80
|
+
closeModal();
|
|
81
|
+
const url = `${accountsUrl}/subscriptions?tab=plans&coupon=${encodeURIComponent(coupon)}&period=${encodeURIComponent(period)}`;
|
|
82
|
+
window.location.assign(url);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Dialog open={isOpen} onOpenChange={() => closeModal()}>
|
|
87
|
+
<DialogContent
|
|
88
|
+
showCloseButton={false}
|
|
89
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
90
|
+
className="border-0 bg-transparent p-0 shadow-none w-[calc(100%-32px)] sm:max-w-[940px] md:w-[924px] lg:w-[924px] max-h-[calc(100dvh-32px)] overflow-y-auto"
|
|
91
|
+
>
|
|
92
|
+
<DialogTitle className="sr-only">{COPY.title}</DialogTitle>
|
|
93
|
+
|
|
94
|
+
<div className="rounded-[12px] p-1" style={{ background: GRADIENT }}>
|
|
95
|
+
<div className="relative overflow-hidden rounded-[10px] bg-white p-8 shadow-[0px_2px_10px_0px_rgba(26,26,26,0.05)]">
|
|
96
|
+
{/* Arte decorativa (festão/bandeira) no canto superior direito */}
|
|
97
|
+
<img
|
|
98
|
+
src="/promo/hexa-confetti.png"
|
|
99
|
+
alt=""
|
|
100
|
+
aria-hidden
|
|
101
|
+
className="pointer-events-none absolute -top-10 right-0 w-[280px] rotate-[16deg] select-none"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<DialogClose className="absolute right-3 top-3 z-10 flex size-[34px] cursor-pointer items-center justify-center rounded-lg bg-white p-2 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-950 focus:outline-none">
|
|
105
|
+
<span className="sr-only">{COPY.close}</span>
|
|
106
|
+
<IconX className="size-[18px]" />
|
|
107
|
+
</DialogClose>
|
|
108
|
+
|
|
109
|
+
{/* Header: logo + bandeira */}
|
|
110
|
+
<div className="relative z-[1] flex w-fit items-center gap-[9px] rounded-lg bg-zinc-100 px-[9px] py-2">
|
|
111
|
+
<img src="/promo/hexa-logo.svg" alt="" aria-hidden className="size-6 select-none" />
|
|
112
|
+
<img src="/promo/hexa-flag.png" alt="" aria-hidden className="size-6 rounded select-none" />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Título */}
|
|
116
|
+
<div className="relative z-[1] mt-4 flex flex-col gap-1">
|
|
117
|
+
<h2 className="font-['Outfit'] text-[32px] font-semibold leading-8 text-zinc-950">
|
|
118
|
+
{COPY.title}
|
|
119
|
+
</h2>
|
|
120
|
+
<p className="paragraph-medium-medium text-zinc-950">
|
|
121
|
+
{COPY.subtitlePrefix}
|
|
122
|
+
<span className="font-bold">{COPY.subtitleDate}</span>
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Cards */}
|
|
127
|
+
<div className="relative z-[1] mt-10 flex flex-col items-stretch gap-3 md:flex-row">
|
|
128
|
+
{COMBOS.map((combo) => (
|
|
129
|
+
<ComboCard key={combo.coupon} combo={combo} onChoose={() => goToPlans(combo.coupon, combo.period)} />
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Rodapé informativo */}
|
|
134
|
+
<div className="relative z-[1] mt-3 flex items-center justify-center gap-2.5 rounded-[10px] bg-zinc-100 p-3 text-center">
|
|
135
|
+
<IconInfoCircle className="size-4 shrink-0 text-zinc-950" />
|
|
136
|
+
<p className="paragraph-small-medium text-zinc-950">
|
|
137
|
+
{COPY.footerPrefix}
|
|
138
|
+
<span className="font-semibold">{COPY.footerBold}</span>
|
|
139
|
+
{COPY.footerSuffix}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</DialogContent>
|
|
145
|
+
</Dialog>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface ComboCardProps {
|
|
150
|
+
combo: HexaCombo;
|
|
151
|
+
onChoose: () => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ComboCard({ combo, onChoose }: ComboCardProps) {
|
|
155
|
+
const { isCopied, copy } = useCopyToClipboard({
|
|
156
|
+
onSuccess: () => {
|
|
157
|
+
toast.custom((t) => <Toast variant="success" message={COPY.couponCopied} toastId={t} />);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
'relative flex flex-1 flex-col rounded-[10px] border bg-white pb-3 md:w-[276px]',
|
|
165
|
+
combo.highlighted ? 'border-cyan-500' : 'border-zinc-200',
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{combo.highlighted && (
|
|
169
|
+
<div className="absolute left-1/2 top-0 z-[1] flex h-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-[5px] rounded-md border-4 border-white bg-cyan-100 px-2">
|
|
170
|
+
<IconStar className="size-3 text-cyan-600" />
|
|
171
|
+
<span className="paragraph-xsmall-semibold whitespace-nowrap text-cyan-600">
|
|
172
|
+
{COPY.mostChosen}
|
|
173
|
+
</span>
|
|
174
|
+
<IconStar className="size-3 text-cyan-600" />
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<h3 className="border-b border-zinc-200 px-4 py-4 text-center font-['Outfit'] text-base font-semibold text-zinc-950">
|
|
179
|
+
{combo.name}
|
|
180
|
+
</h3>
|
|
181
|
+
|
|
182
|
+
<ul className="flex flex-1 flex-col gap-5 px-5 pt-5">
|
|
183
|
+
{combo.features.map((feature) => (
|
|
184
|
+
<li key={feature} className="flex items-center gap-2.5">
|
|
185
|
+
<IconCheck className="size-5 shrink-0 text-cyan-500" />
|
|
186
|
+
<span className="paragraph-small-medium text-zinc-950">{feature}</span>
|
|
187
|
+
</li>
|
|
188
|
+
))}
|
|
189
|
+
</ul>
|
|
190
|
+
|
|
191
|
+
<div className="mx-3 mt-5 flex items-center gap-3 rounded-lg bg-cyan-50 py-2 pl-3.5 pr-2.5">
|
|
192
|
+
<IconTicket className="size-5 shrink-0 text-cyan-600" />
|
|
193
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
194
|
+
<span className="paragraph-xsmall-medium text-zinc-400">{COPY.couponLabel}</span>
|
|
195
|
+
<span className="paragraph-small-semibold text-zinc-950">{combo.coupon}</span>
|
|
196
|
+
</div>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
aria-label={COPY.couponLabel}
|
|
200
|
+
onClick={() => copy(combo.coupon)}
|
|
201
|
+
className="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-cyan-100 hover:text-zinc-950"
|
|
202
|
+
>
|
|
203
|
+
{isCopied ? <IconCheck className="size-[18px]" /> : <IconCopy className="size-[18px]" />}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="mx-3 mt-3">
|
|
208
|
+
<Button
|
|
209
|
+
variant={combo.highlighted ? 'default' : 'secondary'}
|
|
210
|
+
className={cn('h-11! w-full', combo.highlighted && 'bg-cyan-500 text-white hover:bg-cyan-600')}
|
|
211
|
+
onClick={onChoose}
|
|
212
|
+
>
|
|
213
|
+
{COPY.choosePlan}
|
|
214
|
+
</Button>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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, closeModal } = 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
|
+
// Elegibilidade atual visível dentro do setTimeout (que captura o valor do agendamento).
|
|
42
|
+
const eligibleRef = useRef(eligible);
|
|
43
|
+
eligibleRef.current = eligible;
|
|
44
|
+
|
|
45
|
+
const forcedRef = useRef(false);
|
|
46
|
+
|
|
47
|
+
// Nunca deixa a modal aberta para usuário deslogado/inelegível: se ele abriu logado e
|
|
48
|
+
// depois deslogou (ou perdeu a elegibilidade), fecha.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (FORCE_OPEN_FOR_TEST) return;
|
|
51
|
+
if (!eligible && activeModal === 'hexaPromoModal') closeModal();
|
|
52
|
+
}, [eligible, activeModal, closeModal]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (typeof window === 'undefined') return;
|
|
56
|
+
|
|
57
|
+
// TESTE: abre uma vez ao carregar, sem nenhuma condição.
|
|
58
|
+
if (FORCE_OPEN_FOR_TEST) {
|
|
59
|
+
if (forcedRef.current || activeModal) return;
|
|
60
|
+
forcedRef.current = true;
|
|
61
|
+
openModal('hexaPromoModal');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!eligible) return;
|
|
66
|
+
// Outra modal aberta: não empilha em cima. Reavalia quando ela fechar (dep `activeModal`).
|
|
67
|
+
if (activeModal) return;
|
|
68
|
+
|
|
69
|
+
const todayKey = hexaPromoDayKey();
|
|
70
|
+
if (getHexaPromoSeenDay() === todayKey) return; // já exibida hoje/nesta sessão
|
|
71
|
+
|
|
72
|
+
// Início da janela compartilhado entre os apps; reinicia a cada novo dia.
|
|
73
|
+
let since = getHexaPromoSince();
|
|
74
|
+
if (!since || since.day !== todayKey) {
|
|
75
|
+
since = { day: todayKey, ms: Date.now() };
|
|
76
|
+
setHexaPromoSince(since.day, since.ms);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const remaining = Math.max(0, since.ms + ONLINE_DELAY_MS - Date.now());
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
// Revalida no disparo: o usuário pode ter deslogado durante a janela.
|
|
82
|
+
if (!eligibleRef.current) return;
|
|
83
|
+
// Recheca o cookie no disparo: se o outro app já abriu, não reabre aqui.
|
|
84
|
+
if (getHexaPromoSeenDay() === todayKey) return;
|
|
85
|
+
setHexaPromoSeenDay(todayKey);
|
|
86
|
+
openModal('hexaPromoModal');
|
|
87
|
+
}, remaining);
|
|
88
|
+
|
|
89
|
+
return () => clearTimeout(timer);
|
|
90
|
+
}, [eligible, activeModal, openModal]);
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -107,6 +107,8 @@ export { useChargeAction } from "./modules/charges/hooks/charge-action.hook";
|
|
|
107
107
|
export { useIaCredits } from "./modules/ia-credits/hooks/ia-credits.hook";
|
|
108
108
|
export type {
|
|
109
109
|
IaCreditsSummary,
|
|
110
|
+
IaCreditsSummaryData,
|
|
111
|
+
IaCreditAddBatch,
|
|
110
112
|
IaCreditOperation,
|
|
111
113
|
} from "./modules/ia-credits/types";
|
|
112
114
|
export type {
|
|
@@ -199,9 +201,12 @@ export {
|
|
|
199
201
|
export { useBuyCreditsModal } from "./store/useBuyCreditsModal";
|
|
200
202
|
export { useCreditsDisabledModal } from "./store/useCreditsDisabledModal";
|
|
201
203
|
export { usePaidPlanRequiredModal } from "./store/usePaidPlanRequiredModal";
|
|
204
|
+
export { useHexaPromoModal } from "./store/useHexaPromoModal";
|
|
202
205
|
export { default as BuyCreditsModal } from "./components/modals/BuyCreditsModal";
|
|
203
206
|
export { default as CreditsDisabledModal } from "./components/modals/CreditsDisabledModal";
|
|
204
207
|
export { default as PaidPlanRequiredModal } from "./components/modals/PaidPlanRequiredModal";
|
|
208
|
+
export { default as HexaPromoModal } from "./components/modals/HexaPromoModal";
|
|
209
|
+
export { default as HexaPromoModalGate } from "./components/modals/HexaPromoModalGate";
|
|
205
210
|
export { default as AddCardModal } from "./components/modals/cards/AddCardModal";
|
|
206
211
|
export { DeleteCardModal } from "./components/modals/cards/DeleteCardModal";
|
|
207
212
|
export { CannotDeleteCardModal } from "./components/modals/cards/CannotDeleteCardModal";
|
|
@@ -8,7 +8,7 @@ export function createListIaCreditsHandler() {
|
|
|
8
8
|
return async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
9
9
|
return safeRouteHandler(async () => {
|
|
10
10
|
const { id } = await params;
|
|
11
|
-
return iaCreditsService.
|
|
11
|
+
return iaCreditsService.getSummaryData(id);
|
|
12
12
|
});
|
|
13
13
|
};
|
|
14
14
|
}
|
|
@@ -3,67 +3,51 @@
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import { useQuery } from '@tanstack/react-query';
|
|
5
5
|
import { apiFetch } from '../../../infra/http/api-fetch';
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
6
|
+
import type { SuccessResult } from '../../../infra/api/types';
|
|
7
|
+
import type { IaCreditsSummary, IaCreditsSummaryData } from '../types';
|
|
8
8
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
9
9
|
import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const today = new Date();
|
|
13
|
-
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
11
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
function startOfDay(date: Date): Date {
|
|
14
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function calculateSummary(data: IaCreditsSummaryData | null): IaCreditsSummary {
|
|
18
|
+
// Blindagem: se vier um shape inesperado (ex.: resposta/cache antigo do
|
|
19
|
+
// endpoint paginado), degrada para o sumário vazio em vez de crashar.
|
|
20
|
+
const addBatches = Array.isArray(data?.addBatches) ? data.addBatches : [];
|
|
21
|
+
const availableCredits = Number(data?.availableCredits) || 0;
|
|
22
|
+
|
|
23
|
+
const today = startOfDay(new Date());
|
|
17
24
|
|
|
18
25
|
let totalCredits = 0;
|
|
19
26
|
let usedCredits = 0;
|
|
20
27
|
let nextExpirationInDays: number | null = null;
|
|
21
28
|
let nextExpiringCredits: number | null = null;
|
|
22
29
|
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
if (!expirationRaw) continue;
|
|
26
|
-
|
|
27
|
-
const expiration = new Date(expirationRaw);
|
|
28
|
-
const expirationDate = new Date(
|
|
29
|
-
expiration.getFullYear(),
|
|
30
|
-
expiration.getMonth(),
|
|
31
|
-
expiration.getDate()
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const expiredByDate = expirationDate.getTime() < todayDate.getTime();
|
|
35
|
-
const hasExpired = add.has_expired || expiredByDate;
|
|
30
|
+
for (const batch of addBatches) {
|
|
31
|
+
if (batch.has_expired) continue;
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
totalCredits += batch.added_credits;
|
|
34
|
+
usedCredits += batch.consumed_credits;
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
if (batch.remaining_credits <= 0 || !batch.expiration_time) continue;
|
|
40
37
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
.reduce((sum, sub) => sum + sub.operation_value, 0);
|
|
38
|
+
const expirationDate = startOfDay(new Date(batch.expiration_time));
|
|
39
|
+
const days = Math.max(0, Math.ceil((expirationDate.getTime() - today.getTime()) / MS_PER_DAY));
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
usedCredits += consumedForAdd;
|
|
49
|
-
|
|
50
|
-
const diffMs = expirationDate.getTime() - todayDate.getTime();
|
|
51
|
-
const days = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
52
|
-
|
|
53
|
-
if (remainingForAdd > 0) {
|
|
54
|
-
if (nextExpirationInDays === null || days < nextExpirationInDays) {
|
|
55
|
-
nextExpirationInDays = days;
|
|
56
|
-
nextExpiringCredits = remainingForAdd;
|
|
57
|
-
}
|
|
41
|
+
if (nextExpirationInDays === null || days < nextExpirationInDays) {
|
|
42
|
+
nextExpirationInDays = days;
|
|
43
|
+
nextExpiringCredits = batch.remaining_credits;
|
|
58
44
|
}
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
const availableCredits = Math.max(0, totalCredits - usedCredits);
|
|
62
|
-
|
|
63
47
|
return {
|
|
64
48
|
totalCredits,
|
|
65
49
|
usedCredits,
|
|
66
|
-
availableCredits,
|
|
50
|
+
availableCredits: Math.max(0, availableCredits),
|
|
67
51
|
nextExpiringCredits,
|
|
68
52
|
nextExpirationInDays,
|
|
69
53
|
};
|
|
@@ -82,7 +66,7 @@ export function useIaCredits(options: UseIaCreditsOptions = {}) {
|
|
|
82
66
|
const query = useQuery({
|
|
83
67
|
queryKey,
|
|
84
68
|
queryFn: ({ signal }) =>
|
|
85
|
-
apiFetch<
|
|
69
|
+
apiFetch<SuccessResult<IaCreditsSummaryData>>(
|
|
86
70
|
`/api/subscriptions/${subscriptionId}/ia-credits`,
|
|
87
71
|
{ signal }
|
|
88
72
|
),
|
|
@@ -90,14 +74,12 @@ export function useIaCredits(options: UseIaCreditsOptions = {}) {
|
|
|
90
74
|
refetchInterval: options.refetchInterval ?? false,
|
|
91
75
|
});
|
|
92
76
|
|
|
93
|
-
const
|
|
77
|
+
const summaryData = query.data?.data ?? null;
|
|
94
78
|
|
|
95
|
-
|
|
96
|
-
const summary = useMemo(() => calculateSummary(operations), [operations]);
|
|
79
|
+
const summary = useMemo(() => calculateSummary(summaryData), [summaryData]);
|
|
97
80
|
|
|
98
81
|
return {
|
|
99
82
|
subscription,
|
|
100
|
-
operations,
|
|
101
83
|
summary,
|
|
102
84
|
isLoading: isSubscriptionLoading || query.isPending,
|
|
103
85
|
isSubscriptionLoading,
|
|
@@ -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();
|