@greatapps/common 1.1.737 → 1.1.739
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/cards/AddCardModal.mjs +26 -28
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/components/modals/cards/CardFormFields.mjs +10 -9
- package/dist/components/modals/cards/CardFormFields.mjs.map +1 -1
- package/dist/i18n/resolve-locale.mjs +2 -1
- package/dist/i18n/resolve-locale.mjs.map +1 -1
- package/dist/modules/auth/utils/build-wl-override.mjs +2 -1
- package/dist/modules/auth/utils/build-wl-override.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/auth/utils/require-token-not-expired.mjs +2 -1
- package/dist/modules/auth/utils/require-token-not-expired.mjs.map +1 -1
- package/dist/modules/cards/hooks/create-card.hook.mjs +11 -0
- package/dist/modules/cards/hooks/create-card.hook.mjs.map +1 -1
- package/dist/modules/cards/types.mjs +20 -1
- package/dist/modules/cards/types.mjs.map +1 -1
- package/dist/modules/users/services/user.service.mjs +2 -1
- package/dist/modules/users/services/user.service.mjs.map +1 -1
- package/dist/testing/factories/jwt.mjs +5 -2
- package/dist/testing/factories/jwt.mjs.map +1 -1
- package/dist/utils/jwt-payload.mjs +15 -0
- package/dist/utils/jwt-payload.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/modals/cards/AddCardModal.tsx +36 -30
- package/src/components/modals/cards/CardFormFields.tsx +18 -10
- package/src/i18n/resolve-locale.ts +2 -1
- package/src/modules/auth/utils/build-wl-override.ts +2 -1
- package/src/modules/auth/utils/get-user-context.ts +2 -1
- package/src/modules/auth/utils/require-token-not-expired.ts +2 -1
- package/src/modules/cards/hooks/create-card.hook.ts +11 -0
- package/src/modules/cards/types.ts +19 -0
- package/src/modules/users/services/user.service.ts +2 -1
- package/src/testing/factories/jwt.ts +8 -3
- package/src/utils/jwt-payload.ts +32 -0
|
@@ -7,8 +7,8 @@ import { Toast } from "../../ui/feedback/Toast";
|
|
|
7
7
|
import { useModalManager } from "../../../store/useModalManager";
|
|
8
8
|
import { useCreateCard } from "../../../modules/cards/hooks/create-card.hook";
|
|
9
9
|
import { useCreateSetupIntent } from "../../../modules/cards/hooks/create-setup-intent.hook";
|
|
10
|
-
import { useUpdateAccount } from "../../../modules/accounts/hooks/useAccountManagement";
|
|
11
10
|
import { useCurrentAccount } from "../../../modules/accounts/hooks/current-account.hook";
|
|
11
|
+
import { isValidTaxId } from "../../../utils/validators/common";
|
|
12
12
|
import { useExternalContracting } from "../../../providers/whitelabel.provider";
|
|
13
13
|
import { CardFormFields, BillingFormFields, buildCardFormSchema } from "./CardFormFields";
|
|
14
14
|
import { IconX, IconCreditCard, IconCheck, IconChevronRight } from "@tabler/icons-react";
|
|
@@ -75,7 +75,6 @@ function AddCardModal() {
|
|
|
75
75
|
const onCardAdded = modalData.onCardAdded;
|
|
76
76
|
const createSetupIntentMutation = useCreateSetupIntent();
|
|
77
77
|
const createCardMutation = useCreateCard();
|
|
78
|
-
const updateAccountMutation = useUpdateAccount();
|
|
79
78
|
const elements = useElements();
|
|
80
79
|
const stripe = useStripe();
|
|
81
80
|
const form = useForm({
|
|
@@ -192,28 +191,24 @@ function AddCardModal() {
|
|
|
192
191
|
);
|
|
193
192
|
return;
|
|
194
193
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
state: data.state,
|
|
214
|
-
country: data.country
|
|
215
|
-
})
|
|
216
|
-
]);
|
|
194
|
+
const cardResult = await createCardMutation.mutateAsync({
|
|
195
|
+
setup_intent_id: intent.setup_intent_id,
|
|
196
|
+
name: data.name,
|
|
197
|
+
set_default: true,
|
|
198
|
+
...isInternational ? {} : {
|
|
199
|
+
financial_document_type: data.personType === "pf" ? 1 : 2,
|
|
200
|
+
financial_document: data.personType === "pf" ? data.cpf : data.cnpj
|
|
201
|
+
},
|
|
202
|
+
financial_name: data.fullName,
|
|
203
|
+
zipcode: data.cep,
|
|
204
|
+
address: data.street,
|
|
205
|
+
address_number: data.streetNumber || "",
|
|
206
|
+
address_complement: data.complement || "",
|
|
207
|
+
neighborhood: data.neighborhood || "",
|
|
208
|
+
city: data.city,
|
|
209
|
+
state: data.state,
|
|
210
|
+
country: data.country
|
|
211
|
+
});
|
|
217
212
|
if (cardResult.success && cardResult.data?.id != null) {
|
|
218
213
|
onCardAdded?.(cardResult.data.id.toString());
|
|
219
214
|
}
|
|
@@ -222,13 +217,14 @@ function AddCardModal() {
|
|
|
222
217
|
{ duration: 5e3 }
|
|
223
218
|
);
|
|
224
219
|
handleClose();
|
|
225
|
-
} catch {
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const backendMessage = error instanceof Error && error.message ? error.message : null;
|
|
226
222
|
toast.custom(
|
|
227
223
|
(t) => /* @__PURE__ */ jsx(
|
|
228
224
|
Toast,
|
|
229
225
|
{
|
|
230
226
|
variant: "error",
|
|
231
|
-
message: translate("common.cards.add.errorSave"),
|
|
227
|
+
message: backendMessage || translate("common.cards.add.errorSave"),
|
|
232
228
|
toastId: t
|
|
233
229
|
}
|
|
234
230
|
),
|
|
@@ -238,11 +234,13 @@ function AddCardModal() {
|
|
|
238
234
|
setIsAuthenticating(false);
|
|
239
235
|
}
|
|
240
236
|
}
|
|
241
|
-
const isSubmitting = createCardMutation.isPending ||
|
|
237
|
+
const isSubmitting = createCardMutation.isPending || form.formState.isSubmitting;
|
|
242
238
|
const watchedValues = form.watch(["name", "country", "personType", "cpf", "cnpj", "fullName", "cep", "street", "streetNumber", "neighborhood", "city", "state"]);
|
|
243
239
|
const [name, country, personType, cpf, cnpj, fullName, cep, street, streetNumber, neighborhood, city, state] = watchedValues;
|
|
244
240
|
const isBillingValid = isInternational ? !!fullName && !!cep && !!street && !!city && !!state : !!fullName && !!cep && cep.length >= 9 && !!street && !!streetNumber && !!neighborhood && !!city && !!state;
|
|
245
|
-
const isCardValid = isInternational ? !!name && !!country && stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc : !!name && !!country && !!personType &&
|
|
241
|
+
const isCardValid = isInternational ? !!name && !!country && stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc : !!name && !!country && !!personType && /* Dígito verificador, em sincronia com o schema (buildCardFormSchema) e com a validação
|
|
242
|
+
* do backend — a regra antiga de tamanho deixava 111.111.111-11 habilitar o botão. */
|
|
243
|
+
(personType === "pf" ? isValidTaxId(cpf, "cpf") : isValidTaxId(cnpj, "cnpj")) && stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc;
|
|
246
244
|
if (isExternalContracting) {
|
|
247
245
|
return /* @__PURE__ */ jsx(Dialog, { open: isOpen, onOpenChange: handleClose, children: /* @__PURE__ */ jsxs(DialogContent, { className: "flex flex-col p-0 gap-0 w-full md:w-[410px] lg:w-[410px]", children: [
|
|
248
246
|
/* @__PURE__ */ jsxs(DialogHeader, { className: "gap-3 text-left p-5", children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/modals/cards/AddCardModal.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useState, useEffect } from 'react';\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/overlay/Dialog';\nimport { Button } from '../../ui/buttons/Button';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';\nimport { useCreateSetupIntent } from '../../../modules/cards/hooks/create-setup-intent.hook';\nimport { useUpdateAccount } from '../../../modules/accounts/hooks/useAccountManagement';\nimport { useCurrentAccount } from '../../../modules/accounts/hooks/current-account.hook';\nimport { useExternalContracting } from '../../../providers/whitelabel.provider';\nimport { CardFormFields, BillingFormFields, buildCardFormSchema } from './CardFormFields';\nimport type { CardFormData, StripeElementsStatus } from './CardFormFields';\nimport { IconX, IconCreditCard, IconCheck, IconChevronRight } from '@tabler/icons-react';\nimport { cn } from '../../../infra/utils/clsx';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js';\nimport { toast } from 'sonner';\nimport { useTranslations } from 'next-intl';\n\nconst INITIAL_STRIPE_STATUS: StripeElementsStatus = {\n cardNumber: false,\n cardExpiry: false,\n cardCvc: false,\n};\n\nfunction CardStepper({ currentStep }: { readonly currentStep: number }) {\n const translate = useTranslations();\n const steps = [\n { number: 1, label: translate('common.cards.add.stepBilling') },\n { number: 2, label: translate('common.cards.add.stepCard') },\n ];\n return (\n <div className=\"flex items-center justify-start gap-2 px-4 py-3 bg-zinc-50 border-b border-zinc-200 lg:border-b-0\">\n {steps.map((step, idx) => {\n const isCompleted = currentStep > step.number;\n const isCurrent = currentStep === step.number;\n const isPending = !isCompleted && !isCurrent;\n return (\n <div key={step.number} className=\"flex items-center gap-2\">\n <div\n className={cn(\n 'flex items-center justify-center size-6 rounded-full paragraph-xsmall-semibold',\n isCompleted && 'bg-green-50 text-green-600 border border-green-200',\n isCurrent && 'bg-primary text-zinc-950',\n isPending && 'border border-zinc-200 text-zinc-400',\n )}\n >\n {isCompleted ? (\n <IconCheck size={14} stroke={3} />\n ) : (\n step.number\n )}\n </div>\n <span\n className={cn(\n 'paragraph-xsmall-semibold',\n (isCompleted || isCurrent) && 'text-zinc-950',\n isPending && 'text-zinc-400',\n )}\n >\n {step.label}\n </span>\n {idx < steps.length - 1 && (\n <IconChevronRight size={16} className=\"text-zinc-300 ml-1\" />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport default function AddCardModal() {\n const translate = useTranslations();\n const { activeModal, modalData, closeModal } = useModalManager();\n const isOpen = activeModal === 'addCardModal';\n const { isExternalContracting, redirectToExternal } = useExternalContracting();\n const { data: account } = useCurrentAccount();\n const isInternational = account?.gateway === 'stripe_usd';\n const [currentStep, setCurrentStep] = useState(1);\n const [stripeStatus, setStripeStatus] = useState<StripeElementsStatus>(INITIAL_STRIPE_STATUS);\n const [stripeErrors, setStripeErrors] = useState<Record<string, string | undefined>>({});\n const [isAuthenticating, setIsAuthenticating] = useState(false);\n\n const onCardAdded = modalData.onCardAdded as ((cardId: string) => void) | undefined;\n\n const createSetupIntentMutation = useCreateSetupIntent();\n const createCardMutation = useCreateCard();\n const updateAccountMutation = useUpdateAccount();\n const elements = useElements();\n const stripe = useStripe();\n\n const form = useForm<CardFormData>({\n resolver: zodResolver(buildCardFormSchema(isInternational)),\n mode: 'onSubmit',\n reValidateMode: 'onChange',\n defaultValues: {\n country: 'BR',\n personType: 'pf',\n },\n });\n\n useEffect(() => {\n if (isInternational) {\n form.setValue('country', 'US');\n }\n }, [isInternational, form]);\n\n const handleClose = useCallback(() => {\n form.reset();\n setCurrentStep(1);\n setStripeStatus(INITIAL_STRIPE_STATUS);\n setStripeErrors({});\n setIsAuthenticating(false);\n closeModal();\n }, [form, closeModal]);\n\n const handleStripeChange = useCallback(\n (field: keyof StripeElementsStatus, complete: boolean) => {\n setStripeStatus((prev) => ({ ...prev, [field]: complete }));\n if (complete) {\n setStripeErrors((prev) => ({ ...prev, [field]: undefined }));\n }\n },\n []\n );\n\n const handleContinue = useCallback(async () => {\n const fields: (keyof CardFormData)[] = isInternational\n ? ['fullName', 'cep', 'street', 'city', 'state']\n : ['fullName', 'cep', 'street', 'streetNumber', 'neighborhood', 'city', 'state'];\n const isValid = await form.trigger(fields);\n if (!isValid) return;\n setCurrentStep(2);\n }, [form, isInternational]);\n\n const handleBack = useCallback(() => {\n setCurrentStep(1);\n }, []);\n\n async function handleSubmit(data: CardFormData) {\n const newStripeErrors: Record<string, string | undefined> = {};\n if (!stripeStatus.cardNumber) newStripeErrors.cardNumber = translate('common.cards.form.cardNumberRequired');\n if (!stripeStatus.cardExpiry) newStripeErrors.cardExpiry = translate('common.cards.form.cardExpiryRequired');\n if (!stripeStatus.cardCvc) newStripeErrors.cardCvc = translate('common.cards.form.cardCvcRequired');\n setStripeErrors(newStripeErrors);\n\n const stripeValid =\n stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc;\n if (!stripeValid) return;\n if (!stripe || !elements) return;\n\n const cardElement = elements.getElement(CardNumberElement);\n if (!cardElement) return;\n\n setIsAuthenticating(true);\n try {\n const intentResult = await createSetupIntentMutation.mutateAsync();\n const intent = intentResult?.data;\n if (!intent?.client_secret || !intent?.setup_intent_id) {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={translate('common.cards.add.errorInitAuth')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n const billingDetails = isInternational\n ? {\n name: data.name,\n address: {\n line1: data.street,\n line2: data.complement || undefined,\n city: data.city,\n state: data.state,\n postal_code: data.cep,\n country: data.country,\n },\n }\n : { name: data.name };\n\n const { setupIntent, error } = await stripe.confirmCardSetup(intent.client_secret, {\n payment_method: {\n card: cardElement,\n billing_details: billingDetails,\n },\n });\n\n if (error) {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={error.message || translate('common.cards.add.errorAuth')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n if (setupIntent?.status !== 'succeeded') {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={translate('common.cards.add.errorAuthStatus', { status: setupIntent?.status ?? translate('common.cards.add.statusUnknown') })}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n const [cardResult] = await Promise.all([\n createCardMutation.mutateAsync({\n setup_intent_id: intent.setup_intent_id,\n name: data.name,\n set_default: true,\n }),\n updateAccountMutation.mutateAsync({\n ...(isInternational\n ? {}\n : {\n financial_document_type: data.personType === 'pf' ? 1 : 2,\n financial_document: data.personType === 'pf' ? data.cpf : data.cnpj,\n }),\n financial_name: data.fullName,\n zipcode: data.cep,\n address: data.street,\n address_number: data.streetNumber || '',\n address_complement: data.complement || '',\n neighborhood: data.neighborhood || '',\n city: data.city,\n state: data.state,\n country: data.country,\n }),\n ]);\n\n if (cardResult.success && cardResult.data?.id != null) {\n onCardAdded?.(cardResult.data.id.toString());\n }\n\n toast.custom(\n (t) => <Toast variant=\"success\" message={translate('common.cards.add.successToast')} toastId={t} />,\n { duration: 5000 }\n );\n handleClose();\n } catch {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={translate('common.cards.add.errorSave')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n } finally {\n setIsAuthenticating(false);\n }\n }\n\n const isSubmitting =\n createCardMutation.isPending ||\n updateAccountMutation.isPending ||\n form.formState.isSubmitting;\n\n const watchedValues = form.watch(['name', 'country', 'personType', 'cpf', 'cnpj', 'fullName', 'cep', 'street', 'streetNumber', 'neighborhood', 'city', 'state']);\n const [name, country, personType, cpf, cnpj, fullName, cep, street, streetNumber, neighborhood, city, state] = watchedValues;\n const isBillingValid = isInternational\n ? !!fullName && !!cep && !!street && !!city && !!state\n : !!fullName &&\n !!cep && cep.length >= 9 &&\n !!street &&\n !!streetNumber &&\n !!neighborhood &&\n !!city &&\n !!state;\n const isCardValid = isInternational\n ? !!name && !!country && stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc\n : !!name &&\n !!country &&\n !!personType &&\n (personType === 'pf' ? !!cpf && cpf.length >= 14 : !!cnpj && cnpj.length >= 18) &&\n stripeStatus.cardNumber &&\n stripeStatus.cardExpiry &&\n stripeStatus.cardCvc;\n\n if (isExternalContracting) {\n return (\n <Dialog open={isOpen} onOpenChange={handleClose}>\n <DialogContent className=\"flex flex-col p-0 gap-0 w-full md:w-[410px] lg:w-[410px]\">\n <DialogHeader className=\"gap-3 text-left p-5\">\n <div className=\"flex items-center justify-center w-10 h-10 bg-zinc-50 rounded-lg\">\n <IconCreditCard size={24} className=\"text-zinc-950\" />\n </div>\n <div className=\"flex flex-col gap-2\">\n <DialogTitle className=\"paragraph-medium-semibold text-zinc-950\">\n {translate('common.cards.add.externalTitle')}\n </DialogTitle>\n <span className=\"paragraph-small-regular text-zinc-600\">\n {translate('common.cards.add.externalDescription')}\n </span>\n </div>\n </DialogHeader>\n <div className=\"flex items-center gap-2 p-5 border-t border-zinc-200\">\n <Button\n className=\"w-fit h-10!\"\n onClick={() => {\n handleClose();\n redirectToExternal('contracting');\n }}\n >\n {translate('common.cards.add.externalButton')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n }\n\n return (\n <Dialog open={isOpen} onOpenChange={handleClose}>\n <DialogContent\n showCloseButton={false}\n className=\"flex flex-col p-0 gap-0 max-w-full sm:max-w-full border-0 rounded-t-2xl rounded-b-none h-dvh top-0 bottom-0 left-0 right-0 translate-x-0 translate-y-0 lg:max-w-[502px] lg:h-auto lg:max-h-[90vh] lg:rounded-lg lg:border lg:top-[50%] lg:left-[50%] lg:right-auto lg:bottom-auto lg:translate-x-[-50%] lg:translate-y-[-50%] overflow-y-auto\"\n >\n <DialogHeader className=\"p-0\">\n <div className=\"flex items-center justify-center px-4 py-3 relative border-b border-zinc-200 lg:border-b-0\">\n <DialogTitle className=\"paragraph-medium-semibold text-zinc-950 text-center\">\n {translate('common.cards.add.title')}\n </DialogTitle>\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"size-8! p-0 absolute right-4\"\n onClick={handleClose}\n >\n <IconX size={18} />\n </Button>\n </div>\n </DialogHeader>\n\n <CardStepper currentStep={currentStep} />\n\n <form\n onSubmit={form.handleSubmit(handleSubmit)}\n className=\"flex flex-col flex-1 lg:flex-none\"\n >\n <div className={`flex flex-col gap-6 lg:gap-8 p-4 lg:p-5 flex-1 lg:flex-none ${currentStep !== 1 ? 'hidden' : ''}`}>\n <h2 className=\"font-[family-name:var(--font-outfit)] text-[18px] leading-tight font-semibold text-zinc-950\">\n {translate('common.cards.add.billingHeading')}\n </h2>\n <BillingFormFields form={form} isInternational={isInternational} />\n </div>\n <div className={`flex flex-col gap-6 lg:gap-8 p-4 lg:p-5 flex-1 lg:flex-none ${currentStep !== 2 ? 'hidden' : ''}`}>\n <h2 className=\"font-[family-name:var(--font-outfit)] text-[18px] leading-tight font-semibold text-zinc-950\">\n {translate('common.cards.add.cardHeading')}\n </h2>\n <CardFormFields\n form={form}\n onStripeChange={handleStripeChange}\n stripeErrors={stripeErrors}\n isInternational={isInternational}\n />\n </div>\n\n <div className=\"flex justify-between items-center p-4 lg:p-5 border-t border-zinc-200 gap-2 mt-auto lg:mt-0\">\n {currentStep === 1 ? (\n <>\n <Button variant=\"secondary\" type=\"button\" onClick={handleClose} className=\"h-10!\">\n {translate('common.actions.cancel')}\n </Button>\n <Button\n type=\"button\"\n className=\"h-10!\"\n onClick={handleContinue}\n disabled={!isBillingValid}\n >\n {translate('common.actions.continue')}\n </Button>\n </>\n ) : (\n <>\n <Button variant=\"secondary\" type=\"button\" onClick={handleBack} className=\"h-10!\">\n {translate('common.actions.back')}\n </Button>\n <Button\n type=\"submit\"\n className=\"h-10!\"\n disabled={isSubmitting || isAuthenticating || !isCardValid}\n loading={isAuthenticating || isSubmitting}\n >\n {isAuthenticating\n ? translate('common.cards.add.authenticating')\n : isSubmitting\n ? translate('common.cards.add.submitting')\n : translate('common.cards.add.submit')}\n </Button>\n </>\n )}\n </div>\n </form>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/modals/cards/AddCardModal.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useState, useEffect } from 'react';\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/overlay/Dialog';\nimport { Button } from '../../ui/buttons/Button';\nimport { Toast } from '../../ui/feedback/Toast';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';\nimport { useCreateSetupIntent } from '../../../modules/cards/hooks/create-setup-intent.hook';\nimport { useCurrentAccount } from '../../../modules/accounts/hooks/current-account.hook';\nimport { isValidTaxId } from '../../../utils/validators/common';\nimport { useExternalContracting } from '../../../providers/whitelabel.provider';\nimport { CardFormFields, BillingFormFields, buildCardFormSchema } from './CardFormFields';\nimport type { CardFormData, StripeElementsStatus } from './CardFormFields';\nimport { IconX, IconCreditCard, IconCheck, IconChevronRight } from '@tabler/icons-react';\nimport { cn } from '../../../infra/utils/clsx';\nimport { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js';\nimport { toast } from 'sonner';\nimport { useTranslations } from 'next-intl';\n\nconst INITIAL_STRIPE_STATUS: StripeElementsStatus = {\n cardNumber: false,\n cardExpiry: false,\n cardCvc: false,\n};\n\nfunction CardStepper({ currentStep }: { readonly currentStep: number }) {\n const translate = useTranslations();\n const steps = [\n { number: 1, label: translate('common.cards.add.stepBilling') },\n { number: 2, label: translate('common.cards.add.stepCard') },\n ];\n return (\n <div className=\"flex items-center justify-start gap-2 px-4 py-3 bg-zinc-50 border-b border-zinc-200 lg:border-b-0\">\n {steps.map((step, idx) => {\n const isCompleted = currentStep > step.number;\n const isCurrent = currentStep === step.number;\n const isPending = !isCompleted && !isCurrent;\n return (\n <div key={step.number} className=\"flex items-center gap-2\">\n <div\n className={cn(\n 'flex items-center justify-center size-6 rounded-full paragraph-xsmall-semibold',\n isCompleted && 'bg-green-50 text-green-600 border border-green-200',\n isCurrent && 'bg-primary text-zinc-950',\n isPending && 'border border-zinc-200 text-zinc-400',\n )}\n >\n {isCompleted ? (\n <IconCheck size={14} stroke={3} />\n ) : (\n step.number\n )}\n </div>\n <span\n className={cn(\n 'paragraph-xsmall-semibold',\n (isCompleted || isCurrent) && 'text-zinc-950',\n isPending && 'text-zinc-400',\n )}\n >\n {step.label}\n </span>\n {idx < steps.length - 1 && (\n <IconChevronRight size={16} className=\"text-zinc-300 ml-1\" />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nexport default function AddCardModal() {\n const translate = useTranslations();\n const { activeModal, modalData, closeModal } = useModalManager();\n const isOpen = activeModal === 'addCardModal';\n const { isExternalContracting, redirectToExternal } = useExternalContracting();\n const { data: account } = useCurrentAccount();\n const isInternational = account?.gateway === 'stripe_usd';\n const [currentStep, setCurrentStep] = useState(1);\n const [stripeStatus, setStripeStatus] = useState<StripeElementsStatus>(INITIAL_STRIPE_STATUS);\n const [stripeErrors, setStripeErrors] = useState<Record<string, string | undefined>>({});\n const [isAuthenticating, setIsAuthenticating] = useState(false);\n\n const onCardAdded = modalData.onCardAdded as ((cardId: string) => void) | undefined;\n\n const createSetupIntentMutation = useCreateSetupIntent();\n const createCardMutation = useCreateCard();\n const elements = useElements();\n const stripe = useStripe();\n\n const form = useForm<CardFormData>({\n resolver: zodResolver(buildCardFormSchema(isInternational)),\n mode: 'onSubmit',\n reValidateMode: 'onChange',\n defaultValues: {\n country: 'BR',\n personType: 'pf',\n },\n });\n\n useEffect(() => {\n if (isInternational) {\n form.setValue('country', 'US');\n }\n }, [isInternational, form]);\n\n const handleClose = useCallback(() => {\n form.reset();\n setCurrentStep(1);\n setStripeStatus(INITIAL_STRIPE_STATUS);\n setStripeErrors({});\n setIsAuthenticating(false);\n closeModal();\n }, [form, closeModal]);\n\n const handleStripeChange = useCallback(\n (field: keyof StripeElementsStatus, complete: boolean) => {\n setStripeStatus((prev) => ({ ...prev, [field]: complete }));\n if (complete) {\n setStripeErrors((prev) => ({ ...prev, [field]: undefined }));\n }\n },\n []\n );\n\n const handleContinue = useCallback(async () => {\n const fields: (keyof CardFormData)[] = isInternational\n ? ['fullName', 'cep', 'street', 'city', 'state']\n : ['fullName', 'cep', 'street', 'streetNumber', 'neighborhood', 'city', 'state'];\n const isValid = await form.trigger(fields);\n if (!isValid) return;\n setCurrentStep(2);\n }, [form, isInternational]);\n\n const handleBack = useCallback(() => {\n setCurrentStep(1);\n }, []);\n\n async function handleSubmit(data: CardFormData) {\n const newStripeErrors: Record<string, string | undefined> = {};\n if (!stripeStatus.cardNumber) newStripeErrors.cardNumber = translate('common.cards.form.cardNumberRequired');\n if (!stripeStatus.cardExpiry) newStripeErrors.cardExpiry = translate('common.cards.form.cardExpiryRequired');\n if (!stripeStatus.cardCvc) newStripeErrors.cardCvc = translate('common.cards.form.cardCvcRequired');\n setStripeErrors(newStripeErrors);\n\n const stripeValid =\n stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc;\n if (!stripeValid) return;\n if (!stripe || !elements) return;\n\n const cardElement = elements.getElement(CardNumberElement);\n if (!cardElement) return;\n\n setIsAuthenticating(true);\n try {\n const intentResult = await createSetupIntentMutation.mutateAsync();\n const intent = intentResult?.data;\n if (!intent?.client_secret || !intent?.setup_intent_id) {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={translate('common.cards.add.errorInitAuth')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n const billingDetails = isInternational\n ? {\n name: data.name,\n address: {\n line1: data.street,\n line2: data.complement || undefined,\n city: data.city,\n state: data.state,\n postal_code: data.cep,\n country: data.country,\n },\n }\n : { name: data.name };\n\n const { setupIntent, error } = await stripe.confirmCardSetup(intent.client_secret, {\n payment_method: {\n card: cardElement,\n billing_details: billingDetails,\n },\n });\n\n if (error) {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={error.message || translate('common.cards.add.errorAuth')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n if (setupIntent?.status !== 'succeeded') {\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={translate('common.cards.add.errorAuthStatus', { status: setupIntent?.status ?? translate('common.cards.add.statusUnknown') })}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n return;\n }\n\n /* Chamada ÚNICA: os dados fiscais vão no mesmo request do cartão. O backend atualiza a\n * conta ANTES de registrar o cartão, fail-closed — fiscal rejeitado (CPF inválido etc.)\n * = cartão não registra e o erro chega com a mensagem específica no catch abaixo.\n * Substitui o Promise.all de createCard + updateAccount, que não tinha garantia entre as\n * pernas: quando o PUT de conta falhava, o cartão ficava salvo e a conta sem NENHUM dado\n * fiscal — e as faturas seguintes falhavam a emissão de nota em silêncio. */\n const cardResult = await createCardMutation.mutateAsync({\n setup_intent_id: intent.setup_intent_id,\n name: data.name,\n set_default: true,\n ...(isInternational\n ? {}\n : {\n financial_document_type: data.personType === 'pf' ? 1 : 2,\n financial_document: data.personType === 'pf' ? data.cpf : data.cnpj,\n }),\n financial_name: data.fullName,\n zipcode: data.cep,\n address: data.street,\n address_number: data.streetNumber || '',\n address_complement: data.complement || '',\n neighborhood: data.neighborhood || '',\n city: data.city,\n state: data.state,\n country: data.country,\n });\n\n if (cardResult.success && cardResult.data?.id != null) {\n onCardAdded?.(cardResult.data.id.toString());\n }\n\n toast.custom(\n (t) => <Toast variant=\"success\" message={translate('common.cards.add.successToast')} toastId={t} />,\n { duration: 5000 }\n );\n handleClose();\n } catch (error) {\n /* O backend fail-closed devolve a razão exata da recusa (ex.: \"CPF do pagador inválido —\n * confira os dígitos verificadores\"), e o withAction relança como Error com essa mensagem.\n * Engolir num texto genérico deixaria o usuário sem saber o que corrigir. */\n const backendMessage = error instanceof Error && error.message ? error.message : null;\n toast.custom(\n (t) => (\n <Toast\n variant=\"error\"\n message={backendMessage || translate('common.cards.add.errorSave')}\n toastId={t}\n />\n ),\n { duration: 5000 }\n );\n } finally {\n setIsAuthenticating(false);\n }\n }\n\n const isSubmitting =\n createCardMutation.isPending ||\n form.formState.isSubmitting;\n\n const watchedValues = form.watch(['name', 'country', 'personType', 'cpf', 'cnpj', 'fullName', 'cep', 'street', 'streetNumber', 'neighborhood', 'city', 'state']);\n const [name, country, personType, cpf, cnpj, fullName, cep, street, streetNumber, neighborhood, city, state] = watchedValues;\n const isBillingValid = isInternational\n ? !!fullName && !!cep && !!street && !!city && !!state\n : !!fullName &&\n !!cep && cep.length >= 9 &&\n !!street &&\n !!streetNumber &&\n !!neighborhood &&\n !!city &&\n !!state;\n const isCardValid = isInternational\n ? !!name && !!country && stripeStatus.cardNumber && stripeStatus.cardExpiry && stripeStatus.cardCvc\n : !!name &&\n !!country &&\n !!personType &&\n /* Dígito verificador, em sincronia com o schema (buildCardFormSchema) e com a validação\n * do backend — a regra antiga de tamanho deixava 111.111.111-11 habilitar o botão. */\n (personType === 'pf' ? isValidTaxId(cpf, 'cpf') : isValidTaxId(cnpj, 'cnpj')) &&\n stripeStatus.cardNumber &&\n stripeStatus.cardExpiry &&\n stripeStatus.cardCvc;\n\n if (isExternalContracting) {\n return (\n <Dialog open={isOpen} onOpenChange={handleClose}>\n <DialogContent className=\"flex flex-col p-0 gap-0 w-full md:w-[410px] lg:w-[410px]\">\n <DialogHeader className=\"gap-3 text-left p-5\">\n <div className=\"flex items-center justify-center w-10 h-10 bg-zinc-50 rounded-lg\">\n <IconCreditCard size={24} className=\"text-zinc-950\" />\n </div>\n <div className=\"flex flex-col gap-2\">\n <DialogTitle className=\"paragraph-medium-semibold text-zinc-950\">\n {translate('common.cards.add.externalTitle')}\n </DialogTitle>\n <span className=\"paragraph-small-regular text-zinc-600\">\n {translate('common.cards.add.externalDescription')}\n </span>\n </div>\n </DialogHeader>\n <div className=\"flex items-center gap-2 p-5 border-t border-zinc-200\">\n <Button\n className=\"w-fit h-10!\"\n onClick={() => {\n handleClose();\n redirectToExternal('contracting');\n }}\n >\n {translate('common.cards.add.externalButton')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n }\n\n return (\n <Dialog open={isOpen} onOpenChange={handleClose}>\n <DialogContent\n showCloseButton={false}\n className=\"flex flex-col p-0 gap-0 max-w-full sm:max-w-full border-0 rounded-t-2xl rounded-b-none h-dvh top-0 bottom-0 left-0 right-0 translate-x-0 translate-y-0 lg:max-w-[502px] lg:h-auto lg:max-h-[90vh] lg:rounded-lg lg:border lg:top-[50%] lg:left-[50%] lg:right-auto lg:bottom-auto lg:translate-x-[-50%] lg:translate-y-[-50%] overflow-y-auto\"\n >\n <DialogHeader className=\"p-0\">\n <div className=\"flex items-center justify-center px-4 py-3 relative border-b border-zinc-200 lg:border-b-0\">\n <DialogTitle className=\"paragraph-medium-semibold text-zinc-950 text-center\">\n {translate('common.cards.add.title')}\n </DialogTitle>\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"size-8! p-0 absolute right-4\"\n onClick={handleClose}\n >\n <IconX size={18} />\n </Button>\n </div>\n </DialogHeader>\n\n <CardStepper currentStep={currentStep} />\n\n <form\n onSubmit={form.handleSubmit(handleSubmit)}\n className=\"flex flex-col flex-1 lg:flex-none\"\n >\n <div className={`flex flex-col gap-6 lg:gap-8 p-4 lg:p-5 flex-1 lg:flex-none ${currentStep !== 1 ? 'hidden' : ''}`}>\n <h2 className=\"font-[family-name:var(--font-outfit)] text-[18px] leading-tight font-semibold text-zinc-950\">\n {translate('common.cards.add.billingHeading')}\n </h2>\n <BillingFormFields form={form} isInternational={isInternational} />\n </div>\n <div className={`flex flex-col gap-6 lg:gap-8 p-4 lg:p-5 flex-1 lg:flex-none ${currentStep !== 2 ? 'hidden' : ''}`}>\n <h2 className=\"font-[family-name:var(--font-outfit)] text-[18px] leading-tight font-semibold text-zinc-950\">\n {translate('common.cards.add.cardHeading')}\n </h2>\n <CardFormFields\n form={form}\n onStripeChange={handleStripeChange}\n stripeErrors={stripeErrors}\n isInternational={isInternational}\n />\n </div>\n\n <div className=\"flex justify-between items-center p-4 lg:p-5 border-t border-zinc-200 gap-2 mt-auto lg:mt-0\">\n {currentStep === 1 ? (\n <>\n <Button variant=\"secondary\" type=\"button\" onClick={handleClose} className=\"h-10!\">\n {translate('common.actions.cancel')}\n </Button>\n <Button\n type=\"button\"\n className=\"h-10!\"\n onClick={handleContinue}\n disabled={!isBillingValid}\n >\n {translate('common.actions.continue')}\n </Button>\n </>\n ) : (\n <>\n <Button variant=\"secondary\" type=\"button\" onClick={handleBack} className=\"h-10!\">\n {translate('common.actions.back')}\n </Button>\n <Button\n type=\"submit\"\n className=\"h-10!\"\n disabled={isSubmitting || isAuthenticating || !isCardValid}\n loading={isAuthenticating || isSubmitting}\n >\n {isAuthenticating\n ? translate('common.cards.add.authenticating')\n : isSubmitting\n ? translate('common.cards.add.submitting')\n : translate('common.cards.add.submit')}\n </Button>\n </>\n )}\n </div>\n </form>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatCNPJ,
|
|
10
10
|
formatPostalCode
|
|
11
11
|
} from "../../../utils/format/masks";
|
|
12
|
+
import { isValidCPF, isValidCNPJ } from "../../../utils/validators/common";
|
|
12
13
|
import z from "zod";
|
|
13
14
|
import {
|
|
14
15
|
CardCvcElement,
|
|
@@ -45,16 +46,16 @@ function buildCardFormSchema(isInternational) {
|
|
|
45
46
|
complement: z.string().optional()
|
|
46
47
|
});
|
|
47
48
|
if (isInternational) return base;
|
|
48
|
-
return base.
|
|
49
|
-
(data)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
return base.superRefine((data, ctx) => {
|
|
50
|
+
const valid = data.personType === "pf" ? isValidCPF(data.cpf) : isValidCNPJ(data.cnpj);
|
|
51
|
+
if (!valid) {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: "custom",
|
|
54
|
+
message: data.personType === "pf" ? "CPF inv\xE1lido \u2014 confira os d\xEDgitos verificadores" : "CNPJ inv\xE1lido \u2014 confira os d\xEDgitos verificadores",
|
|
55
|
+
path: ["cpf"]
|
|
56
|
+
});
|
|
56
57
|
}
|
|
57
|
-
);
|
|
58
|
+
});
|
|
58
59
|
}
|
|
59
60
|
const cardFormSchema = buildCardFormSchema(false);
|
|
60
61
|
const STEP1_FIELDS = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/modals/cards/CardFormFields.tsx"],"sourcesContent":["\"use client\";\n\nimport { UseFormReturn } from \"react-hook-form\";\nimport { useViaCep } from \"../../../modules/accounts/hooks/useViaCep\";\nimport { FormField } from \"../../ui/form/FormField\";\nimport { SelectField } from \"../../ui/form/SelectField\";\nimport { COUNTRIES } from \"../../../utils/countries\";\nimport {\n formatCPF,\n formatCNPJ,\n formatPostalCode,\n} from \"../../../utils/format/masks\";\nimport z from \"zod\";\nimport {\n CardCvcElement,\n CardExpiryElement,\n CardNumberElement,\n} from \"@stripe/react-stripe-js\";\nimport { useTranslations } from \"next-intl\";\n\nconst stripeElementOptions = {\n style: {\n base: {\n fontSize: \"16px\",\n color: \"#030712\",\n fontFamily: \"Inter, sans-serif\",\n \"::placeholder\": {\n color: \"#9ca3af\",\n },\n },\n },\n};\n\nexport function buildCardFormSchema(isInternational: boolean) {\n const base = z.object({\n name: z.string().min(1, \"Nome é obrigatório\"),\n country: z.string().min(1, \"País é obrigatório\"),\n personType: z.enum([\"pf\", \"pj\"]).optional(),\n cpf: z.string().optional(),\n cnpj: z.string().optional(),\n fullName: z.string().min(1, \"Nome completo é obrigatório\"),\n cep: z.string().min(isInternational ? 1 : 9, isInternational ? \"Código postal é obrigatório\" : \"CEP é obrigatório\"),\n street: z.string().min(1, \"Endereço é obrigatório\"),\n streetNumber: isInternational ? z.string().optional() : z.string().min(1, \"Número é obrigatório\"),\n city: z.string().min(1, \"Cidade é obrigatória\"),\n state: z.string().min(1, \"UF é obrigatório\"),\n neighborhood: isInternational ? z.string().optional() : z.string().min(1, \"Bairro é obrigatório\"),\n complement: z.string().optional(),\n });\n if (isInternational) return base;\n return base.refine(\n (data) => {\n if (data.personType === \"pf\") return !!data.cpf && data.cpf.length >= 14;\n return !!data.cnpj && data.cnpj.length >= 18;\n },\n {\n message: \"Documento é obrigatório\",\n path: [\"cpf\"],\n },\n );\n}\n\nexport const cardFormSchema = buildCardFormSchema(false);\n\nexport type CardFormData = z.infer<typeof cardFormSchema>;\n\nexport type StripeElementsStatus = {\n cardNumber: boolean;\n cardExpiry: boolean;\n cardCvc: boolean;\n};\n\nexport const STEP1_FIELDS = [\n \"name\",\n \"country\",\n \"personType\",\n \"cpf\",\n \"cnpj\",\n] as const;\n\ntype CardFormFieldsProps = {\n readonly form: UseFormReturn<CardFormData>;\n readonly onStripeChange: (\n field: keyof StripeElementsStatus,\n complete: boolean,\n ) => void;\n readonly stripeErrors: {\n cardNumber?: string;\n cardExpiry?: string;\n cardCvc?: string;\n };\n readonly isInternational?: boolean;\n};\n\nexport function CardFormFields({\n form,\n onStripeChange,\n stripeErrors,\n isInternational = false,\n}: CardFormFieldsProps) {\n const translate = useTranslations();\n const {\n register,\n watch,\n setValue,\n formState: { errors },\n } = form;\n\n const personType = watch(\"personType\");\n\n return (\n <div className=\"flex flex-col gap-5\">\n <FormField\n label={translate('common.cards.form.nameOnCard')}\n placeholder={translate('common.cards.form.typeHere')}\n {...register(\"name\")}\n error={!!errors.name}\n errorMessage={errors.name?.message}\n />\n\n <div className=\"flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">\n {translate('common.cards.form.cardNumber')}\n </label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400 relative\">\n <div className=\"w-full\">\n <CardNumberElement\n options={{ ...stripeElementOptions, showIcon: true }}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardNumber\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardNumber && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardNumber}\n </p>\n )}\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1 flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">\n {translate('common.cards.form.cardExpiry')}\n </label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400\">\n <div className=\"w-full\">\n <CardExpiryElement\n options={stripeElementOptions}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardExpiry\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardExpiry && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardExpiry}\n </p>\n )}\n </div>\n\n <div className=\"flex-1 flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">{translate('common.cards.form.cardCvc')}</label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400\">\n <div className=\"w-full\">\n <CardCvcElement\n options={stripeElementOptions}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardCvc\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardCvc && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardCvc}\n </p>\n )}\n </div>\n </div>\n\n <SelectField\n label={translate('common.cards.form.country')}\n placeholder={translate('common.cards.form.select')}\n value={watch(\"country\")}\n onChange={(value) => setValue(\"country\", value as string)}\n options={COUNTRIES.map((c) => ({ value: c.iso, label: c.name }))}\n />\n\n {!isInternational && (\n <div className=\"flex gap-4\">\n <SelectField\n label={translate('common.cards.form.personType')}\n placeholder={translate('common.cards.form.select')}\n value={personType}\n onChange={(value) => {\n setValue(\"personType\", value as \"pf\" | \"pj\");\n setValue(\"cpf\", \"\");\n setValue(\"cnpj\", \"\");\n }}\n options={[\n { value: \"pf\", label: translate('common.cards.form.personTypePf') },\n { value: \"pj\", label: translate('common.cards.form.personTypePj') },\n ]}\n containerClassName=\"flex-1\"\n />\n <div className=\"flex-1\">\n {personType === \"pj\" ? (\n <FormField\n label={translate('common.cards.form.cnpj')}\n placeholder=\"__.___.___/____-__\"\n {...register(\"cnpj\", {\n onChange: (e) => {\n setValue(\"cnpj\", formatCNPJ(e.target.value));\n },\n })}\n error={!!errors.cpf}\n errorMessage={errors.cpf?.message}\n />\n ) : (\n <FormField\n label={translate('common.cards.form.cpf')}\n placeholder=\"___.___.___-__\"\n {...register(\"cpf\", {\n onChange: (e) => {\n setValue(\"cpf\", formatCPF(e.target.value));\n },\n })}\n error={!!errors.cpf}\n errorMessage={errors.cpf?.message}\n />\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\ntype BillingFormFieldsProps = {\n readonly form: UseFormReturn<CardFormData>;\n readonly isInternational?: boolean;\n};\n\nexport function BillingFormFields({ form, isInternational = false }: BillingFormFieldsProps) {\n const translate = useTranslations();\n const {\n register,\n setValue,\n formState: { errors },\n } = form;\n\n const { mutateAsync: lookupCep } = useViaCep();\n\n const handleCepChange = (rawValue: string) => {\n if (isInternational) {\n setValue(\"cep\", rawValue);\n return;\n }\n const masked = formatPostalCode(rawValue);\n setValue(\"cep\", masked);\n\n const digits = rawValue.replace(/\\D/g, \"\");\n if (digits.length === 8) {\n lookupCep(digits)\n .then((data) => {\n setValue(\"street\", data.logradouro, { shouldValidate: true });\n setValue(\"neighborhood\", data.bairro, { shouldValidate: true });\n setValue(\"city\", data.localidade, { shouldValidate: true });\n setValue(\"state\", data.uf, { shouldValidate: true });\n })\n .catch(() => {\n // silently ignore — user can fill manually\n });\n }\n };\n\n return (\n <div className=\"flex flex-col gap-5\">\n <FormField\n label={translate('common.cards.form.fullName')}\n placeholder={translate('common.cards.form.typeHere')}\n {...register(\"fullName\")}\n error={!!errors.fullName}\n errorMessage={errors.fullName?.message}\n />\n\n <FormField\n label={isInternational ? translate('common.cards.form.postalCode') : translate('common.cards.form.cep')}\n placeholder={isInternational ? \"\" : \"_____-___\"}\n {...register(\"cep\", {\n onChange: (e) => handleCepChange(e.target.value),\n })}\n error={!!errors.cep}\n errorMessage={errors.cep?.message}\n />\n\n <div className=\"flex gap-4\">\n <div className=\"flex-[4]\">\n <FormField\n label={translate('common.cards.form.street')}\n placeholder=\"\"\n {...register(\"street\")}\n error={!!errors.street}\n errorMessage={errors.street?.message}\n />\n </div>\n <div className=\"flex-[1]\">\n <FormField\n label={translate('common.cards.form.streetNumber')}\n placeholder=\"\"\n {...register(\"streetNumber\")}\n error={!!errors.streetNumber}\n errorMessage={errors.streetNumber?.message}\n />\n </div>\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1\">\n <FormField\n label={translate('common.cards.form.city')}\n placeholder=\"\"\n {...register(\"city\")}\n error={!!errors.city}\n errorMessage={errors.city?.message}\n />\n </div>\n <div className=\"flex-1\">\n <FormField\n label={isInternational ? translate('common.cards.form.stateProvince') : translate('common.cards.form.state')}\n placeholder=\"\"\n {...register(\"state\")}\n error={!!errors.state}\n errorMessage={errors.state?.message}\n />\n </div>\n </div>\n\n <div className=\"flex gap-4\">\n {!isInternational && (\n <FormField\n label={translate('common.cards.form.neighborhood')}\n placeholder=\"\"\n {...register(\"neighborhood\")}\n error={!!errors.neighborhood}\n errorMessage={errors.neighborhood?.message}\n />\n )}\n <FormField\n label={translate('common.cards.form.complement')}\n optional\n placeholder=\"\"\n {...register(\"complement\")}\n error={!!errors.complement}\n errorMessage={errors.complement?.message}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/modals/cards/CardFormFields.tsx"],"sourcesContent":["\"use client\";\n\nimport { UseFormReturn } from \"react-hook-form\";\nimport { useViaCep } from \"../../../modules/accounts/hooks/useViaCep\";\nimport { FormField } from \"../../ui/form/FormField\";\nimport { SelectField } from \"../../ui/form/SelectField\";\nimport { COUNTRIES } from \"../../../utils/countries\";\nimport {\n formatCPF,\n formatCNPJ,\n formatPostalCode,\n} from \"../../../utils/format/masks\";\nimport { isValidCPF, isValidCNPJ } from \"../../../utils/validators/common\";\nimport z from \"zod\";\nimport {\n CardCvcElement,\n CardExpiryElement,\n CardNumberElement,\n} from \"@stripe/react-stripe-js\";\nimport { useTranslations } from \"next-intl\";\n\nconst stripeElementOptions = {\n style: {\n base: {\n fontSize: \"16px\",\n color: \"#030712\",\n fontFamily: \"Inter, sans-serif\",\n \"::placeholder\": {\n color: \"#9ca3af\",\n },\n },\n },\n};\n\nexport function buildCardFormSchema(isInternational: boolean) {\n const base = z.object({\n name: z.string().min(1, \"Nome é obrigatório\"),\n country: z.string().min(1, \"País é obrigatório\"),\n personType: z.enum([\"pf\", \"pj\"]).optional(),\n cpf: z.string().optional(),\n cnpj: z.string().optional(),\n fullName: z.string().min(1, \"Nome completo é obrigatório\"),\n cep: z.string().min(isInternational ? 1 : 9, isInternational ? \"Código postal é obrigatório\" : \"CEP é obrigatório\"),\n street: z.string().min(1, \"Endereço é obrigatório\"),\n streetNumber: isInternational ? z.string().optional() : z.string().min(1, \"Número é obrigatório\"),\n city: z.string().min(1, \"Cidade é obrigatória\"),\n state: z.string().min(1, \"UF é obrigatório\"),\n neighborhood: isInternational ? z.string().optional() : z.string().min(1, \"Bairro é obrigatório\"),\n complement: z.string().optional(),\n });\n if (isInternational) return base;\n /* Dígito verificador (mesmo algoritmo do backend, ver utils/validators/common): a regra\n * antiga era só tamanho da string mascarada — 111.111.111-11 passava aqui e estourava\n * depois no servidor, que rejeita o cadastro inteiro. path fixo em \"cpf\" de propósito:\n * o campo de CNPJ lê errors.cpf no render. */\n return base.superRefine((data, ctx) => {\n const valid = data.personType === \"pf\" ? isValidCPF(data.cpf) : isValidCNPJ(data.cnpj);\n if (!valid) {\n ctx.addIssue({\n code: \"custom\",\n message:\n data.personType === \"pf\"\n ? \"CPF inválido — confira os dígitos verificadores\"\n : \"CNPJ inválido — confira os dígitos verificadores\",\n path: [\"cpf\"],\n });\n }\n });\n}\n\nexport const cardFormSchema = buildCardFormSchema(false);\n\nexport type CardFormData = z.infer<typeof cardFormSchema>;\n\nexport type StripeElementsStatus = {\n cardNumber: boolean;\n cardExpiry: boolean;\n cardCvc: boolean;\n};\n\nexport const STEP1_FIELDS = [\n \"name\",\n \"country\",\n \"personType\",\n \"cpf\",\n \"cnpj\",\n] as const;\n\ntype CardFormFieldsProps = {\n readonly form: UseFormReturn<CardFormData>;\n readonly onStripeChange: (\n field: keyof StripeElementsStatus,\n complete: boolean,\n ) => void;\n readonly stripeErrors: {\n cardNumber?: string;\n cardExpiry?: string;\n cardCvc?: string;\n };\n readonly isInternational?: boolean;\n};\n\nexport function CardFormFields({\n form,\n onStripeChange,\n stripeErrors,\n isInternational = false,\n}: CardFormFieldsProps) {\n const translate = useTranslations();\n const {\n register,\n watch,\n setValue,\n formState: { errors },\n } = form;\n\n const personType = watch(\"personType\");\n\n return (\n <div className=\"flex flex-col gap-5\">\n <FormField\n label={translate('common.cards.form.nameOnCard')}\n placeholder={translate('common.cards.form.typeHere')}\n {...register(\"name\")}\n error={!!errors.name}\n errorMessage={errors.name?.message}\n />\n\n <div className=\"flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">\n {translate('common.cards.form.cardNumber')}\n </label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400 relative\">\n <div className=\"w-full\">\n <CardNumberElement\n options={{ ...stripeElementOptions, showIcon: true }}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardNumber\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardNumber && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardNumber}\n </p>\n )}\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1 flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">\n {translate('common.cards.form.cardExpiry')}\n </label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400\">\n <div className=\"w-full\">\n <CardExpiryElement\n options={stripeElementOptions}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardExpiry\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardExpiry && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardExpiry}\n </p>\n )}\n </div>\n\n <div className=\"flex-1 flex flex-col gap-1.5\">\n <label className=\"paragraph-xsmall-semibold text-zinc-600\">{translate('common.cards.form.cardCvc')}</label>\n <div className=\"h-10 px-3 flex items-center w-full border border-zinc-200 rounded-lg bg-white transition-colors focus-within:border-zinc-400\">\n <div className=\"w-full\">\n <CardCvcElement\n options={stripeElementOptions}\n onChange={(e: { complete: boolean }) =>\n onStripeChange(\"cardCvc\", e.complete)\n }\n />\n </div>\n </div>\n {stripeErrors.cardCvc && (\n <p className=\"paragraph-xsmall-semibold text-red-600\">\n {stripeErrors.cardCvc}\n </p>\n )}\n </div>\n </div>\n\n <SelectField\n label={translate('common.cards.form.country')}\n placeholder={translate('common.cards.form.select')}\n value={watch(\"country\")}\n onChange={(value) => setValue(\"country\", value as string)}\n options={COUNTRIES.map((c) => ({ value: c.iso, label: c.name }))}\n />\n\n {!isInternational && (\n <div className=\"flex gap-4\">\n <SelectField\n label={translate('common.cards.form.personType')}\n placeholder={translate('common.cards.form.select')}\n value={personType}\n onChange={(value) => {\n setValue(\"personType\", value as \"pf\" | \"pj\");\n setValue(\"cpf\", \"\");\n setValue(\"cnpj\", \"\");\n }}\n options={[\n { value: \"pf\", label: translate('common.cards.form.personTypePf') },\n { value: \"pj\", label: translate('common.cards.form.personTypePj') },\n ]}\n containerClassName=\"flex-1\"\n />\n <div className=\"flex-1\">\n {personType === \"pj\" ? (\n <FormField\n label={translate('common.cards.form.cnpj')}\n placeholder=\"__.___.___/____-__\"\n {...register(\"cnpj\", {\n onChange: (e) => {\n setValue(\"cnpj\", formatCNPJ(e.target.value));\n },\n })}\n error={!!errors.cpf}\n errorMessage={errors.cpf?.message}\n />\n ) : (\n <FormField\n label={translate('common.cards.form.cpf')}\n placeholder=\"___.___.___-__\"\n {...register(\"cpf\", {\n onChange: (e) => {\n setValue(\"cpf\", formatCPF(e.target.value));\n },\n })}\n error={!!errors.cpf}\n errorMessage={errors.cpf?.message}\n />\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\ntype BillingFormFieldsProps = {\n readonly form: UseFormReturn<CardFormData>;\n readonly isInternational?: boolean;\n};\n\nexport function BillingFormFields({ form, isInternational = false }: BillingFormFieldsProps) {\n const translate = useTranslations();\n const {\n register,\n setValue,\n formState: { errors },\n } = form;\n\n const { mutateAsync: lookupCep } = useViaCep();\n\n const handleCepChange = (rawValue: string) => {\n if (isInternational) {\n setValue(\"cep\", rawValue);\n return;\n }\n const masked = formatPostalCode(rawValue);\n setValue(\"cep\", masked);\n\n const digits = rawValue.replace(/\\D/g, \"\");\n if (digits.length === 8) {\n lookupCep(digits)\n .then((data) => {\n setValue(\"street\", data.logradouro, { shouldValidate: true });\n setValue(\"neighborhood\", data.bairro, { shouldValidate: true });\n setValue(\"city\", data.localidade, { shouldValidate: true });\n setValue(\"state\", data.uf, { shouldValidate: true });\n })\n .catch(() => {\n // silently ignore — user can fill manually\n });\n }\n };\n\n return (\n <div className=\"flex flex-col gap-5\">\n <FormField\n label={translate('common.cards.form.fullName')}\n placeholder={translate('common.cards.form.typeHere')}\n {...register(\"fullName\")}\n error={!!errors.fullName}\n errorMessage={errors.fullName?.message}\n />\n\n <FormField\n label={isInternational ? translate('common.cards.form.postalCode') : translate('common.cards.form.cep')}\n placeholder={isInternational ? \"\" : \"_____-___\"}\n {...register(\"cep\", {\n onChange: (e) => handleCepChange(e.target.value),\n })}\n error={!!errors.cep}\n errorMessage={errors.cep?.message}\n />\n\n <div className=\"flex gap-4\">\n <div className=\"flex-[4]\">\n <FormField\n label={translate('common.cards.form.street')}\n placeholder=\"\"\n {...register(\"street\")}\n error={!!errors.street}\n errorMessage={errors.street?.message}\n />\n </div>\n <div className=\"flex-[1]\">\n <FormField\n label={translate('common.cards.form.streetNumber')}\n placeholder=\"\"\n {...register(\"streetNumber\")}\n error={!!errors.streetNumber}\n errorMessage={errors.streetNumber?.message}\n />\n </div>\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1\">\n <FormField\n label={translate('common.cards.form.city')}\n placeholder=\"\"\n {...register(\"city\")}\n error={!!errors.city}\n errorMessage={errors.city?.message}\n />\n </div>\n <div className=\"flex-1\">\n <FormField\n label={isInternational ? translate('common.cards.form.stateProvince') : translate('common.cards.form.state')}\n placeholder=\"\"\n {...register(\"state\")}\n error={!!errors.state}\n errorMessage={errors.state?.message}\n />\n </div>\n </div>\n\n <div className=\"flex gap-4\">\n {!isInternational && (\n <FormField\n label={translate('common.cards.form.neighborhood')}\n placeholder=\"\"\n {...register(\"neighborhood\")}\n error={!!errors.neighborhood}\n errorMessage={errors.neighborhood?.message}\n />\n )}\n <FormField\n label={translate('common.cards.form.complement')}\n optional\n placeholder=\"\"\n {...register(\"complement\")}\n error={!!errors.complement}\n errorMessage={errors.complement?.message}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,13 +3,14 @@ import { cookies, headers } from "next/headers";
|
|
|
3
3
|
import { DEFAULT_LOCALE, LOCALE_COOKIE } from "./config";
|
|
4
4
|
import { localeFromCountry } from "./country-language";
|
|
5
5
|
import { normalizeLocale } from "./normalize";
|
|
6
|
+
import { decodeJwtPayload } from "../utils/jwt-payload";
|
|
6
7
|
const AUTH_COOKIE = "greatapps";
|
|
7
8
|
const GEO_HEADER = "cf-ipcountry";
|
|
8
9
|
const DEFAULT_WHITELABEL_ID = 1;
|
|
9
10
|
function decodeJwtLocaleInfo(token) {
|
|
10
11
|
if (!token) return null;
|
|
11
12
|
try {
|
|
12
|
-
const payload =
|
|
13
|
+
const payload = decodeJwtPayload(token);
|
|
13
14
|
return {
|
|
14
15
|
id_wl: payload?.id_wl != null ? Number(payload.id_wl) : void 0,
|
|
15
16
|
country: payload?.session?.location?.country ?? null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/i18n/resolve-locale.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies, headers } from 'next/headers';\nimport { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';\nimport { localeFromCountry } from './country-language';\nimport { normalizeLocale } from './normalize';\n\nconst AUTH_COOKIE = 'greatapps';\nconst GEO_HEADER = 'cf-ipcountry';\nconst DEFAULT_WHITELABEL_ID = 1;\n\ninterface JwtLocaleInfo {\n id_wl?: number;\n country?: string | null;\n}\n\n/** Lê id_wl e país do JWT sem custo de rede (best-effort). */\nfunction decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {\n if (!token) return null;\n try {\n const payload =
|
|
1
|
+
{"version":3,"sources":["../../src/i18n/resolve-locale.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies, headers } from 'next/headers';\nimport { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';\nimport { localeFromCountry } from './country-language';\nimport { normalizeLocale } from './normalize';\nimport { decodeJwtPayload } from '../utils/jwt-payload';\n\nconst AUTH_COOKIE = 'greatapps';\nconst GEO_HEADER = 'cf-ipcountry';\nconst DEFAULT_WHITELABEL_ID = 1;\n\ninterface JwtLocaleInfo {\n id_wl?: number;\n country?: string | null;\n}\n\n/** Lê id_wl e país do JWT sem custo de rede (best-effort). */\nfunction decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {\n if (!token) return null;\n try {\n const payload = decodeJwtPayload<{ id_wl?: number | string; session?: { location?: { country?: string | null } } }>(token);\n return {\n id_wl: payload?.id_wl != null ? Number(payload.id_wl) : undefined,\n country: payload?.session?.location?.country ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport interface ResolveLocaleOptions {\n /**\n * Whitelabel id já resolvido (ex.: via `findWhitelabel()`), com prioridade sobre o JWT.\n * Permite detectar domínio whitelabel mesmo para visitante não-logado.\n */\n whitelabelId?: number | null;\n /** Idioma da conta (`user.language`), quando já conhecido pelo chamador. */\n userLanguage?: string | null;\n}\n\n/**\n * Resolve o locale da requisição (server-side). Ordem de prioridade:\n *\n * 1. **Whitelabel não-padrão** (`id_wl != 1`) → `pt` (por enquanto, whitelabels não são localizadas).\n * 2. **Preferência da conta** (`userLanguage`), quando fornecida.\n * 3. **Cookie `NEXT_LOCALE`** — cache da preferência ou escolha manual do visitante.\n * 4. **Inferência por país** — JWT (`session.location.country`) ou header `CF-IPCountry`.\n * 5. **Fallback** → {@link DEFAULT_LOCALE}.\n */\nexport async function resolveLocale(options: ResolveLocaleOptions = {}): Promise<Locale> {\n const cookieStore = await cookies();\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE)?.value;\n const jwt = decodeJwtLocaleInfo(token);\n\n // 1. Whitelabel não-padrão → português\n const whitelabelId = options.whitelabelId ?? jwt?.id_wl ?? null;\n if (whitelabelId != null && whitelabelId !== DEFAULT_WHITELABEL_ID) {\n return 'pt-br';\n }\n\n // 2. Preferência explícita da conta\n const fromUser = normalizeLocale(options.userLanguage);\n if (fromUser) return fromUser;\n\n // 3. Cookie persistido\n const fromCookie = normalizeLocale(cookieStore.get(LOCALE_COOKIE)?.value);\n if (fromCookie) return fromCookie;\n\n // 4. Inferência por país (geo)\n const headersList = await headers();\n const country = jwt?.country ?? headersList.get(GEO_HEADER);\n const fromGeo = localeFromCountry(country);\n if (fromGeo) return fromGeo;\n\n // 5. Fallback\n return DEFAULT_LOCALE;\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,SAAS,eAAe;AACjC,SAAS,gBAAwB,qBAAqB;AACtD,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AAEjC,MAAM,cAAc;AACpB,MAAM,aAAa;AACnB,MAAM,wBAAwB;AAQ9B,SAAS,oBAAoB,OAA6C;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,iBAAoG,KAAK;AACzH,WAAO;AAAA,MACL,OAAO,SAAS,SAAS,OAAO,OAAO,QAAQ,KAAK,IAAI;AAAA,MACxD,SAAS,SAAS,SAAS,UAAU,WAAW;AAAA,IAClD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqBA,eAAsB,cAAc,UAAgC,CAAC,GAAoB;AACvF,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,WAAW,GAAG;AAC5E,QAAM,MAAM,oBAAoB,KAAK;AAGrC,QAAM,eAAe,QAAQ,gBAAgB,KAAK,SAAS;AAC3D,MAAI,gBAAgB,QAAQ,iBAAiB,uBAAuB;AAClE,WAAO;AAAA,EACT;AAGA,QAAM,WAAW,gBAAgB,QAAQ,YAAY;AACrD,MAAI,SAAU,QAAO;AAGrB,QAAM,aAAa,gBAAgB,YAAY,IAAI,aAAa,GAAG,KAAK;AACxE,MAAI,WAAY,QAAO;AAGvB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,UAAU,KAAK,WAAW,YAAY,IAAI,UAAU;AAC1D,QAAM,UAAU,kBAAkB,OAAO;AACzC,MAAI,QAAS,QAAO;AAGpB,SAAO;AACT;","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { decodeJwtPayload } from "../../../utils/jwt-payload";
|
|
2
3
|
import { findWhitelabel } from "../../whitelabel/actions/find-whitelabel.action";
|
|
3
4
|
import { whitelabelService } from "../../whitelabel/services/whitelabel.service";
|
|
4
5
|
async function buildWlOverride(jwtWlId) {
|
|
@@ -14,7 +15,7 @@ async function buildWlOverride(jwtWlId) {
|
|
|
14
15
|
}
|
|
15
16
|
async function buildWlOverrideFromJwt(cookie) {
|
|
16
17
|
try {
|
|
17
|
-
const payload =
|
|
18
|
+
const payload = decodeJwtPayload(cookie);
|
|
18
19
|
return buildWlOverride(Number(payload.id_wl));
|
|
19
20
|
} catch {
|
|
20
21
|
return void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/build-wl-override.ts"],"sourcesContent":["import 'server-only';\n\nimport type { RequestConfig } from '../../../infra/api/client';\nimport type { JWTPayload } from '../../users/schema';\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\nimport { whitelabelService } from '../../whitelabel/services/whitelabel.service';\n\n/**\n * Compara um `id_wl` com o whitelabel resolvido pelo hostname.\n * Se forem diferentes (ex: admin com id_wl=1 num domínio de id_wl=30),\n * retorna um RequestConfig com o override correto.\n * Se forem iguais, retorna undefined (sem custo extra).\n */\nexport async function buildWlOverride(\n jwtWlId: number,\n): Promise<RequestConfig | undefined> {\n try {\n const { id: hostWlId } = await findWhitelabel();\n\n if (jwtWlId !== hostWlId) {\n const wlToken = await whitelabelService.getTokenByWhitelabelId(jwtWlId);\n return { whiteLabelId: jwtWlId, authToken: wlToken };\n }\n } catch {\n // Fallback to default whitelabel resolution\n }\n\n return undefined;\n}\n\n/**\n * Extrai `id_wl` do JWT cookie e delega para `buildWlOverride`.\n */\nexport async function buildWlOverrideFromJwt(\n cookie: string,\n): Promise<RequestConfig | undefined> {\n try {\n const payload =
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/build-wl-override.ts"],"sourcesContent":["import 'server-only';\n\nimport type { RequestConfig } from '../../../infra/api/client';\nimport type { JWTPayload } from '../../users/schema';\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\nimport { whitelabelService } from '../../whitelabel/services/whitelabel.service';\n\n/**\n * Compara um `id_wl` com o whitelabel resolvido pelo hostname.\n * Se forem diferentes (ex: admin com id_wl=1 num domínio de id_wl=30),\n * retorna um RequestConfig com o override correto.\n * Se forem iguais, retorna undefined (sem custo extra).\n */\nexport async function buildWlOverride(\n jwtWlId: number,\n): Promise<RequestConfig | undefined> {\n try {\n const { id: hostWlId } = await findWhitelabel();\n\n if (jwtWlId !== hostWlId) {\n const wlToken = await whitelabelService.getTokenByWhitelabelId(jwtWlId);\n return { whiteLabelId: jwtWlId, authToken: wlToken };\n }\n } catch {\n // Fallback to default whitelabel resolution\n }\n\n return undefined;\n}\n\n/**\n * Extrai `id_wl` do JWT cookie e delega para `buildWlOverride`.\n */\nexport async function buildWlOverrideFromJwt(\n cookie: string,\n): Promise<RequestConfig | undefined> {\n try {\n const payload = decodeJwtPayload<JWTPayload>(cookie);\n return buildWlOverride(Number(payload.id_wl));\n } catch {\n return undefined;\n }\n}\n"],"mappings":"AAAA,OAAO;AAIP,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAQlC,eAAsB,gBACpB,SACoC;AACpC,MAAI;AACF,UAAM,EAAE,IAAI,SAAS,IAAI,MAAM,eAAe;AAE9C,QAAI,YAAY,UAAU;AACxB,YAAM,UAAU,MAAM,kBAAkB,uBAAuB,OAAO;AACtE,aAAO,EAAE,cAAc,SAAS,WAAW,QAAQ;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAKA,eAAsB,uBACpB,QACoC;AACpC,MAAI;AACF,UAAM,UAAU,iBAA6B,MAAM;AACnD,WAAO,gBAAgB,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import { cookies } from "next/headers";
|
|
3
3
|
import { ApiError } from "../../../infra/api/types";
|
|
4
|
+
import { decodeJwtPayload } from "../../../utils/jwt-payload";
|
|
4
5
|
const AUTH_COOKIE_NAME = "greatapps";
|
|
5
6
|
async function getUserContext() {
|
|
6
7
|
const cookieStore = await cookies();
|
|
@@ -9,7 +10,7 @@ async function getUserContext() {
|
|
|
9
10
|
throw new ApiError("Usu\xE1rio n\xE3o autenticado", "NOT_AUTHENTICATED_GUC", 401);
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
12
|
-
const payload =
|
|
13
|
+
const payload = decodeJwtPayload(token);
|
|
13
14
|
return {
|
|
14
15
|
id_wl: Number(payload.id_wl),
|
|
15
16
|
id_account: payload.id_account,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/get-user-context.ts"],"sourcesContent":["import 'server-only';\r\n\r\nimport { cookies } from 'next/headers';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { JWTPayload } from '../../users/schema';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\n\r\nexport async function getUserContext(): Promise<{\r\n id_wl: number;\r\n id_account: number;\r\n id_user: number;\r\n}> {\r\n const cookieStore = await cookies();\r\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED_GUC', 401);\r\n }\r\n\r\n try {\r\n const payload =
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/get-user-context.ts"],"sourcesContent":["import 'server-only';\r\n\r\nimport { cookies } from 'next/headers';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { JWTPayload } from '../../users/schema';\r\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\n\r\nexport async function getUserContext(): Promise<{\r\n id_wl: number;\r\n id_account: number;\r\n id_user: number;\r\n}> {\r\n const cookieStore = await cookies();\r\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED_GUC', 401);\r\n }\r\n\r\n try {\r\n const payload = decodeJwtPayload<JWTPayload>(token);\r\n\r\n return {\r\n id_wl: Number(payload.id_wl),\r\n id_account: payload.id_account,\r\n id_user: payload.id_user,\r\n };\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n}\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB;AAEjC,MAAM,mBAAmB;AAEzB,eAAsB,iBAInB;AACD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,gBAAgB,GAAG;AAEjF,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,iCAA2B,yBAAyB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,iBAA6B,KAAK;AAElD,WAAO;AAAA,MACL,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,EAC3D;AACF;","names":[]}
|
|
@@ -2,6 +2,7 @@ import "server-only";
|
|
|
2
2
|
import { cookies } from "next/headers";
|
|
3
3
|
import { decode } from "@greatapps/jwt";
|
|
4
4
|
import { ApiError } from "../../../infra/api/types";
|
|
5
|
+
import { decodeJwtPayload } from "../../../utils/jwt-payload";
|
|
5
6
|
const AUTH_COOKIE_NAME = "greatapps";
|
|
6
7
|
async function requireTokenNotExpired() {
|
|
7
8
|
if (process.env.DUMMY_AUTH_TOKEN) return;
|
|
@@ -13,7 +14,7 @@ async function requireTokenNotExpired() {
|
|
|
13
14
|
throw new ApiError("Sess\xE3o inv\xE1lida ou expirada 1", "SESSION_INVALID", 401);
|
|
14
15
|
}
|
|
15
16
|
try {
|
|
16
|
-
const payload =
|
|
17
|
+
const payload = decodeJwtPayload(token);
|
|
17
18
|
const idWl = payload.id_wl;
|
|
18
19
|
if (!idWl) {
|
|
19
20
|
throw new ApiError("Sess\xE3o inv\xE1lida ou expirada 2", "SESSION_INVALID", 401);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/auth/utils/require-token-not-expired.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies } from 'next/headers';\nimport { decode } from '@greatapps/jwt';\nimport { ApiError } from '../../../infra/api/types';\n\nconst AUTH_COOKIE_NAME = 'greatapps';\n\n/**\n * Verifica se o JWT do cookie `greatapps` é válido (assinatura + expiração)\n * sem bater no backend.\n *\n * Usa `@greatapps/jwt` decode que:\n * 1. Valida a assinatura HMAC com `SECRET_JWT---{id_wl}`\n * 2. Checa `exp` automaticamente (rejeita se expirou)\n *\n * - Se o token não existe → SESSION_INVALID 401\n * - Se a assinatura é inválida → SESSION_INVALID 401\n * - Se `exp` já passou → SESSION_INVALID 401\n * - Se `SECRET_JWT` não está configurado → skip (não bloqueia)\n */\nexport async function requireTokenNotExpired(): Promise<void> {\n if (process.env.DUMMY_AUTH_TOKEN) return;\n\n const secretJwt = process.env.SECRET_JWT;\n if (!secretJwt) return;\n\n const cookieStore = await cookies();\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\n\n if (!token) {\n throw new ApiError('Sessão inválida ou expirada 1', 'SESSION_INVALID', 401);\n }\n\n try {\n const payload =
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/auth/utils/require-token-not-expired.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies } from 'next/headers';\nimport { decode } from '@greatapps/jwt';\nimport { ApiError } from '../../../infra/api/types';\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\n\nconst AUTH_COOKIE_NAME = 'greatapps';\n\n/**\n * Verifica se o JWT do cookie `greatapps` é válido (assinatura + expiração)\n * sem bater no backend.\n *\n * Usa `@greatapps/jwt` decode que:\n * 1. Valida a assinatura HMAC com `SECRET_JWT---{id_wl}`\n * 2. Checa `exp` automaticamente (rejeita se expirou)\n *\n * - Se o token não existe → SESSION_INVALID 401\n * - Se a assinatura é inválida → SESSION_INVALID 401\n * - Se `exp` já passou → SESSION_INVALID 401\n * - Se `SECRET_JWT` não está configurado → skip (não bloqueia)\n */\nexport async function requireTokenNotExpired(): Promise<void> {\n if (process.env.DUMMY_AUTH_TOKEN) return;\n\n const secretJwt = process.env.SECRET_JWT;\n if (!secretJwt) return;\n\n const cookieStore = await cookies();\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\n\n if (!token) {\n throw new ApiError('Sessão inválida ou expirada 1', 'SESSION_INVALID', 401);\n }\n\n try {\n const payload = decodeJwtPayload<{ id_wl?: number | string }>(token);\n const idWl = payload.id_wl;\n\n if (!idWl) {\n throw new ApiError('Sessão inválida ou expirada 2', 'SESSION_INVALID', 401);\n }\n\n const secret = `${secretJwt}---${idWl}`;\n const decoded = await decode(token, secret);\n\n if (!decoded) {\n throw new ApiError('Sessão inválida ou expirada 3', 'SESSION_INVALID', 401);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n throw new ApiError('Sessão inválida ou expirada 4', 'SESSION_INVALID', 401);\n }\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AAEjC,MAAM,mBAAmB;AAezB,eAAsB,yBAAwC;AAC5D,MAAI,QAAQ,IAAI,iBAAkB;AAElC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAW;AAEhB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,iBAA8C,KAAK;AACnE,UAAM,OAAO,QAAQ;AAErB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAEA,UAAM,SAAS,GAAG,SAAS,MAAM,IAAI;AACrC,UAAM,UAAU,MAAM,OAAO,OAAO,MAAM;AAE1C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,eAAe,SAAU,OAAM;AACnC,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AACF;","names":[]}
|
|
@@ -4,9 +4,11 @@ import { withAction } from "../../../utils/withAction";
|
|
|
4
4
|
import { createCardAction } from "../actions/create-card.action";
|
|
5
5
|
import { CARDS_QUERY_KEY } from "./cards.hook";
|
|
6
6
|
import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
|
|
7
|
+
import { useInvalidateAccount } from "../../accounts/hooks/current-account.hook";
|
|
7
8
|
function useCreateCard() {
|
|
8
9
|
const queryClient = useQueryClient();
|
|
9
10
|
const cardsKey = useAuthQueryKey([...CARDS_QUERY_KEY]);
|
|
11
|
+
const invalidateAccount = useInvalidateAccount();
|
|
10
12
|
return useMutation({
|
|
11
13
|
mutationFn: (data) => withAction(createCardAction)(data),
|
|
12
14
|
onSuccess: (result) => {
|
|
@@ -20,6 +22,15 @@ function useCreateCard() {
|
|
|
20
22
|
);
|
|
21
23
|
}
|
|
22
24
|
queryClient.invalidateQueries({ queryKey: cardsKey });
|
|
25
|
+
},
|
|
26
|
+
/* O createCard passou a poder gravar os dados fiscais da conta no mesmo request (o backend
|
|
27
|
+
* atualiza a conta ANTES de registrar o cartão, fail-closed). onSettled, não onSuccess: com
|
|
28
|
+
* essa ordem a conta pode ter sido atualizada mesmo quando o cartão falha depois (ex.:
|
|
29
|
+
* SetupIntent inválido) — o cache da conta não pode ficar stale em nenhum dos desfechos.
|
|
30
|
+
* Substitui a invalidação que morava no useUpdateAccount quando o AddCardModal fazia as
|
|
31
|
+
* duas chamadas separadas. */
|
|
32
|
+
onSettled: () => {
|
|
33
|
+
invalidateAccount();
|
|
23
34
|
}
|
|
24
35
|
});
|
|
25
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/cards/hooks/create-card.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { withAction } from '../../../utils/withAction';\nimport { createCardAction } from '../actions/create-card.action';\nimport type { CreateCardRequest, Card } from '../types';\nimport type { PaginatedSuccessResult } from '../../../infra/api/types';\nimport { CARDS_QUERY_KEY } from './cards.hook';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\n\nexport function useCreateCard() {\n const queryClient = useQueryClient();\n const cardsKey = useAuthQueryKey([...CARDS_QUERY_KEY]);\n\n return useMutation({\n mutationFn: (data: CreateCardRequest) => withAction(createCardAction)(data),\n onSuccess: (result) => {\n if (result?.data) {\n queryClient.setQueriesData<PaginatedSuccessResult<Card>>(\n { queryKey: cardsKey },\n (old) => {\n if (!old?.data) return old;\n return { ...old, data: [...old.data, result.data!] };\n }\n );\n }\n queryClient.invalidateQueries({ queryKey: cardsKey });\n },\n });\n}\n"],"mappings":";AAEA,SAAS,aAAa,sBAAsB;AAC5C,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAGjC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/cards/hooks/create-card.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { withAction } from '../../../utils/withAction';\nimport { createCardAction } from '../actions/create-card.action';\nimport type { CreateCardRequest, Card } from '../types';\nimport type { PaginatedSuccessResult } from '../../../infra/api/types';\nimport { CARDS_QUERY_KEY } from './cards.hook';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport { useInvalidateAccount } from '../../accounts/hooks/current-account.hook';\n\nexport function useCreateCard() {\n const queryClient = useQueryClient();\n const cardsKey = useAuthQueryKey([...CARDS_QUERY_KEY]);\n const invalidateAccount = useInvalidateAccount();\n\n return useMutation({\n mutationFn: (data: CreateCardRequest) => withAction(createCardAction)(data),\n onSuccess: (result) => {\n if (result?.data) {\n queryClient.setQueriesData<PaginatedSuccessResult<Card>>(\n { queryKey: cardsKey },\n (old) => {\n if (!old?.data) return old;\n return { ...old, data: [...old.data, result.data!] };\n }\n );\n }\n queryClient.invalidateQueries({ queryKey: cardsKey });\n },\n /* O createCard passou a poder gravar os dados fiscais da conta no mesmo request (o backend\n * atualiza a conta ANTES de registrar o cartão, fail-closed). onSettled, não onSuccess: com\n * essa ordem a conta pode ter sido atualizada mesmo quando o cartão falha depois (ex.:\n * SetupIntent inválido) — o cache da conta não pode ficar stale em nenhum dos desfechos.\n * Substitui a invalidação que morava no useUpdateAccount quando o AddCardModal fazia as\n * duas chamadas separadas. */\n onSettled: () => {\n invalidateAccount();\n },\n });\n}\n"],"mappings":";AAEA,SAAS,aAAa,sBAAsB;AAC5C,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAGjC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,4BAA4B;AAE9B,SAAS,gBAAgB;AAC9B,QAAM,cAAc,eAAe;AACnC,QAAM,WAAW,gBAAgB,CAAC,GAAG,eAAe,CAAC;AACrD,QAAM,oBAAoB,qBAAqB;AAE/C,SAAO,YAAY;AAAA,IACjB,YAAY,CAAC,SAA4B,WAAW,gBAAgB,EAAE,IAAI;AAAA,IAC1E,WAAW,CAAC,WAAW;AACrB,UAAI,QAAQ,MAAM;AAChB,oBAAY;AAAA,UACV,EAAE,UAAU,SAAS;AAAA,UACrB,CAAC,QAAQ;AACP,gBAAI,CAAC,KAAK,KAAM,QAAO;AACvB,mBAAO,EAAE,GAAG,KAAK,MAAM,CAAC,GAAG,IAAI,MAAM,OAAO,IAAK,EAAE;AAAA,UACrD;AAAA,QACF;AAAA,MACF;AACA,kBAAY,kBAAkB,EAAE,UAAU,SAAS,CAAC;AAAA,IACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,WAAW,MAAM;AACf,wBAAkB;AAAA,IACpB;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -24,7 +24,26 @@ const CardSchema = z.object({
|
|
|
24
24
|
const CreateCardRequestSchema = z.object({
|
|
25
25
|
setup_intent_id: z.string().min(1, "setup_intent_id \xE9 obrigat\xF3rio"),
|
|
26
26
|
name: z.string().min(1, "name \xE9 obrigat\xF3rio"),
|
|
27
|
-
set_default: z.boolean().optional()
|
|
27
|
+
set_default: z.boolean().optional(),
|
|
28
|
+
/* Dados fiscais OPCIONAIS no mesmo request: o backend (gapps-r3-api, POST
|
|
29
|
+
* /accounts/:id/cards) atualiza a conta ANTES de registrar o cartão, fail-closed —
|
|
30
|
+
* fiscal rejeitado (ex.: CPF com dígito verificador errado) = cartão não registra e o
|
|
31
|
+
* erro volta com a mensagem específica. Substitui o par de chamadas paralelas
|
|
32
|
+
* (createCard + updateAccount) que não tinha garantia nenhuma entre as pernas e deixava
|
|
33
|
+
* conta sem cadastro fiscal quando o PUT falhava. Whitelist espelhada do backend
|
|
34
|
+
* (#extractFiscalFields em cards/services.js). */
|
|
35
|
+
financial_document_type: z.number().optional(),
|
|
36
|
+
financial_document: z.string().optional(),
|
|
37
|
+
financial_name: z.string().optional(),
|
|
38
|
+
financial_email: z.string().optional(),
|
|
39
|
+
zipcode: z.string().optional(),
|
|
40
|
+
address: z.string().optional(),
|
|
41
|
+
address_number: z.string().optional(),
|
|
42
|
+
address_complement: z.string().optional(),
|
|
43
|
+
neighborhood: z.string().optional(),
|
|
44
|
+
city: z.string().optional(),
|
|
45
|
+
state: z.string().optional(),
|
|
46
|
+
country: z.string().optional()
|
|
28
47
|
});
|
|
29
48
|
const SetupIntentSchema = z.object({
|
|
30
49
|
client_secret: z.string(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/cards/types.ts"],"sourcesContent":["import z from 'zod';\n\nimport {\n PaginationParamsSchema,\n SearchParamsSchema,\n SortParamsSchema,\n} from '../../infra/api/types';\n\nexport const CardSchema = z.object({\n id: z.union([z.number(), z.string()]),\n id_wl: z.number(),\n id_account: z.union([z.number(), z.string()]),\n id_gateway: z.union([z.number(), z.string()]).optional(),\n gateway_account: z.string(),\n gateway_card: z.string(),\n brand: z.string(),\n name: z.string(),\n number: z.string().nullable(),\n date: z.string().nullable(),\n deleted: z\n .number()\n .transform((val) => val === 1)\n .optional()\n .default(false),\n is_default: z.boolean().optional().default(false),\n datetime_add: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n datetime_alt: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n datetime_del: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n});\n\nexport type Card = z.infer<typeof CardSchema>;\n\nexport const CreateCardRequestSchema = z.object({\n setup_intent_id: z.string().min(1, 'setup_intent_id é obrigatório'),\n name: z.string().min(1, 'name é obrigatório'),\n set_default: z.boolean().optional(),\n});\n\nexport type CreateCardRequest = z.infer<typeof CreateCardRequestSchema>;\n\nexport const SetupIntentSchema = z.object({\n client_secret: z.string(),\n setup_intent_id: z.string(),\n});\n\nexport type SetupIntent = z.infer<typeof SetupIntentSchema>;\n\nexport const FindCardsParamsSchema = PaginationParamsSchema.extend(SearchParamsSchema.shape).extend(\n SortParamsSchema.shape\n);\n\nexport type FindCardsParams = z.infer<typeof FindCardsParamsSchema>;\n"],"mappings":"AAAA,OAAO,OAAO;AAEd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,aAAa,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,EACpC,OAAO,EAAE,OAAO;AAAA,EAChB,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,EAC5C,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS;AAAA,EACvD,iBAAiB,EAAE,OAAO;AAAA,EAC1B,cAAc,EAAE,OAAO;AAAA,EACvB,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO;AAAA,EACf,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,SAAS,EACN,OAAO,EACP,UAAU,CAAC,QAAQ,QAAQ,CAAC,EAC5B,SAAS,EACT,QAAQ,KAAK;AAAA,EAChB,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK;AAAA,EAChD,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACvE,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACvE,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AACzE,CAAC;AAIM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,iBAAiB,EAAE,OAAO,EAAE,IAAI,GAAG,qCAA+B;AAAA,EAClE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,0BAAoB;AAAA,EAC5C,aAAa,EAAE,QAAQ,EAAE,SAAS;
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/cards/types.ts"],"sourcesContent":["import z from 'zod';\n\nimport {\n PaginationParamsSchema,\n SearchParamsSchema,\n SortParamsSchema,\n} from '../../infra/api/types';\n\nexport const CardSchema = z.object({\n id: z.union([z.number(), z.string()]),\n id_wl: z.number(),\n id_account: z.union([z.number(), z.string()]),\n id_gateway: z.union([z.number(), z.string()]).optional(),\n gateway_account: z.string(),\n gateway_card: z.string(),\n brand: z.string(),\n name: z.string(),\n number: z.string().nullable(),\n date: z.string().nullable(),\n deleted: z\n .number()\n .transform((val) => val === 1)\n .optional()\n .default(false),\n is_default: z.boolean().optional().default(false),\n datetime_add: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n datetime_alt: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n datetime_del: z.union([z.date(), z.coerce.date()]).nullable().optional(),\n});\n\nexport type Card = z.infer<typeof CardSchema>;\n\nexport const CreateCardRequestSchema = z.object({\n setup_intent_id: z.string().min(1, 'setup_intent_id é obrigatório'),\n name: z.string().min(1, 'name é obrigatório'),\n set_default: z.boolean().optional(),\n /* Dados fiscais OPCIONAIS no mesmo request: o backend (gapps-r3-api, POST\n * /accounts/:id/cards) atualiza a conta ANTES de registrar o cartão, fail-closed —\n * fiscal rejeitado (ex.: CPF com dígito verificador errado) = cartão não registra e o\n * erro volta com a mensagem específica. Substitui o par de chamadas paralelas\n * (createCard + updateAccount) que não tinha garantia nenhuma entre as pernas e deixava\n * conta sem cadastro fiscal quando o PUT falhava. Whitelist espelhada do backend\n * (#extractFiscalFields em cards/services.js). */\n financial_document_type: z.number().optional(),\n financial_document: z.string().optional(),\n financial_name: z.string().optional(),\n financial_email: z.string().optional(),\n zipcode: z.string().optional(),\n address: z.string().optional(),\n address_number: z.string().optional(),\n address_complement: z.string().optional(),\n neighborhood: z.string().optional(),\n city: z.string().optional(),\n state: z.string().optional(),\n country: z.string().optional(),\n});\n\nexport type CreateCardRequest = z.infer<typeof CreateCardRequestSchema>;\n\nexport const SetupIntentSchema = z.object({\n client_secret: z.string(),\n setup_intent_id: z.string(),\n});\n\nexport type SetupIntent = z.infer<typeof SetupIntentSchema>;\n\nexport const FindCardsParamsSchema = PaginationParamsSchema.extend(SearchParamsSchema.shape).extend(\n SortParamsSchema.shape\n);\n\nexport type FindCardsParams = z.infer<typeof FindCardsParamsSchema>;\n"],"mappings":"AAAA,OAAO,OAAO;AAEd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,aAAa,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,EACpC,OAAO,EAAE,OAAO;AAAA,EAChB,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,EAC5C,YAAY,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS;AAAA,EACvD,iBAAiB,EAAE,OAAO;AAAA,EAC1B,cAAc,EAAE,OAAO;AAAA,EACvB,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO;AAAA,EACf,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,SAAS,EACN,OAAO,EACP,UAAU,CAAC,QAAQ,QAAQ,CAAC,EAC5B,SAAS,EACT,QAAQ,KAAK;AAAA,EAChB,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK;AAAA,EAChD,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACvE,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACvE,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS;AACzE,CAAC;AAIM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,iBAAiB,EAAE,OAAO,EAAE,IAAI,GAAG,qCAA+B;AAAA,EAClE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,0BAAoB;AAAA,EAC5C,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlC,yBAAyB,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7C,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,EACpC,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,EACpC,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,EACxC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,SAAS,EAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAIM,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,eAAe,EAAE,OAAO;AAAA,EACxB,iBAAiB,EAAE,OAAO;AAC5B,CAAC;AAIM,MAAM,wBAAwB,uBAAuB,OAAO,mBAAmB,KAAK,EAAE;AAAA,EAC3F,iBAAiB;AACnB;","names":[]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cookies } from "next/headers";
|
|
2
2
|
import { ApiError } from "../../../infra/api/types";
|
|
3
|
+
import { decodeJwtPayload } from "../../../utils/jwt-payload";
|
|
3
4
|
import { findWhitelabel } from "../../whitelabel/actions/find-whitelabel.action";
|
|
4
5
|
import { buildWlOverride } from "../../auth/utils/build-wl-override";
|
|
5
6
|
const AUTH_COOKIE_NAME = "greatapps";
|
|
@@ -16,7 +17,7 @@ class UserService {
|
|
|
16
17
|
}
|
|
17
18
|
decodeJWT(token) {
|
|
18
19
|
try {
|
|
19
|
-
return
|
|
20
|
+
return decodeJwtPayload(token);
|
|
20
21
|
} catch {
|
|
21
22
|
throw new ApiError("Token inv\xE1lido", "INVALID_TOKEN", 401);
|
|
22
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/users/services/user.service.ts"],"sourcesContent":["\r\nimport { cookies } from 'next/headers';\r\n\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { GetUserDataResponse, JWTPayload, User } from '../schema';\r\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\r\nimport { buildWlOverride } from '../../auth/utils/build-wl-override';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\nconst API_BASE_URL = process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br';\r\n\r\nclass UserService {\r\n private async getAuthToken(): Promise<string> {\r\n if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;\r\n const cookieStore = await cookies();\r\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Não autenticado', 'NOT_AUTHENTICATED_GAT', 401);\r\n }\r\n\r\n return token;\r\n }\r\n\r\n private decodeJWT(token: string): JWTPayload {\r\n try {\r\n return
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/users/services/user.service.ts"],"sourcesContent":["\r\nimport { cookies } from 'next/headers';\r\n\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\r\nimport { GetUserDataResponse, JWTPayload, User } from '../schema';\r\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\r\nimport { buildWlOverride } from '../../auth/utils/build-wl-override';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\nconst API_BASE_URL = process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br';\r\n\r\nclass UserService {\r\n private async getAuthToken(): Promise<string> {\r\n if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;\r\n const cookieStore = await cookies();\r\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Não autenticado', 'NOT_AUTHENTICATED_GAT', 401);\r\n }\r\n\r\n return token;\r\n }\r\n\r\n private decodeJWT(token: string): JWTPayload {\r\n try {\r\n return decodeJwtPayload<JWTPayload>(token);\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n }\r\n\r\n async findById(overrides?: { authToken?: string }): Promise<User> {\r\n const token = await this.getAuthToken();\r\n const payload = this.decodeJWT(token);\r\n\r\n const { id_wl, id_account, id_user } = payload;\r\n const wlOverride = overrides ? undefined : await buildWlOverride(Number(id_wl));\r\n const wlToken = overrides?.authToken ?? wlOverride?.authToken ?? (await findWhitelabel()).token;\r\n\r\n const url = `${API_BASE_URL}/v1/pt-br/${id_wl}/accounts/${id_account}/users/${id_user}`;\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n authorization: wlToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n throw new ApiError('Erro ao buscar dados do usuário', 'FETCH_ERROR', response.status);\r\n }\r\n\r\n const data = (await response.json()) as GetUserDataResponse;\r\n\r\n if (data.status === 0 || !data.data || data.data.length === 0) {\r\n throw new ApiError('Dados do usuário não encontrados', 'USER_NOT_FOUND', 404);\r\n }\r\n\r\n return data.data[0];\r\n }\r\n}\r\n\r\nexport const userService = new UserService();\r\n"],"mappings":"AACA,SAAS,eAAe;AAExB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AAEjC,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAEhC,MAAM,mBAAmB;AACzB,MAAM,eAAe,QAAQ,IAAI,oBAAoB;AAErD,MAAM,YAAY;AAAA,EAChB,MAAc,eAAgC;AAC5C,QAAI,QAAQ,IAAI,iBAAkB,QAAO,QAAQ,IAAI;AACrD,UAAM,cAAc,MAAM,QAAQ;AAClC,UAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,sBAAmB,yBAAyB,GAAG;AAAA,IACpE;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,OAA2B;AAC3C,QAAI;AACF,aAAO,iBAA6B,KAAK;AAAA,IAC3C,QAAQ;AACN,YAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,WAAmD;AAChE,UAAM,QAAQ,MAAM,KAAK,aAAa;AACtC,UAAM,UAAU,KAAK,UAAU,KAAK;AAEpC,UAAM,EAAE,OAAO,YAAY,QAAQ,IAAI;AACvC,UAAM,aAAa,YAAY,SAAY,MAAM,gBAAgB,OAAO,KAAK,CAAC;AAC9E,UAAM,UAAU,WAAW,aAAa,YAAY,cAAc,MAAM,eAAe,GAAG;AAE1F,UAAM,MAAM,GAAG,YAAY,aAAa,KAAK,aAAa,UAAU,UAAU,OAAO;AAErF,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,SAAS,sCAAmC,eAAe,SAAS,MAAM;AAAA,IACtF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,QAAI,KAAK,WAAW,KAAK,CAAC,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AAC7D,YAAM,IAAI,SAAS,0CAAoC,kBAAkB,GAAG;AAAA,IAC9E;AAEA,WAAO,KAAK,KAAK,CAAC;AAAA,EACpB;AACF;AAEO,MAAM,cAAc,IAAI,YAAY;","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { TEST_IDS } from "../constants";
|
|
2
2
|
function b64(value) {
|
|
3
|
-
|
|
3
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
6
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4
7
|
}
|
|
5
8
|
function createTestJwt(claims = {}) {
|
|
6
9
|
const payload = {
|
|
@@ -9,7 +12,7 @@ function createTestJwt(claims = {}) {
|
|
|
9
12
|
continent: "SA",
|
|
10
13
|
country: claims.country ?? "BR",
|
|
11
14
|
region: "SP",
|
|
12
|
-
city: "
|
|
15
|
+
city: "Palho\xE7a",
|
|
13
16
|
latitude: "0",
|
|
14
17
|
longitude: "0"
|
|
15
18
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/factories/jwt.ts"],"sourcesContent":["import type { JWTPayload } from '../../modules/users/schema';\nimport { TEST_IDS } from '../constants';\n\nfunction b64(value: unknown): string {\n //
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/factories/jwt.ts"],"sourcesContent":["import type { JWTPayload } from '../../modules/users/schema';\nimport { TEST_IDS } from '../constants';\n\nfunction b64(value: unknown): string {\n // Produção emite o payload em base64url + UTF-8 (`@greatapps/jwt`), então a fixture faz igual.\n // Com `btoa` puro a fixture não conseguia sequer representar um payload acentuado — foi por isso\n // que o `atob` estrito dos decoders passou batido nos testes por tanto tempo.\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport interface TestJwtClaims {\n id_wl?: number;\n id_account?: number;\n id_user?: number;\n // Country used by resolveLocale (session.location.country).\n country?: string;\n}\n\n// Builds a fake JWT (placeholder signature) whose payload is decodable by getUserContext and\n// resolveLocale. Used as DUMMY_AUTH_TOKEN: the common auth bypasses verification when that env is\n// set, so only the payload matters.\nexport function createTestJwt(claims: TestJwtClaims = {}): string {\n const payload: JWTPayload = {\n session: {\n location: {\n continent: 'SA',\n country: claims.country ?? 'BR',\n region: 'SP',\n city: 'Palhoça',\n latitude: '0',\n longitude: '0',\n },\n ip: '127.0.0.1',\n timezone: 'America/Sao_Paulo',\n agent: 'test',\n code: null,\n },\n id_wl: String(claims.id_wl ?? TEST_IDS.wl),\n id_account: claims.id_account ?? TEST_IDS.account,\n id_user: claims.id_user ?? TEST_IDS.user,\n iss: 'test',\n aud: 'test',\n iat: 1700000000,\n };\n return `${b64({ alg: 'none', typ: 'JWT' })}.${b64(payload)}.sig`;\n}\n"],"mappings":"AACA,SAAS,gBAAgB;AAEzB,SAAS,IAAI,OAAwB;AAInC,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,KAAK,CAAC;AAC5D,MAAI,SAAS;AACb,aAAW,QAAQ,MAAO,WAAU,OAAO,aAAa,IAAI;AAC5D,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAaO,SAAS,cAAc,SAAwB,CAAC,GAAW;AAChE,QAAM,UAAsB;AAAA,IAC1B,SAAS;AAAA,MACP,UAAU;AAAA,QACR,WAAW;AAAA,QACX,SAAS,OAAO,WAAW;AAAA,QAC3B,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,IAAI;AAAA,MACJ,UAAU;AAAA,MACV,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA,OAAO,OAAO,OAAO,SAAS,SAAS,EAAE;AAAA,IACzC,YAAY,OAAO,cAAc,SAAS;AAAA,IAC1C,SAAS,OAAO,WAAW,SAAS;AAAA,IACpC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,SAAO,GAAG,IAAI,EAAE,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC;AAC5D;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function decodeJwtPayload(token) {
|
|
2
|
+
const segment = token.split(".")[1];
|
|
3
|
+
if (!segment) {
|
|
4
|
+
throw new Error("JWT sem segmento de payload");
|
|
5
|
+
}
|
|
6
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
7
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
8
|
+
const binary = atob(padded);
|
|
9
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
10
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
decodeJwtPayload
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=jwt-payload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/jwt-payload.ts"],"sourcesContent":["/**\n * Decodifica o payload de um JWT (o 2º segmento), SEM validar assinatura.\n *\n * Por que não dá pra usar `atob` direto no segmento:\n *\n * 1. **Alfabeto.** Quem emite os tokens é o `@greatapps/jwt`, que codifica com\n * `Buffer.from(...).toString('base64url')` — alfabeto com `-` e `_` e sem o padding `=`.\n * O `atob` é base64 ESTRITO: encontra `-`/`_` e lança `InvalidCharacterError`. Como os\n * dois caracteres só aparecem em certos alinhamentos de byte, o token de um usuário\n * quebra e o do vizinho não — o payload carrega geo-IP, IP e user-agent, então é o\n * conteúdo de cada sessão que decide (um usuário em \"Palhoça\" quebrava; em \"Curitiba\", não).\n *\n * 2. **Encoding.** `atob` devolve uma string latin1. Sem decodificar como UTF-8, todo\n * caractere não-ASCII do payload vira mojibake (`Palhoça` → `Palhoça`) mesmo quando\n * o decode não estoura.\n *\n * Lança se não houver payload ou se ele não for JSON válido — quem chama decide o que fazer.\n */\nexport function decodeJwtPayload<T>(token: string): T {\n const segment = token.split('.')[1];\n\n if (!segment) {\n throw new Error('JWT sem segmento de payload');\n }\n\n const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));\n\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n"],"mappings":"AAkBO,SAAS,iBAAoB,OAAkB;AACpD,QAAM,UAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AAElC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,QAAM,SAAS,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAC3D,QAAM,SAAS,OAAO,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC,IAAI,GAAG,GAAG;AAClE,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,QAAQ,WAAW,KAAK,QAAQ,CAAC,cAAc,UAAU,WAAW,CAAC,CAAC;AAE5E,SAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AACnD;","names":[]}
|
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@ import { Toast } from '../../ui/feedback/Toast';
|
|
|
7
7
|
import { useModalManager } from '../../../store/useModalManager';
|
|
8
8
|
import { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';
|
|
9
9
|
import { useCreateSetupIntent } from '../../../modules/cards/hooks/create-setup-intent.hook';
|
|
10
|
-
import { useUpdateAccount } from '../../../modules/accounts/hooks/useAccountManagement';
|
|
11
10
|
import { useCurrentAccount } from '../../../modules/accounts/hooks/current-account.hook';
|
|
11
|
+
import { isValidTaxId } from '../../../utils/validators/common';
|
|
12
12
|
import { useExternalContracting } from '../../../providers/whitelabel.provider';
|
|
13
13
|
import { CardFormFields, BillingFormFields, buildCardFormSchema } from './CardFormFields';
|
|
14
14
|
import type { CardFormData, StripeElementsStatus } from './CardFormFields';
|
|
@@ -89,7 +89,6 @@ export default function AddCardModal() {
|
|
|
89
89
|
|
|
90
90
|
const createSetupIntentMutation = useCreateSetupIntent();
|
|
91
91
|
const createCardMutation = useCreateCard();
|
|
92
|
-
const updateAccountMutation = useUpdateAccount();
|
|
93
92
|
const elements = useElements();
|
|
94
93
|
const stripe = useStripe();
|
|
95
94
|
|
|
@@ -223,30 +222,32 @@ export default function AddCardModal() {
|
|
|
223
222
|
return;
|
|
224
223
|
}
|
|
225
224
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
225
|
+
/* Chamada ÚNICA: os dados fiscais vão no mesmo request do cartão. O backend atualiza a
|
|
226
|
+
* conta ANTES de registrar o cartão, fail-closed — fiscal rejeitado (CPF inválido etc.)
|
|
227
|
+
* = cartão não registra e o erro chega com a mensagem específica no catch abaixo.
|
|
228
|
+
* Substitui o Promise.all de createCard + updateAccount, que não tinha garantia entre as
|
|
229
|
+
* pernas: quando o PUT de conta falhava, o cartão ficava salvo e a conta sem NENHUM dado
|
|
230
|
+
* fiscal — e as faturas seguintes falhavam a emissão de nota em silêncio. */
|
|
231
|
+
const cardResult = await createCardMutation.mutateAsync({
|
|
232
|
+
setup_intent_id: intent.setup_intent_id,
|
|
233
|
+
name: data.name,
|
|
234
|
+
set_default: true,
|
|
235
|
+
...(isInternational
|
|
236
|
+
? {}
|
|
237
|
+
: {
|
|
238
|
+
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
239
|
+
financial_document: data.personType === 'pf' ? data.cpf : data.cnpj,
|
|
240
|
+
}),
|
|
241
|
+
financial_name: data.fullName,
|
|
242
|
+
zipcode: data.cep,
|
|
243
|
+
address: data.street,
|
|
244
|
+
address_number: data.streetNumber || '',
|
|
245
|
+
address_complement: data.complement || '',
|
|
246
|
+
neighborhood: data.neighborhood || '',
|
|
247
|
+
city: data.city,
|
|
248
|
+
state: data.state,
|
|
249
|
+
country: data.country,
|
|
250
|
+
});
|
|
250
251
|
|
|
251
252
|
if (cardResult.success && cardResult.data?.id != null) {
|
|
252
253
|
onCardAdded?.(cardResult.data.id.toString());
|
|
@@ -257,12 +258,16 @@ export default function AddCardModal() {
|
|
|
257
258
|
{ duration: 5000 }
|
|
258
259
|
);
|
|
259
260
|
handleClose();
|
|
260
|
-
} catch {
|
|
261
|
+
} catch (error) {
|
|
262
|
+
/* O backend fail-closed devolve a razão exata da recusa (ex.: "CPF do pagador inválido —
|
|
263
|
+
* confira os dígitos verificadores"), e o withAction relança como Error com essa mensagem.
|
|
264
|
+
* Engolir num texto genérico deixaria o usuário sem saber o que corrigir. */
|
|
265
|
+
const backendMessage = error instanceof Error && error.message ? error.message : null;
|
|
261
266
|
toast.custom(
|
|
262
267
|
(t) => (
|
|
263
268
|
<Toast
|
|
264
269
|
variant="error"
|
|
265
|
-
message={translate('common.cards.add.errorSave')}
|
|
270
|
+
message={backendMessage || translate('common.cards.add.errorSave')}
|
|
266
271
|
toastId={t}
|
|
267
272
|
/>
|
|
268
273
|
),
|
|
@@ -275,7 +280,6 @@ export default function AddCardModal() {
|
|
|
275
280
|
|
|
276
281
|
const isSubmitting =
|
|
277
282
|
createCardMutation.isPending ||
|
|
278
|
-
updateAccountMutation.isPending ||
|
|
279
283
|
form.formState.isSubmitting;
|
|
280
284
|
|
|
281
285
|
const watchedValues = form.watch(['name', 'country', 'personType', 'cpf', 'cnpj', 'fullName', 'cep', 'street', 'streetNumber', 'neighborhood', 'city', 'state']);
|
|
@@ -294,7 +298,9 @@ export default function AddCardModal() {
|
|
|
294
298
|
: !!name &&
|
|
295
299
|
!!country &&
|
|
296
300
|
!!personType &&
|
|
297
|
-
|
|
301
|
+
/* Dígito verificador, em sincronia com o schema (buildCardFormSchema) e com a validação
|
|
302
|
+
* do backend — a regra antiga de tamanho deixava 111.111.111-11 habilitar o botão. */
|
|
303
|
+
(personType === 'pf' ? isValidTaxId(cpf, 'cpf') : isValidTaxId(cnpj, 'cnpj')) &&
|
|
298
304
|
stripeStatus.cardNumber &&
|
|
299
305
|
stripeStatus.cardExpiry &&
|
|
300
306
|
stripeStatus.cardCvc;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
formatCNPJ,
|
|
11
11
|
formatPostalCode,
|
|
12
12
|
} from "../../../utils/format/masks";
|
|
13
|
+
import { isValidCPF, isValidCNPJ } from "../../../utils/validators/common";
|
|
13
14
|
import z from "zod";
|
|
14
15
|
import {
|
|
15
16
|
CardCvcElement,
|
|
@@ -48,16 +49,23 @@ export function buildCardFormSchema(isInternational: boolean) {
|
|
|
48
49
|
complement: z.string().optional(),
|
|
49
50
|
});
|
|
50
51
|
if (isInternational) return base;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
/* Dígito verificador (mesmo algoritmo do backend, ver utils/validators/common): a regra
|
|
53
|
+
* antiga era só tamanho da string mascarada — 111.111.111-11 passava aqui e estourava
|
|
54
|
+
* depois no servidor, que rejeita o cadastro inteiro. path fixo em "cpf" de propósito:
|
|
55
|
+
* o campo de CNPJ lê errors.cpf no render. */
|
|
56
|
+
return base.superRefine((data, ctx) => {
|
|
57
|
+
const valid = data.personType === "pf" ? isValidCPF(data.cpf) : isValidCNPJ(data.cnpj);
|
|
58
|
+
if (!valid) {
|
|
59
|
+
ctx.addIssue({
|
|
60
|
+
code: "custom",
|
|
61
|
+
message:
|
|
62
|
+
data.personType === "pf"
|
|
63
|
+
? "CPF inválido — confira os dígitos verificadores"
|
|
64
|
+
: "CNPJ inválido — confira os dígitos verificadores",
|
|
65
|
+
path: ["cpf"],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export const cardFormSchema = buildCardFormSchema(false);
|
|
@@ -4,6 +4,7 @@ import { cookies, headers } from 'next/headers';
|
|
|
4
4
|
import { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';
|
|
5
5
|
import { localeFromCountry } from './country-language';
|
|
6
6
|
import { normalizeLocale } from './normalize';
|
|
7
|
+
import { decodeJwtPayload } from '../utils/jwt-payload';
|
|
7
8
|
|
|
8
9
|
const AUTH_COOKIE = 'greatapps';
|
|
9
10
|
const GEO_HEADER = 'cf-ipcountry';
|
|
@@ -18,7 +19,7 @@ interface JwtLocaleInfo {
|
|
|
18
19
|
function decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {
|
|
19
20
|
if (!token) return null;
|
|
20
21
|
try {
|
|
21
|
-
const payload =
|
|
22
|
+
const payload = decodeJwtPayload<{ id_wl?: number | string; session?: { location?: { country?: string | null } } }>(token);
|
|
22
23
|
return {
|
|
23
24
|
id_wl: payload?.id_wl != null ? Number(payload.id_wl) : undefined,
|
|
24
25
|
country: payload?.session?.location?.country ?? null,
|
|
@@ -2,6 +2,7 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import type { RequestConfig } from '../../../infra/api/client';
|
|
4
4
|
import type { JWTPayload } from '../../users/schema';
|
|
5
|
+
import { decodeJwtPayload } from '../../../utils/jwt-payload';
|
|
5
6
|
import { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';
|
|
6
7
|
import { whitelabelService } from '../../whitelabel/services/whitelabel.service';
|
|
7
8
|
|
|
@@ -35,7 +36,7 @@ export async function buildWlOverrideFromJwt(
|
|
|
35
36
|
cookie: string,
|
|
36
37
|
): Promise<RequestConfig | undefined> {
|
|
37
38
|
try {
|
|
38
|
-
const payload =
|
|
39
|
+
const payload = decodeJwtPayload<JWTPayload>(cookie);
|
|
39
40
|
return buildWlOverride(Number(payload.id_wl));
|
|
40
41
|
} catch {
|
|
41
42
|
return undefined;
|
|
@@ -3,6 +3,7 @@ import 'server-only';
|
|
|
3
3
|
import { cookies } from 'next/headers';
|
|
4
4
|
import { ApiError } from '../../../infra/api/types';
|
|
5
5
|
import { JWTPayload } from '../../users/schema';
|
|
6
|
+
import { decodeJwtPayload } from '../../../utils/jwt-payload';
|
|
6
7
|
|
|
7
8
|
const AUTH_COOKIE_NAME = 'greatapps';
|
|
8
9
|
|
|
@@ -19,7 +20,7 @@ export async function getUserContext(): Promise<{
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
|
-
const payload =
|
|
23
|
+
const payload = decodeJwtPayload<JWTPayload>(token);
|
|
23
24
|
|
|
24
25
|
return {
|
|
25
26
|
id_wl: Number(payload.id_wl),
|
|
@@ -3,6 +3,7 @@ import 'server-only';
|
|
|
3
3
|
import { cookies } from 'next/headers';
|
|
4
4
|
import { decode } from '@greatapps/jwt';
|
|
5
5
|
import { ApiError } from '../../../infra/api/types';
|
|
6
|
+
import { decodeJwtPayload } from '../../../utils/jwt-payload';
|
|
6
7
|
|
|
7
8
|
const AUTH_COOKIE_NAME = 'greatapps';
|
|
8
9
|
|
|
@@ -33,7 +34,7 @@ export async function requireTokenNotExpired(): Promise<void> {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
try {
|
|
36
|
-
const payload =
|
|
37
|
+
const payload = decodeJwtPayload<{ id_wl?: number | string }>(token);
|
|
37
38
|
const idWl = payload.id_wl;
|
|
38
39
|
|
|
39
40
|
if (!idWl) {
|
|
@@ -7,10 +7,12 @@ import type { CreateCardRequest, Card } from '../types';
|
|
|
7
7
|
import type { PaginatedSuccessResult } from '../../../infra/api/types';
|
|
8
8
|
import { CARDS_QUERY_KEY } from './cards.hook';
|
|
9
9
|
import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
|
|
10
|
+
import { useInvalidateAccount } from '../../accounts/hooks/current-account.hook';
|
|
10
11
|
|
|
11
12
|
export function useCreateCard() {
|
|
12
13
|
const queryClient = useQueryClient();
|
|
13
14
|
const cardsKey = useAuthQueryKey([...CARDS_QUERY_KEY]);
|
|
15
|
+
const invalidateAccount = useInvalidateAccount();
|
|
14
16
|
|
|
15
17
|
return useMutation({
|
|
16
18
|
mutationFn: (data: CreateCardRequest) => withAction(createCardAction)(data),
|
|
@@ -26,5 +28,14 @@ export function useCreateCard() {
|
|
|
26
28
|
}
|
|
27
29
|
queryClient.invalidateQueries({ queryKey: cardsKey });
|
|
28
30
|
},
|
|
31
|
+
/* O createCard passou a poder gravar os dados fiscais da conta no mesmo request (o backend
|
|
32
|
+
* atualiza a conta ANTES de registrar o cartão, fail-closed). onSettled, não onSuccess: com
|
|
33
|
+
* essa ordem a conta pode ter sido atualizada mesmo quando o cartão falha depois (ex.:
|
|
34
|
+
* SetupIntent inválido) — o cache da conta não pode ficar stale em nenhum dos desfechos.
|
|
35
|
+
* Substitui a invalidação que morava no useUpdateAccount quando o AddCardModal fazia as
|
|
36
|
+
* duas chamadas separadas. */
|
|
37
|
+
onSettled: () => {
|
|
38
|
+
invalidateAccount();
|
|
39
|
+
},
|
|
29
40
|
});
|
|
30
41
|
}
|
|
@@ -34,6 +34,25 @@ export const CreateCardRequestSchema = z.object({
|
|
|
34
34
|
setup_intent_id: z.string().min(1, 'setup_intent_id é obrigatório'),
|
|
35
35
|
name: z.string().min(1, 'name é obrigatório'),
|
|
36
36
|
set_default: z.boolean().optional(),
|
|
37
|
+
/* Dados fiscais OPCIONAIS no mesmo request: o backend (gapps-r3-api, POST
|
|
38
|
+
* /accounts/:id/cards) atualiza a conta ANTES de registrar o cartão, fail-closed —
|
|
39
|
+
* fiscal rejeitado (ex.: CPF com dígito verificador errado) = cartão não registra e o
|
|
40
|
+
* erro volta com a mensagem específica. Substitui o par de chamadas paralelas
|
|
41
|
+
* (createCard + updateAccount) que não tinha garantia nenhuma entre as pernas e deixava
|
|
42
|
+
* conta sem cadastro fiscal quando o PUT falhava. Whitelist espelhada do backend
|
|
43
|
+
* (#extractFiscalFields em cards/services.js). */
|
|
44
|
+
financial_document_type: z.number().optional(),
|
|
45
|
+
financial_document: z.string().optional(),
|
|
46
|
+
financial_name: z.string().optional(),
|
|
47
|
+
financial_email: z.string().optional(),
|
|
48
|
+
zipcode: z.string().optional(),
|
|
49
|
+
address: z.string().optional(),
|
|
50
|
+
address_number: z.string().optional(),
|
|
51
|
+
address_complement: z.string().optional(),
|
|
52
|
+
neighborhood: z.string().optional(),
|
|
53
|
+
city: z.string().optional(),
|
|
54
|
+
state: z.string().optional(),
|
|
55
|
+
country: z.string().optional(),
|
|
37
56
|
});
|
|
38
57
|
|
|
39
58
|
export type CreateCardRequest = z.infer<typeof CreateCardRequestSchema>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { cookies } from 'next/headers';
|
|
3
3
|
|
|
4
4
|
import { ApiError } from '../../../infra/api/types';
|
|
5
|
+
import { decodeJwtPayload } from '../../../utils/jwt-payload';
|
|
5
6
|
import { GetUserDataResponse, JWTPayload, User } from '../schema';
|
|
6
7
|
import { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';
|
|
7
8
|
import { buildWlOverride } from '../../auth/utils/build-wl-override';
|
|
@@ -24,7 +25,7 @@ class UserService {
|
|
|
24
25
|
|
|
25
26
|
private decodeJWT(token: string): JWTPayload {
|
|
26
27
|
try {
|
|
27
|
-
return
|
|
28
|
+
return decodeJwtPayload<JWTPayload>(token);
|
|
28
29
|
} catch {
|
|
29
30
|
throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);
|
|
30
31
|
}
|
|
@@ -2,8 +2,13 @@ import type { JWTPayload } from '../../modules/users/schema';
|
|
|
2
2
|
import { TEST_IDS } from '../constants';
|
|
3
3
|
|
|
4
4
|
function b64(value: unknown): string {
|
|
5
|
-
//
|
|
6
|
-
|
|
5
|
+
// Produção emite o payload em base64url + UTF-8 (`@greatapps/jwt`), então a fixture faz igual.
|
|
6
|
+
// Com `btoa` puro a fixture não conseguia sequer representar um payload acentuado — foi por isso
|
|
7
|
+
// que o `atob` estrito dos decoders passou batido nos testes por tanto tempo.
|
|
8
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
9
|
+
let binary = '';
|
|
10
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
11
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
export interface TestJwtClaims {
|
|
@@ -24,7 +29,7 @@ export function createTestJwt(claims: TestJwtClaims = {}): string {
|
|
|
24
29
|
continent: 'SA',
|
|
25
30
|
country: claims.country ?? 'BR',
|
|
26
31
|
region: 'SP',
|
|
27
|
-
city: '
|
|
32
|
+
city: 'Palhoça',
|
|
28
33
|
latitude: '0',
|
|
29
34
|
longitude: '0',
|
|
30
35
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodifica o payload de um JWT (o 2º segmento), SEM validar assinatura.
|
|
3
|
+
*
|
|
4
|
+
* Por que não dá pra usar `atob` direto no segmento:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Alfabeto.** Quem emite os tokens é o `@greatapps/jwt`, que codifica com
|
|
7
|
+
* `Buffer.from(...).toString('base64url')` — alfabeto com `-` e `_` e sem o padding `=`.
|
|
8
|
+
* O `atob` é base64 ESTRITO: encontra `-`/`_` e lança `InvalidCharacterError`. Como os
|
|
9
|
+
* dois caracteres só aparecem em certos alinhamentos de byte, o token de um usuário
|
|
10
|
+
* quebra e o do vizinho não — o payload carrega geo-IP, IP e user-agent, então é o
|
|
11
|
+
* conteúdo de cada sessão que decide (um usuário em "Palhoça" quebrava; em "Curitiba", não).
|
|
12
|
+
*
|
|
13
|
+
* 2. **Encoding.** `atob` devolve uma string latin1. Sem decodificar como UTF-8, todo
|
|
14
|
+
* caractere não-ASCII do payload vira mojibake (`Palhoça` → `Palhoça`) mesmo quando
|
|
15
|
+
* o decode não estoura.
|
|
16
|
+
*
|
|
17
|
+
* Lança se não houver payload ou se ele não for JSON válido — quem chama decide o que fazer.
|
|
18
|
+
*/
|
|
19
|
+
export function decodeJwtPayload<T>(token: string): T {
|
|
20
|
+
const segment = token.split('.')[1];
|
|
21
|
+
|
|
22
|
+
if (!segment) {
|
|
23
|
+
throw new Error('JWT sem segmento de payload');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');
|
|
27
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
28
|
+
const binary = atob(padded);
|
|
29
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
30
|
+
|
|
31
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as T;
|
|
32
|
+
}
|