@greatapps/common 1.1.741 → 1.1.743
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/modals/BuyCreditsModal.mjs +42 -30
- package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs +14 -30
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -1
- package/dist/components/modals/cards/AddCardModal.mjs +2 -1
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/index.mjs +26 -6
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs +3 -1
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +3 -4
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -1
- package/dist/modules/accounts/utils/billing-data.mjs +10 -14
- package/dist/modules/accounts/utils/billing-data.mjs.map +1 -1
- package/dist/modules/plans/types/plan.type.mjs +15 -0
- package/dist/modules/plans/types/plan.type.mjs.map +1 -1
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +10 -2
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
- package/dist/modules/subscriptions/constants/addons.constants.mjs +15 -2
- package/dist/modules/subscriptions/constants/addons.constants.mjs.map +1 -1
- package/dist/utils/format/currency.mjs +4 -1
- package/dist/utils/format/currency.mjs.map +1 -1
- package/dist/utils/format/gateway.mjs +18 -0
- package/dist/utils/format/gateway.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/modals/BuyCreditsModal.tsx +48 -32
- package/src/components/modals/billing/RequiredBillingDataModal.tsx +32 -66
- package/src/components/modals/cards/AddCardModal.tsx +2 -1
- package/src/index.ts +14 -2
- package/src/modules/accounts/hooks/use-currency-formatter.hook.tsx +3 -0
- package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +3 -3
- package/src/modules/accounts/utils/billing-data.ts +22 -26
- package/src/modules/plans/types/plan.type.ts +17 -0
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +12 -4
- package/src/modules/subscriptions/constants/addons.constants.ts +41 -28
- package/src/utils/format/currency.ts +5 -29
- package/src/utils/format/gateway.ts +22 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatCurrency as formatCurrencyBase,
|
|
6
6
|
formatCurrencyNumber as formatCurrencyNumberBase,
|
|
7
7
|
getCurrencyForGateway,
|
|
8
|
+
getLocaleForCurrency,
|
|
8
9
|
type CurrencyCode,
|
|
9
10
|
} from '../../../utils/format/currency';
|
|
10
11
|
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export function useCurrencyFormatter(): {
|
|
20
21
|
currency: CurrencyCode;
|
|
22
|
+
locale: string;
|
|
21
23
|
formatCurrency: (value: string | number) => string;
|
|
22
24
|
formatCurrencyNumber: (value?: number | null, decimals?: number) => string;
|
|
23
25
|
} {
|
|
@@ -26,6 +28,7 @@ export function useCurrencyFormatter(): {
|
|
|
26
28
|
|
|
27
29
|
return {
|
|
28
30
|
currency,
|
|
31
|
+
locale: getLocaleForCurrency(currency),
|
|
29
32
|
formatCurrency: (value) => formatCurrencyBase(value, currency),
|
|
30
33
|
formatCurrencyNumber: (value, decimals = 2) => formatCurrencyNumberBase(value, decimals, currency),
|
|
31
34
|
};
|
|
@@ -4,14 +4,14 @@ import { useCurrentAccount } from './current-account.hook';
|
|
|
4
4
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
5
5
|
import { hasPaidSubscription } from '../../subscriptions/utils/has-paid-subscription';
|
|
6
6
|
import { useManagementPermissions } from '../../auth/hooks/useManagementPermissions';
|
|
7
|
-
import { hasCompleteBillingData,
|
|
7
|
+
import { hasCompleteBillingData, isBrazilianBillingAccount } from '../utils/billing-data';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Contas que já pagaram mas nunca preencheram o cadastro de cobrança ficam sem
|
|
11
11
|
* nota fiscal (chamado "Nota fiscal não emitida em pagamentos confirmados").
|
|
12
12
|
* Este hook diz quando o preenchimento vira obrigatório — o modal que ele
|
|
13
13
|
* alimenta é bloqueante, então só liga com os dados já carregados e só para
|
|
14
|
-
* quem consegue resolver: owner/admin.
|
|
14
|
+
* quem consegue resolver: owner/admin de conta brasileira.
|
|
15
15
|
*/
|
|
16
16
|
export function useRequiredBillingData() {
|
|
17
17
|
const { isOwnerOrAdmin } = useManagementPermissions();
|
|
@@ -26,6 +26,7 @@ export function useRequiredBillingData() {
|
|
|
26
26
|
const isRequired =
|
|
27
27
|
!isLoading &&
|
|
28
28
|
isOwnerOrAdmin &&
|
|
29
|
+
isBrazilianBillingAccount(account) &&
|
|
29
30
|
hasPaidSubscription(subscription) &&
|
|
30
31
|
!hasCompleteBillingData(account);
|
|
31
32
|
|
|
@@ -33,6 +34,5 @@ export function useRequiredBillingData() {
|
|
|
33
34
|
isRequired,
|
|
34
35
|
isLoading,
|
|
35
36
|
account,
|
|
36
|
-
isInternational: isInternationalAccount(account),
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { Account } from '../types';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Gateway das contas cobradas em USD. Elas não têm CPF/CNPJ, bairro nem número —
|
|
5
|
-
* mesmo critério usado no AddCardModal (`account.gateway === 'stripe_usd'`).
|
|
6
|
-
*/
|
|
7
|
-
export const INTERNATIONAL_GATEWAY = 'stripe_usd';
|
|
8
|
-
|
|
9
3
|
export type BillingDataSnapshot = Pick<
|
|
10
4
|
Account,
|
|
11
|
-
| 'gateway'
|
|
12
5
|
| 'financial_document'
|
|
13
6
|
| 'financial_name'
|
|
14
7
|
| 'financial_email'
|
|
@@ -21,38 +14,45 @@ export type BillingDataSnapshot = Pick<
|
|
|
21
14
|
| 'country'
|
|
22
15
|
>;
|
|
23
16
|
|
|
24
|
-
type BillingField = keyof
|
|
17
|
+
type BillingField = keyof BillingDataSnapshot;
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
|
-
* Campos exigidos
|
|
20
|
+
* Campos exigidos pra emitir a NF-e — sem qualquer um deles a nota não sai.
|
|
28
21
|
*
|
|
29
22
|
* `financial_email` NÃO entra: o backend cai no e-mail do usuário quando ele está
|
|
30
23
|
* vazio (`financial_email || userDocument.email`, accounts/services.js), então a
|
|
31
24
|
* nota sai do mesmo jeito. Exigi-lo aqui bloquearia 4.7k contas pagantes da wl 1
|
|
32
25
|
* em vez das ~460 que de fato estão sem cadastro fiscal. O campo continua no
|
|
33
26
|
* formulário, pré-preenchido — só não é critério de bloqueio.
|
|
27
|
+
*
|
|
28
|
+
* `address_complement` é opcional de verdade.
|
|
34
29
|
*/
|
|
35
|
-
const
|
|
30
|
+
const REQUIRED_BILLING_FIELDS: readonly BillingField[] = [
|
|
31
|
+
'financial_document',
|
|
36
32
|
'financial_name',
|
|
37
33
|
'zipcode',
|
|
38
34
|
'address',
|
|
35
|
+
'address_number',
|
|
36
|
+
'neighborhood',
|
|
39
37
|
'city',
|
|
40
38
|
'state',
|
|
41
39
|
'country',
|
|
42
40
|
];
|
|
43
41
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export function
|
|
53
|
-
account: Pick<BillingDataSnapshot, '
|
|
42
|
+
/**
|
|
43
|
+
* Conta sujeita à NF-e brasileira. Só ela entra no bloqueio: o formulário pede
|
|
44
|
+
* CPF/CNPJ e CEP de 8 dígitos, que um cliente de fora não tem como fornecer —
|
|
45
|
+
* e o modal é bloqueante, então seria tranca sem saída.
|
|
46
|
+
*
|
|
47
|
+
* País vazio conta como Brasil: é o default do cadastro e a origem da esmagadora
|
|
48
|
+
* maioria da base (155 contas pagas da wl 1 estão assim).
|
|
49
|
+
*/
|
|
50
|
+
export function isBrazilianBillingAccount(
|
|
51
|
+
account: Pick<BillingDataSnapshot, 'country'> | null | undefined,
|
|
54
52
|
): boolean {
|
|
55
|
-
|
|
53
|
+
if (!account) return false;
|
|
54
|
+
const country = account.country?.trim().toUpperCase();
|
|
55
|
+
return !country || country === 'BR';
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function isFilled(value: string | null | undefined): boolean {
|
|
@@ -69,11 +69,7 @@ export function hasCompleteBillingData(
|
|
|
69
69
|
): boolean {
|
|
70
70
|
if (!account) return false;
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
? INTERNATIONAL_BILLING_FIELDS
|
|
74
|
-
: BR_BILLING_FIELDS;
|
|
75
|
-
|
|
76
|
-
for (const field of fields) {
|
|
72
|
+
for (const field of REQUIRED_BILLING_FIELDS) {
|
|
77
73
|
if (!isFilled(account[field])) return false;
|
|
78
74
|
}
|
|
79
75
|
|
|
@@ -14,6 +14,22 @@ export const PlanItemSchema = z.object({
|
|
|
14
14
|
|
|
15
15
|
export type PlanItem = z.infer<typeof PlanItemSchema>;
|
|
16
16
|
|
|
17
|
+
export const PlanPeriodPricingSchema = z.object({
|
|
18
|
+
charged: z.number(),
|
|
19
|
+
list: z.number(),
|
|
20
|
+
discount: z.number(),
|
|
21
|
+
discount_percent: z.number(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const PlanApiPricingSchema = z.object({
|
|
25
|
+
currency: z.enum(['brl', 'usd']),
|
|
26
|
+
monthly: PlanPeriodPricingSchema,
|
|
27
|
+
semester: PlanPeriodPricingSchema,
|
|
28
|
+
annual: PlanPeriodPricingSchema,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type PlanApiPricing = z.infer<typeof PlanApiPricingSchema>;
|
|
32
|
+
|
|
17
33
|
export const PlanSchema = z.object({
|
|
18
34
|
id: z.number(),
|
|
19
35
|
id_plan: z.number().optional(),
|
|
@@ -28,6 +44,7 @@ export const PlanSchema = z.object({
|
|
|
28
44
|
type: z.string().optional(),
|
|
29
45
|
discount_semester: z.number().default(0),
|
|
30
46
|
discount_annual: z.number().default(0),
|
|
47
|
+
pricing: PlanApiPricingSchema.optional(),
|
|
31
48
|
items: z.array(PlanItemSchema).optional().default([]),
|
|
32
49
|
});
|
|
33
50
|
|
|
@@ -83,14 +83,22 @@ function buildMainFeaturesFromItems(
|
|
|
83
83
|
return { pages, domains, users };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function computePricingByPeriod(apiPlan: Plan
|
|
87
|
-
// Backend agora resolve value por moeda da conta; mapper apenas lê.
|
|
88
|
-
// value pode ser null (ex.: conta USD, preço USD não cadastrado).
|
|
86
|
+
function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
|
|
89
87
|
const monthly = apiPlan.value;
|
|
90
88
|
if (monthly == null) {
|
|
91
89
|
return { monthly: 0, semiannual: 0, annual: 0 };
|
|
92
90
|
}
|
|
93
91
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
92
|
+
|
|
93
|
+
const pricing = apiPlan.pricing;
|
|
94
|
+
if (pricing) {
|
|
95
|
+
return {
|
|
96
|
+
monthly,
|
|
97
|
+
semiannual: round2(pricing.semester.charged / 6),
|
|
98
|
+
annual: round2(pricing.annual.charged / 12),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
return {
|
|
95
103
|
monthly,
|
|
96
104
|
semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),
|
|
@@ -102,7 +110,7 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
|
|
|
102
110
|
const popular = plan.id === 2 || plan.id_plan === 2;
|
|
103
111
|
const buttonVariant = popular ? "brand" : "default";
|
|
104
112
|
|
|
105
|
-
const pricingByPeriod = computePricingByPeriod(plan
|
|
113
|
+
const pricingByPeriod = computePricingByPeriod(plan);
|
|
106
114
|
const monthlyFinal = pricingByPeriod.monthly;
|
|
107
115
|
|
|
108
116
|
const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
export const ADDON_IDS = {
|
|
2
|
-
DOMAINS: 1,
|
|
3
|
-
PAGES: 2,
|
|
4
|
-
PROJECTS: 3,
|
|
5
|
-
AB_TEST: 4,
|
|
6
|
-
HEATMAP: 5,
|
|
7
|
-
AI: 6,
|
|
8
|
-
} as const;
|
|
9
|
-
|
|
10
|
-
export type AddonKindEnum = (typeof ADDON_IDS)[keyof typeof ADDON_IDS];
|
|
11
|
-
|
|
12
|
-
export interface AiCreditOption {
|
|
13
|
-
credits: number;
|
|
14
|
-
price: number;
|
|
15
|
-
priceLabel
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
export const ADDON_IDS = {
|
|
2
|
+
DOMAINS: 1,
|
|
3
|
+
PAGES: 2,
|
|
4
|
+
PROJECTS: 3,
|
|
5
|
+
AB_TEST: 4,
|
|
6
|
+
HEATMAP: 5,
|
|
7
|
+
AI: 6,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type AddonKindEnum = (typeof ADDON_IDS)[keyof typeof ADDON_IDS];
|
|
11
|
+
|
|
12
|
+
export interface AiCreditOption {
|
|
13
|
+
credits: number;
|
|
14
|
+
price: number;
|
|
15
|
+
priceLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AI_CREDIT_SIZES = [0, 250, 500, 1000, 2500, 5000, 10000] as const;
|
|
19
|
+
|
|
20
|
+
export function resolveAiCreditPrice(quantity: number, perCreditPrice: number): number {
|
|
21
|
+
return Math.round(quantity * perCreditPrice * 100) / 100;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildAiCreditOptions(perCreditPrice: number): AiCreditOption[] {
|
|
25
|
+
return AI_CREDIT_SIZES.map((credits) => ({
|
|
26
|
+
credits,
|
|
27
|
+
price: resolveAiCreditPrice(credits, perCreditPrice),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AI_CREDIT_OPTIONS: AiCreditOption[] = [
|
|
32
|
+
{ credits: 0, price: 0, priceLabel: 'R$ 0,00' },
|
|
33
|
+
{ credits: 250, price: 9.99, priceLabel: 'R$ 9,99' },
|
|
34
|
+
{ credits: 500, price: 19.98, priceLabel: 'R$ 19,98' },
|
|
35
|
+
{ credits: 1000, price: 39.96, priceLabel: 'R$ 39,96' },
|
|
36
|
+
{ credits: 2500, price: 99.90, priceLabel: 'R$ 99,90' },
|
|
37
|
+
{ credits: 5000, price: 199.80, priceLabel: 'R$ 199,80' },
|
|
38
|
+
{ credits: 10000, price: 399.60, priceLabel: 'R$ 399,60' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const AI_CREDIT_VALUES: ReadonlySet<number> = new Set(AI_CREDIT_SIZES);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 'stripe_usd' → USD; demais gateways → BRL.
|
|
4
|
-
*/
|
|
1
|
+
import { isUsdGateway } from './gateway';
|
|
2
|
+
|
|
5
3
|
export type CurrencyCode = 'brl' | 'usd';
|
|
6
4
|
|
|
7
5
|
const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }> = {
|
|
@@ -9,21 +7,11 @@ const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }
|
|
|
9
7
|
usd: { locale: 'en-US', currency: 'USD' },
|
|
10
8
|
};
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
* Deriva a moeda de exibição a partir do gateway da conta.
|
|
14
|
-
* A moeda é definida pelo backend no roteamento de gateway — read-only para o usuário.
|
|
15
|
-
* @param gateway - account.gateway ('stripe_usd' | 'stripe_brl' | 'vindi' | ...)
|
|
16
|
-
*/
|
|
17
10
|
export const getCurrencyForGateway = (gateway?: string | null): CurrencyCode =>
|
|
18
|
-
gateway
|
|
11
|
+
isUsdGateway(gateway) ? 'usd' : 'brl';
|
|
12
|
+
|
|
13
|
+
export const getLocaleForCurrency = (currency: CurrencyCode = 'brl'): string => CURRENCY_FORMAT[currency].locale;
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* Formata um valor (string ou número) como moeda.
|
|
22
|
-
* String: assume centavos (remove não-dígitos e divide por 100) — comportamento legado.
|
|
23
|
-
* @param value - valor a formatar
|
|
24
|
-
* @param currency - moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
25
|
-
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
26
|
-
*/
|
|
27
15
|
export const formatCurrency = (value: string | number, currency: CurrencyCode = 'brl'): string => {
|
|
28
16
|
const fmt = CURRENCY_FORMAT[currency];
|
|
29
17
|
if (typeof value === 'number') {
|
|
@@ -41,23 +29,11 @@ export const formatCurrency = (value: string | number, currency: CurrencyCode =
|
|
|
41
29
|
});
|
|
42
30
|
};
|
|
43
31
|
|
|
44
|
-
/**
|
|
45
|
-
* Converte uma string de moeda para número (centavos / 100). Independente de moeda.
|
|
46
|
-
* @param value - String com valor de moeda
|
|
47
|
-
* @returns Número decimal
|
|
48
|
-
*/
|
|
49
32
|
export const parseCurrencyToNumber = (value: string): number => {
|
|
50
33
|
const numericValue = value.replace(/\D/g, '');
|
|
51
34
|
return parseFloat(numericValue) / 100;
|
|
52
35
|
};
|
|
53
36
|
|
|
54
|
-
/**
|
|
55
|
-
* Formata um número (opcional) como moeda.
|
|
56
|
-
* @param value - Número a formatar (pode ser undefined/null → 0)
|
|
57
|
-
* @param decimals - Casas decimais (padrão 2)
|
|
58
|
-
* @param currency - Moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
59
|
-
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
60
|
-
*/
|
|
61
37
|
export const formatCurrencyNumber = (value?: number | null, decimals = 2, currency: CurrencyCode = 'brl'): string => {
|
|
62
38
|
const safeValue = value ?? 0;
|
|
63
39
|
const fmt = CURRENCY_FORMAT[currency];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const USD_GATEWAY = 'stripe_usd';
|
|
2
|
+
|
|
3
|
+
export type PaymentMethodKind = 'card' | 'pix' | 'boleto';
|
|
4
|
+
|
|
5
|
+
export type AccountGatewayLike = { gateway?: string | null } | null | undefined;
|
|
6
|
+
|
|
7
|
+
const normalizeGateway = (gateway?: string | null): string => (gateway ?? '').trim().toLowerCase();
|
|
8
|
+
|
|
9
|
+
export const isUsdGateway = (gateway?: string | null): boolean => normalizeGateway(gateway) === USD_GATEWAY;
|
|
10
|
+
|
|
11
|
+
export const isUsdAccount = (account: AccountGatewayLike): boolean => isUsdGateway(account?.gateway);
|
|
12
|
+
|
|
13
|
+
const USD_PAYMENT_METHODS: readonly PaymentMethodKind[] = ['card'];
|
|
14
|
+
const BRL_PAYMENT_METHODS: readonly PaymentMethodKind[] = ['card', 'pix', 'boleto'];
|
|
15
|
+
|
|
16
|
+
export const allowedPaymentMethods = (account: AccountGatewayLike): readonly PaymentMethodKind[] =>
|
|
17
|
+
isUsdAccount(account) ? USD_PAYMENT_METHODS : BRL_PAYMENT_METHODS;
|
|
18
|
+
|
|
19
|
+
export const isPaymentMethodAllowed = (account: AccountGatewayLike, method: PaymentMethodKind): boolean =>
|
|
20
|
+
allowedPaymentMethods(account).includes(method);
|
|
21
|
+
|
|
22
|
+
export const canBuyStandaloneAiCredits = (account: AccountGatewayLike): boolean => !isUsdAccount(account);
|