@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.
- package/dist/components/layouts/AppMobileNavBar.mjs +5 -64
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/layouts/AppNavBar.mjs +0 -2
- package/dist/components/layouts/AppNavBar.mjs.map +1 -1
- package/dist/components/layouts/NavBar.mjs +52 -111
- package/dist/components/layouts/NavBar.mjs.map +1 -1
- package/dist/components/modals/cards/AddCardModal.mjs +3 -2
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/components/ui/form/PhoneInput.mjs +1 -1
- package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
- package/dist/handlers.mjs +0 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +1 -3
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +1 -3
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +1 -3
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +3 -10
- package/dist/index.mjs.map +1 -1
- package/dist/infra/api/client.mjs +1 -5
- package/dist/infra/api/client.mjs.map +1 -1
- package/dist/infra/api/types.mjs +1 -1
- package/dist/infra/api/types.mjs.map +1 -1
- package/dist/infra/route/safe-route-handler.mjs +1 -4
- package/dist/infra/route/safe-route-handler.mjs.map +1 -1
- package/dist/middlewares/create-auth-middleware.mjs +11 -1
- package/dist/middlewares/create-auth-middleware.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs +5 -6
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-management-subscription.mjs +14 -7
- package/dist/modules/auth/utils/assert-management-subscription.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-paid-subscription.mjs +12 -7
- package/dist/modules/auth/utils/assert-paid-subscription.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-subscription-addon.mjs +10 -2
- package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
- package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/actions/update-subscription-payment.action.mjs.map +1 -1
- package/dist/modules/subscriptions/constants/subscription.constants.mjs +0 -2
- package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
- package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs +13 -2
- package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/hooks/update-subscription-payment.hook.mjs +10 -2
- package/dist/modules/subscriptions/hooks/update-subscription-payment.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/services/subscriptions.service.mjs +12 -27
- package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
- package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs +1 -1
- package/dist/modules/subscriptions/utils/has-active-paid-subscription.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +9 -6
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +28 -2
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/providers/auth.provider.mjs +12 -9
- package/dist/providers/auth.provider.mjs.map +1 -1
- package/dist/server.mjs +0 -4
- package/dist/server.mjs.map +1 -1
- package/dist/store/useMdSidebarStore.mjs +4 -17
- package/dist/store/useMdSidebarStore.mjs.map +1 -1
- package/dist/utils/redirect.mjs +10 -0
- package/dist/utils/redirect.mjs.map +1 -1
- package/dist/utils/safeServerAction.mjs +1 -4
- package/dist/utils/safeServerAction.mjs.map +1 -1
- package/dist/utils/withAction.mjs +1 -2
- package/dist/utils/withAction.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/layouts/AppMobileNavBar.tsx +6 -90
- package/src/components/layouts/AppNavBar.tsx +1 -5
- package/src/components/layouts/NavBar.tsx +66 -160
- package/src/components/modals/cards/AddCardModal.tsx +15 -2
- package/src/components/ui/form/PhoneInput.tsx +1 -1
- package/src/handlers.ts +0 -1
- package/src/i18n/messages/en-us.ts +0 -2
- package/src/i18n/messages/es-es.ts +0 -2
- package/src/i18n/messages/pt-br.ts +0 -2
- package/src/index.ts +3 -8
- package/src/infra/api/client.ts +3 -11
- package/src/infra/api/types.ts +5 -7
- package/src/infra/route/safe-route-handler.ts +1 -4
- package/src/middlewares/create-auth-middleware.ts +12 -1
- package/src/modules/auth/utils/assert-account-not-frozen.ts +6 -7
- package/src/modules/auth/utils/assert-management-subscription.ts +23 -15
- package/src/modules/auth/utils/assert-paid-subscription.ts +15 -7
- package/src/modules/auth/utils/assert-subscription-addon.ts +12 -2
- package/src/modules/plans/hooks/use-plan-by-id.hook.ts +1 -1
- package/src/modules/subscriptions/actions/update-subscription-payment.action.ts +6 -1
- package/src/modules/subscriptions/constants/subscription.constants.ts +0 -3
- package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +17 -7
- package/src/modules/subscriptions/hooks/update-subscription-payment.hook.ts +21 -3
- package/src/modules/subscriptions/services/subscriptions.service.ts +46 -38
- package/src/modules/subscriptions/types/subscription.type.ts +27 -2
- package/src/modules/subscriptions/utils/has-active-paid-subscription.ts +2 -1
- package/src/modules/subscriptions/utils/has-paid-subscription.ts +2 -1
- package/src/modules/whitelabel/actions/find-whitelabel.action.ts +13 -9
- package/src/modules/whitelabel/services/whitelabel.service.ts +34 -2
- package/src/providers/auth.provider.tsx +13 -10
- package/src/server.ts +0 -2
- package/src/store/useMdSidebarStore.ts +5 -20
- package/src/utils/redirect.ts +10 -0
- package/src/utils/safeServerAction.ts +6 -4
- package/src/utils/withAction.ts +1 -2
- package/dist/components/layouts/GreatDomainIcon.mjs +0 -40
- package/dist/components/layouts/GreatDomainIcon.mjs.map +0 -1
- package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs +0 -10
- package/dist/modules/subscriptions/actions/get-current-subscription.action.mjs.map +0 -1
- package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs +0 -17
- package/dist/modules/subscriptions/handlers/current-subscription.handler.mjs.map +0 -1
- package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs +0 -28
- package/dist/modules/subscriptions/hooks/use-current-subscription.hook.mjs.map +0 -1
- package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs +0 -16
- package/dist/modules/subscriptions/utils/group-subscriptions-by-product.mjs.map +0 -1
- package/dist/modules/subscriptions/utils/read-current-subscription.mjs +0 -21
- package/dist/modules/subscriptions/utils/read-current-subscription.mjs.map +0 -1
- package/src/components/layouts/GreatDomainIcon.tsx +0 -43
- package/src/modules/subscriptions/actions/get-current-subscription.action.ts +0 -8
- package/src/modules/subscriptions/handlers/current-subscription.handler.ts +0 -16
- package/src/modules/subscriptions/hooks/use-current-subscription.hook.ts +0 -38
- package/src/modules/subscriptions/utils/group-subscriptions-by-product.ts +0 -23
- 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 {
|
|
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
|
|
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
|
|
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: {
|
|
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);
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
204
|
-
const currentOrigin = window.location.origin;
|
|
205
|
-
const pagesOrigin = pagesUrl ? new URL(pagesUrl).origin : "";
|
|
204
|
+
const host = window.location.host;
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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:
|
|
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
|
-
}
|
package/src/utils/redirect.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|
package/src/utils/withAction.ts
CHANGED
|
@@ -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
|
|
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":[]}
|