@greatapps/common 1.1.784 → 1.1.786

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 (121) hide show
  1. package/dist/components/layouts/AppMobileNavBar.mjs +5 -64
  2. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  3. package/dist/components/layouts/AppNavBar.mjs +0 -2
  4. package/dist/components/layouts/AppNavBar.mjs.map +1 -1
  5. package/dist/components/layouts/NavBar.mjs +52 -111
  6. package/dist/components/layouts/NavBar.mjs.map +1 -1
  7. package/dist/components/modals/cards/AddCardModal.mjs +3 -2
  8. package/dist/components/modals/cards/AddCardModal.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 +0 -2
  12. package/dist/handlers.mjs.map +1 -1
  13. package/dist/i18n/messages/en-us.mjs +1 -3
  14. package/dist/i18n/messages/en-us.mjs.map +1 -1
  15. package/dist/i18n/messages/es-es.mjs +1 -3
  16. package/dist/i18n/messages/es-es.mjs.map +1 -1
  17. package/dist/i18n/messages/pt-br.mjs +1 -3
  18. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  19. package/dist/index.mjs +3 -10
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/infra/api/client.mjs +1 -5
  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 +1 -4
  26. package/dist/infra/route/safe-route-handler.mjs.map +1 -1
  27. package/dist/middlewares/create-auth-middleware.mjs +11 -1
  28. package/dist/middlewares/create-auth-middleware.mjs.map +1 -1
  29. package/dist/modules/auth/utils/assert-account-not-frozen.mjs +5 -6
  30. package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
  31. package/dist/modules/auth/utils/assert-management-subscription.mjs +14 -7
  32. package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
  33. package/dist/modules/auth/utils/assert-paid-subscription.mjs +12 -7
  34. package/dist/modules/auth/utils/assert-paid-subscription.mjs.map +1 -1
  35. package/dist/modules/auth/utils/assert-subscription-addon.mjs +10 -2
  36. package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
  37. package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
  38. package/dist/modules/subscriptions/actions/update-subscription-payment.action.mjs.map +1 -1
  39. package/dist/modules/subscriptions/constants/subscription.constants.mjs +0 -2
  40. package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
  41. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs +13 -2
  42. package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
  43. package/dist/modules/subscriptions/hooks/update-subscription-payment.hook.mjs +10 -2
  44. package/dist/modules/subscriptions/hooks/update-subscription-payment.hook.mjs.map +1 -1
  45. package/dist/modules/subscriptions/services/subscriptions.service.mjs +12 -27
  46. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
  47. package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
  48. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs +1 -1
  49. package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs.map +1 -1
  50. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
  51. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
  52. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +9 -6
  53. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
  54. package/dist/modules/whitelabel/services/whitelabel.service.mjs +28 -2
  55. package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
  56. package/dist/providers/auth.provider.mjs +12 -9
  57. package/dist/providers/auth.provider.mjs.map +1 -1
  58. package/dist/server.mjs +0 -4
  59. package/dist/server.mjs.map +1 -1
  60. package/dist/store/useMdSidebarStore.mjs +4 -17
  61. package/dist/store/useMdSidebarStore.mjs.map +1 -1
  62. package/dist/utils/redirect.mjs +10 -0
  63. package/dist/utils/redirect.mjs.map +1 -1
  64. package/dist/utils/safeServerAction.mjs +1 -4
  65. package/dist/utils/safeServerAction.mjs.map +1 -1
  66. package/dist/utils/withAction.mjs +1 -2
  67. package/dist/utils/withAction.mjs.map +1 -1
  68. package/package.json +1 -1
  69. package/src/components/layouts/AppMobileNavBar.tsx +6 -90
  70. package/src/components/layouts/AppNavBar.tsx +1 -5
  71. package/src/components/layouts/NavBar.tsx +66 -160
  72. package/src/components/modals/cards/AddCardModal.tsx +15 -2
  73. package/src/components/ui/form/PhoneInput.tsx +1 -1
  74. package/src/handlers.ts +0 -1
  75. package/src/i18n/messages/en-us.ts +0 -2
  76. package/src/i18n/messages/es-es.ts +0 -2
  77. package/src/i18n/messages/pt-br.ts +0 -2
  78. package/src/index.ts +3 -8
  79. package/src/infra/api/client.ts +3 -11
  80. package/src/infra/api/types.ts +5 -7
  81. package/src/infra/route/safe-route-handler.ts +1 -4
  82. package/src/middlewares/create-auth-middleware.ts +12 -1
  83. package/src/modules/auth/utils/assert-account-not-frozen.ts +6 -7
  84. package/src/modules/auth/utils/assert-management-subscription.ts +23 -15
  85. package/src/modules/auth/utils/assert-paid-subscription.ts +15 -7
  86. package/src/modules/auth/utils/assert-subscription-addon.ts +12 -2
  87. package/src/modules/plans/hooks/use-plan-by-id.hook.ts +1 -1
  88. package/src/modules/subscriptions/actions/update-subscription-payment.action.ts +6 -1
  89. package/src/modules/subscriptions/constants/subscription.constants.ts +0 -3
  90. package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +17 -7
  91. package/src/modules/subscriptions/hooks/update-subscription-payment.hook.ts +21 -3
  92. package/src/modules/subscriptions/services/subscriptions.service.ts +46 -38
  93. package/src/modules/subscriptions/types/subscription.type.ts +27 -2
  94. package/src/modules/subscriptions/utils/has-active-paid-subscription.ts +2 -1
  95. package/src/modules/subscriptions/utils/has-paid-subscription.ts +2 -1
  96. package/src/modules/whitelabel/actions/find-whitelabel.action.ts +13 -9
  97. package/src/modules/whitelabel/services/whitelabel.service.ts +34 -2
  98. package/src/providers/auth.provider.tsx +13 -10
  99. package/src/server.ts +0 -2
  100. package/src/store/useMdSidebarStore.ts +5 -20
  101. package/src/utils/redirect.ts +10 -0
  102. package/src/utils/safeServerAction.ts +6 -4
  103. package/src/utils/withAction.ts +1 -2
  104. package/dist/components/layouts/GreatDomainIcon.mjs +0 -40
  105. package/dist/components/layouts/GreatDomainIcon.mjs.map +0 -1
  106. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs +0 -10
  107. package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs.map +0 -1
  108. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs +0 -17
  109. package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs.map +0 -1
  110. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs +0 -28
  111. package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs.map +0 -1
  112. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs +0 -16
  113. package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs.map +0 -1
  114. package/dist/modules/subscriptions/utils/read-current-subscription.mjs +0 -21
  115. package/dist/modules/subscriptions/utils/read-current-subscription.mjs.map +0 -1
  116. package/src/components/layouts/GreatDomainIcon.tsx +0 -43
  117. package/src/modules/subscriptions/actions/get-current-subscription.action.ts +0 -8
  118. package/src/modules/subscriptions/handlers/current-subscription.handler.ts +0 -16
  119. package/src/modules/subscriptions/hooks/use-current-subscription.hook.ts +0 -38
  120. package/src/modules/subscriptions/utils/group-subscriptions-by-product.ts +0 -23
  121. package/src/modules/subscriptions/utils/read-current-subscription.ts +0 -34
@@ -1,14 +1,24 @@
1
1
  import "server-only";
2
2
 
3
3
  import { ApiError } from "../../../infra/api/types";
4
- import { readCurrentSubscription } from "../../subscriptions/utils/read-current-subscription";
4
+ import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
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 subscription = await readCurrentSubscription();
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];
12
22
 
13
23
  if (!subscription) {
14
24
  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<SuccessResult<Plan>>({
14
+ return useQuery({
15
15
  queryKey: [...plansKey, idPlan],
16
16
  queryFn: ({ signal }) =>
17
17
  apiFetch<SuccessResult<Plan>>(`/api/plans/${idPlan}`, { signal }),
@@ -5,7 +5,12 @@ import { subscriptionsService } from "../services/subscriptions.service";
5
5
 
6
6
  export async function updateSubscriptionPaymentAction(
7
7
  subscriptionId: number | string,
8
- data: { id_card?: string; payment_method_id?: string; payment_method: number },
8
+ data: {
9
+ id_card?: string;
10
+ payment_method_id?: string;
11
+ payment_method: number;
12
+ charge_now?: boolean;
13
+ },
9
14
  ) {
10
15
  return safeMutationAction(async () => {
11
16
  return subscriptionsService.updateSubscriptionPayment(subscriptionId, data);
@@ -6,6 +6,3 @@ 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;
@@ -1,12 +1,22 @@
1
1
  "use client";
2
2
 
3
- import { useCurrentSubscription } from "./use-current-subscription.hook";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { apiFetch } from "../../../infra/http/api-fetch";
5
+ import type { PaginatedSuccessResult } from "../../../infra/api/types";
6
+ import type { Subscription } from "../types/subscription.type";
7
+ import { SUBSCRIPTIONS_QUERY_KEY } from "../constants/query-keys.constants";
8
+ import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
4
9
 
5
- /**
6
- * @deprecated Use `useCurrentSubscription`, mesma leitura (mesma chave de
7
- * cache, uma única requisição); só o nome muda para o vocabulário certo
8
- * ("corrente", não "ativa"). Sai na 1.1.784.
9
- */
10
10
  export function useActiveSubscription() {
11
- return useCurrentSubscription();
11
+ const queryKey = useAuthQueryKey([...SUBSCRIPTIONS_QUERY_KEY, "active"]);
12
+
13
+ return useQuery({
14
+ queryKey,
15
+ queryFn: ({ signal }) =>
16
+ apiFetch<PaginatedSuccessResult<Subscription>>('/api/subscriptions', {
17
+ params: { limit: 1 },
18
+ signal,
19
+ }),
20
+ staleTime: 10_000,
21
+ });
12
22
  }
@@ -3,12 +3,15 @@
3
3
  import { useMutation, useQueryClient } from '@tanstack/react-query';
4
4
  import { withAction } from '../../../utils/withAction';
5
5
  import { updateSubscriptionPaymentAction } from '../actions/update-subscription-payment.action';
6
- import { SUBSCRIPTIONS_QUERY_KEY } from '../constants/query-keys.constants';
6
+ import { SUBSCRIPTIONS_QUERY_KEY, CHARGES_QUERY_KEY } from '../constants/query-keys.constants';
7
7
  import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
8
+ import { CARDS_QUERY_KEY } from '../../cards/hooks/cards.hook';
8
9
 
9
10
  export function useUpdateSubscriptionPayment() {
10
11
  const queryClient = useQueryClient();
11
12
  const subscriptionsKey = useAuthQueryKey([...SUBSCRIPTIONS_QUERY_KEY]);
13
+ const cardsKey = useAuthQueryKey([...CARDS_QUERY_KEY]);
14
+ const chargesKey = useAuthQueryKey([...CHARGES_QUERY_KEY]);
12
15
 
13
16
  return useMutation({
14
17
  mutationFn: ({
@@ -16,10 +19,25 @@ export function useUpdateSubscriptionPayment() {
16
19
  data,
17
20
  }: {
18
21
  subscriptionId: number | string;
19
- data: { id_card?: string; payment_method_id?: string; payment_method: number };
22
+ data: {
23
+ id_card?: string;
24
+ payment_method_id?: string;
25
+ payment_method: number;
26
+ charge_now?: boolean;
27
+ };
20
28
  }) => withAction(updateSubscriptionPaymentAction)(subscriptionId, data),
21
- onSuccess: () => {
29
+ /* `onSettled`, e não `onSuccess`, pelo mesmo motivo do irmão `updateSubscriptionPlan`: a troca
30
+ * é aplicada no backend ANTES do 3DS, então a falha de autenticação também precisa refletir na
31
+ * tela. */
32
+ onSettled: () => {
22
33
  queryClient.invalidateQueries({ queryKey: subscriptionsKey });
34
+ /* A troca reaponta o cartão padrão da conta na mesma operação, então sem esta invalidação o
35
+ * selo "padrão" continuaria no cartão antigo depois de uma troca que já aconteceu. */
36
+ queryClient.invalidateQueries({ queryKey: cardsKey });
37
+ /* A operação passou a mutar um TERCEIRO estado: com `charge_now`, ela paga a fatura. A mesma
38
+ * tela lê isso por `useCharges`, e com `refetchOnWindowFocus: false` nada refaz a busca —
39
+ * a fatura recém-paga seguiria com selo "pendente" e botão "Pagar". */
40
+ queryClient.invalidateQueries({ queryKey: chargesKey });
23
41
  },
24
42
  });
25
43
  }
@@ -15,6 +15,7 @@ import {
15
15
  Subscription,
16
16
  SubscriptionSchema,
17
17
  FindSubscriptionsParams,
18
+ type ChargeNowResult,
18
19
  } from "../types/subscription.type";
19
20
  import {
20
21
  type CalculateSubscriptionRequest,
@@ -85,40 +86,6 @@ class SubscriptionsService {
85
86
  } satisfies PaginatedSuccessResult<Subscription>;
86
87
  }
87
88
 
88
- /**
89
- * A assinatura corrente do plano da conta, pela regra única do backend
90
- * (`GET .../subscriptions/action/current`): contrato de domínio nunca é
91
- * corrente, encerrada é decidida por `active`. Sem `id_product`, paridade
92
- * com hoje (melhor rank entre todos os produtos, nunca item/domínio).
93
- */
94
- async getCurrentSubscription(
95
- params?: { id_product?: number },
96
- ): Promise<SuccessResult<Subscription | null>> {
97
- const { id_account } = await getUserContext();
98
-
99
- const query = buildQueryParams(
100
- params as Record<string, string | number | undefined>,
101
- );
102
- const url = `/accounts/${id_account}/subscriptions/action/current${query ? `?${query}` : ""}`;
103
-
104
- const response = await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);
105
-
106
- if (response.status === 0) {
107
- throw new ApiError(
108
- response.message || "Erro ao consultar assinatura corrente",
109
- "GET_CURRENT_SUBSCRIPTION_FAILED",
110
- 400,
111
- );
112
- }
113
-
114
- const raw = response.data[0];
115
-
116
- return {
117
- success: true,
118
- data: raw ? SubscriptionSchema.parse(raw) : null,
119
- };
120
- }
121
-
122
89
  async calculateSubscription(
123
90
  data: CalculateSubscriptionRequest,
124
91
  ): Promise<SuccessResult<CalculateSubscriptionResponse>> {
@@ -195,12 +162,31 @@ class SubscriptionsService {
195
162
 
196
163
  async updateSubscriptionPayment(
197
164
  subscriptionId: number | string,
198
- data: { id_card?: string; payment_method_id?: string; payment_method: number },
199
- ): Promise<SuccessResult<void>> {
165
+ data: {
166
+ id_card?: string;
167
+ payment_method_id?: string;
168
+ payment_method: number;
169
+ charge_now?: boolean;
170
+ },
171
+ ): Promise<
172
+ SuccessResult<void> & {
173
+ applied: boolean;
174
+ client_secret?: string;
175
+ charge_now?: ChargeNowResult;
176
+ }
177
+ > {
200
178
  await assertManagementPermission("manage_subscriptions");
201
179
  const { id_account } = await getUserContext();
202
180
 
203
- const response = await api.apps.put<ApiActionResult<void>>(
181
+ /* O `status: 2` do guard de 3DS pendente não cabe em `ApiActionResult`, que só conhece 0 e 1.
182
+ * Declarar o terceiro estado aqui é o que permite tratá-lo — sem isso o TypeScript apaga o
183
+ * ramo como inalcançável e o caso volta a atravessar como sucesso. */
184
+ const response = await api.apps.put<
185
+ (ApiActionResult<void> | { status: 2; message?: string }) & {
186
+ charge_now?: ChargeNowResult;
187
+ client_secret?: string;
188
+ }
189
+ >(
204
190
  `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,
205
191
  data,
206
192
  { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },
@@ -214,7 +200,29 @@ class SubscriptionsService {
214
200
  );
215
201
  }
216
202
 
217
- return { success: true };
203
+ /* `status: 2` NÃO é sucesso: o guard de 3DS pendente do paymentEdit devolve esse status sem
204
+ * aplicar a troca, e como só o `status === 0` era tratado como erro, a chamada caía no
205
+ * `success: true` e a tela dizia "sua assinatura passou a ser cobrada neste cartão" para uma
206
+ * troca que não aconteceu — descartando junto o `client_secret` que resolveria a pendência.
207
+ *
208
+ * Por isso o retorno carrega `applied`: quem chama precisa poder distinguir "trocou" de
209
+ * "não trocou, conclua o pagamento pendente primeiro". */
210
+ if (response.status === 2) {
211
+ return {
212
+ success: true,
213
+ applied: false,
214
+ ...(response.client_secret ? { client_secret: response.client_secret } : {}),
215
+ };
216
+ }
217
+
218
+ /* O desfecho da cobrança imediata precisa atravessar: descartá-lo (como este método fazia com
219
+ * o corpo inteiro) apagaria tanto a recusa quanto o `client_secret` do 3DS, e a tela
220
+ * anunciaria "tudo certo" para uma fatura que continua em aberto. */
221
+ return {
222
+ success: true,
223
+ applied: true,
224
+ ...(response.charge_now ? { charge_now: response.charge_now } : {}),
225
+ };
218
226
  }
219
227
 
220
228
  async getFreezeNotice(): Promise<FreezeNotice> {
@@ -107,6 +107,33 @@ export const SubscriptionSchema = z.object({
107
107
 
108
108
  export type Subscription = z.infer<typeof SubscriptionSchema>;
109
109
 
110
+ /* Desfecho da cobrança imediata pedida com `charge_now` na troca de forma de pagamento
111
+ * (gapps-r3-api, PUT subscriptions/:id/action/payment).
112
+ *
113
+ * Vem separado do `status` da resposta de propósito: quando isto chega, a troca JÁ está firmada
114
+ * no banco e na Stripe. Uma cobrança recusada aqui não desfaz a troca — o dunning da Stripe segue
115
+ * armado no cartão novo —, então a tela precisa contar as duas coisas em separado.
116
+ *
117
+ * `outcome` é a ÚNICA fonte da verdade sobre o que aconteceu, e a tela deve falar só a partir dele.
118
+ * Substituiu um par de booleanos (`attempted`/`paid`) que não distinguia recusa de 3DS pendente: a
119
+ * tela lia `paid: false` depois de um desafio bem-sucedido e anunciava falha para um pagamento que
120
+ * tinha acabado de completar.
121
+ *
122
+ * `amount` vem em centavos, na `currency` da fatura, e é o valor REALMENTE cobrado — não uma
123
+ * previsão feita antes da chamada. */
124
+ export interface ChargeNowResult {
125
+ outcome: 'charged' | 'requires_action' | 'declined' | 'none';
126
+ invoice_id?: string;
127
+ amount?: number | null;
128
+ currency?: string | null;
129
+ client_secret?: string;
130
+ /* Presente em `declined`: a razão que o emissor deu. */
131
+ message?: string;
132
+ /* Presente em `none`: por que não houve o que cobrar (`sem_fatura_de_ciclo_aberta`,
133
+ * `deteccao_falhou`, `sem_contexto_de_cobranca`). */
134
+ reason?: string;
135
+ }
136
+
110
137
  export interface FindSubscriptionsParams {
111
138
  active?: boolean;
112
139
  trial?: boolean;
@@ -115,6 +142,4 @@ export interface FindSubscriptionsParams {
115
142
  date_hiring_start?: string;
116
143
  date_hiring_end?: string;
117
144
  limit?: number;
118
- page?: number;
119
- id_product?: number;
120
145
  }
@@ -1,8 +1,9 @@
1
1
  import { toCalendarDate, daysBetween } from "../../../infra/utils/date";
2
2
  import { getSubscriptionCancellationState } from "./get-subscription-cancellation-state";
3
- import { FREE_PLAN_ID } from "../constants/subscription.constants";
4
3
  import type { Subscription } from "../types/subscription.type";
5
4
 
5
+ const FREE_PLAN_ID = 5;
6
+
6
7
  type SubscriptionSnapshot = Pick<
7
8
  Subscription,
8
9
  | "type"
@@ -1,5 +1,6 @@
1
1
  import type { Subscription } from '../types/subscription.type';
2
- import { FREE_PLAN_ID } from '../constants/subscription.constants';
2
+
3
+ const FREE_PLAN_ID = 5;
3
4
 
4
5
  type PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan' | 'charged'>;
5
6
 
@@ -6,14 +6,7 @@ import { whitelabelService } from "../services/whitelabel.service";
6
6
  import { ApiError } from "../../../infra/api/types";
7
7
  import { cache } from "react";
8
8
 
9
- export const findWhitelabel = cache(async () => {
10
- const headersList = await headers();
11
- const host = headersList.get('host');
12
-
13
- if (!host) {
14
- throw new ApiError('Host header not found', 'HOST_NOT_FOUND', 400);
15
- }
16
-
9
+ const findWhitelabelByHost = cache(async (host: string) => {
17
10
  const whitelabel = await whitelabelService.getTokenByDomain(host);
18
11
 
19
12
  if (!whitelabel) {
@@ -21,4 +14,15 @@ export const findWhitelabel = cache(async () => {
21
14
  }
22
15
 
23
16
  return whitelabel;
24
- });
17
+ });
18
+
19
+ export const findWhitelabel = async () => {
20
+ const headersList = await headers();
21
+ const host = headersList.get('host');
22
+
23
+ if (!host) {
24
+ throw new ApiError('Host header not found', 'HOST_NOT_FOUND', 400);
25
+ }
26
+
27
+ return findWhitelabelByHost(host);
28
+ };
@@ -92,17 +92,49 @@ class WhitelabelService {
92
92
  });
93
93
 
94
94
  if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
95
- console.log("[WhitelabelService] Cache hit for domain", { hostname });
96
- return JSON.parse(cachedData.data) as WhitelabelTokenData;
95
+ const cachedWhitelabel = JSON.parse(cachedData.data) as WhitelabelTokenData;
96
+
97
+ if (this.#matchesHostname(cachedWhitelabel, hostname)) {
98
+ console.log("[WhitelabelService] Cache hit for domain", { hostname });
99
+ return cachedWhitelabel;
100
+ }
101
+
102
+ console.error("[WhitelabelService] Cached whitelabel does not match hostname", {
103
+ hostname,
104
+ cacheKey,
105
+ whitelabelId: cachedWhitelabel.id,
106
+ whitelabelDomain: cachedWhitelabel.domain,
107
+ });
108
+
109
+ await cache.delete(cacheKey);
97
110
  }
98
111
 
99
112
  const data = await this.fetchFromApi(hostname);
100
113
 
114
+ if (!this.#matchesHostname(data, hostname)) {
115
+ console.error("[WhitelabelService] API returned whitelabel of another hostname", {
116
+ hostname,
117
+ whitelabelId: data.id,
118
+ whitelabelDomain: data.domain,
119
+ });
120
+ return data;
121
+ }
122
+
101
123
  await cache.insert(cacheKey, JSON.stringify(data), 604800);
102
124
 
103
125
  return data;
104
126
  }
105
127
 
128
+ #matchesHostname(data: WhitelabelTokenData, hostname: string): boolean {
129
+ if (!data.domain) return true;
130
+
131
+ try {
132
+ return normalizeHostname(new URL(data.domain).hostname) === hostname;
133
+ } catch {
134
+ return true;
135
+ }
136
+ }
137
+
106
138
  async getTokenByWhitelabelId(idWl: number): Promise<string> {
107
139
  const apiUrl = this.getApiUrl();
108
140
  const masterToken = this.getToken();
@@ -46,6 +46,7 @@ import { useWhitelabelUrls, useWhitelabel } from "./whitelabel.provider";
46
46
  import { useWlQueryKey } from "../hooks/useAuthQueryKey";
47
47
  import { normalizeLocale } from "../i18n/normalize";
48
48
  import { setLocaleCookie } from "../utils/intl/locale-cookie";
49
+ import { isSameSiteUrl } from "../utils/redirect";
49
50
 
50
51
  interface AuthContextProps extends AuthState {
51
52
  login: (credentials: LoginRequest) => Promise<LoginResult>;
@@ -200,19 +201,21 @@ export function AuthProvider({ children, publicRoutes = [] }: AuthProviderProps)
200
201
  );
201
202
 
202
203
  const resolveLogoutRedirect = useCallback(() => {
203
- try {
204
- const currentOrigin = window.location.origin;
205
- const pagesOrigin = pagesUrl ? new URL(pagesUrl).origin : "";
204
+ const host = window.location.host;
206
205
 
207
- if (pagesOrigin && currentOrigin === pagesOrigin) {
208
- return `${accountsUrl}/login?redirect=${encodeURIComponent(pagesUrl)}`;
209
- }
210
- } catch {
211
- // fallback below
206
+ if (accountsUrl && !isSameSiteUrl(accountsUrl, host)) {
207
+ console.error("[auth-provider] accounts_url fora do site do host atual", {
208
+ host,
209
+ whitelabelId: whitelabel?.id,
210
+ accountsUrl,
211
+ });
212
+ return "/login";
212
213
  }
213
214
 
214
- return `${accountsUrl}/login?redirect=${encodeURIComponent(pagesUrl || "/")}`;
215
- }, [accountsUrl, pagesUrl]);
215
+ const safePagesUrl = pagesUrl && isSameSiteUrl(pagesUrl, host) ? pagesUrl : "/";
216
+
217
+ return `${accountsUrl}/login?redirect=${encodeURIComponent(safePagesUrl)}`;
218
+ }, [accountsUrl, pagesUrl, whitelabel?.id]);
216
219
 
217
220
  const logout = useCallback(async (): Promise<void> => {
218
221
  try {
package/src/server.ts CHANGED
@@ -28,8 +28,6 @@ export { findUserById } from './modules/users/action/find-user-by-id.action';
28
28
  export { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';
29
29
  export { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';
30
30
  export { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';
31
- export { getCurrentSubscriptionAction } from './modules/subscriptions/actions/get-current-subscription.action';
32
- export { readCurrentSubscription } from './modules/subscriptions/utils/read-current-subscription';
33
31
  export { ackFreezeNoticeAction } from './modules/subscriptions/actions/ack-freeze-notice.action';
34
32
  export { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';
35
33
  export { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';
@@ -1,21 +1,21 @@
1
1
  import { create } from "zustand";
2
- import { useEffect } from "react";
3
2
  import { readStorage, writeStorage } from "../utils/browser/safe-storage";
4
3
 
5
4
  const STORAGE_KEY = "md-sidebar-expanded";
6
5
 
6
+ function getInitialState(): boolean {
7
+ return readStorage(STORAGE_KEY) === "true";
8
+ }
9
+
7
10
  type MdSidebarStore = {
8
11
  isExpanded: boolean;
9
- _hydrated: boolean;
10
12
  toggle: () => void;
11
13
  expand: () => void;
12
14
  collapse: () => void;
13
- _hydrate: () => void;
14
15
  };
15
16
 
16
17
  export const useMdSidebarStore = create<MdSidebarStore>((set) => ({
17
- isExpanded: false,
18
- _hydrated: false,
18
+ isExpanded: getInitialState(),
19
19
  toggle: () => set((state) => {
20
20
  const next = !state.isExpanded;
21
21
  writeStorage(STORAGE_KEY, String(next));
@@ -29,19 +29,4 @@ export const useMdSidebarStore = create<MdSidebarStore>((set) => ({
29
29
  writeStorage(STORAGE_KEY, "false");
30
30
  return set({ isExpanded: false });
31
31
  },
32
- _hydrate: () => {
33
- const value = readStorage(STORAGE_KEY) === "true";
34
- set({ isExpanded: value, _hydrated: true });
35
- },
36
32
  }));
37
-
38
- export function useMdSidebarHydration() {
39
- const hydrate = useMdSidebarStore((s) => s._hydrate);
40
- const hydrated = useMdSidebarStore((s) => s._hydrated);
41
-
42
- useEffect(() => {
43
- if (!hydrated) {
44
- hydrate();
45
- }
46
- }, [hydrate, hydrated]);
47
- }
@@ -36,6 +36,16 @@ export function resolveSafeRedirect(
36
36
  return url.toString();
37
37
  }
38
38
 
39
+ export function isSameSiteUrl(target: string, currentHost: string): boolean {
40
+ let hostname: string;
41
+ try {
42
+ hostname = new URL(target).hostname;
43
+ } catch {
44
+ return false;
45
+ }
46
+ return isSameSite(hostname, currentHost.split(':')[0]);
47
+ }
48
+
39
49
  function isSameSite(target: string, current: string): boolean {
40
50
  const host = target.toLowerCase();
41
51
  const base = baseDomain(current.toLowerCase());
@@ -10,10 +10,12 @@ export async function safeServerAction<T>(
10
10
  return { success: true, data };
11
11
  } catch (error) {
12
12
  if (isRedirectError(error)) throw error;
13
- if (error instanceof ApiError) {
14
- return { success: false, error: error.message, code: error.code, statusCode: error.statusCode };
15
- }
16
- const message = error instanceof Error ? error.message : 'Erro desconhecido';
13
+ const message =
14
+ error instanceof ApiError
15
+ ? error.message
16
+ : error instanceof Error
17
+ ? error.message
18
+ : 'Erro desconhecido';
17
19
  return { success: false, error: message };
18
20
  }
19
21
  }
@@ -1,4 +1,3 @@
1
- import { ApiError } from '../infra/api/types';
2
1
  import type { ActionResult } from '../infra/api/types';
3
2
 
4
3
  export function withAction<TArgs extends any[], TData>(
@@ -7,7 +6,7 @@ export function withAction<TArgs extends any[], TData>(
7
6
  return async (...args) => {
8
7
  const result = await action(...args);
9
8
  if (!result.success) {
10
- throw new ApiError(result.error, result.code, result.statusCode);
9
+ throw new Error(result.error ?? 'Erro desconhecido');
11
10
  }
12
11
  return result.data as TData;
13
12
  };
@@ -1,40 +0,0 @@
1
- "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { cn } from "../../infra/utils/clsx";
4
- function GreatDomainIcon({ active, className }) {
5
- return /* @__PURE__ */ jsxs(
6
- "svg",
7
- {
8
- width: "23",
9
- height: "23",
10
- viewBox: "0 0 23 23",
11
- fill: "none",
12
- xmlns: "http://www.w3.org/2000/svg",
13
- className,
14
- "aria-hidden": "true",
15
- children: [
16
- /* @__PURE__ */ jsx(
17
- "path",
18
- {
19
- d: "M11.8444 17.0777C11.8444 20.3487 9.19292 23.0003 5.92218 23.0003C2.65145 23.0003 0 20.3487 0 17.0777C0 13.8068 2.65145 11.1551 5.92218 11.1551C9.19292 11.1551 11.8444 13.8068 11.8444 17.0777Z",
20
- className: cn(
21
- "transition-colors duration-300",
22
- active ? "fill-[#984DFF]" : "fill-[#650CE4]"
23
- )
24
- }
25
- ),
26
- /* @__PURE__ */ jsx(
27
- "path",
28
- {
29
- d: "M11.8436 3.06666C11.8436 1.37299 13.2166 0 14.9103 0H19.9331C21.6267 0 22.9997 1.37299 22.9997 3.06666V19.9333C22.9997 21.6269 21.6267 22.9999 19.9331 22.9999H14.9103C13.2166 22.9999 11.8436 21.6269 11.8436 19.9333V3.06666Z",
30
- fill: "#C9B2FF"
31
- }
32
- )
33
- ]
34
- }
35
- );
36
- }
37
- export {
38
- GreatDomainIcon
39
- };
40
- //# sourceMappingURL=GreatDomainIcon.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/components/layouts/GreatDomainIcon.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../../infra/utils/clsx\";\n\ntype GreatDomainIconProps = {\n /** Em repouso o círculo é #650CE4; só no estado ativo vira #984DFF. */\n active?: boolean;\n className?: string;\n};\n\n/**\n * Glifo do GreatDomain — geometria exportada do Figma (node 5929:2700), 23×23.\n * O círculo só troca de cor quando ativo. No hover o container ganha fundo cinza\n * (igual ao GreatPages) e o glifo fica como está.\n */\nfunction GreatDomainIcon({ active, className }: GreatDomainIconProps) {\n return (\n <svg\n width=\"23\"\n height=\"23\"\n viewBox=\"0 0 23 23\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n aria-hidden=\"true\"\n >\n <path\n d=\"M11.8444 17.0777C11.8444 20.3487 9.19292 23.0003 5.92218 23.0003C2.65145 23.0003 0 20.3487 0 17.0777C0 13.8068 2.65145 11.1551 5.92218 11.1551C9.19292 11.1551 11.8444 13.8068 11.8444 17.0777Z\"\n className={cn(\n \"transition-colors duration-300\",\n active ? \"fill-[#984DFF]\" : \"fill-[#650CE4]\",\n )}\n />\n <path\n d=\"M11.8436 3.06666C11.8436 1.37299 13.2166 0 14.9103 0H19.9331C21.6267 0 22.9997 1.37299 22.9997 3.06666V19.9333C22.9997 21.6269 21.6267 22.9999 19.9331 22.9999H14.9103C13.2166 22.9999 11.8436 21.6269 11.8436 19.9333V3.06666Z\"\n fill=\"#C9B2FF\"\n />\n </svg>\n );\n}\n\nexport { GreatDomainIcon };\nexport type { GreatDomainIconProps };\n"],"mappings":";AAiBI,SASE,KATF;AAfJ,SAAS,UAAU;AAanB,SAAS,gBAAgB,EAAE,QAAQ,UAAU,GAAyB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,QAAO;AAAA,MACP,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN;AAAA,MACA,eAAY;AAAA,MAEZ;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,GAAE;AAAA,YACF,WAAW;AAAA,cACT;AAAA,cACA,SAAS,mBAAmB;AAAA,YAC9B;AAAA;AAAA,QACF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,GAAE;AAAA,YACF,MAAK;AAAA;AAAA,QACP;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1,10 +0,0 @@
1
- "use server";
2
- import { safeServerAction } from "../../../utils/safeServerAction";
3
- import { subscriptionsService } from "../services/subscriptions.service";
4
- async function getCurrentSubscriptionAction(params) {
5
- return safeServerAction(() => subscriptionsService.getCurrentSubscription(params));
6
- }
7
- export {
8
- getCurrentSubscriptionAction
9
- };
10
- //# sourceMappingURL=get-current-subscription.action.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/modules/subscriptions/actions/get-current-subscription.action.ts"],"sourcesContent":["'use server';\n\nimport { safeServerAction } from '../../../utils/safeServerAction';\nimport { subscriptionsService } from '../services/subscriptions.service';\n\nexport async function getCurrentSubscriptionAction(params?: { id_product?: number }) {\n return safeServerAction(() => subscriptionsService.getCurrentSubscription(params));\n}\n"],"mappings":";AAEA,SAAS,wBAAwB;AACjC,SAAS,4BAA4B;AAErC,eAAsB,6BAA6B,QAAkC;AACnF,SAAO,iBAAiB,MAAM,qBAAqB,uBAAuB,MAAM,CAAC;AACnF;","names":[]}
@@ -1,17 +0,0 @@
1
- import "server-only";
2
- import { safeRouteHandler } from "../../../infra/route/safe-route-handler";
3
- import { searchParamsToParams } from "../../../infra/route/search-params";
4
- import { subscriptionsService } from "../services/subscriptions.service";
5
- function createCurrentSubscriptionHandler() {
6
- return async function GET(req) {
7
- return safeRouteHandler(
8
- () => subscriptionsService.getCurrentSubscription(
9
- searchParamsToParams(req.nextUrl.searchParams)
10
- )
11
- );
12
- };
13
- }
14
- export {
15
- createCurrentSubscriptionHandler
16
- };
17
- //# sourceMappingURL=current-subscription.handler.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/modules/subscriptions/handlers/current-subscription.handler.ts"],"sourcesContent":["import 'server-only';\n\nimport type { NextRequest } from 'next/server';\nimport { safeRouteHandler } from '../../../infra/route/safe-route-handler';\nimport { searchParamsToParams } from '../../../infra/route/search-params';\nimport { subscriptionsService } from '../services/subscriptions.service';\n\nexport function createCurrentSubscriptionHandler() {\n return async function GET(req: NextRequest) {\n return safeRouteHandler(() =>\n subscriptionsService.getCurrentSubscription(\n searchParamsToParams<{ id_product?: number }>(req.nextUrl.searchParams),\n ),\n );\n };\n}\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,wBAAwB;AACjC,SAAS,4BAA4B;AACrC,SAAS,4BAA4B;AAE9B,SAAS,mCAAmC;AACjD,SAAO,eAAe,IAAI,KAAkB;AAC1C,WAAO;AAAA,MAAiB,MACtB,qBAAqB;AAAA,QACnB,qBAA8C,IAAI,QAAQ,YAAY;AAAA,MACxE;AAAA,IACF;AAAA,EACF;AACF;","names":[]}