@greatapps/common 1.1.675 → 1.1.677
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/account/ConfigurationsMyAccountModal.mjs +8 -6
- package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -1
- package/dist/components/account/ConfirmDeleteAccountModal.mjs +20 -19
- package/dist/components/account/ConfirmDeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +10 -11
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -1
- package/dist/components/account/DeleteAccountModal.mjs +34 -28
- package/dist/components/account/DeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/DisableTwoFactorAuthModal.mjs +7 -5
- package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -1
- package/dist/components/account/IsntPossibleDeleteAccountModal.mjs +8 -9
- package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/TwoFactorAuthModal.mjs +22 -23
- package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -1
- package/dist/components/account/constants.mjs +4 -4
- package/dist/components/account/constants.mjs.map +1 -1
- package/dist/components/account/hooks/useChangeEmailForm.mjs +5 -3
- package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -1
- package/dist/components/account/hooks/useChangePasswordForm.mjs +26 -3
- package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -1
- package/dist/components/account/hooks/useChangePhoneForm.mjs +18 -9
- package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -1
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +26 -3
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -1
- package/dist/components/account/hooks/useOtpVerification.mjs +19 -3
- package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -1
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +27 -4
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -1
- package/dist/components/account/sections/ChangeEmailModal.mjs +25 -28
- package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -1
- package/dist/components/account/sections/ChangePasswordSection.mjs +8 -6
- package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -1
- package/dist/components/account/sections/ChangePhoneModal.mjs +24 -27
- package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -1
- package/dist/components/account/sections/MyProfileSection.mjs +37 -22
- package/dist/components/account/sections/MyProfileSection.mjs.map +1 -1
- package/dist/components/account/sections/PreferencesSection.mjs +48 -24
- package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
- package/dist/components/account/sections/SecuritySection.mjs +11 -9
- package/dist/components/account/sections/SecuritySection.mjs.map +1 -1
- package/dist/components/account/sections/TokenSection.mjs +24 -8
- package/dist/components/account/sections/TokenSection.mjs.map +1 -1
- package/dist/components/layouts/AppMobileNavBar.mjs +2 -148
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/layouts/NavBar.mjs +5 -3
- package/dist/components/layouts/NavBar.mjs.map +1 -1
- package/dist/components/layouts/NotificationsPopover.mjs +5 -3
- package/dist/components/layouts/NotificationsPopover.mjs.map +1 -1
- package/dist/components/layouts/ProfilePopover.mjs +6 -4
- package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
- package/dist/components/layouts/UsersSelectorPopover.mjs +9 -7
- package/dist/components/layouts/UsersSelectorPopover.mjs.map +1 -1
- package/dist/components/modals/BuyCreditsModal.mjs +17 -18
- package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
- package/dist/components/modals/CreditsDisabledModal.mjs +6 -4
- package/dist/components/modals/CreditsDisabledModal.mjs.map +1 -1
- package/dist/components/modals/PaidPlanRequiredModal.mjs +5 -3
- package/dist/components/modals/PaidPlanRequiredModal.mjs.map +1 -1
- package/dist/components/modals/cards/AddCardModal.mjs +64 -46
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/components/modals/cards/CannotDeleteCardModal.mjs +9 -7
- package/dist/components/modals/cards/CannotDeleteCardModal.mjs.map +1 -1
- package/dist/components/modals/cards/CardFormFields.mjs +65 -51
- package/dist/components/modals/cards/CardFormFields.mjs.map +1 -1
- package/dist/components/modals/cards/DeleteCardModal.mjs +10 -8
- package/dist/components/modals/cards/DeleteCardModal.mjs.map +1 -1
- package/dist/components/navigation/CancelledSubscriptionBanner.mjs +4 -2
- package/dist/components/navigation/CancelledSubscriptionBanner.mjs.map +1 -1
- package/dist/components/navigation/OverdueInvoiceBanner.mjs +4 -2
- package/dist/components/navigation/OverdueInvoiceBanner.mjs.map +1 -1
- package/dist/components/navigation/PendingCancellationBanner.mjs +4 -6
- package/dist/components/navigation/PendingCancellationBanner.mjs.map +1 -1
- package/dist/components/navigation/ProjectSelectorPopover.mjs +8 -6
- package/dist/components/navigation/ProjectSelectorPopover.mjs.map +1 -1
- package/dist/components/navigation/UpcomingInvoiceBanner.mjs +4 -6
- package/dist/components/navigation/UpcomingInvoiceBanner.mjs.map +1 -1
- package/dist/components/navigation/subcomponents/BottomLinks.mjs +5 -3
- package/dist/components/navigation/subcomponents/BottomLinks.mjs.map +1 -1
- package/dist/components/navigation/subcomponents/CreditsCard.mjs +3 -1
- package/dist/components/navigation/subcomponents/CreditsCard.mjs.map +1 -1
- package/dist/components/navigation/subcomponents/PlanCard.mjs +3 -1
- package/dist/components/navigation/subcomponents/PlanCard.mjs.map +1 -1
- package/dist/components/navigation/subcomponents/ProjectSelector.mjs +5 -3
- package/dist/components/navigation/subcomponents/ProjectSelector.mjs.map +1 -1
- package/dist/components/pages/NotFoundPage.mjs +5 -3
- package/dist/components/pages/NotFoundPage.mjs.map +1 -1
- package/dist/components/pages/notifications/Notifications.mjs +13 -10
- package/dist/components/pages/notifications/Notifications.mjs.map +1 -1
- package/dist/components/ui/buttons/CopyButton.mjs +9 -5
- package/dist/components/ui/buttons/CopyButton.mjs.map +1 -1
- package/dist/components/ui/data-display/CardItem.mjs +7 -5
- package/dist/components/ui/data-display/CardItem.mjs.map +1 -1
- package/dist/components/ui/data-display/PaymentInfoCard.mjs +10 -8
- package/dist/components/ui/data-display/PaymentInfoCard.mjs.map +1 -1
- package/dist/components/ui/feedback/CircularProgress.mjs.map +1 -1
- package/dist/components/ui/feedback/DefaultCircularProgress.mjs +3 -1
- package/dist/components/ui/feedback/DefaultCircularProgress.mjs.map +1 -1
- package/dist/components/ui/form/ComboboxField.mjs +11 -6
- package/dist/components/ui/form/ComboboxField.mjs.map +1 -1
- package/dist/components/ui/form/DatePicker.mjs +5 -2
- package/dist/components/ui/form/DatePicker.mjs.map +1 -1
- package/dist/components/ui/form/DateRangePicker.mjs +5 -2
- package/dist/components/ui/form/DateRangePicker.mjs.map +1 -1
- package/dist/components/ui/form/PhoneInput.mjs +5 -3
- package/dist/components/ui/form/PhoneInput.mjs.map +1 -1
- package/dist/components/ui/form/SelectField.mjs +6 -3
- package/dist/components/ui/form/SelectField.mjs.map +1 -1
- package/dist/components/widgets/ImageUpload/ImageCropModal.mjs +11 -18
- package/dist/components/widgets/ImageUpload/ImageCropModal.mjs.map +1 -1
- package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs +6 -15
- package/dist/components/widgets/ImageUpload/ImageTooSmallModal.mjs.map +1 -1
- package/dist/i18n/config.mjs +13 -0
- package/dist/i18n/config.mjs.map +1 -0
- package/dist/i18n/country-language.mjs +74 -0
- package/dist/i18n/country-language.mjs.map +1 -0
- package/dist/i18n/messages/en-us.mjs +494 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -0
- package/dist/i18n/messages/es-es.mjs +494 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -0
- package/dist/i18n/messages/index.mjs +16 -0
- package/dist/i18n/messages/index.mjs.map +1 -0
- package/dist/i18n/messages/pt-br.mjs +507 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -0
- package/dist/i18n/normalize.mjs +12 -0
- package/dist/i18n/normalize.mjs.map +1 -0
- package/dist/i18n/resolve-locale.mjs +42 -0
- package/dist/i18n/resolve-locale.mjs.map +1 -0
- package/dist/i18n.mjs +14 -0
- package/dist/i18n.mjs.map +1 -0
- package/dist/index.mjs +19 -3
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs +20 -0
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs.map +1 -0
- package/dist/modules/cards/hooks/handle-delete-card.hook.mjs +2 -1
- package/dist/modules/cards/hooks/handle-delete-card.hook.mjs.map +1 -1
- package/dist/modules/images/utils/validate-image.mjs +8 -8
- package/dist/modules/images/utils/validate-image.mjs.map +1 -1
- package/dist/modules/plans/hooks/list-plans.hook.mjs +2 -2
- package/dist/modules/plans/hooks/list-plans.hook.mjs.map +1 -1
- package/dist/modules/plans/types/plan.type.mjs +5 -1
- package/dist/modules/plans/types/plan.type.mjs.map +1 -1
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +61 -35
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
- package/dist/modules/projects/services/projects.service.mjs +9 -0
- package/dist/modules/projects/services/projects.service.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/periodicity.mjs +20 -14
- package/dist/modules/subscriptions/utils/periodicity.mjs.map +1 -1
- package/dist/providers/auth.provider.mjs +9 -1
- package/dist/providers/auth.provider.mjs.map +1 -1
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -1
- package/dist/utils/format/currency.mjs +32 -4
- package/dist/utils/format/currency.mjs.map +1 -1
- package/dist/utils/intl/cookie-domain.mjs +17 -0
- package/dist/utils/intl/cookie-domain.mjs.map +1 -0
- package/dist/utils/intl/locale-cookie.mjs +13 -0
- package/dist/utils/intl/locale-cookie.mjs.map +1 -0
- package/dist/utils/intl/locales.mjs +3 -2
- package/dist/utils/intl/locales.mjs.map +1 -1
- package/package.json +7 -1
- package/src/components/account/ConfigurationsMyAccountModal.tsx +10 -6
- package/src/components/account/ConfirmDeleteAccountModal.tsx +22 -13
- package/src/components/account/ConfirmGlobalPreferencesModal.tsx +12 -10
- package/src/components/account/DeleteAccountModal.tsx +34 -24
- package/src/components/account/DisableTwoFactorAuthModal.tsx +11 -6
- package/src/components/account/IsntPossibleDeleteAccountModal.tsx +8 -6
- package/src/components/account/TwoFactorAuthModal.tsx +33 -29
- package/src/components/account/constants.ts +8 -5
- package/src/components/account/hooks/useChangeEmailForm.tsx +5 -3
- package/src/components/account/hooks/useChangePasswordForm.tsx +20 -6
- package/src/components/account/hooks/useChangePhoneForm.tsx +16 -10
- package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +20 -6
- package/src/components/account/hooks/useOtpVerification.tsx +16 -4
- package/src/components/account/hooks/useTwoFactorAuthForm.tsx +21 -7
- package/src/components/account/sections/ChangeEmailModal.tsx +35 -21
- package/src/components/account/sections/ChangePasswordSection.tsx +10 -6
- package/src/components/account/sections/ChangePhoneModal.tsx +50 -22
- package/src/components/account/sections/MyProfileSection.tsx +37 -23
- package/src/components/account/sections/PreferencesSection.tsx +52 -25
- package/src/components/account/sections/SecuritySection.tsx +19 -10
- package/src/components/account/sections/TokenSection.tsx +22 -10
- package/src/components/layouts/AppMobileNavBar.tsx +1 -154
- package/src/components/layouts/NavBar.tsx +5 -3
- package/src/components/layouts/NotificationsPopover.tsx +5 -3
- package/src/components/layouts/ProfilePopover.tsx +6 -4
- package/src/components/layouts/UsersSelectorPopover.tsx +9 -7
- package/src/components/modals/BuyCreditsModal.tsx +18 -16
- package/src/components/modals/CreditsDisabledModal.tsx +6 -6
- package/src/components/modals/PaidPlanRequiredModal.tsx +5 -3
- package/src/components/modals/cards/AddCardModal.tsx +89 -62
- package/src/components/modals/cards/CannotDeleteCardModal.tsx +9 -8
- package/src/components/modals/cards/CardFormFields.tsx +93 -75
- package/src/components/modals/cards/DeleteCardModal.tsx +10 -8
- package/src/components/navigation/CancelledSubscriptionBanner.tsx +4 -2
- package/src/components/navigation/OverdueInvoiceBanner.tsx +4 -2
- package/src/components/navigation/PendingCancellationBanner.tsx +4 -2
- package/src/components/navigation/ProjectSelectorPopover.tsx +8 -6
- package/src/components/navigation/UpcomingInvoiceBanner.tsx +4 -2
- package/src/components/navigation/subcomponents/BottomLinks.tsx +5 -3
- package/src/components/navigation/subcomponents/CreditsCard.tsx +3 -1
- package/src/components/navigation/subcomponents/PlanCard.tsx +3 -1
- package/src/components/navigation/subcomponents/ProjectSelector.tsx +5 -3
- package/src/components/pages/NotFoundPage.tsx +5 -3
- package/src/components/pages/notifications/Notifications.tsx +13 -10
- package/src/components/ui/buttons/CopyButton.tsx +10 -5
- package/src/components/ui/data-display/CardItem.tsx +7 -5
- package/src/components/ui/data-display/PaymentInfoCard.tsx +10 -8
- package/src/components/ui/feedback/CircularProgress.tsx +4 -0
- package/src/components/ui/feedback/DefaultCircularProgress.tsx +3 -1
- package/src/components/ui/form/ComboboxField.tsx +11 -6
- package/src/components/ui/form/DatePicker.tsx +5 -2
- package/src/components/ui/form/DateRangePicker.tsx +5 -2
- package/src/components/ui/form/PhoneInput.tsx +5 -3
- package/src/components/ui/form/SelectField.tsx +6 -3
- package/src/components/widgets/ImageUpload/ImageCropModal.tsx +19 -11
- package/src/components/widgets/ImageUpload/ImageTooSmallModal.tsx +6 -5
- package/src/i18n/config.ts +17 -0
- package/src/i18n/country-language.ts +60 -0
- package/src/i18n/messages/en-us.ts +514 -0
- package/src/i18n/messages/es-es.ts +515 -0
- package/src/i18n/messages/index.ts +24 -0
- package/src/i18n/messages/pt-br.ts +538 -0
- package/src/i18n/normalize.ts +15 -0
- package/src/i18n/resolve-locale.ts +77 -0
- package/src/i18n.ts +10 -0
- package/src/index.ts +13 -2
- package/src/modules/accounts/hooks/use-currency-formatter.hook.tsx +32 -0
- package/src/modules/cards/hooks/handle-delete-card.hook.ts +8 -3
- package/src/modules/images/utils/validate-image.ts +23 -8
- package/src/modules/plans/hooks/list-plans.hook.ts +2 -2
- package/src/modules/plans/types/plan.type.ts +4 -0
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +109 -47
- package/src/modules/projects/services/projects.service.ts +12 -0
- package/src/modules/subscriptions/utils/periodicity.ts +29 -12
- package/src/providers/auth.provider.tsx +13 -1
- package/src/server.ts +2 -0
- package/src/utils/format/currency.ts +62 -7
- package/src/utils/intl/cookie-domain.ts +29 -0
- package/src/utils/intl/locale-cookie.ts +18 -0
- package/src/utils/intl/locales.ts +3 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCurrentAccount } from './current-account.hook';
|
|
4
|
+
import {
|
|
5
|
+
formatCurrency as formatCurrencyBase,
|
|
6
|
+
formatCurrencyNumber as formatCurrencyNumberBase,
|
|
7
|
+
getCurrencyForGateway,
|
|
8
|
+
type CurrencyCode,
|
|
9
|
+
} from '../../../utils/format/currency';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Formatadores de moeda já amarrados à moeda da conta atual (derivada de account.gateway).
|
|
13
|
+
*
|
|
14
|
+
* Use em componentes client para exibir preços sem passar a moeda manualmente — a moeda é
|
|
15
|
+
* determinada pelo backend (roteamento de gateway) e é read-only para o usuário.
|
|
16
|
+
*
|
|
17
|
+
* Conta stripe_usd → USD ('$1,234.56'); demais → BRL ('R$ 1.234,56').
|
|
18
|
+
*/
|
|
19
|
+
export function useCurrencyFormatter(): {
|
|
20
|
+
currency: CurrencyCode;
|
|
21
|
+
formatCurrency: (value: string | number) => string;
|
|
22
|
+
formatCurrencyNumber: (value?: number | null, decimals?: number) => string;
|
|
23
|
+
} {
|
|
24
|
+
const { data: account } = useCurrentAccount();
|
|
25
|
+
const currency = getCurrencyForGateway(account?.gateway);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
currency,
|
|
29
|
+
formatCurrency: (value) => formatCurrencyBase(value, currency),
|
|
30
|
+
formatCurrencyNumber: (value, decimals = 2) => formatCurrencyNumberBase(value, decimals, currency),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
4
|
import { useModalManager } from '../../../store/useModalManager';
|
|
5
5
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
6
|
+
import { hasSubscriptionExpired } from '../../subscriptions/utils/has-subscription-expired';
|
|
6
7
|
import type { Card } from '../types';
|
|
7
8
|
|
|
8
9
|
type UseHandleDeleteCardOptions = {
|
|
@@ -18,11 +19,15 @@ export function useHandleDeleteCard({
|
|
|
18
19
|
|
|
19
20
|
return useCallback(
|
|
20
21
|
(card: Card) => {
|
|
22
|
+
// Só bloqueia a exclusão quando o cartão é o método de pagamento de uma
|
|
23
|
+
// assinatura REALMENTE ativa (mesma definição de ConfigurationsMyAccountModal).
|
|
24
|
+
// Se a assinatura foi cancelada ou já expirou, ela não está mais ativa —
|
|
25
|
+
// pula a verificação e segue direto pra exclusão do cartão.
|
|
21
26
|
const isCardLinkedToSubscription =
|
|
22
27
|
!!activeSubscription &&
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
activeSubscription.active === true &&
|
|
29
|
+
!activeSubscription.date_cancellation &&
|
|
30
|
+
!hasSubscriptionExpired(activeSubscription.date_due) &&
|
|
26
31
|
card.id.toString() === activeSubscription.id_card?.toString();
|
|
27
32
|
|
|
28
33
|
if (isCardLinkedToSubscription) {
|
|
@@ -3,17 +3,24 @@ import {
|
|
|
3
3
|
MAX_FILE_SIZE_MB,
|
|
4
4
|
} from '../constants/image.constants'
|
|
5
5
|
|
|
6
|
+
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
7
|
+
|
|
6
8
|
export interface ImageValidationResult {
|
|
7
9
|
valid: boolean
|
|
8
10
|
error?: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function validateImageFormat(
|
|
13
|
+
export function validateImageFormat(
|
|
14
|
+
file: File,
|
|
15
|
+
translate?: TranslateFn
|
|
16
|
+
): ImageValidationResult {
|
|
12
17
|
// Bloqueia SVG explicitamente (segurança + não funciona com crop)
|
|
13
18
|
if (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg')) {
|
|
14
19
|
return {
|
|
15
20
|
valid: false,
|
|
16
|
-
error:
|
|
21
|
+
error: translate
|
|
22
|
+
? translate('common.imageUpload.errorSvg')
|
|
23
|
+
: 'Formato SVG não é suportado. Use PNG, JPEG, WebP ou GIF.',
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
|
|
@@ -24,7 +31,9 @@ export function validateImageFormat(file: File): ImageValidationResult {
|
|
|
24
31
|
) {
|
|
25
32
|
return {
|
|
26
33
|
valid: false,
|
|
27
|
-
error:
|
|
34
|
+
error: translate
|
|
35
|
+
? translate('common.imageUpload.errorFormat')
|
|
36
|
+
: 'Formato não suportado. Use PNG, JPEG, WebP ou GIF.',
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
39
|
return { valid: true }
|
|
@@ -32,22 +41,28 @@ export function validateImageFormat(file: File): ImageValidationResult {
|
|
|
32
41
|
|
|
33
42
|
export function validateImageSize(
|
|
34
43
|
file: File,
|
|
35
|
-
maxSizeMB: number = MAX_FILE_SIZE_MB
|
|
44
|
+
maxSizeMB: number = MAX_FILE_SIZE_MB,
|
|
45
|
+
translate?: TranslateFn
|
|
36
46
|
): ImageValidationResult {
|
|
37
47
|
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
38
48
|
return {
|
|
39
49
|
valid: false,
|
|
40
|
-
error:
|
|
50
|
+
error: translate
|
|
51
|
+
? translate('common.imageUpload.errorTooLarge', { maxSize: maxSizeMB })
|
|
52
|
+
: `Arquivo muito grande. Máximo ${maxSizeMB}MB.`,
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
return { valid: true }
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
export function validateImage(
|
|
47
|
-
|
|
58
|
+
export function validateImage(
|
|
59
|
+
file: File,
|
|
60
|
+
translate?: TranslateFn
|
|
61
|
+
): ImageValidationResult {
|
|
62
|
+
const formatValidation = validateImageFormat(file, translate)
|
|
48
63
|
if (!formatValidation.valid) return formatValidation
|
|
49
64
|
|
|
50
|
-
const sizeValidation = validateImageSize(file)
|
|
65
|
+
const sizeValidation = validateImageSize(file, MAX_FILE_SIZE_MB, translate)
|
|
51
66
|
if (!sizeValidation.valid) return sizeValidation
|
|
52
67
|
|
|
53
68
|
return { valid: true }
|
|
@@ -13,11 +13,11 @@ export function usePlans() {
|
|
|
13
13
|
return useQuery<Plan[]>({
|
|
14
14
|
queryKey,
|
|
15
15
|
queryFn: ({ signal }) => apiFetch<Plan[]>('/api/plans', { signal }),
|
|
16
|
-
staleTime:
|
|
16
|
+
staleTime: 60 * 1000,
|
|
17
17
|
gcTime: Infinity,
|
|
18
18
|
refetchOnWindowFocus: false,
|
|
19
19
|
refetchOnReconnect: false,
|
|
20
|
-
refetchOnMount:
|
|
20
|
+
refetchOnMount: true,
|
|
21
21
|
retry: 1,
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -5,6 +5,7 @@ export const PlanItemSchema = z.object({
|
|
|
5
5
|
id_addon: z.number(),
|
|
6
6
|
quantity: z.number(),
|
|
7
7
|
value: z.number(),
|
|
8
|
+
value_usd: z.coerce.number().nullish(),
|
|
8
9
|
});
|
|
9
10
|
|
|
10
11
|
export type PlanItem = z.infer<typeof PlanItemSchema>;
|
|
@@ -14,6 +15,9 @@ export const PlanSchema = z.object({
|
|
|
14
15
|
id_plan: z.number().optional(),
|
|
15
16
|
name: z.string(),
|
|
16
17
|
value: z.number().default(0),
|
|
18
|
+
// Preço em USD (conta stripe_usd). Operador cadastra no backend; ausente em planos
|
|
19
|
+
// sem suporte internacional. Espelha a coluna value_usd de gapps-r3-api.
|
|
20
|
+
value_usd: z.coerce.number().nullish(),
|
|
17
21
|
type: z.string().optional(),
|
|
18
22
|
discount_semester: z.number().default(0),
|
|
19
23
|
discount_annual: z.number().default(0),
|
|
@@ -6,45 +6,85 @@ import type {
|
|
|
6
6
|
UiPlan,
|
|
7
7
|
} from "../types/plan.type";
|
|
8
8
|
import { ADDON_IDS } from "../../subscriptions/constants/addons.constants";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
formatCurrencyNumber,
|
|
11
|
+
type CurrencyCode,
|
|
12
|
+
} from "../../../utils/format/currency";
|
|
10
13
|
|
|
11
14
|
export type PlanBillingPeriod = "monthly" | "semiannual" | "annual";
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
{ icon: "check", text: "Visitas ilimitadas" },
|
|
15
|
-
{ icon: "check", text: "Leads ilimitadas" },
|
|
16
|
-
{ icon: "check", text: "Compartilhar páginas", tooltip: "sharePages" },
|
|
17
|
-
{ icon: "check", text: "Hospedagem inclusa" },
|
|
18
|
-
{ icon: "check", text: "SSL (HTTPS) + CDN" },
|
|
19
|
-
{ icon: "check", text: "Templates gratuitos" },
|
|
20
|
-
];
|
|
16
|
+
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
const BASE_FEATURES_KEYS = [
|
|
19
|
+
{ icon: "check", key: "plans.features.unlimitedVisits", fallback: "Visitas ilimitadas" },
|
|
20
|
+
{ icon: "check", key: "plans.features.unlimitedLeads", fallback: "Leads ilimitadas" },
|
|
21
|
+
{ icon: "check", key: "plans.features.sharePages", fallback: "Compartilhar páginas", tooltip: "sharePages" },
|
|
22
|
+
{ icon: "check", key: "plans.features.hosting", fallback: "Hospedagem inclusa" },
|
|
23
|
+
// SSL (HTTPS) + CDN — brand/acronym: never translated
|
|
24
|
+
{ icon: "check", key: null, fallback: "SSL (HTTPS) + CDN" },
|
|
25
|
+
{ icon: "check", key: "plans.features.freeTemplates", fallback: "Templates gratuitos" },
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
function buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {
|
|
29
|
+
return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {
|
|
30
|
+
const text = key && translate ? translate(`common.${key}`) : fallback;
|
|
31
|
+
const feature: PlanFeature = { icon, text };
|
|
32
|
+
if ("tooltip" in rest && rest.tooltip) feature.tooltip = rest.tooltip;
|
|
33
|
+
return feature;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildMainFeaturesFromItems(
|
|
38
|
+
items: Plan["items"],
|
|
39
|
+
translate?: TranslateFn
|
|
40
|
+
): PlanMainFeatures {
|
|
23
41
|
const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);
|
|
24
42
|
const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);
|
|
25
43
|
|
|
26
44
|
const pagesQty = pagesItem?.quantity ?? 0;
|
|
27
45
|
const domainsQty = domainsItem?.quantity ?? 1;
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
let pages: string;
|
|
48
|
+
if (pagesQty === 0) {
|
|
49
|
+
pages = translate
|
|
50
|
+
? translate("common.plans.mainFeatures.pagesUnlimited")
|
|
51
|
+
: "Páginas ilimitadas";
|
|
52
|
+
} else if (pagesQty === 1) {
|
|
53
|
+
pages = translate
|
|
54
|
+
? translate("common.plans.mainFeatures.pageSingular")
|
|
55
|
+
: "1 página";
|
|
56
|
+
} else {
|
|
57
|
+
pages = translate
|
|
58
|
+
? translate("common.plans.mainFeatures.pagesPlural", { count: pagesQty })
|
|
59
|
+
: `${pagesQty} páginas`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let domains: string;
|
|
63
|
+
if (domainsQty >= 999) {
|
|
64
|
+
domains = translate
|
|
65
|
+
? translate("common.plans.mainFeatures.domainsUnlimited")
|
|
66
|
+
: "Domínios ilimitados";
|
|
67
|
+
} else if (domainsQty === 1) {
|
|
68
|
+
domains = translate
|
|
69
|
+
? translate("common.plans.mainFeatures.domainSingular")
|
|
70
|
+
: "1 domínio externo";
|
|
71
|
+
} else {
|
|
72
|
+
domains = translate
|
|
73
|
+
? translate("common.plans.mainFeatures.domainsPlural", { count: domainsQty })
|
|
74
|
+
: `${domainsQty} domínios externos`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const users = translate
|
|
78
|
+
? translate("common.plans.mainFeatures.usersUnlimited")
|
|
79
|
+
: "Usuários ilimitados";
|
|
80
|
+
|
|
81
|
+
return { pages, domains, users };
|
|
44
82
|
}
|
|
45
83
|
|
|
46
|
-
function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
|
|
47
|
-
|
|
84
|
+
function computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {
|
|
85
|
+
// Conta USD usa value_usd; fallback pro value (BRL) evita NaN quando o operador ainda
|
|
86
|
+
// não cadastrou o preço internacional. Os descontos por período são percentuais — aplicam igual.
|
|
87
|
+
const monthly = currency === "usd" ? (apiPlan.value_usd ?? apiPlan.value) : apiPlan.value;
|
|
48
88
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
49
89
|
return {
|
|
50
90
|
monthly,
|
|
@@ -53,46 +93,68 @@ function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
|
|
|
53
93
|
};
|
|
54
94
|
}
|
|
55
95
|
|
|
56
|
-
|
|
96
|
+
function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {
|
|
57
97
|
const popular = plan.id === 2 || plan.id_plan === 2;
|
|
58
98
|
const buttonVariant = popular ? "brand" : "default";
|
|
59
99
|
|
|
60
|
-
const pricingByPeriod = computePricingByPeriod(plan);
|
|
100
|
+
const pricingByPeriod = computePricingByPeriod(plan, currency);
|
|
61
101
|
const monthlyFinal = pricingByPeriod.monthly;
|
|
62
102
|
|
|
63
103
|
const lowerName = plan.name.toLowerCase();
|
|
64
104
|
const hasProjectAndOnboarding =
|
|
65
105
|
lowerName.includes("agência") || lowerName.includes("agencia");
|
|
66
106
|
|
|
107
|
+
const onboardingText = translate
|
|
108
|
+
? translate("common.plans.features.onboarding")
|
|
109
|
+
: "Reunião de Onboarding";
|
|
110
|
+
const projectMgmtText = translate
|
|
111
|
+
? translate("common.plans.features.projectManagement")
|
|
112
|
+
: "Gestão de projetos";
|
|
113
|
+
|
|
67
114
|
const features: PlanFeature[] = [
|
|
68
|
-
...
|
|
115
|
+
...buildBaseFeatures(translate),
|
|
69
116
|
hasProjectAndOnboarding
|
|
70
|
-
? { icon: "check", text:
|
|
71
|
-
: { icon: "x", text:
|
|
117
|
+
? { icon: "check", text: onboardingText }
|
|
118
|
+
: { icon: "x", text: onboardingText, disabled: true },
|
|
72
119
|
hasProjectAndOnboarding
|
|
73
|
-
? {
|
|
74
|
-
|
|
75
|
-
text: "Gestão de projetos",
|
|
76
|
-
tooltip: "projectManagement",
|
|
77
|
-
}
|
|
78
|
-
: {
|
|
79
|
-
icon: "x",
|
|
80
|
-
text: "Gestão de projetos",
|
|
81
|
-
disabled: true,
|
|
82
|
-
tooltip: "projectManagement",
|
|
83
|
-
},
|
|
120
|
+
? { icon: "check", text: projectMgmtText, tooltip: "projectManagement" }
|
|
121
|
+
: { icon: "x", text: projectMgmtText, disabled: true, tooltip: "projectManagement" },
|
|
84
122
|
];
|
|
85
123
|
|
|
124
|
+
const currencyAwareItems = plan.items.map((item) => ({
|
|
125
|
+
...item,
|
|
126
|
+
value: currency === "usd" ? (item.value_usd ?? item.value) : item.value,
|
|
127
|
+
}));
|
|
128
|
+
|
|
86
129
|
return {
|
|
87
130
|
planId: plan.id,
|
|
88
131
|
name: plan.name,
|
|
89
132
|
isPopular: popular,
|
|
90
133
|
buttonVariant,
|
|
91
|
-
mainFeatures: buildMainFeaturesFromItems(plan.items),
|
|
134
|
+
mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
|
|
92
135
|
features,
|
|
93
|
-
originalPrice: formatCurrencyNumber(monthlyFinal),
|
|
94
|
-
price: formatCurrencyNumber(monthlyFinal),
|
|
136
|
+
originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
137
|
+
price: formatCurrencyNumber(monthlyFinal, 2, currency),
|
|
95
138
|
pricingByPeriod,
|
|
96
|
-
items:
|
|
139
|
+
items: currencyAwareItems,
|
|
97
140
|
};
|
|
98
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Mapeia um plano da API para UiPlan em BRL.
|
|
145
|
+
* Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.
|
|
146
|
+
* Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).
|
|
147
|
+
*/
|
|
148
|
+
export function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {
|
|
149
|
+
return buildUiPlan(plan, "brl", translate);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).
|
|
154
|
+
* Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.
|
|
155
|
+
* Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.
|
|
156
|
+
*/
|
|
157
|
+
export const mapApiPlanToUiPlanForCurrency =
|
|
158
|
+
(currency: CurrencyCode, translate?: TranslateFn) =>
|
|
159
|
+
(plan: Plan): UiPlan =>
|
|
160
|
+
buildUiPlan(plan, currency, translate);
|
|
@@ -131,6 +131,18 @@ class ProjectsService {
|
|
|
131
131
|
body: JSON.stringify({ id_project_to_move: moveToProjectId }),
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
// Alguns backends respondem `null` no delete quando a exclusão foi bem-sucedida.
|
|
135
|
+
// Nesse caso não há payload para validar e o fluxo deve seguir sem erro.
|
|
136
|
+
if (response == null) {
|
|
137
|
+
console.warn('[ProjectsService.delete] null response treated as success', {
|
|
138
|
+
id_account,
|
|
139
|
+
id_user,
|
|
140
|
+
projectId,
|
|
141
|
+
moveToProjectId,
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
if (response.status === 0) {
|
|
135
147
|
throw new ApiError(
|
|
136
148
|
response.message || 'Erro ao deletar projeto',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { BillingPeriod } from '../types/billing-period.type';
|
|
2
2
|
import type { CalculateSubscriptionResponse } from '../types/calculate-subscription.type';
|
|
3
3
|
|
|
4
|
+
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
5
|
+
|
|
4
6
|
export function periodicityToBillingPeriod(
|
|
5
7
|
periodicity: number | string | null | undefined
|
|
6
8
|
): BillingPeriod | null {
|
|
@@ -16,23 +18,38 @@ export function billingPeriodToNumeric(billingPeriod: BillingPeriod): 30 | 180 |
|
|
|
16
18
|
return 30;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function getPeriodicityLabel(
|
|
21
|
+
export function getPeriodicityLabel(
|
|
22
|
+
periodicity: number | string | null | undefined,
|
|
23
|
+
translate?: TranslateFn
|
|
24
|
+
): string {
|
|
20
25
|
const period = periodicityToBillingPeriod(periodicity);
|
|
21
|
-
if (period === 'semiannual')
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
if (period === 'semiannual')
|
|
27
|
+
return translate ? translate('common.periodicity.semiannual') : 'Semestral';
|
|
28
|
+
if (period === 'annual')
|
|
29
|
+
return translate ? translate('common.periodicity.annual') : 'Anual';
|
|
30
|
+
return translate ? translate('common.periodicity.monthly') : 'Mensal';
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
export function getBillingPeriodSuffix(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
export function getBillingPeriodSuffix(
|
|
34
|
+
billingPeriod: BillingPeriod,
|
|
35
|
+
translate?: TranslateFn
|
|
36
|
+
): string {
|
|
37
|
+
if (billingPeriod === 'semiannual')
|
|
38
|
+
return translate ? translate('common.periodicity.suffixSemiannual') : '/semestre';
|
|
39
|
+
if (billingPeriod === 'annual')
|
|
40
|
+
return translate ? translate('common.periodicity.suffixAnnual') : '/ano';
|
|
41
|
+
return translate ? translate('common.periodicity.suffixMonthly') : '/mês';
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
export function getBillingPeriodLabel(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
export function getBillingPeriodLabel(
|
|
45
|
+
billingPeriod: BillingPeriod,
|
|
46
|
+
translate?: TranslateFn
|
|
47
|
+
): string {
|
|
48
|
+
if (billingPeriod === 'semiannual')
|
|
49
|
+
return translate ? translate('common.periodicity.labelSemiannual') : 'semestral';
|
|
50
|
+
if (billingPeriod === 'annual')
|
|
51
|
+
return translate ? translate('common.periodicity.labelAnnual') : 'anual';
|
|
52
|
+
return translate ? translate('common.periodicity.labelMonthly') : 'mensal';
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
export function getBillingPeriodMonths(billingPeriod: BillingPeriod): number {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ReactNode,
|
|
6
6
|
useCallback,
|
|
7
7
|
useContext,
|
|
8
|
+
useEffect,
|
|
8
9
|
useMemo,
|
|
9
10
|
} from "react";
|
|
10
11
|
import { useQueryClient } from "@tanstack/react-query";
|
|
@@ -41,8 +42,10 @@ import {
|
|
|
41
42
|
useCurrentAccount,
|
|
42
43
|
ACCOUNT_QUERY_KEY,
|
|
43
44
|
} from "../modules/accounts/hooks/current-account.hook";
|
|
44
|
-
import { useWhitelabelUrls } from "./whitelabel.provider";
|
|
45
|
+
import { useWhitelabelUrls, useWhitelabel } from "./whitelabel.provider";
|
|
45
46
|
import { useWlQueryKey } from "../hooks/useAuthQueryKey";
|
|
47
|
+
import { normalizeLocale } from "../i18n/normalize";
|
|
48
|
+
import { setLocaleCookie } from "../utils/intl/locale-cookie";
|
|
46
49
|
|
|
47
50
|
interface AuthContextProps extends AuthState {
|
|
48
51
|
login: (credentials: LoginRequest) => Promise<LoginResult>;
|
|
@@ -68,8 +71,17 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
68
71
|
const queryClient = useQueryClient();
|
|
69
72
|
const setUserData = useSetUserData();
|
|
70
73
|
const { accountsUrl, pagesUrl } = useWhitelabelUrls();
|
|
74
|
+
const { whitelabel } = useWhitelabel();
|
|
71
75
|
const accountKey = useWlQueryKey([...ACCOUNT_QUERY_KEY]);
|
|
72
76
|
|
|
77
|
+
// Mantém o cookie NEXT_LOCALE alinhado à preferência da conta (login, sessão existente,
|
|
78
|
+
// atualização). Compartilhado entre subdomínios via domínio do whitelabel. A próxima
|
|
79
|
+
// navegação/requisição passa a resolver o locale pela preferência do usuário.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const locale = normalizeLocale(user?.language);
|
|
82
|
+
if (locale) setLocaleCookie(locale, whitelabel?.domain);
|
|
83
|
+
}, [user?.language, whitelabel?.domain]);
|
|
84
|
+
|
|
73
85
|
const { mutateAsync: loginMutate, isPending: isLogging } = useLogin();
|
|
74
86
|
const { mutateAsync: registerMutate, isPending: isRegistering } =
|
|
75
87
|
useRegister();
|
package/src/server.ts
CHANGED
|
@@ -74,6 +74,8 @@ export {
|
|
|
74
74
|
// Server Utils
|
|
75
75
|
export { getClientInfoFromRequest } from './infra/utils/client-info';
|
|
76
76
|
export { getUserContext } from './modules/auth/utils/get-user-context';
|
|
77
|
+
export { resolveLocale } from './i18n/resolve-locale';
|
|
78
|
+
export type { ResolveLocaleOptions } from './i18n/resolve-locale';
|
|
77
79
|
export { requireValidSession } from './modules/auth/utils/require-valid-session';
|
|
78
80
|
export { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';
|
|
79
81
|
export { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';
|
|
@@ -1,14 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param decimals - Casas decimais (padrão: 2)
|
|
5
|
-
* @returns String formatada como "R$ X.XXX,XX"
|
|
2
|
+
* Moeda de exibição. Espelha getCurrencyForGateway do backend (gapps-r3-api):
|
|
3
|
+
* 'stripe_usd' → USD; demais gateways → BRL.
|
|
6
4
|
*/
|
|
7
|
-
export
|
|
5
|
+
export type CurrencyCode = 'brl' | 'usd';
|
|
6
|
+
|
|
7
|
+
const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }> = {
|
|
8
|
+
brl: { locale: 'pt-BR', currency: 'BRL' },
|
|
9
|
+
usd: { locale: 'en-US', currency: 'USD' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Deriva a moeda de exibição a partir do gateway da conta.
|
|
14
|
+
* A moeda é definida pelo backend no roteamento de gateway — read-only para o usuário.
|
|
15
|
+
* @param gateway - account.gateway ('stripe_usd' | 'stripe_brl' | 'vindi' | ...)
|
|
16
|
+
*/
|
|
17
|
+
export const getCurrencyForGateway = (gateway?: string | null): CurrencyCode =>
|
|
18
|
+
gateway === 'stripe_usd' ? 'usd' : 'brl';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Formata um valor (string ou número) como moeda.
|
|
22
|
+
* String: assume centavos (remove não-dígitos e divide por 100) — comportamento legado.
|
|
23
|
+
* @param value - valor a formatar
|
|
24
|
+
* @param currency - moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
25
|
+
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
26
|
+
*/
|
|
27
|
+
export const formatCurrency = (value: string | number, currency: CurrencyCode = 'brl'): string => {
|
|
28
|
+
const fmt = CURRENCY_FORMAT[currency];
|
|
29
|
+
if (typeof value === 'number') {
|
|
30
|
+
return value.toLocaleString(fmt.locale, {
|
|
31
|
+
style: 'currency',
|
|
32
|
+
currency: fmt.currency,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const numericValue = value.replace(/\D/g, '');
|
|
37
|
+
const numberValue = parseFloat(numericValue) / 100;
|
|
38
|
+
return numberValue.toLocaleString(fmt.locale, {
|
|
39
|
+
style: 'currency',
|
|
40
|
+
currency: fmt.currency,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converte uma string de moeda para número (centavos / 100). Independente de moeda.
|
|
46
|
+
* @param value - String com valor de moeda
|
|
47
|
+
* @returns Número decimal
|
|
48
|
+
*/
|
|
49
|
+
export const parseCurrencyToNumber = (value: string): number => {
|
|
50
|
+
const numericValue = value.replace(/\D/g, '');
|
|
51
|
+
return parseFloat(numericValue) / 100;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Formata um número (opcional) como moeda.
|
|
56
|
+
* @param value - Número a formatar (pode ser undefined/null → 0)
|
|
57
|
+
* @param decimals - Casas decimais (padrão 2)
|
|
58
|
+
* @param currency - Moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
59
|
+
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
60
|
+
*/
|
|
61
|
+
export const formatCurrencyNumber = (value?: number | null, decimals = 2, currency: CurrencyCode = 'brl'): string => {
|
|
8
62
|
const safeValue = value ?? 0;
|
|
9
|
-
|
|
63
|
+
const fmt = CURRENCY_FORMAT[currency];
|
|
64
|
+
return safeValue.toLocaleString(fmt.locale, {
|
|
10
65
|
style: 'currency',
|
|
11
|
-
currency:
|
|
66
|
+
currency: fmt.currency,
|
|
12
67
|
minimumFractionDigits: decimals,
|
|
13
68
|
maximumFractionDigits: decimals,
|
|
14
69
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deriva o domínio de cookie compartilhável entre subdomínios a partir do domínio do
|
|
3
|
+
* whitelabel (ex.: `https://app.greatpages.com.br` → `.app.greatpages.com.br`).
|
|
4
|
+
*
|
|
5
|
+
* Mesma normalização usada pelo cookie de auth `greatapps` (auth.service.normalizeCookieDomain),
|
|
6
|
+
* para que o cookie de locale acompanhe o mesmo escopo. Pura (sem APIs de servidor) → client+server.
|
|
7
|
+
* Retorna `undefined` para localhost/host inválido (cookie fica host-scoped).
|
|
8
|
+
*/
|
|
9
|
+
export function cookieDomainFromDomain(domain?: string | null): string | undefined {
|
|
10
|
+
if (!domain) return undefined;
|
|
11
|
+
|
|
12
|
+
const normalized = domain.trim();
|
|
13
|
+
if (!normalized) return undefined;
|
|
14
|
+
|
|
15
|
+
let rawHost: string;
|
|
16
|
+
try {
|
|
17
|
+
rawHost = new URL(normalized).hostname;
|
|
18
|
+
} catch {
|
|
19
|
+
rawHost = normalized
|
|
20
|
+
.replace(/^https?:\/\//i, '')
|
|
21
|
+
.replace(/^www\./i, '')
|
|
22
|
+
.split('/')[0]
|
|
23
|
+
.split(':')[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!rawHost || rawHost === 'localhost') return undefined;
|
|
27
|
+
|
|
28
|
+
return rawHost.startsWith('.') ? rawHost : `.${rawHost}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LOCALE_COOKIE } from '../../i18n/config';
|
|
2
|
+
import { cookieDomainFromDomain } from './cookie-domain';
|
|
3
|
+
|
|
4
|
+
const ONE_YEAR = 60 * 60 * 24 * 365;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persiste o locale escolhido no cookie `NEXT_LOCALE` (lido por `resolveLocale` no servidor).
|
|
8
|
+
*
|
|
9
|
+
* Passe `whitelabelDomain` (de `useWhitelabel().whitelabel?.domain`) para o cookie ser
|
|
10
|
+
* compartilhado entre subdomínios (accounts. → pages. → apps.), no mesmo escopo do cookie
|
|
11
|
+
* de auth. Sem ele, o cookie fica host-scoped.
|
|
12
|
+
*/
|
|
13
|
+
export function setLocaleCookie(locale: string, whitelabelDomain?: string | null): void {
|
|
14
|
+
if (typeof document === 'undefined') return;
|
|
15
|
+
const domain = cookieDomainFromDomain(whitelabelDomain);
|
|
16
|
+
const domainAttr = domain ? `; domain=${domain}` : '';
|
|
17
|
+
document.cookie = `${LOCALE_COOKIE}=${locale}; path=/; max-age=${ONE_YEAR}; samesite=lax${domainAttr}`;
|
|
18
|
+
}
|
|
@@ -6,8 +6,9 @@ export type LocaleOption = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export const LANGUAGE_OPTIONS: LocaleOption[] = [
|
|
9
|
-
{ value: 'pt-
|
|
10
|
-
{ value: 'en-
|
|
9
|
+
{ value: 'pt-br', label: 'Português (Brasil)', displayValue: 'Português (Brasil)', apiValue: 'pt-br' },
|
|
10
|
+
{ value: 'en-us', label: 'Inglês (EUA)', displayValue: 'Inglês (EUA)', apiValue: 'en' },
|
|
11
|
+
{ value: 'es-es', label: 'Espanhol', displayValue: 'Espanhol', apiValue: 'es' },
|
|
11
12
|
];
|
|
12
13
|
|
|
13
14
|
export function buildLocaleOptions(): LocaleOption[] {
|