@greatapps/common 1.1.6 → 1.1.8

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.
Files changed (46) hide show
  1. package/dist/components/layouts/ProfilePopover.mjs +16 -5
  2. package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
  3. package/dist/index.mjs +4 -0
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/infra/api/client.mjs +14 -6
  6. package/dist/infra/api/client.mjs.map +1 -1
  7. package/dist/modules/auth/utils/get-user-context.mjs +24 -0
  8. package/dist/modules/auth/utils/get-user-context.mjs.map +1 -0
  9. package/dist/modules/ia-credits/actions/list-ia-credits.action.mjs +9 -0
  10. package/dist/modules/ia-credits/actions/list-ia-credits.action.mjs.map +1 -0
  11. package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs +75 -0
  12. package/dist/modules/ia-credits/hooks/ia-credits.hook.mjs.map +1 -0
  13. package/dist/modules/ia-credits/services/ia-credits.service.mjs +31 -0
  14. package/dist/modules/ia-credits/services/ia-credits.service.mjs.map +1 -0
  15. package/dist/modules/ia-credits/types.mjs +25 -0
  16. package/dist/modules/ia-credits/types.mjs.map +1 -0
  17. package/dist/modules/subscriptions/actions/list-subscriptions.action.mjs +9 -0
  18. package/dist/modules/subscriptions/actions/list-subscriptions.action.mjs.map +1 -0
  19. package/dist/modules/subscriptions/hooks/list-subscriptions.hook.mjs +14 -0
  20. package/dist/modules/subscriptions/hooks/list-subscriptions.hook.mjs.map +1 -0
  21. package/dist/modules/subscriptions/services/subscriptions.service.mjs +32 -0
  22. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -0
  23. package/dist/modules/subscriptions/types.mjs +12 -0
  24. package/dist/modules/subscriptions/types.mjs.map +1 -0
  25. package/dist/modules/users/services/user.service.mjs +1 -1
  26. package/dist/modules/users/services/user.service.mjs.map +1 -1
  27. package/dist/modules/whitelabel/services/whitelabel.service.mjs +2 -2
  28. package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
  29. package/dist/server.mjs +12 -1
  30. package/dist/server.mjs.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/layouts/ProfilePopover.tsx +115 -97
  33. package/src/index.ts +4 -0
  34. package/src/infra/api/client.ts +19 -8
  35. package/src/modules/auth/utils/get-user-context.ts +29 -0
  36. package/src/modules/ia-credits/actions/list-ia-credits.action.ts +11 -0
  37. package/src/modules/ia-credits/hooks/ia-credits.hook.ts +96 -0
  38. package/src/modules/ia-credits/services/ia-credits.service.ts +37 -0
  39. package/src/modules/ia-credits/types.ts +33 -0
  40. package/src/modules/subscriptions/actions/list-subscriptions.action.ts +11 -0
  41. package/src/modules/subscriptions/hooks/list-subscriptions.hook.ts +14 -0
  42. package/src/modules/subscriptions/services/subscriptions.service.ts +38 -0
  43. package/src/modules/subscriptions/types.ts +17 -0
  44. package/src/modules/users/services/user.service.ts +1 -1
  45. package/src/modules/whitelabel/services/whitelabel.service.ts +2 -2
  46. package/src/server.ts +6 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["\r\nimport greatCache from '@greatapps/cache';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from '../schema';\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.R3_API_URL;\r\n if (!apiUrl) {\r\n throw new ApiError('R3_API_URL not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return apiUrl;\r\n }\r\n\r\n private getToken(): string {\r\n const token = process.env.WHITELABEL_TOKEN_MASTER;\r\n if (!token) {\r\n throw new ApiError('WHITELABEL_TOKEN_MASTER not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return token;\r\n }\r\n\r\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = 'greatapps.com.br';\r\n\r\n const apiUrl = this.getApiUrl();\r\n const whitelabelMasterToken = this.getToken();\r\n\r\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\r\n\r\n console.log('[WhitelabelService] Fetching token for domain', { url });\r\n\r\n const cache = new greatCache({\r\n service: 'whitelabel-service',\r\n version: '1.0',\r\n domain: 'whitelabel-cache.greatapps.com.br',\r\n ambient: process.env.NODE_ENV || 'development',\r\n });\r\n\r\n const cachedData = await cache.select(`whitelabel-token-${hostname}`);\r\n\r\n if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {\r\n console.log('[WhitelabelService] Cache hit for domain', { hostname });\r\n\r\n return JSON.parse(cachedData.data) as WhitelabelTokenData;\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n authorization: whitelabelMasterToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n console.error('[WhitelabelService] Failed to fetch whitelabel token', { response });\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n 'FETCH_ERROR',\r\n response.status\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError('Whitelabel token not found', 'TOKEN_NOT_FOUND', 404);\r\n }\r\n\r\n await cache.insert(`whitelabel-token-${hostname}`, JSON.stringify(result.data[0]), 604800);\r\n\r\n return result.data[0];\r\n }\r\n}\r\n\r\nexport const whitelabelService = new WhitelabelService();\r\n"],"mappings":"AACA,OAAO,gBAAgB;AACvB,SAAS,gBAAgB;AAGzB,MAAM,kBAAkB;AAAA,EACd,YAAoB;AAC1B,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,SAAS,6BAA6B,gBAAgB,GAAG;AAAA,IACrE;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAmB;AACzB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,0CAA0C,gBAAgB,GAAG;AAAA,IAClF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBAAiB,UAAgD;AACrE,eAAW;AAEX,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,wBAAwB,KAAK,SAAS;AAE5C,UAAM,MAAM,GAAG,MAAM,0BAA0B,QAAQ;AAEvD,YAAQ,IAAI,iDAAiD,EAAE,IAAI,CAAC;AAEpE,UAAM,QAAQ,IAAI,WAAW;AAAA,MAC3B,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS,QAAQ,IAAI,YAAY;AAAA,IACnC,CAAC;AAED,UAAM,aAAa,MAAM,MAAM,OAAO,oBAAoB,QAAQ,EAAE;AAEpE,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,cAAQ,IAAI,4CAA4C,EAAE,SAAS,CAAC;AAEpE,aAAO,KAAK,MAAM,WAAW,IAAI;AAAA,IACnC;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,wDAAwD,EAAE,SAAS,CAAC;AAClF,YAAM,IAAI;AAAA,QACR,qCAAqC,SAAS,MAAM;AAAA,QACpD;AAAA,QACA,SAAS;AAAA,MACX;AAAA,IACF;AAEA,UAAM,SAAqC,MAAM,SAAS,KAAK;AAE/D,QAAI,OAAO,WAAW,KAAK,CAAC,OAAO,MAAM,QAAQ;AAC/C,YAAM,IAAI,SAAS,8BAA8B,mBAAmB,GAAG;AAAA,IACzE;AAEA,UAAM,MAAM,OAAO,oBAAoB,QAAQ,IAAI,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,GAAG,MAAM;AAEzF,WAAO,OAAO,KAAK,CAAC;AAAA,EACtB;AACF;AAEO,MAAM,oBAAoB,IAAI,kBAAkB;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["\r\nimport greatCache from '@greatapps/cache';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from '../schema';\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.GAPPS_R3_API_URL;\r\n if (!apiUrl) {\r\n throw new ApiError('GAPPS_R3_API_URL not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return apiUrl;\r\n }\r\n\r\n private getToken(): string {\r\n const token = process.env.WHITELABEL_TOKEN_MASTER;\r\n if (!token) {\r\n throw new ApiError('WHITELABEL_TOKEN_MASTER not configured', 'CONFIG_ERROR', 500);\r\n }\r\n return token;\r\n }\r\n\r\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = 'greatapps.com.br';\r\n\r\n const apiUrl = this.getApiUrl();\r\n const whitelabelMasterToken = this.getToken();\r\n\r\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\r\n\r\n console.log('[WhitelabelService] Fetching token for domain', { url });\r\n\r\n const cache = new greatCache({\r\n service: 'whitelabel-service',\r\n version: '1.0',\r\n domain: 'whitelabel-cache.greatapps.com.br',\r\n ambient: process.env.NODE_ENV || 'development',\r\n });\r\n\r\n const cachedData = await cache.select(`whitelabel-token-${hostname}`);\r\n\r\n if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {\r\n console.log('[WhitelabelService] Cache hit for domain', { hostname });\r\n\r\n return JSON.parse(cachedData.data) as WhitelabelTokenData;\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n authorization: whitelabelMasterToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n console.error('[WhitelabelService] Failed to fetch whitelabel token', { response });\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n 'FETCH_ERROR',\r\n response.status\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError('Whitelabel token not found', 'TOKEN_NOT_FOUND', 404);\r\n }\r\n\r\n await cache.insert(`whitelabel-token-${hostname}`, JSON.stringify(result.data[0]), 604800);\r\n\r\n return result.data[0];\r\n }\r\n}\r\n\r\nexport const whitelabelService = new WhitelabelService();\r\n"],"mappings":"AACA,OAAO,gBAAgB;AACvB,SAAS,gBAAgB;AAGzB,MAAM,kBAAkB;AAAA,EACd,YAAoB;AAC1B,UAAM,SAAS,QAAQ,IAAI;AAC3B,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,SAAS,mCAAmC,gBAAgB,GAAG;AAAA,IAC3E;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,WAAmB;AACzB,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,0CAA0C,gBAAgB,GAAG;AAAA,IAClF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBAAiB,UAAgD;AACrE,eAAW;AAEX,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,wBAAwB,KAAK,SAAS;AAE5C,UAAM,MAAM,GAAG,MAAM,0BAA0B,QAAQ;AAEvD,YAAQ,IAAI,iDAAiD,EAAE,IAAI,CAAC;AAEpE,UAAM,QAAQ,IAAI,WAAW;AAAA,MAC3B,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS,QAAQ,IAAI,YAAY;AAAA,IACnC,CAAC;AAED,UAAM,aAAa,MAAM,MAAM,OAAO,oBAAoB,QAAQ,EAAE;AAEpE,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,cAAQ,IAAI,4CAA4C,EAAE,SAAS,CAAC;AAEpE,aAAO,KAAK,MAAM,WAAW,IAAI;AAAA,IACnC;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,wDAAwD,EAAE,SAAS,CAAC;AAClF,YAAM,IAAI;AAAA,QACR,qCAAqC,SAAS,MAAM;AAAA,QACpD;AAAA,QACA,SAAS;AAAA,MACX;AAAA,IACF;AAEA,UAAM,SAAqC,MAAM,SAAS,KAAK;AAE/D,QAAI,OAAO,WAAW,KAAK,CAAC,OAAO,MAAM,QAAQ;AAC/C,YAAM,IAAI,SAAS,8BAA8B,mBAAmB,GAAG;AAAA,IACzE;AAEA,UAAM,MAAM,OAAO,oBAAoB,QAAQ,IAAI,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,GAAG,MAAM;AAEzF,WAAO,OAAO,KAAK,CAAC;AAAA,EACtB;AACF;AAEO,MAAM,oBAAoB,IAAI,kBAAkB;","names":[]}
package/dist/server.mjs CHANGED
@@ -1,16 +1,27 @@
1
1
  import "server-only";
2
- import { apiClient, ApiClient } from "./infra/api/client";
2
+ import { ApiClient, api, apiClient } from "./infra/api/client";
3
3
  import { authService } from "./modules/auth/services/auth.service";
4
4
  import { whitelabelService } from "./modules/whitelabel/services/whitelabel.service";
5
+ import { subscriptionsService } from "./modules/subscriptions/services/subscriptions.service";
6
+ import { iaCreditsService } from "./modules/ia-credits/services/ia-credits.service";
5
7
  import { findWhitelabel } from "./modules/whitelabel/actions/find-whitelabel.action";
6
8
  import { validateSessionAction } from "./modules/auth/actions/validate-session.action";
9
+ import { listSubscriptionsAction } from "./modules/subscriptions/actions/list-subscriptions.action";
10
+ import { listIaCreditsAction } from "./modules/ia-credits/actions/list-ia-credits.action";
7
11
  import { getClientInfoFromRequest } from "./infra/utils/client-info";
12
+ import { getUserContext } from "./modules/auth/utils/get-user-context";
8
13
  export {
9
14
  ApiClient,
15
+ api,
10
16
  apiClient,
11
17
  authService,
12
18
  findWhitelabel,
13
19
  getClientInfoFromRequest,
20
+ getUserContext,
21
+ iaCreditsService,
22
+ listIaCreditsAction,
23
+ listSubscriptionsAction,
24
+ subscriptionsService,
14
25
  validateSessionAction,
15
26
  whitelabelService
16
27
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { apiClient, ApiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,iBAAiB;AAGrC,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAGlC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AAGtC,SAAS,gCAAgC;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,4BAA4B;AACrC,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AAGpC,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -1,16 +1,17 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import * as PopoverPrimitive from '@radix-ui/react-popover';
6
- import { Popover } from '../ui/overlay/Popover';
7
- import { Button } from '../ui/buttons/Button';
8
- import { Progress } from '../ui/feedback/Progress';
9
- import { Separator } from '../ui/data-display/Separator';
10
- import { NavBarItem } from './NavBarItem';
11
- import { UserAvatar } from '../ui/data-display/UserAvatar';
12
- import { useAuth } from '../../providers/auth.provider';
13
- import { cn } from '../../infra/utils/clsx';
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
6
+ import { Popover } from "../ui/overlay/Popover";
7
+ import { Button } from "../ui/buttons/Button";
8
+ import { Progress } from "../ui/feedback/Progress";
9
+ import { Separator } from "../ui/data-display/Separator";
10
+ import { NavBarItem } from "./NavBarItem";
11
+ import { UserAvatar } from "../ui/data-display/UserAvatar";
12
+ import { useAuth } from "../../providers/auth.provider";
13
+ import { cn } from "../../infra/utils/clsx";
14
+ import { useIaCredits } from "../../modules/ia-credits/hooks/ia-credits.hook";
14
15
 
15
16
  export interface ProfileMenuItem {
16
17
  icon?: React.ReactNode;
@@ -19,11 +20,10 @@ export interface ProfileMenuItem {
19
20
  }
20
21
 
21
22
  export interface ProfilePopoverProps {
22
- side?: 'top' | 'bottom' | 'left' | 'right';
23
- align?: 'start' | 'center' | 'end';
23
+ side?: "top" | "bottom" | "left" | "right";
24
+ align?: "start" | "center" | "end";
24
25
  contentClassName?: string;
25
26
  onEditProfile?: () => void;
26
- credits?: { used: number; total: number };
27
27
  onCreditsClick?: () => void;
28
28
  menuItems?: ProfileMenuItem[];
29
29
  logoutRedirect?: string;
@@ -31,35 +31,41 @@ export interface ProfilePopoverProps {
31
31
  }
32
32
 
33
33
  function ProfilePopover({
34
- side = 'top',
35
- align = 'start',
36
- contentClassName = 'absolute bottom-2 left-[26px]',
34
+ side = "top",
35
+ align = "start",
36
+ contentClassName = "absolute bottom-2 left-[26px]",
37
37
  onEditProfile,
38
- credits,
39
38
  onCreditsClick,
40
39
  menuItems = [],
41
- logoutRedirect = '/login',
40
+ logoutRedirect = "/login",
42
41
  logoutOverlay,
43
42
  }: ProfilePopoverProps) {
44
43
  const router = useRouter();
45
44
  const { user, logout } = useAuth();
46
45
  const [isLoggingOut, setIsLoggingOut] = useState(false);
47
46
 
47
+ const { summary, subscription, isSubscriptionLoading, isCreditsLoading } =
48
+ useIaCredits();
49
+
48
50
  const handleLogout = async () => {
49
51
  try {
50
52
  setIsLoggingOut(true);
51
53
  await logout();
52
54
  router.push(logoutRedirect);
53
55
  } catch (error) {
54
- console.error('Erro ao fazer logout:', error);
56
+ console.error("Erro ao fazer logout:", error);
55
57
  } finally {
56
58
  setIsLoggingOut(false);
57
59
  }
58
60
  };
59
61
 
62
+ const hasSubscription = !isSubscriptionLoading && subscription !== null;
63
+ const showCreditsSkeleton = hasSubscription && isCreditsLoading;
64
+ const showCredits =
65
+ hasSubscription && !isCreditsLoading && summary.totalCredits > 0;
60
66
  const creditsPercentage =
61
- credits && credits.total > 0
62
- ? (credits.used / credits.total) * 100
67
+ showCredits && summary.totalCredits > 0
68
+ ? (summary.usedCredits / summary.totalCredits) * 100
63
69
  : 0;
64
70
 
65
71
  return (
@@ -78,89 +84,101 @@ function ProfilePopover({
78
84
  </PopoverPrimitive.Trigger>
79
85
 
80
86
  <PopoverPrimitive.Portal>
81
- <PopoverPrimitive.Content
82
- className={cn(
83
- 'w-64.75 p-0 bg-white rounded-2xl shadow-md border border-gray-200 z-99',
84
- contentClassName
85
- )}
86
- side={side}
87
- align={align}
88
- sideOffset={8}
89
- >
90
- <div className="grid grid-cols-[32px_1fr_auto] items-center gap-3 p-3 w-full">
91
- {user && (
92
- <UserAvatar
93
- key={user.id}
94
- photo={user.photo}
95
- name={user.name}
96
- size={32}
97
- />
87
+ <PopoverPrimitive.Content
88
+ className={cn(
89
+ "w-64.75 p-0 bg-white rounded-2xl shadow-md border border-gray-200 z-99",
90
+ contentClassName,
98
91
  )}
92
+ side={side}
93
+ align={align}
94
+ sideOffset={8}
95
+ >
96
+ <div className="grid grid-cols-[32px_1fr_auto] items-center gap-3 p-3 w-full">
97
+ {user && (
98
+ <UserAvatar
99
+ key={user.id}
100
+ photo={user.photo}
101
+ name={user.name}
102
+ size={32}
103
+ />
104
+ )}
99
105
 
100
- <div className="flex flex-col gap-0.5 min-w-0">
101
- <span className="paragraph-small-semibold text-gray-950 truncate">
102
- {user?.name || ''}
103
- </span>
104
- <span className="paragraph-xsmall-medium text-gray-500 truncate">
105
- {user?.email || ''}
106
- </span>
107
- </div>
106
+ <div className="flex flex-col gap-0.5 min-w-0">
107
+ <span className="paragraph-small-semibold text-gray-950 truncate">
108
+ {user?.name || ""}
109
+ </span>
110
+ <span className="paragraph-xsmall-medium text-gray-500 truncate">
111
+ {user?.email || ""}
112
+ </span>
113
+ </div>
108
114
 
109
- {onEditProfile && (
110
- <Button
111
- variant="secondary"
112
- className="paragraph-small-semibold h-8! whitespace-nowrap px-3 w-fit"
113
- onClick={onEditProfile}
114
- >
115
- Editar Perfil
116
- </Button>
117
- )}
118
- </div>
115
+ {onEditProfile && (
116
+ <Button
117
+ variant="secondary"
118
+ className="paragraph-small-semibold h-8! whitespace-nowrap px-3 w-fit"
119
+ onClick={onEditProfile}
120
+ >
121
+ Editar Perfil
122
+ </Button>
123
+ )}
124
+ </div>
119
125
 
120
- <div className="flex flex-col gap-2 border-t border-gray-200 rounded-2xl p-1.5">
121
- {credits && (
122
- <div
123
- className={cn(
124
- 'flex flex-col bg-gray-950 rounded-lg gap-4 w-full p-4 transition-colors',
125
- onCreditsClick && 'cursor-pointer hover:bg-gray-900'
126
- )}
127
- onClick={onCreditsClick}
128
- >
129
- <div className="flex items-center justify-between gap-2">
130
- <span className="paragraph-xsmall-semibold text-white truncate">
131
- Créditos usados
132
- </span>
133
- <span className="paragraph-xsmall-medium text-white/50">
134
- {credits.used.toLocaleString('pt-BR')}/
135
- {credits.total.toLocaleString('pt-BR')}
136
- </span>
126
+ <div className="flex flex-col gap-2 border-t border-gray-200 rounded-2xl p-1.5">
127
+ {showCreditsSkeleton && (
128
+ <div className="flex flex-col bg-gray-950 rounded-lg gap-4 w-full p-4">
129
+ <div className="flex items-center justify-between gap-2">
130
+ <span className="paragraph-xsmall-semibold text-white truncate">
131
+ Créditos usados
132
+ </span>
133
+ <div className="h-3.5 w-16 rounded bg-white/20 animate-pulse" />
134
+ </div>
135
+ <div className="h-1.5 w-full rounded-full bg-white/20 animate-pulse" />
137
136
  </div>
138
- <Progress
139
- className="bg-white/10"
140
- indicatorColor="bg-white"
141
- value={creditsPercentage}
142
- />
143
- </div>
144
- )}
137
+ )}
145
138
 
146
- <div className="flex flex-col gap-2">
147
- {menuItems.length > 0 && (
148
- <div className="flex flex-col">
149
- {menuItems.map((item, index) => (
150
- <NavBarItem
151
- key={index}
152
- icon={item.icon}
153
- label={item.label}
154
- onClick={item.onClick}
155
- />
156
- ))}
139
+ {showCredits && (
140
+ <div
141
+ className={cn(
142
+ "flex flex-col bg-gray-950 rounded-lg gap-4 w-full p-4 transition-colors",
143
+ onCreditsClick && "cursor-pointer hover:bg-gray-900",
144
+ )}
145
+ onClick={onCreditsClick}
146
+ >
147
+ <div className="flex items-center justify-between gap-2">
148
+ <span className="paragraph-xsmall-semibold text-white truncate">
149
+ Créditos usados
150
+ </span>
151
+ <span className="paragraph-xsmall-medium text-white/50">
152
+ {summary.usedCredits.toLocaleString("pt-BR")}/
153
+ {summary.totalCredits.toLocaleString("pt-BR")}
154
+ </span>
155
+ </div>
156
+ <Progress
157
+ className="bg-white/10"
158
+ indicatorColor="bg-white"
159
+ value={creditsPercentage}
160
+ />
157
161
  </div>
158
162
  )}
159
- <Separator />
160
- <NavBarItem label="Sair" onClick={handleLogout} />
163
+
164
+ <div className="flex flex-col gap-2">
165
+ {menuItems.length > 0 && (
166
+ <div className="flex flex-col">
167
+ {menuItems.map((item, index) => (
168
+ <NavBarItem
169
+ key={index}
170
+ icon={item.icon}
171
+ label={item.label}
172
+ onClick={item.onClick}
173
+ />
174
+ ))}
175
+ </div>
176
+ )}
177
+ <Separator />
178
+ <NavBarItem label="Sair" onClick={handleLogout} />
179
+ </div>
161
180
  </div>
162
- </div>
163
- </PopoverPrimitive.Content>
181
+ </PopoverPrimitive.Content>
164
182
  </PopoverPrimitive.Portal>
165
183
  </Popover>
166
184
  </>
package/src/index.ts CHANGED
@@ -11,6 +11,10 @@ export * from './providers/whitelabel.provider';
11
11
 
12
12
  // Hooks
13
13
  export { useUserQuery, useUserValidateSession, useInvalidateUser, useSetUserData, USER_QUERY_KEY } from './modules/auth/hooks/useUserQuery';
14
+ export { useActiveSubscription } from './modules/subscriptions/hooks/list-subscriptions.hook';
15
+ export { useIaCredits } from './modules/ia-credits/hooks/ia-credits.hook';
16
+ export type { IaCreditsSummary, IaCreditOperation } from './modules/ia-credits/types';
17
+ export type { Subscription, FindSubscriptionsParams } from './modules/subscriptions/types';
14
18
 
15
19
  // Providers
16
20
  export { QueryProvider } from './providers/query.provider';
@@ -1,19 +1,23 @@
1
1
  import { ApiError } from './types';
2
2
  import { findWhitelabel } from '../../modules/whitelabel/actions/find-whitelabel.action';
3
3
 
4
- const API_BASE_URL = process.env.R3_API_URL || 'https://r3-api.greatapps.dev.br';
5
- const API_VERSION = 'v1';
6
- const API_LOCALE = 'pt-br';
7
-
8
4
  interface RequestConfig extends RequestInit {
9
5
  timeout?: number;
10
6
  }
11
7
 
12
8
  class ApiClient {
9
+ private readonly baseUrl: string;
10
+ private readonly apiVersion = 'v1';
11
+ private readonly apiLocale = 'pt-br';
12
+
13
+ constructor(baseUrl: string) {
14
+ this.baseUrl = baseUrl;
15
+ }
16
+
13
17
  private async buildUrl(endpoint: string, whiteLabelId: number): Promise<string> {
14
- const url = `${API_BASE_URL}/${API_VERSION}/${API_LOCALE}/${whiteLabelId}${endpoint}`;
18
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.apiLocale}/${whiteLabelId}${endpoint}`;
15
19
  console.log('[ApiClient] buildUrl', {
16
- baseURL: API_BASE_URL,
20
+ baseURL: this.baseUrl,
17
21
  whiteLabelId,
18
22
  endpoint,
19
23
  fullUrl: url,
@@ -135,5 +139,12 @@ class ApiClient {
135
139
  }
136
140
  }
137
141
 
138
- export { ApiClient };
139
- export const apiClient = new ApiClient();
142
+ const api = {
143
+ apps: new ApiClient(process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br'),
144
+ pages: new ApiClient(process.env.GPAGES_R3_API_URL || 'https://r3-api.greatpages.dev.br'),
145
+ }
146
+
147
+ /** @deprecated use api.apps */
148
+ export const apiClient = api.apps;
149
+
150
+ export { api, ApiClient };
@@ -0,0 +1,29 @@
1
+ import 'server-only';
2
+
3
+ import { cookies } from 'next/headers';
4
+ import { ApiError, JWTPayload } from '../../../infra/api/types';
5
+
6
+ const AUTH_COOKIE_NAME = 'greatapps';
7
+
8
+ export async function getUserContext(): Promise<{
9
+ id_account: number;
10
+ id_user: number;
11
+ }> {
12
+ const cookieStore = await cookies();
13
+ const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
14
+
15
+ if (!token) {
16
+ throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED', 401);
17
+ }
18
+
19
+ try {
20
+ const payload = JSON.parse(atob(token.split('.')[1])) as JWTPayload;
21
+
22
+ return {
23
+ id_account: payload.id_account,
24
+ id_user: payload.id_user,
25
+ };
26
+ } catch {
27
+ throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ 'use server';
2
+
3
+ import { PaginatedSuccessResult } from '../../../infra/api/types';
4
+ import type { IaCreditOperation } from '../types';
5
+ import { iaCreditsService } from '../services/ia-credits.service';
6
+
7
+ export async function listIaCreditsAction(
8
+ subscriptionId: number | string
9
+ ): Promise<PaginatedSuccessResult<IaCreditOperation>> {
10
+ return iaCreditsService.listOperations(subscriptionId);
11
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useQuery } from '@tanstack/react-query';
5
+ import { useActiveSubscription } from '../../subscriptions/hooks/list-subscriptions.hook';
6
+ import { listIaCreditsAction } from '../actions/list-ia-credits.action';
7
+ import type { IaCreditOperation, IaCreditsSummary } from '../types';
8
+
9
+ function calculateSummary(operations: IaCreditOperation[]): IaCreditsSummary {
10
+ const today = new Date();
11
+ const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
12
+
13
+ const adds = operations.filter((op) => op.operation === 'add');
14
+ const subtracts = operations.filter((op) => op.operation === 'subtract');
15
+
16
+ let totalCredits = 0;
17
+ let usedCredits = 0;
18
+ let nextExpirationInDays: number | null = null;
19
+ let nextExpiringCredits: number | null = null;
20
+
21
+ for (const add of adds) {
22
+ const expirationRaw = add.expiration_time;
23
+ if (!expirationRaw) continue;
24
+
25
+ const expiration = new Date(expirationRaw);
26
+ const expirationDate = new Date(
27
+ expiration.getFullYear(),
28
+ expiration.getMonth(),
29
+ expiration.getDate()
30
+ );
31
+
32
+ const expiredByDate = expirationDate.getTime() < todayDate.getTime();
33
+ const hasExpired = add.has_expired || expiredByDate;
34
+
35
+ if (hasExpired) continue;
36
+
37
+ const addedCredits = add.operation_value;
38
+
39
+ const consumedForAdd = subtracts
40
+ .filter((sub) => String(sub.consumed_from_add_id) === String(add.id))
41
+ .reduce((sum, sub) => sum + sub.operation_value, 0);
42
+
43
+ const remainingForAdd = Math.max(0, addedCredits - consumedForAdd);
44
+
45
+ totalCredits += addedCredits;
46
+ usedCredits += consumedForAdd;
47
+
48
+ const diffMs = expirationDate.getTime() - todayDate.getTime();
49
+ const days = Math.max(0, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
50
+
51
+ if (remainingForAdd > 0) {
52
+ if (nextExpirationInDays === null || days < nextExpirationInDays) {
53
+ nextExpirationInDays = days;
54
+ nextExpiringCredits = remainingForAdd;
55
+ }
56
+ }
57
+ }
58
+
59
+ const availableCredits = Math.max(0, totalCredits - usedCredits);
60
+
61
+ return {
62
+ totalCredits,
63
+ usedCredits,
64
+ availableCredits,
65
+ nextExpiringCredits,
66
+ nextExpirationInDays,
67
+ };
68
+ }
69
+
70
+ export function useIaCredits() {
71
+ const { data: subscriptionData, isPending: isSubscriptionLoading } = useActiveSubscription();
72
+ const subscription = subscriptionData?.data?.[0] ?? null;
73
+ const subscriptionId = subscription?.id;
74
+
75
+ const query = useQuery({
76
+ queryKey: ['ia-credits', subscriptionId],
77
+ queryFn: () => listIaCreditsAction(subscriptionId!),
78
+ enabled: subscriptionId != null,
79
+ });
80
+
81
+ const operations = query.data?.data ?? [];
82
+
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ const summary = useMemo(() => calculateSummary(operations), [operations]);
85
+
86
+ return {
87
+ subscription,
88
+ operations,
89
+ summary,
90
+ isLoading: isSubscriptionLoading || query.isPending,
91
+ isSubscriptionLoading,
92
+ isCreditsLoading: query.isPending,
93
+ isError: query.isError,
94
+ error: query.error,
95
+ };
96
+ }
@@ -0,0 +1,37 @@
1
+ import 'server-only';
2
+
3
+ import { api } from '../../../infra/api/client';
4
+ import { ApiError, ApiPaginatedActionResult, PaginatedSuccessResult } from '../../../infra/api/types';
5
+ import { getUserContext } from '../../auth/utils/get-user-context';
6
+ import { IaCreditOperation, IaCreditOperationSchema } from '../types';
7
+
8
+ class IaCreditsService {
9
+ async listOperations(
10
+ subscriptionId: number | string
11
+ ): Promise<PaginatedSuccessResult<IaCreditOperation>> {
12
+ const { id_account } = await getUserContext();
13
+
14
+ const endpoint = `/accounts/${id_account}/subscriptions/${subscriptionId}/items/balance/list?page=1&limit=20&sort=id:DESC`;
15
+
16
+ const response = await api.apps.get<ApiPaginatedActionResult<IaCreditOperation>>(endpoint);
17
+
18
+ if (response.status === 0) {
19
+ throw new ApiError(
20
+ response.message || 'Erro ao listar créditos de IA',
21
+ 'LIST_IA_CREDITS_FAILED',
22
+ 400
23
+ );
24
+ }
25
+
26
+ const raw = response.data ?? [];
27
+ const data = raw.map((item) => IaCreditOperationSchema.parse(item));
28
+
29
+ return {
30
+ data,
31
+ total: response.total ?? data.length,
32
+ success: true,
33
+ };
34
+ }
35
+ }
36
+
37
+ export const iaCreditsService = new IaCreditsService();
@@ -0,0 +1,33 @@
1
+ import z from 'zod';
2
+
3
+ export const IaCreditOperationSchema = z
4
+ .object({
5
+ id: z.union([z.number(), z.string()]),
6
+ deleted: z.number(),
7
+ datetime_add: z.string(),
8
+ id_wl: z.number(),
9
+ id_subscription: z.union([z.number(), z.string()]),
10
+ id_addon: z.number(),
11
+ operation: z.string(), // 'add' | 'subtract'
12
+ operation_value: z.union([z.number(), z.string()]).transform((v) => Number(v)),
13
+ balance_after_operation: z.union([z.number(), z.string()]).optional(),
14
+ balance_before_operation: z.union([z.number(), z.string()]).optional(),
15
+ operation_ref: z.string().nullable().optional(),
16
+ operation_origin: z.string().nullable().optional(),
17
+ expiration_time: z.string().nullable(),
18
+ has_expired: z.boolean(),
19
+ consumed_from_add_id: z.union([z.number(), z.string()]).nullable(),
20
+ id_user_add: z.union([z.number(), z.string()]).nullable(),
21
+ user_name: z.string().nullable().optional(),
22
+ })
23
+ .passthrough();
24
+
25
+ export type IaCreditOperation = z.infer<typeof IaCreditOperationSchema>;
26
+
27
+ export type IaCreditsSummary = {
28
+ totalCredits: number;
29
+ usedCredits: number;
30
+ availableCredits: number;
31
+ nextExpiringCredits: number | null;
32
+ nextExpirationInDays: number | null;
33
+ };
@@ -0,0 +1,11 @@
1
+ 'use server';
2
+
3
+ import { PaginatedSuccessResult } from '../../../infra/api/types';
4
+ import type { Subscription, FindSubscriptionsParams } from '../types';
5
+ import { subscriptionsService } from '../services/subscriptions.service';
6
+
7
+ export async function listSubscriptionsAction(
8
+ params?: Partial<FindSubscriptionsParams>
9
+ ): Promise<PaginatedSuccessResult<Subscription>> {
10
+ return subscriptionsService.listSubscriptions(params);
11
+ }
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { listSubscriptionsAction } from '../actions/list-subscriptions.action';
5
+ import type { FindSubscriptionsParams } from '../types';
6
+
7
+ const SUBSCRIPTIONS_QUERY_KEY = ['subscriptions'];
8
+
9
+ export function useActiveSubscription() {
10
+ return useQuery({
11
+ queryKey: [...SUBSCRIPTIONS_QUERY_KEY, 'active'],
12
+ queryFn: () => listSubscriptionsAction({ active: true } as Partial<FindSubscriptionsParams>),
13
+ });
14
+ }