@greatapps/common 1.1.786 → 1.1.787

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 (98) hide show
  1. package/dist/components/layouts/AppMobileNavBar.mjs +64 -5
  2. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  3. package/dist/components/layouts/AppNavBar.mjs +2 -0
  4. package/dist/components/layouts/AppNavBar.mjs.map +1 -1
  5. package/dist/components/layouts/GreatDomainIcon.mjs +40 -0
  6. package/dist/components/layouts/GreatDomainIcon.mjs.map +1 -0
  7. package/dist/components/layouts/NavBar.mjs +111 -52
  8. package/dist/components/layouts/NavBar.mjs.map +1 -1
  9. package/dist/components/ui/form/PhoneInput.mjs +1 -1
  10. package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
  11. package/dist/handlers.mjs +2 -0
  12. package/dist/handlers.mjs.map +1 -1
  13. package/dist/i18n/messages/en-us.mjs +3 -1
  14. package/dist/i18n/messages/en-us.mjs.map +1 -1
  15. package/dist/i18n/messages/es-es.mjs +3 -1
  16. package/dist/i18n/messages/es-es.mjs.map +1 -1
  17. package/dist/i18n/messages/pt-br.mjs +3 -1
  18. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  19. package/dist/index.mjs +9 -1
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/infra/api/client.mjs +5 -1
  22. package/dist/infra/api/client.mjs.map +1 -1
  23. package/dist/infra/api/types.mjs +1 -1
  24. package/dist/infra/api/types.mjs.map +1 -1
  25. package/dist/infra/route/safe-route-handler.mjs +4 -1
  26. package/dist/infra/route/safe-route-handler.mjs.map +1 -1
  27. package/dist/modules/auth/utils/assert-account-not-frozen.mjs +6 -5
  28. package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
  29. package/dist/modules/auth/utils/assert-management-subscription.mjs +7 -14
  30. package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
  31. package/dist/modules/auth/utils/assert-paid-subscription.mjs +7 -12
  32. package/dist/modules/auth/utils/assert-paid-subscription.mjs.map +1 -1
  33. package/dist/modules/auth/utils/assert-subscription-addon.mjs +2 -10
  34. package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
  35. package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
  36. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs +10 -0
  37. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs.map +1 -0
  38. package/dist/modules/subscriptions/constants/subscription.constants.mjs +2 -0
  39. package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
  40. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs +17 -0
  41. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs.map +1 -0
  42. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs +2 -13
  43. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
  44. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs +28 -0
  45. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs.map +1 -0
  46. package/dist/modules/subscriptions/services/subscriptions.service.mjs +26 -0
  47. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
  48. package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
  49. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs +16 -0
  50. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs.map +1 -0
  51. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs +1 -1
  52. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs.map +1 -1
  53. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
  54. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
  55. package/dist/modules/subscriptions/utils/read-current-subscription.mjs +21 -0
  56. package/dist/modules/subscriptions/utils/read-current-subscription.mjs.map +1 -0
  57. package/dist/server.mjs +4 -0
  58. package/dist/server.mjs.map +1 -1
  59. package/dist/store/useMdSidebarStore.mjs +17 -4
  60. package/dist/store/useMdSidebarStore.mjs.map +1 -1
  61. package/dist/utils/safeServerAction.mjs +4 -1
  62. package/dist/utils/safeServerAction.mjs.map +1 -1
  63. package/dist/utils/withAction.mjs +2 -1
  64. package/dist/utils/withAction.mjs.map +1 -1
  65. package/package.json +1 -1
  66. package/src/components/layouts/AppMobileNavBar.tsx +90 -6
  67. package/src/components/layouts/AppNavBar.tsx +5 -1
  68. package/src/components/layouts/GreatDomainIcon.tsx +43 -0
  69. package/src/components/layouts/NavBar.tsx +160 -66
  70. package/src/components/ui/form/PhoneInput.tsx +1 -1
  71. package/src/handlers.ts +1 -0
  72. package/src/i18n/messages/en-us.ts +2 -0
  73. package/src/i18n/messages/es-es.ts +2 -0
  74. package/src/i18n/messages/pt-br.ts +2 -0
  75. package/src/index.ts +7 -1
  76. package/src/infra/api/client.ts +11 -3
  77. package/src/infra/api/types.ts +7 -5
  78. package/src/infra/route/safe-route-handler.ts +4 -1
  79. package/src/modules/auth/utils/assert-account-not-frozen.ts +7 -6
  80. package/src/modules/auth/utils/assert-management-subscription.ts +15 -23
  81. package/src/modules/auth/utils/assert-paid-subscription.ts +7 -15
  82. package/src/modules/auth/utils/assert-subscription-addon.ts +2 -12
  83. package/src/modules/plans/hooks/use-plan-by-id.hook.ts +1 -1
  84. package/src/modules/subscriptions/actions/get-current-subscription.action.ts +8 -0
  85. package/src/modules/subscriptions/constants/subscription.constants.ts +3 -0
  86. package/src/modules/subscriptions/handlers/current-subscription.handler.ts +16 -0
  87. package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +7 -17
  88. package/src/modules/subscriptions/hooks/use-current-subscription.hook.ts +38 -0
  89. package/src/modules/subscriptions/services/subscriptions.service.ts +34 -0
  90. package/src/modules/subscriptions/types/subscription.type.ts +2 -0
  91. package/src/modules/subscriptions/utils/group-subscriptions-by-product.ts +23 -0
  92. package/src/modules/subscriptions/utils/has-active-paid-subscription.ts +1 -2
  93. package/src/modules/subscriptions/utils/has-paid-subscription.ts +1 -2
  94. package/src/modules/subscriptions/utils/read-current-subscription.ts +34 -0
  95. package/src/server.ts +2 -0
  96. package/src/store/useMdSidebarStore.ts +20 -5
  97. package/src/utils/safeServerAction.ts +4 -6
  98. package/src/utils/withAction.ts +2 -1
@@ -6,11 +6,13 @@ import Link from "next/link";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { IconChevronRight } from "@tabler/icons-react";
8
8
  import { Separator } from "../ui/data-display/Separator";
9
+ import { GreatDomainIcon } from "./GreatDomainIcon";
9
10
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
10
11
  import {
11
12
  useDesktopSidebarStore,
12
13
  useDesktopSidebarHydration,
13
14
  } from "../../store/useDesktopSidebarStore";
15
+ import { useMdSidebarHydration } from "../../store/useMdSidebarStore";
14
16
  import {
15
17
  useWhitelabel,
16
18
  useIsDefaultWhitelabel,
@@ -20,7 +22,9 @@ import { cn } from "../../infra/utils/clsx";
20
22
  type NavBarProps = {
21
23
  logoHref: string;
22
24
  appIconHref: string;
23
- activeApp?: "gapps" | "gpages";
25
+ /** Sem href o ícone do GreatDomain não é renderizado: é a flag do app. */
26
+ domainIconHref?: string;
27
+ activeApp?: "gapps" | "gpages" | "gdomain";
24
28
  showExpandButton?: boolean;
25
29
  onExpandClick?: () => void;
26
30
  children?: React.ReactNode;
@@ -30,6 +34,7 @@ type NavBarProps = {
30
34
  function NavBar({
31
35
  logoHref,
32
36
  appIconHref,
37
+ domainIconHref,
33
38
  activeApp,
34
39
  showExpandButton = false,
35
40
  onExpandClick,
@@ -40,6 +45,7 @@ function NavBar({
40
45
  const { isExpanded: isDesktopExpanded, toggle: toggleDesktop } =
41
46
  useDesktopSidebarStore();
42
47
  useDesktopSidebarHydration();
48
+ useMdSidebarHydration();
43
49
  const { whitelabel } = useWhitelabel();
44
50
  const isDefaultWl = useIsDefaultWhitelabel();
45
51
  const primaryColor = whitelabel?.general?.primary_color;
@@ -130,7 +136,9 @@ function NavBar({
130
136
  <IconChevronRight size={16} className="text-gray-500" />
131
137
  </button>
132
138
  </TooltipTrigger>
133
- <TooltipContent side="right">{translate('common.layout.navbar.expandMenu')}</TooltipContent>
139
+ <TooltipContent side="right">
140
+ {translate("common.layout.navbar.expandMenu")}
141
+ </TooltipContent>
134
142
  </Tooltip>
135
143
  )}
136
144
  {/* Desktop expand button */}
@@ -144,78 +152,164 @@ function NavBar({
144
152
  <IconChevronRight size={16} className="text-gray-500" />
145
153
  </button>
146
154
  </TooltipTrigger>
147
- <TooltipContent side="right">{translate('common.layout.navbar.expandMenu')}</TooltipContent>
155
+ <TooltipContent side="right">
156
+ {translate("common.layout.navbar.expandMenu")}
157
+ </TooltipContent>
148
158
  </Tooltip>
149
159
  )}
150
160
  </div>
151
161
 
152
- {/* GreatPages icon */}
153
- <div className="relative">
154
- <Tooltip>
155
- <TooltipTrigger asChild>
156
- <Link
157
- href={appIconHref}
162
+ {/* Ícones de app — 8px entre eles (Figma 5929:2676) */}
163
+ <div className="flex flex-col gap-2 items-center">
164
+ {/* GreatPages icon */}
165
+ <div className="relative">
166
+ <Tooltip>
167
+ <TooltipTrigger asChild>
168
+ <Link
169
+ href={appIconHref}
170
+ className={cn(
171
+ "size-10 rounded-lg cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
172
+ activeApp === "gpages"
173
+ ? isDefaultWl
174
+ ? "bg-cyan-100 border-white ring-cyan-300"
175
+ : "border-white ring-transparent"
176
+ : isDefaultWl
177
+ ? "hover:bg-gray-100 border-transparent ring-transparent"
178
+ : "border-transparent ring-transparent",
179
+ )}
180
+ style={
181
+ !isDefaultWl
182
+ ? {
183
+ backgroundColor: primaryColor,
184
+ boxShadow:
185
+ activeApp === "gpages"
186
+ ? `0 0 0 2px ${primaryColor}40`
187
+ : undefined,
188
+ opacity: activeApp !== "gpages" ? 0.2 : undefined,
189
+ }
190
+ : undefined
191
+ }
192
+ >
193
+ {isDefaultWl ? (
194
+ <Image
195
+ src="/icons/icon-navbar.svg"
196
+ alt="GreatPages"
197
+ width={24}
198
+ height={24}
199
+ />
200
+ ) : (
201
+ <svg
202
+ xmlns="http://www.w3.org/2000/svg"
203
+ width="24"
204
+ height="24"
205
+ viewBox="0 0 24 24"
206
+ fill="none"
207
+ stroke="white"
208
+ strokeWidth="2"
209
+ strokeLinecap="round"
210
+ strokeLinejoin="round"
211
+ >
212
+ <path d="M4 8h16" />
213
+ <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
214
+ <path d="M8 4v4" />
215
+ </svg>
216
+ )}
217
+ </Link>
218
+ </TooltipTrigger>
219
+ <TooltipContent side="right">
220
+ {isDefaultWl
221
+ ? translate("common.layout.navbar.greatPages")
222
+ : translate("common.layout.navbar.pages")}
223
+ </TooltipContent>
224
+ </Tooltip>
225
+ <div
226
+ className={cn(
227
+ "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
228
+ isDefaultWl ? "bg-cyan-300" : "",
229
+ activeApp === "gpages" ? "opacity-100" : "opacity-0",
230
+ )}
231
+ style={
232
+ !isDefaultWl ? { backgroundColor: primaryColor } : undefined
233
+ }
234
+ />
235
+ </div>
236
+
237
+ {/* GreatDomain icon */}
238
+ {domainIconHref ? (
239
+ <div className="relative">
240
+ <Tooltip>
241
+ <TooltipTrigger asChild>
242
+ <Link
243
+ href={domainIconHref}
244
+ aria-label={
245
+ isDefaultWl
246
+ ? translate("common.layout.navbar.greatDomain")
247
+ : translate("common.layout.navbar.domains")
248
+ }
249
+ className={cn(
250
+ "size-10 rounded-[10px] cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
251
+ activeApp === "gdomain"
252
+ ? isDefaultWl
253
+ ? "bg-[#EDE9FE] border-white ring-[#C092FF]"
254
+ : "border-white ring-transparent"
255
+ : isDefaultWl
256
+ ? "hover:bg-gray-100 border-transparent ring-transparent"
257
+ : "border-transparent ring-transparent",
258
+ )}
259
+ style={
260
+ !isDefaultWl
261
+ ? {
262
+ backgroundColor: primaryColor,
263
+ boxShadow:
264
+ activeApp === "gdomain"
265
+ ? `0 0 0 2px ${primaryColor}40`
266
+ : undefined,
267
+ opacity: activeApp !== "gdomain" ? 0.2 : undefined,
268
+ }
269
+ : undefined
270
+ }
271
+ >
272
+ {isDefaultWl ? (
273
+ <GreatDomainIcon active={activeApp === "gdomain"} />
274
+ ) : (
275
+ <svg
276
+ xmlns="http://www.w3.org/2000/svg"
277
+ width="24"
278
+ height="24"
279
+ viewBox="0 0 24 24"
280
+ fill="none"
281
+ stroke="white"
282
+ strokeWidth="2"
283
+ strokeLinecap="round"
284
+ strokeLinejoin="round"
285
+ >
286
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
287
+ <path d="M3.6 9h16.8" />
288
+ <path d="M3.6 15h16.8" />
289
+ <path d="M11.5 3a17 17 0 0 0 0 18" />
290
+ <path d="M12.5 3a17 17 0 0 1 0 18" />
291
+ </svg>
292
+ )}
293
+ </Link>
294
+ </TooltipTrigger>
295
+ <TooltipContent side="right">
296
+ {isDefaultWl
297
+ ? translate("common.layout.navbar.greatDomain")
298
+ : translate("common.layout.navbar.domains")}
299
+ </TooltipContent>
300
+ </Tooltip>
301
+ <div
158
302
  className={cn(
159
- "size-10 rounded-lg cursor-pointer flex items-center justify-center border-2 ring-2 transition-colors duration-300",
160
- activeApp === "gpages"
161
- ? isDefaultWl
162
- ? "bg-cyan-100 border-white ring-cyan-300"
163
- : "border-white ring-transparent"
164
- : isDefaultWl
165
- ? "hover:bg-gray-100 border-transparent ring-transparent"
166
- : "border-transparent ring-transparent",
303
+ "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
304
+ isDefaultWl ? "bg-[#984DFF]" : "",
305
+ activeApp === "gdomain" ? "opacity-100" : "opacity-0",
167
306
  )}
168
307
  style={
169
- !isDefaultWl
170
- ? {
171
- backgroundColor: primaryColor,
172
- boxShadow:
173
- activeApp === "gpages"
174
- ? `0 0 0 2px ${primaryColor}40`
175
- : undefined,
176
- opacity: activeApp !== "gpages" ? 0.2 : undefined,
177
- }
178
- : undefined
308
+ !isDefaultWl ? { backgroundColor: primaryColor } : undefined
179
309
  }
180
- >
181
- {isDefaultWl ? (
182
- <Image
183
- src="/icons/icon-navbar.svg"
184
- alt="GreatPages"
185
- width={24}
186
- height={24}
187
- />
188
- ) : (
189
- <svg
190
- xmlns="http://www.w3.org/2000/svg"
191
- width="24"
192
- height="24"
193
- viewBox="0 0 24 24"
194
- fill="none"
195
- stroke="white"
196
- strokeWidth="2"
197
- strokeLinecap="round"
198
- strokeLinejoin="round"
199
- >
200
- <path d="M4 8h16" />
201
- <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
202
- <path d="M8 4v4" />
203
- </svg>
204
- )}
205
- </Link>
206
- </TooltipTrigger>
207
- <TooltipContent side="right">
208
- {isDefaultWl ? translate('common.layout.navbar.greatPages') : translate('common.layout.navbar.pages')}
209
- </TooltipContent>
210
- </Tooltip>
211
- <div
212
- className={cn(
213
- "absolute -left-4 top-1/2 -translate-y-1/2 z-10 w-[3px] h-5 rounded-r-xl transition-opacity duration-300",
214
- isDefaultWl ? "bg-cyan-300" : "",
215
- activeApp === "gpages" ? "opacity-100" : "opacity-0",
216
- )}
217
- style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
218
- />
310
+ />
311
+ </div>
312
+ ) : null}
219
313
  </div>
220
314
  </div>
221
315
 
@@ -45,7 +45,7 @@ export default function PhoneInput({
45
45
  const translate = useTranslations();
46
46
  const parsed = parsePhoneValue(value ?? '');
47
47
  const [selectedCountry, setSelectedCountry] = useState<Country>(parsed.country);
48
- const [phoneNumber, setPhoneNumber] = useState(parsed.number);
48
+ const [phoneNumber, setPhoneNumber] = useState(formatNational(parsed.number, parsed.country.iso));
49
49
  const [search, setSearch] = useState('');
50
50
  const [open, setOpen] = useState(false);
51
51
  const prevValueRef = useRef(value);
package/src/handlers.ts CHANGED
@@ -20,6 +20,7 @@ export { createListAvailableUsersHandler } from './modules/projects/handlers/lis
20
20
  export { createListAllAccountUsersHandler } from './modules/projects/handlers/list-all-account-users.handler';
21
21
 
22
22
  export { createListSubscriptionsHandler } from './modules/subscriptions/handlers/list-subscriptions.handler';
23
+ export { createCurrentSubscriptionHandler } from './modules/subscriptions/handlers/current-subscription.handler';
23
24
  export { createFreezeNoticeHandler } from './modules/subscriptions/handlers/freeze-notice.handler';
24
25
 
25
26
  export { createListMessagesHandler } from './modules/users/handlers/list-messages.handler';
@@ -540,6 +540,8 @@ const messages = {
540
540
  expandMenu: 'Expand menu',
541
541
  greatPages: 'GreatPages',
542
542
  pages: 'Pages',
543
+ greatDomain: 'GreatDomain',
544
+ domains: 'Domains',
543
545
  },
544
546
  profile: {
545
547
  usedCredits: 'Credits used',
@@ -541,6 +541,8 @@ const messages = {
541
541
  expandMenu: 'Expandir menú',
542
542
  greatPages: 'GreatPages',
543
543
  pages: 'Páginas',
544
+ greatDomain: 'GreatDomain',
545
+ domains: 'Dominios',
544
546
  },
545
547
  profile: {
546
548
  usedCredits: 'Créditos utilizados',
@@ -620,6 +620,8 @@ const messages = {
620
620
  expandMenu: 'Expandir menu',
621
621
  greatPages: 'GreatPages',
622
622
  pages: 'Páginas',
623
+ greatDomain: 'GreatDomain',
624
+ domains: 'Domínios',
623
625
  },
624
626
  profile: {
625
627
  usedCredits: 'Créditos usados',
package/src/index.ts CHANGED
@@ -86,6 +86,8 @@ export {
86
86
  CHARGES_QUERY_KEY,
87
87
  FREEZE_NOTICE_QUERY_KEY,
88
88
  } from "./modules/subscriptions/constants/query-keys.constants";
89
+ export { useCurrentSubscription } from "./modules/subscriptions/hooks/use-current-subscription.hook";
90
+ /** @deprecated Use `useCurrentSubscription`. Sai na 1.1.784. */
89
91
  export { useActiveSubscription } from "./modules/subscriptions/hooks/find-active-subscription.hook";
90
92
  export { useCancelledSubscriptionGuard } from "./modules/subscriptions/hooks/use-cancelled-subscription-guard";
91
93
  export { useAccountFreeze } from "./modules/subscriptions/hooks/use-account-freeze.hook";
@@ -222,7 +224,11 @@ export {
222
224
  confirmPaymentIfRequired,
223
225
  PaymentAuthenticationError,
224
226
  } from "./modules/subscriptions/utils/confirm-payment-if-required";
225
- export { SUBSCRIPTION_GRACE_PERIOD_DAYS } from "./modules/subscriptions/constants/subscription.constants";
227
+ export {
228
+ SUBSCRIPTION_GRACE_PERIOD_DAYS,
229
+ FREE_PLAN_ID,
230
+ } from "./modules/subscriptions/constants/subscription.constants";
231
+ export { groupSubscriptionsByProduct } from "./modules/subscriptions/utils/group-subscriptions-by-product";
226
232
  export { getSubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
227
233
  export type { SubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
228
234
  export {
@@ -31,6 +31,11 @@ export interface ApiClientOptions {
31
31
  emitLogs?: boolean;
32
32
  }
33
33
 
34
+ interface ApiErrorResponseBody {
35
+ message?: string | { message?: string };
36
+ code?: string;
37
+ }
38
+
34
39
  export interface RequestConfig extends RequestInit {
35
40
  timeout?: number;
36
41
  /** Padrão `json`. Use `text` quando a API devolve corpo não-JSON. */
@@ -169,9 +174,12 @@ class ApiClient {
169
174
  let message = `HTTP error ${response.status} ${response.statusText} ${errBody}`;
170
175
  let code = "HTTP_ERROR";
171
176
  try {
172
- const errorData = JSON.parse(errBody) as Record<string, unknown>;
173
- // @ts-ignore -- msg pode ser string ou objeto com .message
174
- message = errorData.message?.message ?? errorData.message ?? message;
177
+ const errorData: ApiErrorResponseBody = JSON.parse(errBody);
178
+ if (typeof errorData.message === "string") {
179
+ message = errorData.message;
180
+ } else if (errorData.message?.message) {
181
+ message = errorData.message.message;
182
+ }
175
183
  if (typeof errorData.code === "string") code = errorData.code;
176
184
  } catch {
177
185
  if (errBody.trim()) message = errBody.trim();
@@ -1,9 +1,9 @@
1
1
  import z from 'zod';
2
2
 
3
- export const PaginationParamsSchema = z.object({
4
- page: z.number().min(1).optional().default(1),
5
- limit: z.number().optional().default(20),
6
- });
3
+ export const PaginationParamsSchema = z.object({
4
+ page: z.number().min(1).optional().default(1),
5
+ limit: z.number().optional().default(20),
6
+ });
7
7
 
8
8
  export interface PaginationParams extends z.infer<typeof PaginationParamsSchema> {}
9
9
 
@@ -51,6 +51,8 @@ export interface PaginatedSuccessResult<T> extends Omit<SuccessResult<T>, 'data'
51
51
  export interface ErrorResult {
52
52
  success: false;
53
53
  error?: string;
54
+ code?: string;
55
+ statusCode?: number;
54
56
  }
55
57
 
56
58
  export type ActionResult<T> = SuccessResult<T> | ErrorResult;
@@ -60,7 +62,7 @@ export class ApiError extends Error {
60
62
  public readonly code: string;
61
63
  public readonly statusCode: number;
62
64
 
63
- constructor(message: string, code: string = 'UNKNOWN_ERROR', statusCode: number = 500) {
65
+ constructor(message: string = 'Erro desconhecido', code: string = 'UNKNOWN_ERROR', statusCode: number = 500) {
64
66
  super(message);
65
67
  this.name = 'ApiError';
66
68
  this.code = code;
@@ -8,7 +8,10 @@ export async function safeRouteHandler<T>(fn: () => Promise<T>): Promise<Respons
8
8
  return Response.json({ success: true, data });
9
9
  } catch (error) {
10
10
  if (error instanceof ApiError) {
11
- return Response.json({ success: false, error: error.message }, { status: error.statusCode });
11
+ return Response.json(
12
+ { success: false, error: error.message, code: error.code },
13
+ { status: error.statusCode },
14
+ );
12
15
  }
13
16
  console.error('[safeRouteHandler] erro inesperado', error);
14
17
  return Response.json({ success: false, error: 'Erro interno do servidor' }, { status: 500 });
@@ -1,22 +1,23 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
5
  import { getAccountFreezeState } from "../../subscriptions/utils/get-account-freeze-state";
6
+ import type { Subscription } from "../../subscriptions/types/subscription.type";
6
7
 
7
8
  export async function assertAccountNotFrozen(): Promise<void> {
8
- const result = await listSubscriptionsAction({ active: true });
9
+ let subscription: Subscription | null;
9
10
 
10
- if (!result.success) {
11
+ try {
12
+ subscription = await readCurrentSubscription();
13
+ } catch (error) {
11
14
  console.error(
12
15
  "[assertAccountNotFrozen] failed to read subscription context:",
13
- result.error,
16
+ error,
14
17
  );
15
18
  return;
16
19
  }
17
20
 
18
- const subscription = result.data?.data?.[0];
19
-
20
21
  if (!subscription) return;
21
22
 
22
23
  if (getAccountFreezeState(subscription) !== "frozen") return;
@@ -1,32 +1,23 @@
1
1
  import "server-only";
2
2
 
3
- import { cache } from "react";
4
-
5
3
  import { ApiError } from "../../../infra/api/types";
6
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
+ import { getSubscriptionCancellationState } from "../../subscriptions/utils/get-subscription-cancellation-state";
7
6
  import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
8
7
 
9
8
  /**
10
- * Deduplicado por request via React.cache, como `getCurrentUserProfile` e `requireValidSession`.
9
+ * A leitura por trás (`readCurrentSubscription`) já é deduplicada por request via
10
+ * React.cache, então não precisa de outra camada aqui: em produção (26/08) o mesmo
11
+ * carregamento disparava até quatro cópias da mesma pergunta em paralelo, com 727 ms,
12
+ * 4,2 s, 5,0 s e 7,0 s, contra um p50 de 528 ms; agora é uma leitura só,
13
+ * compartilhada pelos quatro gates.
11
14
  *
12
- * A leitura por trás é `/accounts/{id}/subscriptions?limit=1`, e ela é MAIS cara que o gate de
13
- * perfil: para conta Stripe a listagem consulta a API do gateway ao vivo. Em produção (26/08) o
14
- * mesmo carregamento disparou quatro dessas em paralelo — 727 ms, 4,2 s, 5,0 s e 7,0 s, contra um
15
- * p50 de 528 ms para o endpoint. Elas se atrasam entre si: são cópias da mesma pergunta disputando
16
- * a mesma fila.
15
+ * A checagem de cancelamento é obrigatória além da de vencimento: a corrente pode
16
+ * vir com `active: false` (baixa por falha de pagamento) com `date_due` ainda no
17
+ * futuro, caso que só `getSubscriptionExpiryDate` não cobre.
17
18
  */
18
- export const assertManagementSubscription = cache(async () => {
19
- const result = await listSubscriptionsAction({ active: true });
20
-
21
- if (!result.success) {
22
- throw new ApiError(
23
- result.error || "Não foi possível validar a assinatura atual",
24
- "SUBSCRIPTION_CONTEXT_FAILED",
25
- 403,
26
- );
27
- }
28
-
29
- const subscription = result.data?.data?.[0];
19
+ export async function assertManagementSubscription(): Promise<void> {
20
+ const subscription = await readCurrentSubscription();
30
21
 
31
22
  if (!subscription) {
32
23
  throw new ApiError(
@@ -36,14 +27,15 @@ export const assertManagementSubscription = cache(async () => {
36
27
  );
37
28
  }
38
29
 
30
+ const isCancelled = getSubscriptionCancellationState(subscription) === "cancelled";
39
31
  const expiryDate = getSubscriptionExpiryDate(subscription);
40
32
  const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();
41
33
 
42
- if (isExpired) {
34
+ if (isCancelled || isExpired) {
43
35
  throw new ApiError(
44
36
  "Sua assinatura está vencida. Assine um novo plano para continuar.",
45
37
  "SUBSCRIPTION_EXPIRED",
46
38
  403,
47
39
  );
48
40
  }
49
- });
41
+ }
@@ -1,21 +1,13 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
+ import { getSubscriptionCancellationState } from "../../subscriptions/utils/get-subscription-cancellation-state";
5
6
  import { getSubscriptionExpiryDate } from "../../subscriptions/utils/get-subscription-expiry-date";
7
+ import { FREE_PLAN_ID } from "../../subscriptions/constants/subscription.constants";
6
8
 
7
9
  export async function assertPaidSubscription() {
8
- const result = await listSubscriptionsAction({ active: true });
9
-
10
- if (!result.success) {
11
- throw new ApiError(
12
- result.error || "Não foi possível validar a assinatura atual",
13
- "SUBSCRIPTION_CONTEXT_FAILED",
14
- 403,
15
- );
16
- }
17
-
18
- const subscription = result.data?.data?.[0];
10
+ const subscription = await readCurrentSubscription();
19
11
 
20
12
  if (!subscription) {
21
13
  throw new ApiError(
@@ -25,10 +17,11 @@ export async function assertPaidSubscription() {
25
17
  );
26
18
  }
27
19
 
20
+ const isCancelled = getSubscriptionCancellationState(subscription) === "cancelled";
28
21
  const expiryDate = getSubscriptionExpiryDate(subscription);
29
22
  const isExpired = !!expiryDate && Date.now() >= expiryDate.getTime();
30
23
 
31
- if (isExpired) {
24
+ if (isCancelled || isExpired) {
32
25
  throw new ApiError(
33
26
  "Sua assinatura está vencida. Assine um novo plano para continuar.",
34
27
  "SUBSCRIPTION_EXPIRED",
@@ -36,8 +29,7 @@ export async function assertPaidSubscription() {
36
29
  );
37
30
  }
38
31
 
39
-
40
- if (subscription.type === 'free' || subscription.id_plan === 5) {
32
+ if (subscription.type === 'free' || subscription.id_plan === FREE_PLAN_ID) {
41
33
  throw new ApiError(
42
34
  "Este recurso está disponível apenas para planos pagos. Faça upgrade do seu plano para continuar.",
43
35
  "PAID_SUBSCRIPTION_REQUIRED",
@@ -1,24 +1,14 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
4
+ import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
5
5
  import type { AddonKindEnum } from "../../subscriptions/constants/addons.constants";
6
6
 
7
7
  export async function assertSubscriptionAddon(
8
8
  addonId: AddonKindEnum,
9
9
  addonLabel: string,
10
10
  ) {
11
- const result = await listSubscriptionsAction({ active: true });
12
-
13
- if (!result.success) {
14
- throw new ApiError(
15
- result.error || "Não foi possível validar a assinatura atual",
16
- "SUBSCRIPTION_CONTEXT_FAILED",
17
- 403,
18
- );
19
- }
20
-
21
- const subscription = result.data?.data?.[0];
11
+ const subscription = await readCurrentSubscription();
22
12
 
23
13
  if (!subscription) {
24
14
  throw new ApiError(
@@ -11,7 +11,7 @@ export function usePlanById(idPlan?: number | string | null) {
11
11
  const queryClient = useQueryClient();
12
12
  const plansKey = useAuthQueryKey([...PLANS_QUERY_KEY]);
13
13
 
14
- return useQuery({
14
+ return useQuery<SuccessResult<Plan>>({
15
15
  queryKey: [...plansKey, idPlan],
16
16
  queryFn: ({ signal }) =>
17
17
  apiFetch<SuccessResult<Plan>>(`/api/plans/${idPlan}`, { signal }),
@@ -0,0 +1,8 @@
1
+ 'use server';
2
+
3
+ import { safeServerAction } from '../../../utils/safeServerAction';
4
+ import { subscriptionsService } from '../services/subscriptions.service';
5
+
6
+ export async function getCurrentSubscriptionAction(params?: { id_product?: number }) {
7
+ return safeServerAction(() => subscriptionsService.getCurrentSubscription(params));
8
+ }
@@ -6,3 +6,6 @@ export const FREEZE_WARNING_DAYS_BEFORE = 1;
6
6
  export const FREEZE_ELIGIBLE_WHITELABEL_ID = 1;
7
7
 
8
8
  export const MAX_PAYMENT_ATTEMPTS = 8;
9
+
10
+ /** Plano legado "FREE" identificado por `id_plan`, fora do `type: 'free'` moderno. */
11
+ export const FREE_PLAN_ID = 5;
@@ -0,0 +1,16 @@
1
+ import 'server-only';
2
+
3
+ import type { NextRequest } from 'next/server';
4
+ import { safeRouteHandler } from '../../../infra/route/safe-route-handler';
5
+ import { searchParamsToParams } from '../../../infra/route/search-params';
6
+ import { subscriptionsService } from '../services/subscriptions.service';
7
+
8
+ export function createCurrentSubscriptionHandler() {
9
+ return async function GET(req: NextRequest) {
10
+ return safeRouteHandler(() =>
11
+ subscriptionsService.getCurrentSubscription(
12
+ searchParamsToParams<{ id_product?: number }>(req.nextUrl.searchParams),
13
+ ),
14
+ );
15
+ };
16
+ }