@greatapps/common 1.1.747 → 1.1.749
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/AiCreditsOfferGate.mjs +92 -0
- package/dist/components/layouts/AiCreditsOfferGate.mjs.map +1 -0
- package/dist/components/modals/AccountFrozenModal.mjs +20 -14
- package/dist/components/modals/AccountFrozenModal.mjs.map +1 -1
- package/dist/components/modals/promo/AiCreditsOfferModal/AiCreditsOfferModal.mjs +275 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/AiCreditsOfferModal.mjs.map +1 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/LoseOfferDialog.mjs +55 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/LoseOfferDialog.mjs.map +1 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/OfferIllustration.mjs +78 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/OfferIllustration.mjs.map +1 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/OfferPaymentField.mjs +60 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/components/OfferPaymentField.mjs.map +1 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/index.mjs +5 -0
- package/dist/components/modals/promo/AiCreditsOfferModal/index.mjs.map +1 -0
- package/dist/components/navigation/SubscriptionBanner.mjs.map +1 -1
- package/dist/handlers.mjs +5 -1
- package/dist/handlers.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +32 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +32 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +32 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +71 -43
- package/dist/index.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -1
- package/dist/modules/auth/utils/get-user-context.mjs +2 -1
- package/dist/modules/auth/utils/get-user-context.mjs.map +1 -1
- package/dist/modules/cards/hooks/cards.hook.mjs +3 -2
- package/dist/modules/cards/hooks/cards.hook.mjs.map +1 -1
- package/dist/modules/ia-credits/constants/ai-credits-offer.constants.mjs +26 -0
- package/dist/modules/ia-credits/constants/ai-credits-offer.constants.mjs.map +1 -0
- package/dist/modules/ia-credits/handlers/get-offer-eligibility.handler.mjs +19 -0
- package/dist/modules/ia-credits/handlers/get-offer-eligibility.handler.mjs.map +1 -0
- package/dist/modules/ia-credits/handlers/purchase-ia-credits.handler.mjs +33 -0
- package/dist/modules/ia-credits/handlers/purchase-ia-credits.handler.mjs.map +1 -0
- package/dist/modules/ia-credits/hooks/purchase-ia-credits.hook.mjs +29 -0
- package/dist/modules/ia-credits/hooks/purchase-ia-credits.hook.mjs.map +1 -0
- package/dist/modules/ia-credits/hooks/use-offer-eligibility.hook.mjs +18 -0
- package/dist/modules/ia-credits/hooks/use-offer-eligibility.hook.mjs.map +1 -0
- package/dist/modules/ia-credits/services/ia-credits.service.mjs +67 -0
- package/dist/modules/ia-credits/services/ia-credits.service.mjs.map +1 -1
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs +34 -0
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs.map +1 -0
- package/dist/modules/ia-credits/utils/offer-stage-cookie.mjs +41 -0
- package/dist/modules/ia-credits/utils/offer-stage-cookie.mjs.map +1 -0
- package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs.map +1 -1
- package/dist/store/useAiCreditsOfferModal.mjs +14 -0
- package/dist/store/useAiCreditsOfferModal.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/layouts/AiCreditsOfferGate.tsx +171 -0
- package/src/components/modals/AccountFrozenModal.tsx +26 -37
- package/src/components/modals/promo/AiCreditsOfferModal/AiCreditsOfferModal.tsx +400 -0
- package/src/components/modals/promo/AiCreditsOfferModal/components/LoseOfferDialog.tsx +97 -0
- package/src/components/modals/promo/AiCreditsOfferModal/components/OfferIllustration.tsx +121 -0
- package/src/components/modals/promo/AiCreditsOfferModal/components/OfferPaymentField.tsx +91 -0
- package/src/components/modals/promo/AiCreditsOfferModal/index.ts +1 -0
- package/src/components/navigation/SubscriptionBanner.tsx +0 -8
- package/src/handlers.ts +2 -0
- package/src/i18n/messages/en-us.ts +33 -0
- package/src/i18n/messages/es-es.ts +33 -0
- package/src/i18n/messages/pt-br.ts +33 -0
- package/src/index.ts +25 -0
- package/src/modules/auth/utils/assert-account-not-frozen.ts +0 -9
- package/src/modules/auth/utils/get-user-context.ts +3 -0
- package/src/modules/cards/hooks/cards.hook.ts +6 -1
- package/src/modules/ia-credits/constants/ai-credits-offer.constants.ts +33 -0
- package/src/modules/ia-credits/handlers/get-offer-eligibility.handler.ts +24 -0
- package/src/modules/ia-credits/handlers/purchase-ia-credits.handler.ts +42 -0
- package/src/modules/ia-credits/hooks/purchase-ia-credits.hook.ts +43 -0
- package/src/modules/ia-credits/hooks/use-offer-eligibility.hook.ts +28 -0
- package/src/modules/ia-credits/services/ia-credits.service.ts +92 -0
- package/src/modules/ia-credits/types/ai-credits-offer.type.ts +87 -0
- package/src/modules/ia-credits/utils/offer-stage-cookie.ts +66 -0
- package/src/modules/subscriptions/constants/subscription.constants.ts +0 -5
- package/src/modules/subscriptions/utils/get-account-freeze-date.ts +0 -13
- package/src/modules/subscriptions/utils/get-account-freeze-state.ts +0 -7
- package/src/store/useAiCreditsOfferModal.ts +24 -0
|
@@ -3,12 +3,28 @@ import 'server-only';
|
|
|
3
3
|
import { api } from '../../../infra/api/client';
|
|
4
4
|
import { ApiError, ApiPaginatedActionResult, SuccessResult } from '../../../infra/api/types';
|
|
5
5
|
import { getUserContext } from '../../auth/utils/get-user-context';
|
|
6
|
+
import { subscriptionsService } from '../../subscriptions/services/subscriptions.service';
|
|
6
7
|
import {
|
|
7
8
|
IaCreditAddBatch,
|
|
8
9
|
IaCreditAddBatchSchema,
|
|
9
10
|
IaCreditOperation,
|
|
10
11
|
IaCreditsSummaryData,
|
|
11
12
|
} from '../types';
|
|
13
|
+
import { AI_CREDITS_FIRST_PURCHASE_OFFER } from '../constants/ai-credits-offer.constants';
|
|
14
|
+
import type {
|
|
15
|
+
AiCreditsOfferEligibility,
|
|
16
|
+
PurchaseIaCreditsInput,
|
|
17
|
+
PurchaseIaCreditsResult,
|
|
18
|
+
} from '../types/ai-credits-offer.type';
|
|
19
|
+
|
|
20
|
+
/** Shape da API pra elegibilidade — `price_cents`/`anchor_cents` viram camelCase ao sair do service. */
|
|
21
|
+
interface AiCreditsOfferEligibilityApiItem {
|
|
22
|
+
eligible: boolean;
|
|
23
|
+
reason: AiCreditsOfferEligibility['reason'];
|
|
24
|
+
credits: number;
|
|
25
|
+
price_cents: number | null;
|
|
26
|
+
anchor_cents: number | null;
|
|
27
|
+
}
|
|
12
28
|
|
|
13
29
|
class IaCreditsService {
|
|
14
30
|
async getSummaryData(
|
|
@@ -68,6 +84,82 @@ class IaCreditsService {
|
|
|
68
84
|
const raw = response.data ?? [];
|
|
69
85
|
return raw.map((item) => IaCreditAddBatchSchema.parse(item));
|
|
70
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Elegibilidade da oferta de primeira compra, pra assinatura ATIVA da conta — resolvida aqui (e
|
|
90
|
+
* não recebida por parâmetro) porque o consumidor (`createGetAiCreditsOfferEligibilityHandler`)
|
|
91
|
+
* não tem id de assinatura: a rota é `/api/ai-credits-offer/eligibility`, sem `:id`.
|
|
92
|
+
*
|
|
93
|
+
* Sem assinatura ativa a resposta já sai inelegível — nem vale bater no backend, que exige
|
|
94
|
+
* `id_subscription` na URL.
|
|
95
|
+
*/
|
|
96
|
+
async offerEligibility(): Promise<AiCreditsOfferEligibility> {
|
|
97
|
+
const { id_account } = await getUserContext();
|
|
98
|
+
const { data: subscriptions } = await subscriptionsService.listSubscriptions({ limit: 1 });
|
|
99
|
+
const subscription = subscriptions[0];
|
|
100
|
+
|
|
101
|
+
if (!subscription) {
|
|
102
|
+
return {
|
|
103
|
+
eligible: false,
|
|
104
|
+
reason: 'no_active_subscription',
|
|
105
|
+
credits: AI_CREDITS_FIRST_PURCHASE_OFFER.credits,
|
|
106
|
+
priceCents: null,
|
|
107
|
+
anchorCents: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const endpoint = `/accounts/${id_account}/subscriptions/${subscription.id}/items/balance/purchase-offer/${AI_CREDITS_FIRST_PURCHASE_OFFER.slug}`;
|
|
112
|
+
const response = await api.apps.get<ApiPaginatedActionResult<AiCreditsOfferEligibilityApiItem>>(
|
|
113
|
+
endpoint
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (response.status === 0) {
|
|
117
|
+
throw new ApiError(
|
|
118
|
+
response.message || 'Erro ao consultar elegibilidade da oferta de créditos de IA',
|
|
119
|
+
'GET_OFFER_ELIGIBILITY_FAILED',
|
|
120
|
+
400
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const raw = response.data[0];
|
|
125
|
+
if (!raw) {
|
|
126
|
+
throw new ApiError(
|
|
127
|
+
'Resposta vazia ao consultar elegibilidade da oferta de créditos de IA',
|
|
128
|
+
'GET_OFFER_ELIGIBILITY_EMPTY',
|
|
129
|
+
502
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
eligible: raw.eligible,
|
|
135
|
+
reason: raw.reason,
|
|
136
|
+
credits: raw.credits,
|
|
137
|
+
priceCents: raw.price_cents,
|
|
138
|
+
anchorCents: raw.anchor_cents,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compra de créditos de IA — avulsa comum (`offer` ausente) OU a oferta de primeira compra
|
|
144
|
+
* (`offer: AI_CREDITS_FIRST_PURCHASE_OFFER.slug`). Devolve o envelope CRU do gapps-r3-api
|
|
145
|
+
* (`status` 1/2/0), sem reembrulhar em `SuccessResult`: quem consome é sempre o hook de compra,
|
|
146
|
+
* que já sabe interpretar esses três status (ver `usePurchaseIaCredits`).
|
|
147
|
+
*/
|
|
148
|
+
async purchase(
|
|
149
|
+
subscriptionId: number | string,
|
|
150
|
+
input: PurchaseIaCreditsInput
|
|
151
|
+
): Promise<PurchaseIaCreditsResult> {
|
|
152
|
+
const { id_account } = await getUserContext();
|
|
153
|
+
|
|
154
|
+
return api.apps.post<PurchaseIaCreditsResult>(
|
|
155
|
+
`/accounts/${id_account}/subscriptions/${subscriptionId}/items/balance/purchase`,
|
|
156
|
+
{
|
|
157
|
+
...(input.offer ? { offer: input.offer } : { quantity: input.quantity }),
|
|
158
|
+
payment_method: input.paymentMethod,
|
|
159
|
+
...(input.idCard ? { id_card: input.idCard } : {}),
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
71
163
|
}
|
|
72
164
|
|
|
73
165
|
export const iaCreditsService = new IaCreditsService();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { PAYMENT_METHOD_CARD, PAYMENT_METHOD_PIX } from '../constants/ai-credits-offer.constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `reason` é sempre um código de máquina do backend — nunca renderizar como texto: o modal usa uma
|
|
6
|
+
* mensagem de erro genérica já existente no catálogo (ver `AiCreditsOfferModal`).
|
|
7
|
+
*/
|
|
8
|
+
export type AiCreditsOfferIneligibilityReason =
|
|
9
|
+
| 'not_default_wl'
|
|
10
|
+
| 'gateway_not_supported'
|
|
11
|
+
| 'no_active_subscription'
|
|
12
|
+
| 'already_charged'
|
|
13
|
+
| 'has_paid_plan'
|
|
14
|
+
| 'has_card'
|
|
15
|
+
| 'offer_consumed'
|
|
16
|
+
| 'offer_card_only'
|
|
17
|
+
| null;
|
|
18
|
+
|
|
19
|
+
export interface AiCreditsOfferEligibility {
|
|
20
|
+
eligible: boolean;
|
|
21
|
+
reason: AiCreditsOfferIneligibilityReason;
|
|
22
|
+
credits: number;
|
|
23
|
+
/** Preço promocional em centavos. `null` quando `eligible` é `false` sem cálculo de preço. */
|
|
24
|
+
priceCents: number | null;
|
|
25
|
+
/** Preço de tabela em centavos, derivado de `getAddonUnitPrice` no backend — nunca hardcoded. */
|
|
26
|
+
anchorCents: number | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resposta do route handler local — a elegibilidade do backend + o carimbo da sessão de login. */
|
|
30
|
+
export interface AiCreditsOfferEligibilityResponse extends AiCreditsOfferEligibility {
|
|
31
|
+
/** `iat` do JWT no momento da consulta — usado pelo gate para saber se é uma sessão nova. */
|
|
32
|
+
sessionIssuedAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PurchaseIaCreditsInput {
|
|
36
|
+
/** Ignorado pelo backend quando `offer` é enviado (a oferta força a quantidade do pacote). */
|
|
37
|
+
quantity?: number;
|
|
38
|
+
paymentMethod: number;
|
|
39
|
+
idCard?: string;
|
|
40
|
+
/** Slug da oferta (`AI_CREDITS_FIRST_PURCHASE_OFFER.slug`). Ausente = compra avulsa comum. */
|
|
41
|
+
offer?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Corpo aceito pela rota local de compra (`createPurchaseIaCreditsHandler`) — validado no handler
|
|
46
|
+
* porque o JSON do request não é confiável: `payment_method` fora do union (1 = cartão, 3 = pix,
|
|
47
|
+
* únicos métodos que o gapps-r3-api reconhece) nem chega a virar chamada de backend.
|
|
48
|
+
*/
|
|
49
|
+
export const PurchaseIaCreditsRequestBodySchema = z.object({
|
|
50
|
+
quantity: z.number().optional(),
|
|
51
|
+
payment_method: z.union([z.literal(PAYMENT_METHOD_CARD), z.literal(PAYMENT_METHOD_PIX)]),
|
|
52
|
+
id_card: z.string().optional(),
|
|
53
|
+
offer: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
export type PurchaseIaCreditsRequestBody = z.infer<typeof PurchaseIaCreditsRequestBodySchema>;
|
|
56
|
+
|
|
57
|
+
/** Envelope PIX dentro de `data`, presente só quando a compra foi feita com `payment_method: 3`. */
|
|
58
|
+
const PurchaseIaCreditsPixDataSchema = z.object({
|
|
59
|
+
correlation_id: z.string().optional(),
|
|
60
|
+
qr_code: z.string().optional(),
|
|
61
|
+
qr_code_image: z.string().optional(),
|
|
62
|
+
payment_link: z.string().optional(),
|
|
63
|
+
expires_at: z.string().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Envelope CRU do gapps-r3-api — status 1 (pago), 2 (3DS pendente) ou 0 (recusado). Devolvido
|
|
68
|
+
* sem reembrulho pelo `createPurchaseIaCreditsHandler` (ver o comentário no handler); o hook valida
|
|
69
|
+
* a resposta contra este schema em vez de confiar cegamente no `fetch`.
|
|
70
|
+
*/
|
|
71
|
+
export const PurchaseIaCreditsResultSchema = z.object({
|
|
72
|
+
status: z.number(),
|
|
73
|
+
message: z.string().optional(),
|
|
74
|
+
client_secret: z.string().optional(),
|
|
75
|
+
data: z
|
|
76
|
+
.array(
|
|
77
|
+
z.object({
|
|
78
|
+
credited: z.number().optional(),
|
|
79
|
+
invoice_id: z.string().optional(),
|
|
80
|
+
pending: z.number().optional(),
|
|
81
|
+
payment_method: z.number().optional(),
|
|
82
|
+
pix: PurchaseIaCreditsPixDataSchema.optional(),
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
.optional(),
|
|
86
|
+
});
|
|
87
|
+
export type PurchaseIaCreditsResult = z.infer<typeof PurchaseIaCreditsResultSchema>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cookieDomainFromDomain } from '../../../utils/intl/cookie-domain';
|
|
2
|
+
import { AiCreditsOfferStage } from '../constants/ai-credits-offer.constants';
|
|
3
|
+
|
|
4
|
+
const COOKIE_NAME = 'ai-credits-offer-stage';
|
|
5
|
+
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
|
|
6
|
+
|
|
7
|
+
export interface OfferStageRecord {
|
|
8
|
+
stage: AiCreditsOfferStage;
|
|
9
|
+
/** `iat` do JWT na sessão em que este estágio foi gravado — é o que diferencia "sessão nova". */
|
|
10
|
+
lastSessionIat: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const EMPTY_RECORD: OfferStageRecord = {
|
|
14
|
+
stage: AiCreditsOfferStage.NONE,
|
|
15
|
+
lastSessionIat: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cookie `ai-credits-offer-stage`, no domínio pai da whitelabel (`cookieDomainFromDomain` — mesmo
|
|
20
|
+
* mecanismo do cookie de locale, que por sua vez espelha o cookie de auth `greatapps`): sustenta a
|
|
21
|
+
* escada de estágios entre app/accounts/editor, 1 ano de validade.
|
|
22
|
+
*
|
|
23
|
+
* Valor: `${idAccount}:${stage}:${lastSessionIat}`. Registro de OUTRA conta
|
|
24
|
+
* (id diferente do gravado) lê como estágio zerado — last-writer-wins, aceito porque o cookie é por
|
|
25
|
+
* navegador e trocar de conta paga no mesmo device durante a janela da oferta é evento raro.
|
|
26
|
+
*/
|
|
27
|
+
export function readOfferStageCookie(idAccount: number): OfferStageRecord {
|
|
28
|
+
if (typeof document === 'undefined') return EMPTY_RECORD;
|
|
29
|
+
|
|
30
|
+
const match = new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=([^;]+)`).exec(document.cookie);
|
|
31
|
+
if (!match) return EMPTY_RECORD;
|
|
32
|
+
|
|
33
|
+
const raw = decodeURIComponent(match[1]);
|
|
34
|
+
const [rawAccount, rawStage, rawIat] = raw.split(':');
|
|
35
|
+
|
|
36
|
+
if (Number(rawAccount) !== idAccount) return EMPTY_RECORD;
|
|
37
|
+
|
|
38
|
+
const stage = Number(rawStage);
|
|
39
|
+
if (!(stage in AiCreditsOfferStage)) return EMPTY_RECORD;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
stage,
|
|
43
|
+
lastSessionIat: Number(rawIat) || 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeOfferStageCookie(
|
|
48
|
+
idAccount: number,
|
|
49
|
+
record: OfferStageRecord,
|
|
50
|
+
whitelabelDomain?: string | null
|
|
51
|
+
): void {
|
|
52
|
+
if (typeof document === 'undefined') return;
|
|
53
|
+
|
|
54
|
+
const value = `${idAccount}:${record.stage}:${record.lastSessionIat}`;
|
|
55
|
+
const domain = cookieDomainFromDomain(whitelabelDomain);
|
|
56
|
+
const parts = [
|
|
57
|
+
`${COOKIE_NAME}=${encodeURIComponent(value)}`,
|
|
58
|
+
'path=/',
|
|
59
|
+
`max-age=${ONE_YEAR_SECONDS}`,
|
|
60
|
+
'samesite=lax',
|
|
61
|
+
domain ? `domain=${domain}` : '',
|
|
62
|
+
typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'secure' : '',
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
|
|
65
|
+
document.cookie = parts.join('; ');
|
|
66
|
+
}
|
|
@@ -3,11 +3,6 @@ export const SUBSCRIPTION_GRACE_PERIOD_DAYS = 15;
|
|
|
3
3
|
export const ACCOUNT_FREEZE_AFTER_DAYS = 7;
|
|
4
4
|
export const FREEZE_WARNING_DAYS_BEFORE = 1;
|
|
5
5
|
|
|
6
|
-
/* Congelamento só vale para a wl 1: o aviso de véspera e o e-mail de bloqueio saem do
|
|
7
|
-
* cron `dailyNotifications` do gapps-r3-api, que só roda para ela. Congelar as demais
|
|
8
|
-
* seria bloquear sem avisar. Espelhado nos gates dos dois backends. */
|
|
9
6
|
export const FREEZE_ELIGIBLE_WHITELABEL_ID = 1;
|
|
10
7
|
|
|
11
|
-
/* Stripe Smart Retries (dunning): até 8 tentativas de cobrança no cartão antes
|
|
12
|
-
* do cancelamento automático. `tentativas restantes = MAX - attempt_count`. */
|
|
13
8
|
export const MAX_PAYMENT_ATTEMPTS = 8;
|
|
@@ -5,19 +5,6 @@ import {
|
|
|
5
5
|
import { toCalendarDate } from "../../../infra/utils/date";
|
|
6
6
|
import type { Subscription } from "../types/subscription.type";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Data em que a conta é congelada por falta de pagamento: date_due + ACCOUNT_FREEZE_AFTER_DAYS.
|
|
10
|
-
*
|
|
11
|
-
* Só congela assinatura `client` da whitelabel 1 — MESMA régua do
|
|
12
|
-
* `isSubscriptionFrozen` (gapps-r3-api) e do `isAccountFrozen` (gpages-r3-api).
|
|
13
|
-
* Se as três divergirem, o usuário é bloqueado por uma camada e liberado por outra.
|
|
14
|
-
*
|
|
15
|
-
* - `type !== "client"` cobre free, trial, partner e whitelabel de uma vez (a coluna
|
|
16
|
-
* aceita mais valores do que a lista que o SubscriptionBanner enumera).
|
|
17
|
-
* - `id_wl !== 1`: o pipeline de aviso (cron `dailyNotifications` do gapps-r3-api) só
|
|
18
|
-
* existe para a wl 1. Congelar conta de whitelabel seria bloquear sem nunca avisar —
|
|
19
|
-
* o defeito do ID-5029 que este card existe para não repetir.
|
|
20
|
-
*/
|
|
21
8
|
export function getAccountFreezeDate(
|
|
22
9
|
subscription:
|
|
23
10
|
| Pick<Subscription, "type" | "date_due" | "id_wl">
|
|
@@ -6,13 +6,6 @@ import type { Subscription } from "../types/subscription.type";
|
|
|
6
6
|
|
|
7
7
|
export type AccountFreezeState = "none" | "warning" | "frozen";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Estado de congelamento por falta de pagamento (date_due + ACCOUNT_FREEZE_AFTER_DAYS).
|
|
11
|
-
*
|
|
12
|
-
* Conta cancelada/pendente de cancelamento é tratada pelos ramos de cancelamento
|
|
13
|
-
* (mesma ordem do SubscriptionBanner) — aqui sempre "none" nesse caso, pra não
|
|
14
|
-
* disputar mensagem com o CancelledSubscriptionBanner/PendingCancellationBanner.
|
|
15
|
-
*/
|
|
16
9
|
export function getAccountFreezeState(
|
|
17
10
|
subscription:
|
|
18
11
|
| Pick<
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useModalManager } from './useModalManager';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Oferta de primeira compra de créditos de IA. Quem abre é o `AiCreditsOfferGate` (1s depois do
|
|
6
|
+
* gatilho decidir que é a vez dela) — daí não existir payload: a oferta é sempre a mesma.
|
|
7
|
+
*
|
|
8
|
+
* Os callbacks vão em `useCallback` (as ações do zustand já são estáveis): o gate arma um
|
|
9
|
+
* `setTimeout` num efeito que tem `openModal` nas dependências, e uma identidade nova a cada render
|
|
10
|
+
* limparia o timer antes de ele disparar — a oferta nunca abriria.
|
|
11
|
+
*
|
|
12
|
+
* O fechamento usa `closeModalByKey` e não `closeModal`: o modal abre o `addCardModal` por cima (a
|
|
13
|
+
* pilha do `useModalManager` empilha), e um `closeModal` disparado com o cartão em cima derrubaria
|
|
14
|
+
* o topo da pilha, não a oferta.
|
|
15
|
+
*/
|
|
16
|
+
export const useAiCreditsOfferModal = () => {
|
|
17
|
+
const { activeModal, openModal, closeModalByKey } = useModalManager();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
open: activeModal === 'aiCreditsOfferModal',
|
|
21
|
+
openModal: useCallback(() => openModal('aiCreditsOfferModal'), [openModal]),
|
|
22
|
+
closeModal: useCallback(() => closeModalByKey('aiCreditsOfferModal'), [closeModalByKey]),
|
|
23
|
+
};
|
|
24
|
+
};
|