@greatapps/common 1.1.739 → 1.1.741
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs +448 -0
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -0
- package/dist/i18n/messages/en-us.mjs +39 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +39 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +40 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +48 -32
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/actions/account-management.action.mjs +15 -0
- package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +26 -0
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -0
- package/dist/modules/accounts/hooks/useAccountManagement.mjs +13 -1
- package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -1
- package/dist/modules/accounts/utils/billing-data.mjs +35 -0
- package/dist/modules/accounts/utils/billing-data.mjs.map +1 -0
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +13 -0
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -0
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/modals/billing/RequiredBillingDataModal.tsx +548 -0
- package/src/i18n/messages/en-us.ts +40 -0
- package/src/i18n/messages/es-es.ts +40 -0
- package/src/i18n/messages/pt-br.ts +42 -0
- package/src/index.ts +11 -0
- package/src/modules/accounts/actions/account-management.action.ts +24 -0
- package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +38 -0
- package/src/modules/accounts/hooks/useAccountManagement.ts +13 -0
- package/src/modules/accounts/types.ts +11 -0
- package/src/modules/accounts/utils/billing-data.ts +81 -0
- package/src/modules/subscriptions/utils/has-paid-subscription.ts +27 -0
- package/src/server.ts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/accounts/utils/billing-data.ts"],"sourcesContent":["import type { Account } from '../types';\n\n/**\n * Gateway das contas cobradas em USD. Elas não têm CPF/CNPJ, bairro nem número —\n * mesmo critério usado no AddCardModal (`account.gateway === 'stripe_usd'`).\n */\nexport const INTERNATIONAL_GATEWAY = 'stripe_usd';\n\nexport type BillingDataSnapshot = Pick<\n Account,\n | 'gateway'\n | 'financial_document'\n | 'financial_name'\n | 'financial_email'\n | 'zipcode'\n | 'address'\n | 'address_number'\n | 'neighborhood'\n | 'city'\n | 'state'\n | 'country'\n>;\n\ntype BillingField = keyof Omit<BillingDataSnapshot, 'gateway'>;\n\n/**\n * Campos exigidos em qualquer país — sem eles a nota fiscal não sai.\n *\n * `financial_email` NÃO entra: o backend cai no e-mail do usuário quando ele está\n * vazio (`financial_email || userDocument.email`, accounts/services.js), então a\n * nota sai do mesmo jeito. Exigi-lo aqui bloquearia 4.7k contas pagantes da wl 1\n * em vez das ~460 que de fato estão sem cadastro fiscal. O campo continua no\n * formulário, pré-preenchido — só não é critério de bloqueio.\n */\nconst INTERNATIONAL_BILLING_FIELDS: readonly BillingField[] = [\n 'financial_name',\n 'zipcode',\n 'address',\n 'city',\n 'state',\n 'country',\n];\n\n/** No Brasil a NF-e também exige documento fiscal, número e bairro. */\nconst BR_BILLING_FIELDS: readonly BillingField[] = [\n ...INTERNATIONAL_BILLING_FIELDS,\n 'financial_document',\n 'address_number',\n 'neighborhood',\n];\n\nexport function isInternationalAccount(\n account: Pick<BillingDataSnapshot, 'gateway'> | null | undefined,\n): boolean {\n return account?.gateway === INTERNATIONAL_GATEWAY;\n}\n\nfunction isFilled(value: string | null | undefined): boolean {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\n/**\n * A conta tem todos os dados de cobrança/fiscais necessários pra emissão de nota.\n * Retorna `false` quando a conta ainda não carregou — quem decide bloquear é o\n * chamador, que checa o loading antes (ver `useRequiredBillingData`).\n */\nexport function hasCompleteBillingData(\n account: BillingDataSnapshot | null | undefined,\n): boolean {\n if (!account) return false;\n\n const fields = isInternationalAccount(account)\n ? INTERNATIONAL_BILLING_FIELDS\n : BR_BILLING_FIELDS;\n\n for (const field of fields) {\n if (!isFilled(account[field])) return false;\n }\n\n return true;\n}\n"],"mappings":"AAMO,MAAM,wBAAwB;AA4BrC,MAAM,+BAAwD;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,MAAM,oBAA6C;AAAA,EACjD,GAAG;AAAA,EACH;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,uBACd,SACS;AACT,SAAO,SAAS,YAAY;AAC9B;AAEA,SAAS,SAAS,OAA2C;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS;AAC5D;AAOO,SAAS,uBACd,SACS;AACT,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,uBAAuB,OAAO,IACzC,+BACA;AAEJ,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,EAAG,QAAO;AAAA,EACxC;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const FREE_PLAN_ID = 5;
|
|
2
|
+
function hasPaidSubscription(subscription) {
|
|
3
|
+
if (!subscription) return false;
|
|
4
|
+
if (subscription.type === "free" || subscription.id_plan === FREE_PLAN_ID) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
if (subscription.type === "trial") return false;
|
|
8
|
+
return subscription.charged === true;
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
hasPaidSubscription
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=has-paid-subscription.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/subscriptions/utils/has-paid-subscription.ts"],"sourcesContent":["import type { Subscription } from '../types/subscription.type';\n\nconst FREE_PLAN_ID = 5;\n\ntype PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan' | 'charged'>;\n\n/**\n * A conta já teve pagamento confirmado numa assinatura paga.\n *\n * Diferente de `hasActivePaidSubscription`, aqui NÃO importa se a assinatura\n * segue vigente: quem pagou e depois cancelou/atrasou continua precisando de\n * nota fiscal do que já foi cobrado. Por isso o sinal é `charged` (marcado pelo\n * backend no `bill_paid`), e não `active`/`date_due`.\n *\n * Free e trial ficam de fora — nunca houve cobrança a documentar.\n */\nexport function hasPaidSubscription(\n subscription: PaidSubscriptionSnapshot | undefined | null,\n): boolean {\n if (!subscription) return false;\n if (subscription.type === 'free' || subscription.id_plan === FREE_PLAN_ID) {\n return false;\n }\n if (subscription.type === 'trial') return false;\n\n return subscription.charged === true;\n}\n"],"mappings":"AAEA,MAAM,eAAe;AAcd,SAAS,oBACd,cACS;AACT,MAAI,CAAC,aAAc,QAAO;AAC1B,MAAI,aAAa,SAAS,UAAU,aAAa,YAAY,cAAc;AACzE,WAAO;AAAA,EACT;AACA,MAAI,aAAa,SAAS,QAAS,QAAO;AAE1C,SAAO,aAAa,YAAY;AAClC;","names":[]}
|
package/dist/server.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
updateUserAction,
|
|
48
48
|
updateAccountAction,
|
|
49
49
|
updateAccountUserByIdAction,
|
|
50
|
+
updateBillingDataAction,
|
|
50
51
|
deleteAccountUserAction,
|
|
51
52
|
deleteAccountAction,
|
|
52
53
|
createAccountUserAction,
|
|
@@ -135,6 +136,7 @@ export {
|
|
|
135
136
|
unlinkGoogleAction,
|
|
136
137
|
updateAccountAction,
|
|
137
138
|
updateAccountUserByIdAction,
|
|
139
|
+
updateBillingDataAction,
|
|
138
140
|
updateProjectAction,
|
|
139
141
|
updateSubscriptionPaymentAction,
|
|
140
142
|
updateSubscriptionPlanAction,
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { chargesService } from './modules/charges/services/charges.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { socialGoogleLoginAction } from './modules/auth/actions/social-google-login.action';\r\nexport { socialGoogleOnboardingAction } from './modules/auth/actions/social-google-onboarding.action';\r\nexport { unlinkGoogleAction } from './modules/auth/actions/unlink-google.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { updateSubscriptionPaymentAction } from './modules/subscriptions/actions/update-subscription-payment.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { createSetupIntentAction } from './modules/cards/actions/create-setup-intent.action';\r\nexport { chargeActionAction } from './modules/charges/actions/charge-action.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { hasActivePaidSubscription } from './modules/subscriptions/utils/has-active-paid-subscription';\r\n\r\n// Project Services & Actions (server-only)\r\nexport { projectsService } from './modules/projects/services/projects.service';\r\nexport { listProjectsAction } from './modules/projects/actions/list-projects.action';\r\nexport { findProjectAction } from './modules/projects/actions/find-project.action';\r\nexport { createProjectAction } from './modules/projects/actions/create-project.action';\r\nexport { updateProjectAction } from './modules/projects/actions/update-project.action';\r\nexport { deleteProjectAction } from './modules/projects/actions/delete-project.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { resolveLocale } from './i18n/resolve-locale';\r\nexport type { ResolveLocaleOptions } from './i18n/resolve-locale';\r\nexport { requireValidSession } from './modules/auth/utils/require-valid-session';\r\nexport { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\nexport { safeMutationAction } from './utils/safeMutationAction';\r\n\r\n// Image Processing (server-side)\r\nexport { processImageAction } from './modules/images/actions/process-image.action';\r\nexport { processImage } from './modules/images/services/image-processing.service';\r\nexport type {\r\n ProcessImageInput,\r\n ProcessImageOutput,\r\n ProcessImageActionInput,\r\n ProcessImageActionOutput,\r\n} from './modules/images/types/image.type';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,oCAAoC;AAC7C,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAG1C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { chargesService } from './modules/charges/services/charges.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { socialGoogleLoginAction } from './modules/auth/actions/social-google-login.action';\r\nexport { socialGoogleOnboardingAction } from './modules/auth/actions/social-google-onboarding.action';\r\nexport { unlinkGoogleAction } from './modules/auth/actions/unlink-google.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { updateSubscriptionPaymentAction } from './modules/subscriptions/actions/update-subscription-payment.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { createSetupIntentAction } from './modules/cards/actions/create-setup-intent.action';\r\nexport { chargeActionAction } from './modules/charges/actions/charge-action.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { hasActivePaidSubscription } from './modules/subscriptions/utils/has-active-paid-subscription';\r\n\r\n// Project Services & Actions (server-only)\r\nexport { projectsService } from './modules/projects/services/projects.service';\r\nexport { listProjectsAction } from './modules/projects/actions/list-projects.action';\r\nexport { findProjectAction } from './modules/projects/actions/find-project.action';\r\nexport { createProjectAction } from './modules/projects/actions/create-project.action';\r\nexport { updateProjectAction } from './modules/projects/actions/update-project.action';\r\nexport { deleteProjectAction } from './modules/projects/actions/delete-project.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n updateBillingDataAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { resolveLocale } from './i18n/resolve-locale';\r\nexport type { ResolveLocaleOptions } from './i18n/resolve-locale';\r\nexport { requireValidSession } from './modules/auth/utils/require-valid-session';\r\nexport { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\nexport { safeMutationAction } from './utils/safeMutationAction';\r\n\r\n// Image Processing (server-side)\r\nexport { processImageAction } from './modules/images/actions/process-image.action';\r\nexport { processImage } from './modules/images/services/image-processing.service';\r\nexport type {\r\n ProcessImageInput,\r\n ProcessImageOutput,\r\n ProcessImageActionInput,\r\n ProcessImageActionOutput,\r\n} from './modules/images/types/image.type';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,oCAAoC;AAC7C,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAG1C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- o generic Translator do next-intl não é
|
|
2
|
+
expressável estruturalmente; o `any` do TranslateFn é intencional e está contido neste arquivo
|
|
3
|
+
(mesmo padrão de modules/plans/utils/map-api-plan-to-ui.ts). */
|
|
4
|
+
'use client';
|
|
5
|
+
|
|
6
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
9
|
+
import { useTranslations } from 'next-intl';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import z from 'zod';
|
|
12
|
+
import { IconFileInvoice } from '@tabler/icons-react';
|
|
13
|
+
|
|
14
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/overlay/Dialog';
|
|
15
|
+
import { Button } from '../../ui/buttons/Button';
|
|
16
|
+
import { FormField } from '../../ui/form/FormField';
|
|
17
|
+
import { SelectField } from '../../ui/form/SelectField';
|
|
18
|
+
import { Toast } from '../../ui/feedback/Toast';
|
|
19
|
+
import { useAuth } from '../../../providers/auth.provider';
|
|
20
|
+
import { useViaCep } from '../../../modules/accounts/hooks/useViaCep';
|
|
21
|
+
import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccountManagement';
|
|
22
|
+
import { useRequiredBillingData } from '../../../modules/accounts/hooks/use-required-billing-data.hook';
|
|
23
|
+
import { COUNTRIES } from '../../../utils/countries';
|
|
24
|
+
import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
|
|
25
|
+
import { formatCNPJ, formatCPF, formatPostalCode } from '../../../utils/format/masks';
|
|
26
|
+
import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
|
|
27
|
+
import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
|
|
28
|
+
|
|
29
|
+
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
30
|
+
|
|
31
|
+
type BillingDataFormValues = {
|
|
32
|
+
personType: 'pf' | 'pj';
|
|
33
|
+
document: string;
|
|
34
|
+
financialName: string;
|
|
35
|
+
financialEmail: string;
|
|
36
|
+
cep: string;
|
|
37
|
+
street: string;
|
|
38
|
+
streetNumber: string;
|
|
39
|
+
complement?: string;
|
|
40
|
+
neighborhood: string;
|
|
41
|
+
city: string;
|
|
42
|
+
state: string;
|
|
43
|
+
country: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) => ({
|
|
47
|
+
value: country.iso,
|
|
48
|
+
label: country.name,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
function buildBillingDataSchema(isInternational: boolean, translate: TranslateFn) {
|
|
52
|
+
const requiredText = z
|
|
53
|
+
.string()
|
|
54
|
+
.trim()
|
|
55
|
+
.min(1, translate('common.billing.requiredData.requiredField'));
|
|
56
|
+
|
|
57
|
+
const base = z.object({
|
|
58
|
+
personType: z.enum(['pf', 'pj']),
|
|
59
|
+
document: z.string().trim(),
|
|
60
|
+
financialName: requiredText,
|
|
61
|
+
financialEmail: z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(1, translate('common.billing.requiredData.requiredField'))
|
|
65
|
+
.email(translate('common.billing.requiredData.invalidEmail')),
|
|
66
|
+
cep: requiredText,
|
|
67
|
+
street: requiredText,
|
|
68
|
+
streetNumber: z.string().trim(),
|
|
69
|
+
complement: z.string().trim().optional(),
|
|
70
|
+
neighborhood: z.string().trim(),
|
|
71
|
+
city: requiredText,
|
|
72
|
+
state: requiredText,
|
|
73
|
+
country: requiredText,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/* Conta internacional não tem CPF/CNPJ, número nem bairro — mesmo recorte do
|
|
77
|
+
* AddCardModal. Exigir esses campos aqui trancaria o cliente fora do app, já que
|
|
78
|
+
* o modal é bloqueante. */
|
|
79
|
+
if (isInternational) return base;
|
|
80
|
+
|
|
81
|
+
return base.superRefine((data, ctx) => {
|
|
82
|
+
if (!data.streetNumber) {
|
|
83
|
+
ctx.addIssue({
|
|
84
|
+
code: 'custom',
|
|
85
|
+
path: ['streetNumber'],
|
|
86
|
+
message: translate('common.billing.requiredData.requiredField'),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!data.neighborhood) {
|
|
91
|
+
ctx.addIssue({
|
|
92
|
+
code: 'custom',
|
|
93
|
+
path: ['neighborhood'],
|
|
94
|
+
message: translate('common.billing.requiredData.requiredField'),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (data.cep.replace(/\D/g, '').length !== 8) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: 'custom',
|
|
101
|
+
path: ['cep'],
|
|
102
|
+
message: translate('common.billing.requiredData.invalidCep'),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isCompany = data.personType === 'pj';
|
|
107
|
+
const digits = data.document.replace(/\D/g, '');
|
|
108
|
+
|
|
109
|
+
if (digits.length !== (isCompany ? 14 : 11)) {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: 'custom',
|
|
112
|
+
path: ['document'],
|
|
113
|
+
message: isCompany
|
|
114
|
+
? translate('common.billing.requiredData.invalidCnpj')
|
|
115
|
+
: translate('common.billing.requiredData.invalidCpf'),
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Dígito verificador, mesmo algoritmo do backend: sem isso 111.111.111-11 passa
|
|
121
|
+
* aqui e o PUT da conta é recusado depois, sem o usuário saber o motivo. */
|
|
122
|
+
const isValid = isCompany ? isValidCNPJ(digits) : isValidCPF(digits);
|
|
123
|
+
if (!isValid) {
|
|
124
|
+
ctx.addIssue({
|
|
125
|
+
code: 'custom',
|
|
126
|
+
path: ['document'],
|
|
127
|
+
message: isCompany
|
|
128
|
+
? translate('common.billing.requiredData.invalidCnpjChecksum')
|
|
129
|
+
: translate('common.billing.requiredData.invalidCpfChecksum'),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatDocument(document: string | null | undefined, isCompany: boolean) {
|
|
136
|
+
if (!document) return '';
|
|
137
|
+
return isCompany ? formatCNPJ(document) : formatCPF(document);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function RequiredBillingDataModalContent() {
|
|
141
|
+
const translate = useTranslations();
|
|
142
|
+
const { user } = useAuth();
|
|
143
|
+
const { isRequired, account, isInternational } = useRequiredBillingData();
|
|
144
|
+
const { mutateAsync: updateBillingData } = useUpdateBillingData();
|
|
145
|
+
const { mutateAsync: lookupCep } = useViaCep();
|
|
146
|
+
|
|
147
|
+
/* O modal fecha na hora do submit e não reabre enquanto o invalidate do account
|
|
148
|
+
* não volta — sem isso o usuário vê o modal piscar de novo com o cache antigo. */
|
|
149
|
+
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
150
|
+
const isPrefilled = useRef(false);
|
|
151
|
+
|
|
152
|
+
const schema = useMemo(
|
|
153
|
+
() => buildBillingDataSchema(isInternational, translate),
|
|
154
|
+
[isInternational, translate],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const form = useForm<BillingDataFormValues>({
|
|
158
|
+
resolver: zodResolver(schema),
|
|
159
|
+
mode: 'onChange',
|
|
160
|
+
defaultValues: {
|
|
161
|
+
personType: 'pf',
|
|
162
|
+
document: '',
|
|
163
|
+
financialName: '',
|
|
164
|
+
financialEmail: '',
|
|
165
|
+
cep: '',
|
|
166
|
+
street: '',
|
|
167
|
+
streetNumber: '',
|
|
168
|
+
complement: '',
|
|
169
|
+
neighborhood: '',
|
|
170
|
+
city: '',
|
|
171
|
+
state: '',
|
|
172
|
+
country: 'BR',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const {
|
|
177
|
+
register,
|
|
178
|
+
control,
|
|
179
|
+
handleSubmit,
|
|
180
|
+
reset,
|
|
181
|
+
setValue,
|
|
182
|
+
watch,
|
|
183
|
+
formState: { errors, isSubmitting, isValid },
|
|
184
|
+
} = form;
|
|
185
|
+
|
|
186
|
+
const personType = watch('personType');
|
|
187
|
+
const isCompany = personType === 'pj';
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!account || isPrefilled.current) return;
|
|
191
|
+
isPrefilled.current = true;
|
|
192
|
+
|
|
193
|
+
const accountIsCompany = account.financial_document_type === 2;
|
|
194
|
+
reset({
|
|
195
|
+
personType: accountIsCompany ? 'pj' : 'pf',
|
|
196
|
+
document: formatDocument(account.financial_document, accountIsCompany),
|
|
197
|
+
financialName: account.financial_name ?? '',
|
|
198
|
+
financialEmail: account.financial_email || user?.email || '',
|
|
199
|
+
cep: account.zipcode ?? '',
|
|
200
|
+
street: account.address ?? '',
|
|
201
|
+
streetNumber: account.address_number ?? '',
|
|
202
|
+
complement: account.address_complement ?? '',
|
|
203
|
+
neighborhood: account.neighborhood ?? '',
|
|
204
|
+
city: account.city ?? '',
|
|
205
|
+
state: account.state ?? '',
|
|
206
|
+
country: account.country || (isInternational ? 'US' : 'BR'),
|
|
207
|
+
});
|
|
208
|
+
}, [account, isInternational, reset, user?.email]);
|
|
209
|
+
|
|
210
|
+
const handleDocumentChange = (rawValue: string) => {
|
|
211
|
+
setValue('document', isCompany ? formatCNPJ(rawValue) : formatCPF(rawValue), {
|
|
212
|
+
shouldValidate: true,
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handlePersonTypeChange = (value: 'pf' | 'pj') => {
|
|
217
|
+
if (value === personType) return;
|
|
218
|
+
setValue('document', '');
|
|
219
|
+
/* shouldValidate reroda o resolver: sem isso o `isValid` fica preso no valor
|
|
220
|
+
* do documento antigo e o botão continua habilitado com o campo já limpo. */
|
|
221
|
+
setValue('personType', value, { shouldValidate: true });
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleCepChange = (rawValue: string) => {
|
|
225
|
+
if (isInternational) {
|
|
226
|
+
setValue('cep', rawValue, { shouldValidate: true });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setValue('cep', formatPostalCode(rawValue), { shouldValidate: true });
|
|
231
|
+
|
|
232
|
+
const digits = rawValue.replace(/\D/g, '');
|
|
233
|
+
if (digits.length !== 8) return;
|
|
234
|
+
|
|
235
|
+
lookupCep(digits)
|
|
236
|
+
.then((data) => {
|
|
237
|
+
setValue('street', data.logradouro, { shouldValidate: true });
|
|
238
|
+
setValue('neighborhood', data.bairro, { shouldValidate: true });
|
|
239
|
+
setValue('city', data.localidade, { shouldValidate: true });
|
|
240
|
+
setValue('state', data.uf, { shouldValidate: true });
|
|
241
|
+
})
|
|
242
|
+
.catch(() => {
|
|
243
|
+
// CEP não encontrado: o usuário preenche o endereço na mão.
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
async function onSubmit(data: BillingDataFormValues) {
|
|
248
|
+
const payload: UpdateBillingDataRequest = {
|
|
249
|
+
...(isInternational
|
|
250
|
+
? {}
|
|
251
|
+
: {
|
|
252
|
+
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
253
|
+
financial_document: data.document,
|
|
254
|
+
}),
|
|
255
|
+
financial_name: data.financialName,
|
|
256
|
+
financial_email: data.financialEmail,
|
|
257
|
+
zipcode: data.cep,
|
|
258
|
+
address: data.street,
|
|
259
|
+
address_number: data.streetNumber,
|
|
260
|
+
address_complement: data.complement ?? '',
|
|
261
|
+
neighborhood: data.neighborhood,
|
|
262
|
+
city: data.city,
|
|
263
|
+
state: data.state,
|
|
264
|
+
country: data.country,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await updateBillingData(payload);
|
|
269
|
+
setIsSubmitted(true);
|
|
270
|
+
toast.custom(
|
|
271
|
+
(t) => (
|
|
272
|
+
<Toast
|
|
273
|
+
variant="success"
|
|
274
|
+
message={translate('common.billing.requiredData.successToast')}
|
|
275
|
+
toastId={t}
|
|
276
|
+
/>
|
|
277
|
+
),
|
|
278
|
+
{ duration: 5000 },
|
|
279
|
+
);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
/* O backend devolve a razão exata da recusa (ex.: documento fiscal inválido).
|
|
282
|
+
* Trocar por um texto genérico deixaria o usuário travado sem saber o que corrigir. */
|
|
283
|
+
const message =
|
|
284
|
+
error instanceof Error && error.message
|
|
285
|
+
? error.message
|
|
286
|
+
: translate('common.billing.requiredData.errorToast');
|
|
287
|
+
toast.custom((t) => <Toast variant="error" message={message} toastId={t} />, {
|
|
288
|
+
duration: 5000,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!isRequired || isSubmitted) return null;
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<Dialog open onOpenChange={() => {}}>
|
|
297
|
+
<DialogContent
|
|
298
|
+
showCloseButton={false}
|
|
299
|
+
onEscapeKeyDown={(event: Event) => event.preventDefault()}
|
|
300
|
+
onPointerDownOutside={(event: Event) => event.preventDefault()}
|
|
301
|
+
onInteractOutside={(event: Event) => event.preventDefault()}
|
|
302
|
+
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-hidden"
|
|
303
|
+
>
|
|
304
|
+
<DialogHeader className="gap-3 text-left p-4 lg:p-5 border-b border-zinc-200 shrink-0">
|
|
305
|
+
<div className="flex items-center justify-center w-10 h-10 bg-zinc-50 rounded-lg">
|
|
306
|
+
<IconFileInvoice size={24} className="text-zinc-950" />
|
|
307
|
+
</div>
|
|
308
|
+
<div className="flex flex-col gap-2">
|
|
309
|
+
<DialogTitle className="paragraph-medium-semibold text-zinc-950">
|
|
310
|
+
{translate('common.billing.requiredData.title')}
|
|
311
|
+
</DialogTitle>
|
|
312
|
+
<span className="paragraph-small-regular text-zinc-600">
|
|
313
|
+
{translate('common.billing.requiredData.description')}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
</DialogHeader>
|
|
317
|
+
|
|
318
|
+
<form
|
|
319
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
320
|
+
className="flex flex-col flex-1 min-h-0"
|
|
321
|
+
noValidate
|
|
322
|
+
>
|
|
323
|
+
<div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
|
|
324
|
+
<div className="flex flex-col gap-5">
|
|
325
|
+
{!isInternational && (
|
|
326
|
+
<div className="flex gap-4">
|
|
327
|
+
<Controller
|
|
328
|
+
name="personType"
|
|
329
|
+
control={control}
|
|
330
|
+
render={({ field }) => (
|
|
331
|
+
<SelectField
|
|
332
|
+
label={translate('common.billing.requiredData.personType')}
|
|
333
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
334
|
+
value={field.value}
|
|
335
|
+
onChange={(value) => handlePersonTypeChange(value as 'pf' | 'pj')}
|
|
336
|
+
options={[
|
|
337
|
+
{
|
|
338
|
+
value: 'pf',
|
|
339
|
+
label: translate('common.billing.requiredData.personTypePf'),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
value: 'pj',
|
|
343
|
+
label: translate('common.billing.requiredData.personTypePj'),
|
|
344
|
+
},
|
|
345
|
+
]}
|
|
346
|
+
containerClassName="flex-1"
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
/>
|
|
350
|
+
<div className="flex-1">
|
|
351
|
+
<FormField
|
|
352
|
+
label={
|
|
353
|
+
isCompany
|
|
354
|
+
? translate('common.billing.requiredData.cnpj')
|
|
355
|
+
: translate('common.billing.requiredData.cpf')
|
|
356
|
+
}
|
|
357
|
+
placeholder={isCompany ? '__.___.___/____-__' : '___.___.___-__'}
|
|
358
|
+
error={!!errors.document}
|
|
359
|
+
errorMessage={errors.document?.message}
|
|
360
|
+
{...register('document', {
|
|
361
|
+
onChange: (event) => handleDocumentChange(event.target.value),
|
|
362
|
+
})}
|
|
363
|
+
/>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
<FormField
|
|
369
|
+
label={
|
|
370
|
+
isCompany
|
|
371
|
+
? translate('common.billing.requiredData.companyName')
|
|
372
|
+
: translate('common.billing.requiredData.fullName')
|
|
373
|
+
}
|
|
374
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
375
|
+
error={!!errors.financialName}
|
|
376
|
+
errorMessage={errors.financialName?.message}
|
|
377
|
+
{...register('financialName')}
|
|
378
|
+
/>
|
|
379
|
+
|
|
380
|
+
<FormField
|
|
381
|
+
label={translate('common.billing.requiredData.financialEmail')}
|
|
382
|
+
placeholder={translate('common.billing.requiredData.financialEmailPlaceholder')}
|
|
383
|
+
type="email"
|
|
384
|
+
error={!!errors.financialEmail}
|
|
385
|
+
errorMessage={errors.financialEmail?.message}
|
|
386
|
+
{...register('financialEmail')}
|
|
387
|
+
/>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div className="h-px bg-zinc-200" />
|
|
391
|
+
|
|
392
|
+
<div className="flex flex-col gap-5">
|
|
393
|
+
<span className="paragraph-small-semibold text-zinc-950">
|
|
394
|
+
{translate('common.billing.requiredData.addressHeading')}
|
|
395
|
+
</span>
|
|
396
|
+
|
|
397
|
+
<FormField
|
|
398
|
+
label={
|
|
399
|
+
isInternational
|
|
400
|
+
? translate('common.billing.requiredData.postalCode')
|
|
401
|
+
: translate('common.billing.requiredData.cep')
|
|
402
|
+
}
|
|
403
|
+
placeholder={isInternational ? '' : '_____-___'}
|
|
404
|
+
error={!!errors.cep}
|
|
405
|
+
errorMessage={errors.cep?.message}
|
|
406
|
+
{...register('cep', {
|
|
407
|
+
onChange: (event) => handleCepChange(event.target.value),
|
|
408
|
+
})}
|
|
409
|
+
/>
|
|
410
|
+
|
|
411
|
+
<div className="flex gap-4">
|
|
412
|
+
<div className="flex-[3]">
|
|
413
|
+
<FormField
|
|
414
|
+
label={translate('common.billing.requiredData.street')}
|
|
415
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
416
|
+
error={!!errors.street}
|
|
417
|
+
errorMessage={errors.street?.message}
|
|
418
|
+
{...register('street')}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
{!isInternational && (
|
|
422
|
+
<div className="flex-1">
|
|
423
|
+
<FormField
|
|
424
|
+
label={translate('common.billing.requiredData.streetNumber')}
|
|
425
|
+
placeholder="000"
|
|
426
|
+
error={!!errors.streetNumber}
|
|
427
|
+
errorMessage={errors.streetNumber?.message}
|
|
428
|
+
{...register('streetNumber')}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div className="flex gap-4">
|
|
435
|
+
{!isInternational && (
|
|
436
|
+
<div className="flex-1">
|
|
437
|
+
<FormField
|
|
438
|
+
label={translate('common.billing.requiredData.neighborhood')}
|
|
439
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
440
|
+
error={!!errors.neighborhood}
|
|
441
|
+
errorMessage={errors.neighborhood?.message}
|
|
442
|
+
{...register('neighborhood')}
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
<div className="flex-1">
|
|
447
|
+
<FormField
|
|
448
|
+
label={translate('common.billing.requiredData.complement')}
|
|
449
|
+
optional
|
|
450
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
451
|
+
{...register('complement')}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div className="flex gap-4">
|
|
457
|
+
<div className="flex-1">
|
|
458
|
+
<FormField
|
|
459
|
+
label={translate('common.billing.requiredData.city')}
|
|
460
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
461
|
+
error={!!errors.city}
|
|
462
|
+
errorMessage={errors.city?.message}
|
|
463
|
+
{...register('city')}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
<div className="flex-1">
|
|
467
|
+
{isInternational ? (
|
|
468
|
+
<FormField
|
|
469
|
+
label={translate('common.billing.requiredData.stateProvince')}
|
|
470
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
471
|
+
error={!!errors.state}
|
|
472
|
+
errorMessage={errors.state?.message}
|
|
473
|
+
{...register('state')}
|
|
474
|
+
/>
|
|
475
|
+
) : (
|
|
476
|
+
<Controller
|
|
477
|
+
name="state"
|
|
478
|
+
control={control}
|
|
479
|
+
render={({ field }) => (
|
|
480
|
+
<SelectField
|
|
481
|
+
label={translate('common.billing.requiredData.state')}
|
|
482
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
483
|
+
value={field.value || undefined}
|
|
484
|
+
onChange={(value) =>
|
|
485
|
+
setValue('state', value as string, { shouldValidate: true })
|
|
486
|
+
}
|
|
487
|
+
options={BR_STATE_OPTIONS}
|
|
488
|
+
error={!!errors.state}
|
|
489
|
+
errorMessage={errors.state?.message}
|
|
490
|
+
/>
|
|
491
|
+
)}
|
|
492
|
+
/>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<Controller
|
|
498
|
+
name="country"
|
|
499
|
+
control={control}
|
|
500
|
+
render={({ field }) => (
|
|
501
|
+
<SelectField
|
|
502
|
+
label={translate('common.billing.requiredData.country')}
|
|
503
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
504
|
+
value={field.value || undefined}
|
|
505
|
+
onChange={(value) =>
|
|
506
|
+
setValue('country', value as string, { shouldValidate: true })
|
|
507
|
+
}
|
|
508
|
+
options={COUNTRY_OPTIONS}
|
|
509
|
+
error={!!errors.country}
|
|
510
|
+
errorMessage={errors.country?.message}
|
|
511
|
+
/>
|
|
512
|
+
)}
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<div className="flex items-center justify-end p-4 lg:p-5 border-t border-zinc-200 shrink-0">
|
|
518
|
+
<Button
|
|
519
|
+
type="submit"
|
|
520
|
+
className="h-10! w-full lg:w-fit"
|
|
521
|
+
disabled={!isValid || isSubmitting}
|
|
522
|
+
loading={isSubmitting}
|
|
523
|
+
>
|
|
524
|
+
{isSubmitting
|
|
525
|
+
? translate('common.billing.requiredData.submitting')
|
|
526
|
+
: translate('common.billing.requiredData.submit')}
|
|
527
|
+
</Button>
|
|
528
|
+
</div>
|
|
529
|
+
</form>
|
|
530
|
+
</DialogContent>
|
|
531
|
+
</Dialog>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Modal bloqueante que exige o cadastro de cobrança/fiscal de contas que já
|
|
537
|
+
* pagaram sem ele — sem esses dados a nota fiscal não é emitida. Monte uma vez
|
|
538
|
+
* no layout raiz do app; ele mesmo decide se aparece (ver `useRequiredBillingData`).
|
|
539
|
+
*/
|
|
540
|
+
export default function RequiredBillingDataModal() {
|
|
541
|
+
const { user, isLoading } = useAuth();
|
|
542
|
+
|
|
543
|
+
/* Sem sessão não há conta nem assinatura pra consultar — evita disparar as
|
|
544
|
+
* queries (e tomar 401) em /login, /register e demais rotas públicas. */
|
|
545
|
+
if (isLoading || !user) return null;
|
|
546
|
+
|
|
547
|
+
return <RequiredBillingDataModalContent />;
|
|
548
|
+
}
|