@greatapps/common 1.1.794 → 1.1.796
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/billing/BillingDataForm.mjs +406 -0
- package/dist/components/modals/billing/BillingDataForm.mjs.map +1 -0
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs +5 -405
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -1
- package/dist/i18n/resolve-locale.mjs +1 -1
- package/dist/i18n/resolve-locale.mjs.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs +3 -0
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs.map +1 -1
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +1 -5
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
- package/dist/modules/whitelabel/constants/whitelabel.constants.mjs +11 -0
- package/dist/modules/whitelabel/constants/whitelabel.constants.mjs.map +1 -0
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +69 -11
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/providers/query.provider.mjs +3 -13
- package/dist/providers/query.provider.mjs.map +1 -1
- package/dist/providers/share-query-cache.mjs +54 -0
- package/dist/providers/share-query-cache.mjs.map +1 -0
- package/package.json +10 -9
- package/src/components/modals/billing/BillingDataForm.tsx +506 -0
- package/src/components/modals/billing/RequiredBillingDataModal.tsx +3 -475
- package/src/i18n/resolve-locale.ts +1 -1
- package/src/index.ts +3 -0
- package/src/modules/ia-credits/types/ai-credits-offer.type.ts +3 -0
- package/src/modules/whitelabel/actions/find-whitelabel.action.ts +1 -7
- package/src/modules/whitelabel/constants/whitelabel.constants.ts +14 -0
- package/src/modules/whitelabel/services/whitelabel.service.ts +282 -209
- package/src/providers/query.provider.tsx +3 -13
- package/src/providers/share-query-cache.ts +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/share-query-cache.ts"],"sourcesContent":["import type { Query, QueryClient, QueryKey, QueryState } from \"@tanstack/react-query\";\n\ntype CacheMessage =\n | { type: \"removed\"; queryHash: string; queryKey: QueryKey }\n | { type: \"updated\"; queryHash: string; queryKey: QueryKey; state: QueryState };\n\nexport function isShareableQuery(query: Query): boolean {\n if (query.state.status === \"error\") return false;\n if (query.meta?.persist === false) return false;\n if (query.state.dataUpdateCount > 10) return false;\n return true;\n}\n\nexport function shareQueryCacheBetweenTabs(queryClient: QueryClient): () => void {\n if (typeof BroadcastChannel === \"undefined\") return () => {};\n const channel = new BroadcastChannel(\"greatapps-query-cache\");\n const queryCache = queryClient.getQueryCache();\n let applyingRemoteMessage = false;\n\n const unsubscribe = queryCache.subscribe((event) => {\n if (applyingRemoteMessage) return;\n const { query } = event;\n const { queryHash, queryKey } = query;\n if (event.type === \"removed\" && query.observers.length > 0) {\n channel.postMessage({ type: \"removed\", queryHash, queryKey });\n }\n if (event.type === \"updated\" && event.action.type === \"success\" && isShareableQuery(query)) {\n try {\n channel.postMessage({ type: \"updated\", queryHash, queryKey, state: query.state });\n } catch {\n console.warn(\"Falha ao espelhar query entre abas\", queryHash);\n }\n }\n });\n\n channel.addEventListener(\"message\", (event: MessageEvent<CacheMessage>) => {\n const message = event.data;\n const query = queryCache.get(message.queryHash);\n applyingRemoteMessage = true;\n try {\n if (message.type === \"removed\" && query) queryCache.remove(query);\n if (message.type === \"updated\") {\n if (!query) {\n queryCache.build(queryClient, { queryKey: message.queryKey, queryHash: message.queryHash }, message.state);\n } else if (isShareableQuery(query) && message.state.dataUpdatedAt >= query.state.dataUpdatedAt) {\n query.setState(message.state);\n }\n }\n } finally {\n applyingRemoteMessage = false;\n }\n });\n\n return () => {\n unsubscribe();\n channel.close();\n };\n}\n"],"mappings":"AAMO,SAAS,iBAAiB,OAAuB;AACtD,MAAI,MAAM,MAAM,WAAW,QAAS,QAAO;AAC3C,MAAI,MAAM,MAAM,YAAY,MAAO,QAAO;AAC1C,MAAI,MAAM,MAAM,kBAAkB,GAAI,QAAO;AAC7C,SAAO;AACT;AAEO,SAAS,2BAA2B,aAAsC;AAC/E,MAAI,OAAO,qBAAqB,YAAa,QAAO,MAAM;AAAA,EAAC;AAC3D,QAAM,UAAU,IAAI,iBAAiB,uBAAuB;AAC5D,QAAM,aAAa,YAAY,cAAc;AAC7C,MAAI,wBAAwB;AAE5B,QAAM,cAAc,WAAW,UAAU,CAAC,UAAU;AAClD,QAAI,sBAAuB;AAC3B,UAAM,EAAE,MAAM,IAAI;AAClB,UAAM,EAAE,WAAW,SAAS,IAAI;AAChC,QAAI,MAAM,SAAS,aAAa,MAAM,UAAU,SAAS,GAAG;AAC1D,cAAQ,YAAY,EAAE,MAAM,WAAW,WAAW,SAAS,CAAC;AAAA,IAC9D;AACA,QAAI,MAAM,SAAS,aAAa,MAAM,OAAO,SAAS,aAAa,iBAAiB,KAAK,GAAG;AAC1F,UAAI;AACF,gBAAQ,YAAY,EAAE,MAAM,WAAW,WAAW,UAAU,OAAO,MAAM,MAAM,CAAC;AAAA,MAClF,QAAQ;AACN,gBAAQ,KAAK,sCAAsC,SAAS;AAAA,MAC9D;AAAA,IACF;AAAA,EACF,CAAC;AAED,UAAQ,iBAAiB,WAAW,CAAC,UAAsC;AACzE,UAAM,UAAU,MAAM;AACtB,UAAM,QAAQ,WAAW,IAAI,QAAQ,SAAS;AAC9C,4BAAwB;AACxB,QAAI;AACF,UAAI,QAAQ,SAAS,aAAa,MAAO,YAAW,OAAO,KAAK;AAChE,UAAI,QAAQ,SAAS,WAAW;AAC9B,YAAI,CAAC,OAAO;AACV,qBAAW,MAAM,aAAa,EAAE,UAAU,QAAQ,UAAU,WAAW,QAAQ,UAAU,GAAG,QAAQ,KAAK;AAAA,QAC3G,WAAW,iBAAiB,KAAK,KAAK,QAAQ,MAAM,iBAAiB,MAAM,MAAM,eAAe;AAC9F,gBAAM,SAAS,QAAQ,KAAK;AAAA,QAC9B;AAAA,MACF;AAAA,IACF,UAAE;AACA,8BAAwB;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO,MAAM;AACX,gBAAY;AACZ,YAAQ,MAAM;AAAA,EAChB;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greatapps/common",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.796",
|
|
4
4
|
"description": "Shared library for GreatApps frontend applications",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
"dist",
|
|
47
47
|
"src"
|
|
48
48
|
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"prepublishOnly": "npm run build"
|
|
56
|
+
},
|
|
49
57
|
"keywords": [],
|
|
50
58
|
"author": "",
|
|
51
59
|
"license": "ISC",
|
|
@@ -142,12 +150,5 @@
|
|
|
142
150
|
"@faker-js/faker": {
|
|
143
151
|
"optional": true
|
|
144
152
|
}
|
|
145
|
-
},
|
|
146
|
-
"scripts": {
|
|
147
|
-
"build": "tsup",
|
|
148
|
-
"dev": "tsup --watch",
|
|
149
|
-
"typecheck": "tsc --noEmit",
|
|
150
|
-
"test": "vitest run",
|
|
151
|
-
"test:watch": "vitest"
|
|
152
153
|
}
|
|
153
|
-
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- o generic Translator do next-intl não é
|
|
2
|
+
expressável estruturalmente; o `any` do TranslateFn é intencional e está contido neste arquivo
|
|
3
|
+
(mesmo padrão de modules/plans/utils/map-api-plan-to-ui.ts). */
|
|
4
|
+
'use client';
|
|
5
|
+
|
|
6
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
9
|
+
import { useTranslations } from 'next-intl';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import z from 'zod';
|
|
12
|
+
|
|
13
|
+
import { Button } from '../../ui/buttons/Button';
|
|
14
|
+
import { FormField } from '../../ui/form/FormField';
|
|
15
|
+
import { SelectField } from '../../ui/form/SelectField';
|
|
16
|
+
import { Toast } from '../../ui/feedback/Toast';
|
|
17
|
+
import { useAuth } from '../../../providers/auth.provider';
|
|
18
|
+
import { useViaCep } from '../../../modules/accounts/hooks/useViaCep';
|
|
19
|
+
import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccountManagement';
|
|
20
|
+
import { COUNTRIES } from '../../../utils/countries';
|
|
21
|
+
import { readGeoCountry } from '../../../utils/geo-country';
|
|
22
|
+
import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
|
|
23
|
+
import { formatCNPJ, formatCPF, formatPostalCode } from '../../../utils/format/masks';
|
|
24
|
+
import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
|
|
25
|
+
import type { Account } from '../../../modules/accounts/types';
|
|
26
|
+
import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
|
|
27
|
+
|
|
28
|
+
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
29
|
+
|
|
30
|
+
type BillingDataFormValues = {
|
|
31
|
+
personType: 'pf' | 'pj';
|
|
32
|
+
document: string;
|
|
33
|
+
financialName: string;
|
|
34
|
+
financialEmail: string;
|
|
35
|
+
cep: string;
|
|
36
|
+
street: string;
|
|
37
|
+
streetNumber: string;
|
|
38
|
+
complement?: string;
|
|
39
|
+
neighborhood: string;
|
|
40
|
+
city: string;
|
|
41
|
+
state: string;
|
|
42
|
+
country: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) => ({
|
|
46
|
+
value: country.iso,
|
|
47
|
+
label: country.name,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const BR_REQUIRED_ADDRESS_FIELDS: readonly (keyof BillingDataFormValues)[] = [
|
|
51
|
+
'street',
|
|
52
|
+
'streetNumber',
|
|
53
|
+
'neighborhood',
|
|
54
|
+
'city',
|
|
55
|
+
'state',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function buildBillingDataSchema(translate: TranslateFn) {
|
|
59
|
+
const base = z.object({
|
|
60
|
+
personType: z.enum(['pf', 'pj']),
|
|
61
|
+
document: z.string().trim(),
|
|
62
|
+
financialName: z
|
|
63
|
+
.string()
|
|
64
|
+
.trim()
|
|
65
|
+
.min(1, translate('common.billing.requiredData.requiredField')),
|
|
66
|
+
financialEmail: z
|
|
67
|
+
.string()
|
|
68
|
+
.trim()
|
|
69
|
+
.min(1, translate('common.billing.requiredData.requiredField'))
|
|
70
|
+
.email(translate('common.billing.requiredData.invalidEmail')),
|
|
71
|
+
cep: z.string().trim(),
|
|
72
|
+
street: z.string().trim(),
|
|
73
|
+
streetNumber: z.string().trim(),
|
|
74
|
+
complement: z.string().trim().optional(),
|
|
75
|
+
neighborhood: z.string().trim(),
|
|
76
|
+
city: z.string().trim(),
|
|
77
|
+
state: z.string().trim(),
|
|
78
|
+
country: z.string().trim().min(1, translate('common.billing.requiredData.requiredField')),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return base.superRefine((data, ctx) => {
|
|
82
|
+
if (data.country !== 'BR') return;
|
|
83
|
+
|
|
84
|
+
for (const field of BR_REQUIRED_ADDRESS_FIELDS) {
|
|
85
|
+
if (!data[field]) {
|
|
86
|
+
ctx.addIssue({
|
|
87
|
+
code: 'custom',
|
|
88
|
+
path: [field],
|
|
89
|
+
message: translate('common.billing.requiredData.requiredField'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (data.cep.replace(/\D/g, '').length !== 8) {
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: 'custom',
|
|
97
|
+
path: ['cep'],
|
|
98
|
+
message: translate('common.billing.requiredData.invalidCep'),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isCompany = data.personType === 'pj';
|
|
103
|
+
const digits = data.document.replace(/\D/g, '');
|
|
104
|
+
|
|
105
|
+
if (digits.length !== (isCompany ? 14 : 11)) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: 'custom',
|
|
108
|
+
path: ['document'],
|
|
109
|
+
message: isCompany
|
|
110
|
+
? translate('common.billing.requiredData.invalidCnpj')
|
|
111
|
+
: translate('common.billing.requiredData.invalidCpf'),
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Dígito verificador, mesmo algoritmo do backend: sem isso 111.111.111-11 passa
|
|
117
|
+
* aqui e o PUT da conta é recusado depois, sem o usuário saber o motivo. */
|
|
118
|
+
const isValid = isCompany ? isValidCNPJ(digits) : isValidCPF(digits);
|
|
119
|
+
if (!isValid) {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: 'custom',
|
|
122
|
+
path: ['document'],
|
|
123
|
+
message: isCompany
|
|
124
|
+
? translate('common.billing.requiredData.invalidCnpjChecksum')
|
|
125
|
+
: translate('common.billing.requiredData.invalidCpfChecksum'),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatDocument(document: string | null | undefined, isCompany: boolean) {
|
|
132
|
+
if (!document) return '';
|
|
133
|
+
return isCompany ? formatCNPJ(document) : formatCPF(document);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface BillingDataFormProps {
|
|
137
|
+
account: Account | undefined;
|
|
138
|
+
onSaved: () => void;
|
|
139
|
+
/** Fixa o formulário em modo Brasil (documento, CEP e endereço obrigatórios) e esconde o select
|
|
140
|
+
* de país — usado por fluxos PIX, que só existem no Brasil. Sem isso, o país vem da conta ou da
|
|
141
|
+
* geolocalização do visitante. */
|
|
142
|
+
country?: 'BR';
|
|
143
|
+
submitLabel?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Formulário de cadastro fiscal/cobrança, reaproveitado por qualquer fluxo que precise dele
|
|
148
|
+
* bloqueante (`RequiredBillingDataModal`) ou como etapa dentro de outro modal.
|
|
149
|
+
*/
|
|
150
|
+
export function BillingDataForm({ account, onSaved, country, submitLabel }: BillingDataFormProps) {
|
|
151
|
+
const translate = useTranslations();
|
|
152
|
+
const { user } = useAuth();
|
|
153
|
+
const { mutateAsync: updateBillingData } = useUpdateBillingData();
|
|
154
|
+
const { mutateAsync: lookupCep } = useViaCep();
|
|
155
|
+
|
|
156
|
+
const isPrefilled = useRef(false);
|
|
157
|
+
|
|
158
|
+
const schema = useMemo(
|
|
159
|
+
() => buildBillingDataSchema(translate),
|
|
160
|
+
[translate],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const form = useForm<BillingDataFormValues>({
|
|
164
|
+
resolver: zodResolver(schema),
|
|
165
|
+
mode: 'onChange',
|
|
166
|
+
defaultValues: {
|
|
167
|
+
personType: 'pf',
|
|
168
|
+
document: '',
|
|
169
|
+
financialName: '',
|
|
170
|
+
financialEmail: '',
|
|
171
|
+
cep: '',
|
|
172
|
+
street: '',
|
|
173
|
+
streetNumber: '',
|
|
174
|
+
complement: '',
|
|
175
|
+
neighborhood: '',
|
|
176
|
+
city: '',
|
|
177
|
+
state: '',
|
|
178
|
+
country: country ?? 'BR',
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const {
|
|
183
|
+
register,
|
|
184
|
+
control,
|
|
185
|
+
handleSubmit,
|
|
186
|
+
reset,
|
|
187
|
+
setValue,
|
|
188
|
+
watch,
|
|
189
|
+
formState: { errors, isSubmitting, isValid },
|
|
190
|
+
} = form;
|
|
191
|
+
|
|
192
|
+
const personType = watch('personType');
|
|
193
|
+
const isCompany = personType === 'pj';
|
|
194
|
+
const isBrazil = watch('country') === 'BR';
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!account || isPrefilled.current) return;
|
|
198
|
+
isPrefilled.current = true;
|
|
199
|
+
|
|
200
|
+
const accountIsCompany = account.financial_document_type === 2;
|
|
201
|
+
reset({
|
|
202
|
+
personType: accountIsCompany ? 'pj' : 'pf',
|
|
203
|
+
document: formatDocument(account.financial_document, accountIsCompany),
|
|
204
|
+
financialName: account.financial_name ?? '',
|
|
205
|
+
financialEmail: account.financial_email || user?.email || '',
|
|
206
|
+
cep: account.zipcode ?? '',
|
|
207
|
+
street: account.address ?? '',
|
|
208
|
+
streetNumber: account.address_number ?? '',
|
|
209
|
+
complement: account.address_complement ?? '',
|
|
210
|
+
neighborhood: account.neighborhood ?? '',
|
|
211
|
+
city: account.city ?? '',
|
|
212
|
+
state: account.state ?? '',
|
|
213
|
+
country: country ?? (account.country || readGeoCountry() || 'BR'),
|
|
214
|
+
});
|
|
215
|
+
}, [account, country, reset, user?.email]);
|
|
216
|
+
|
|
217
|
+
const handleDocumentChange = (rawValue: string) => {
|
|
218
|
+
setValue('document', isCompany ? formatCNPJ(rawValue) : formatCPF(rawValue), {
|
|
219
|
+
shouldValidate: true,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handlePersonTypeChange = (value: 'pf' | 'pj') => {
|
|
224
|
+
if (value === personType) return;
|
|
225
|
+
setValue('document', '');
|
|
226
|
+
/* shouldValidate reroda o resolver: sem isso o `isValid` fica preso no valor
|
|
227
|
+
* do documento antigo e o botão continua habilitado com o campo já limpo. */
|
|
228
|
+
setValue('personType', value, { shouldValidate: true });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleCepChange = (rawValue: string) => {
|
|
232
|
+
setValue('cep', formatPostalCode(rawValue), { shouldValidate: true });
|
|
233
|
+
|
|
234
|
+
const digits = rawValue.replace(/\D/g, '');
|
|
235
|
+
if (digits.length !== 8) return;
|
|
236
|
+
|
|
237
|
+
lookupCep(digits)
|
|
238
|
+
.then((data) => {
|
|
239
|
+
setValue('street', data.logradouro, { shouldValidate: true });
|
|
240
|
+
setValue('neighborhood', data.bairro, { shouldValidate: true });
|
|
241
|
+
setValue('city', data.localidade, { shouldValidate: true });
|
|
242
|
+
setValue('state', data.uf, { shouldValidate: true });
|
|
243
|
+
})
|
|
244
|
+
.catch(() => {
|
|
245
|
+
// CEP não encontrado: o usuário preenche o endereço na mão.
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
async function onSubmit(data: BillingDataFormValues) {
|
|
250
|
+
const payload: UpdateBillingDataRequest =
|
|
251
|
+
data.country === 'BR'
|
|
252
|
+
? {
|
|
253
|
+
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
254
|
+
financial_document: data.document,
|
|
255
|
+
financial_name: data.financialName,
|
|
256
|
+
financial_email: data.financialEmail,
|
|
257
|
+
zipcode: data.cep,
|
|
258
|
+
address: data.street,
|
|
259
|
+
address_number: data.streetNumber,
|
|
260
|
+
address_complement: data.complement ?? '',
|
|
261
|
+
neighborhood: data.neighborhood,
|
|
262
|
+
city: data.city,
|
|
263
|
+
state: data.state,
|
|
264
|
+
country: data.country,
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
financial_name: data.financialName,
|
|
268
|
+
financial_email: data.financialEmail,
|
|
269
|
+
country: data.country,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await updateBillingData(payload);
|
|
274
|
+
toast.custom(
|
|
275
|
+
(t) => (
|
|
276
|
+
<Toast
|
|
277
|
+
variant="success"
|
|
278
|
+
message={translate('common.billing.requiredData.successToast')}
|
|
279
|
+
toastId={t}
|
|
280
|
+
/>
|
|
281
|
+
),
|
|
282
|
+
{ duration: 5000 },
|
|
283
|
+
);
|
|
284
|
+
onSaved();
|
|
285
|
+
} catch (error) {
|
|
286
|
+
/* O backend devolve a razão exata da recusa (ex.: documento fiscal inválido).
|
|
287
|
+
* Trocar por um texto genérico deixaria o usuário travado sem saber o que corrigir. */
|
|
288
|
+
const message =
|
|
289
|
+
error instanceof Error && error.message
|
|
290
|
+
? error.message
|
|
291
|
+
: translate('common.billing.requiredData.errorToast');
|
|
292
|
+
toast.custom((t) => <Toast variant="error" message={message} toastId={t} />, {
|
|
293
|
+
duration: 5000,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0" noValidate>
|
|
300
|
+
<div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
|
|
301
|
+
<div className="flex flex-col gap-5">
|
|
302
|
+
{!country && (
|
|
303
|
+
<>
|
|
304
|
+
{/* Reage ao watch('country'): só quem está aqui dentro sabe quando o usuário
|
|
305
|
+
* troca de país no select logo abaixo. */}
|
|
306
|
+
<span className="paragraph-small-regular text-zinc-600">
|
|
307
|
+
{isBrazil
|
|
308
|
+
? translate('common.billing.requiredData.description')
|
|
309
|
+
: translate('common.billing.requiredData.descriptionInternational')}
|
|
310
|
+
</span>
|
|
311
|
+
|
|
312
|
+
<Controller
|
|
313
|
+
name="country"
|
|
314
|
+
control={control}
|
|
315
|
+
render={({ field }) => (
|
|
316
|
+
<SelectField
|
|
317
|
+
label={translate('common.billing.requiredData.country')}
|
|
318
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
319
|
+
value={field.value || undefined}
|
|
320
|
+
onChange={(value) =>
|
|
321
|
+
setValue('country', String(value), { shouldValidate: true })
|
|
322
|
+
}
|
|
323
|
+
options={COUNTRY_OPTIONS}
|
|
324
|
+
error={!!errors.country}
|
|
325
|
+
errorMessage={errors.country?.message}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
/>
|
|
329
|
+
</>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{isBrazil && (
|
|
333
|
+
<div className="flex gap-4">
|
|
334
|
+
<Controller
|
|
335
|
+
name="personType"
|
|
336
|
+
control={control}
|
|
337
|
+
render={({ field }) => (
|
|
338
|
+
<SelectField
|
|
339
|
+
label={translate('common.billing.requiredData.personType')}
|
|
340
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
341
|
+
value={field.value}
|
|
342
|
+
onChange={(value) => handlePersonTypeChange(value as 'pf' | 'pj')}
|
|
343
|
+
options={[
|
|
344
|
+
{
|
|
345
|
+
value: 'pf',
|
|
346
|
+
label: translate('common.billing.requiredData.personTypePf'),
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
value: 'pj',
|
|
350
|
+
label: translate('common.billing.requiredData.personTypePj'),
|
|
351
|
+
},
|
|
352
|
+
]}
|
|
353
|
+
containerClassName="flex-1"
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
/>
|
|
357
|
+
<div className="flex-1">
|
|
358
|
+
<FormField
|
|
359
|
+
label={
|
|
360
|
+
isCompany
|
|
361
|
+
? translate('common.billing.requiredData.cnpj')
|
|
362
|
+
: translate('common.billing.requiredData.cpf')
|
|
363
|
+
}
|
|
364
|
+
placeholder={isCompany ? '__.___.___/____-__' : '___.___.___-__'}
|
|
365
|
+
error={!!errors.document}
|
|
366
|
+
errorMessage={errors.document?.message}
|
|
367
|
+
{...register('document', {
|
|
368
|
+
onChange: (event) => handleDocumentChange(event.target.value),
|
|
369
|
+
})}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
<FormField
|
|
376
|
+
label={
|
|
377
|
+
isBrazil && isCompany
|
|
378
|
+
? translate('common.billing.requiredData.companyName')
|
|
379
|
+
: translate('common.billing.requiredData.fullName')
|
|
380
|
+
}
|
|
381
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
382
|
+
error={!!errors.financialName}
|
|
383
|
+
errorMessage={errors.financialName?.message}
|
|
384
|
+
{...register('financialName')}
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
<FormField
|
|
388
|
+
label={translate('common.billing.requiredData.financialEmail')}
|
|
389
|
+
placeholder={translate('common.billing.requiredData.financialEmailPlaceholder')}
|
|
390
|
+
type="email"
|
|
391
|
+
error={!!errors.financialEmail}
|
|
392
|
+
errorMessage={errors.financialEmail?.message}
|
|
393
|
+
{...register('financialEmail')}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{isBrazil && (
|
|
398
|
+
<>
|
|
399
|
+
<div className="h-px bg-zinc-200" />
|
|
400
|
+
|
|
401
|
+
<div className="flex flex-col gap-5">
|
|
402
|
+
<span className="paragraph-small-semibold text-zinc-950">
|
|
403
|
+
{translate('common.billing.requiredData.addressHeading')}
|
|
404
|
+
</span>
|
|
405
|
+
|
|
406
|
+
<FormField
|
|
407
|
+
label={translate('common.billing.requiredData.cep')}
|
|
408
|
+
placeholder="_____-___"
|
|
409
|
+
error={!!errors.cep}
|
|
410
|
+
errorMessage={errors.cep?.message}
|
|
411
|
+
{...register('cep', {
|
|
412
|
+
onChange: (event) => handleCepChange(event.target.value),
|
|
413
|
+
})}
|
|
414
|
+
/>
|
|
415
|
+
|
|
416
|
+
<div className="flex gap-4">
|
|
417
|
+
<div className="flex-[3]">
|
|
418
|
+
<FormField
|
|
419
|
+
label={translate('common.billing.requiredData.street')}
|
|
420
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
421
|
+
error={!!errors.street}
|
|
422
|
+
errorMessage={errors.street?.message}
|
|
423
|
+
{...register('street')}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
<div className="flex-1">
|
|
427
|
+
<FormField
|
|
428
|
+
label={translate('common.billing.requiredData.streetNumber')}
|
|
429
|
+
placeholder="000"
|
|
430
|
+
error={!!errors.streetNumber}
|
|
431
|
+
errorMessage={errors.streetNumber?.message}
|
|
432
|
+
{...register('streetNumber')}
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div className="flex gap-4">
|
|
438
|
+
<div className="flex-1">
|
|
439
|
+
<FormField
|
|
440
|
+
label={translate('common.billing.requiredData.neighborhood')}
|
|
441
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
442
|
+
error={!!errors.neighborhood}
|
|
443
|
+
errorMessage={errors.neighborhood?.message}
|
|
444
|
+
{...register('neighborhood')}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<div className="flex-1">
|
|
448
|
+
<FormField
|
|
449
|
+
label={translate('common.billing.requiredData.complement')}
|
|
450
|
+
optional
|
|
451
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
452
|
+
{...register('complement')}
|
|
453
|
+
/>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<div className="flex gap-4">
|
|
458
|
+
<div className="flex-1">
|
|
459
|
+
<FormField
|
|
460
|
+
label={translate('common.billing.requiredData.city')}
|
|
461
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
462
|
+
error={!!errors.city}
|
|
463
|
+
errorMessage={errors.city?.message}
|
|
464
|
+
{...register('city')}
|
|
465
|
+
/>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex-1">
|
|
468
|
+
<Controller
|
|
469
|
+
name="state"
|
|
470
|
+
control={control}
|
|
471
|
+
render={({ field }) => (
|
|
472
|
+
<SelectField
|
|
473
|
+
label={translate('common.billing.requiredData.state')}
|
|
474
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
475
|
+
value={field.value || undefined}
|
|
476
|
+
onChange={(value) =>
|
|
477
|
+
setValue('state', String(value), { shouldValidate: true })
|
|
478
|
+
}
|
|
479
|
+
options={BR_STATE_OPTIONS}
|
|
480
|
+
error={!!errors.state}
|
|
481
|
+
errorMessage={errors.state?.message}
|
|
482
|
+
/>
|
|
483
|
+
)}
|
|
484
|
+
/>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<div className="flex items-center justify-end p-4 lg:p-5 border-t border-zinc-200 shrink-0">
|
|
493
|
+
<Button
|
|
494
|
+
type="submit"
|
|
495
|
+
className="h-10! w-full lg:w-fit"
|
|
496
|
+
disabled={!isValid || isSubmitting}
|
|
497
|
+
loading={isSubmitting}
|
|
498
|
+
>
|
|
499
|
+
{isSubmitting
|
|
500
|
+
? translate('common.billing.requiredData.submitting')
|
|
501
|
+
: submitLabel ?? translate('common.billing.requiredData.submit')}
|
|
502
|
+
</Button>
|
|
503
|
+
</div>
|
|
504
|
+
</form>
|
|
505
|
+
);
|
|
506
|
+
}
|