@greatapps/common 1.1.677 → 1.1.679

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.
Files changed (33) hide show
  1. package/dist/components/account/sections/PreferencesSection.mjs +8 -5
  2. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
  3. package/dist/components/layouts/AppMobileNavBar.mjs +148 -2
  4. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
  5. package/dist/components/modals/BuyCreditsModal.mjs +1 -1
  6. package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
  7. package/dist/components/ui/feedback/CircularProgress.mjs.map +1 -1
  8. package/dist/i18n/messages/en-us.mjs +2 -1
  9. package/dist/i18n/messages/en-us.mjs.map +1 -1
  10. package/dist/i18n/messages/es-es.mjs +2 -1
  11. package/dist/i18n/messages/es-es.mjs.map +1 -1
  12. package/dist/i18n/messages/pt-br.mjs +2 -1
  13. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  14. package/dist/modules/plans/handlers/list-plans.handler.mjs +2 -1
  15. package/dist/modules/plans/handlers/list-plans.handler.mjs.map +1 -1
  16. package/dist/modules/plans/services/plans.service.mjs +2 -2
  17. package/dist/modules/plans/services/plans.service.mjs.map +1 -1
  18. package/dist/modules/plans/types/plan.type.mjs +7 -4
  19. package/dist/modules/plans/types/plan.type.mjs.map +1 -1
  20. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +10 -4
  21. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/account/sections/PreferencesSection.tsx +8 -5
  24. package/src/components/layouts/AppMobileNavBar.tsx +154 -1
  25. package/src/components/modals/BuyCreditsModal.tsx +1 -1
  26. package/src/components/ui/feedback/CircularProgress.tsx +1 -1
  27. package/src/i18n/messages/en-us.ts +1 -0
  28. package/src/i18n/messages/es-es.ts +1 -0
  29. package/src/i18n/messages/pt-br.ts +1 -0
  30. package/src/modules/plans/handlers/list-plans.handler.ts +3 -1
  31. package/src/modules/plans/services/plans.service.ts +2 -2
  32. package/src/modules/plans/types/plan.type.ts +8 -4
  33. package/src/modules/plans/utils/map-api-plan-to-ui.ts +13 -6
@@ -44,7 +44,10 @@ function buildMainFeaturesFromItems(items, translate) {
44
44
  return { pages, domains, users };
45
45
  }
46
46
  function computePricingByPeriod(apiPlan, currency) {
47
- const monthly = currency === "usd" ? apiPlan.value_usd ?? apiPlan.value : apiPlan.value;
47
+ const monthly = apiPlan.value;
48
+ if (monthly == null) {
49
+ return { monthly: 0, semiannual: 0, annual: 0 };
50
+ }
48
51
  const round2 = (n) => Math.round(n * 100) / 100;
49
52
  return {
50
53
  monthly,
@@ -68,8 +71,10 @@ function buildUiPlan(plan, currency, translate) {
68
71
  ];
69
72
  const currencyAwareItems = plan.items.map((item) => ({
70
73
  ...item,
71
- value: currency === "usd" ? item.value_usd ?? item.value : item.value
74
+ value: item.value ?? 0
72
75
  }));
76
+ const priceUnavailable = plan.value == null;
77
+ const unavailableLabel = translate ? translate("common.plans.usdNotConfigured") : "USD n\xE3o configurado";
73
78
  return {
74
79
  planId: plan.id,
75
80
  name: plan.name,
@@ -77,9 +82,10 @@ function buildUiPlan(plan, currency, translate) {
77
82
  buttonVariant,
78
83
  mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
79
84
  features,
80
- originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
81
- price: formatCurrencyNumber(monthlyFinal, 2, currency),
85
+ originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
86
+ price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
82
87
  pricingByPeriod,
88
+ priceUnavailable,
83
89
  items: currencyAwareItems
84
90
  };
85
91
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {\n // Conta USD usa value_usd; fallback pro value (BRL) evita NaN quando o operador ainda\n // não cadastrou o preço internacional. Os descontos por período são percentuais — aplicam igual.\n const monthly = currency === \"usd\" ? (apiPlan.value_usd ?? apiPlan.value) : apiPlan.value;\n const round2 = (n: number) => Math.round(n * 100) / 100;\n return {\n monthly,\n semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),\n annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),\n };\n}\n\nfunction buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {\n const popular = plan.id === 2 || plan.id_plan === 2;\n const buttonVariant = popular ? \"brand\" : \"default\";\n\n const pricingByPeriod = computePricingByPeriod(plan, currency);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const lowerName = plan.name.toLowerCase();\n const hasProjectAndOnboarding =\n lowerName.includes(\"agência\") || lowerName.includes(\"agencia\");\n\n const onboardingText = translate\n ? translate(\"common.plans.features.onboarding\")\n : \"Reunião de Onboarding\";\n const projectMgmtText = translate\n ? translate(\"common.plans.features.projectManagement\")\n : \"Gestão de projetos\";\n\n const features: PlanFeature[] = [\n ...buildBaseFeatures(translate),\n hasProjectAndOnboarding\n ? { icon: \"check\", text: onboardingText }\n : { icon: \"x\", text: onboardingText, disabled: true },\n hasProjectAndOnboarding\n ? { icon: \"check\", text: projectMgmtText, tooltip: \"projectManagement\" }\n : { icon: \"x\", text: projectMgmtText, disabled: true, tooltip: \"projectManagement\" },\n ];\n\n const currencyAwareItems = plan.items.map((item) => ({\n ...item,\n value: currency === \"usd\" ? (item.value_usd ?? item.value) : item.value,\n }));\n\n return {\n planId: plan.id,\n name: plan.name,\n isPopular: popular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(plan.items, translate),\n features,\n originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),\n price: formatCurrencyNumber(monthlyFinal, 2, currency),\n pricingByPeriod,\n items: currencyAwareItems,\n };\n}\n\n/**\n * Mapeia um plano da API para UiPlan em BRL.\n * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.\n * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).\n */\nexport function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {\n return buildUiPlan(plan, \"brl\", translate);\n}\n\n/**\n * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).\n * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.\n * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.\n */\nexport const mapApiPlanToUiPlanForCurrency =\n (currency: CurrencyCode, translate?: TranslateFn) =>\n (plan: Plan): UiPlan =>\n buildUiPlan(plan, currency, translate);\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan, currency: CurrencyCode): PlanPricingByPeriod {\n // Backend agora resolve value por moeda da conta; mapper apenas lê.\n // value pode ser null (ex.: conta USD, preço USD não cadastrado).\n const monthly = apiPlan.value;\n if (monthly == null) {\n return { monthly: 0, semiannual: 0, annual: 0 };\n }\n const round2 = (n: number) => Math.round(n * 100) / 100;\n return {\n monthly,\n semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),\n annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),\n };\n}\n\nfunction buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {\n const popular = plan.id === 2 || plan.id_plan === 2;\n const buttonVariant = popular ? \"brand\" : \"default\";\n\n const pricingByPeriod = computePricingByPeriod(plan, currency);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const lowerName = plan.name.toLowerCase();\n const hasProjectAndOnboarding =\n lowerName.includes(\"agência\") || lowerName.includes(\"agencia\");\n\n const onboardingText = translate\n ? translate(\"common.plans.features.onboarding\")\n : \"Reunião de Onboarding\";\n const projectMgmtText = translate\n ? translate(\"common.plans.features.projectManagement\")\n : \"Gestão de projetos\";\n\n const features: PlanFeature[] = [\n ...buildBaseFeatures(translate),\n hasProjectAndOnboarding\n ? { icon: \"check\", text: onboardingText }\n : { icon: \"x\", text: onboardingText, disabled: true },\n hasProjectAndOnboarding\n ? { icon: \"check\", text: projectMgmtText, tooltip: \"projectManagement\" }\n : { icon: \"x\", text: projectMgmtText, disabled: true, tooltip: \"projectManagement\" },\n ];\n\n const currencyAwareItems = plan.items.map((item) => ({\n ...item,\n value: item.value ?? 0,\n }));\n\n const priceUnavailable = plan.value == null;\n const unavailableLabel = translate ? translate(\"common.plans.usdNotConfigured\") : \"USD não configurado\";\n\n return {\n planId: plan.id,\n name: plan.name,\n isPopular: popular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(plan.items, translate),\n features,\n originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n pricingByPeriod,\n priceUnavailable,\n items: currencyAwareItems,\n };\n}\n\n/**\n * Mapeia um plano da API para UiPlan em BRL.\n * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.\n * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).\n */\nexport function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {\n return buildUiPlan(plan, \"brl\", translate);\n}\n\n/**\n * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).\n * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.\n * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.\n */\nexport const mapApiPlanToUiPlanForCurrency =\n (currency: CurrencyCode, translate?: TranslateFn) =>\n (plan: Plan): UiPlan =>\n buildUiPlan(plan, currency, translate);\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.677",
3
+ "version": "1.1.679",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -30,6 +30,7 @@ import {
30
30
  } from '../constants';
31
31
  import { useTimezones } from '../../../modules/accounts/hooks/useTimezones';
32
32
  import { useCurrencies } from '../../../modules/accounts/hooks/useCurrencies';
33
+ import { getCurrencyForGateway } from '../../../utils/format/currency';
33
34
 
34
35
  interface PreferencesFormValues {
35
36
  companyName: string;
@@ -98,7 +99,9 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
98
99
  language: LANGUAGE_OPTIONS.find(
99
100
  (o) => o.apiValue === user.language?.toLowerCase()
100
101
  )?.value ?? 'pt-br',
101
- currency: account.currency,
102
+ // A moeda é derivada do gateway da conta (read-only para o usuário) —
103
+ // espelha o que useCurrencyFormatter usa no display de preços.
104
+ currency: getCurrencyForGateway(account.gateway).toUpperCase(),
102
105
  timezone: account.timezone,
103
106
  timeFormat: '24h',
104
107
  receive_sms: user.receive_sms ?? false,
@@ -145,7 +148,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
145
148
  const onSubmit = (data: PreferencesFormValues) => {
146
149
  const accountFields: UpdateAccountRequest = {
147
150
  ...(isOwner && dirtyFields.companyName && { name: data.companyName }),
148
- ...(canEditPreferences && dirtyFields.currency && { currency: data.currency }),
151
+ // currency é read-only (derivada do gateway) não enviada no update.
149
152
  ...(canEditPreferences && dirtyFields.timezone && { timezone: data.timezone }),
150
153
  };
151
154
  const globalFieldChanged = Object.keys(accountFields).length > 0;
@@ -205,9 +208,9 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
205
208
  containerClassName="w-full"
206
209
  options={currencies}
207
210
  value={watch('currency')}
208
- onChange={(value) => setValue('currency', value, { shouldDirty: true })}
209
- icon={!canEditPreferences ? <IconLock className="size-4 text-gray-400" /> : undefined}
210
- disabled={!canEditPreferences}
211
+ onChange={() => undefined}
212
+ icon={<IconLock className="size-4 text-gray-400" />}
213
+ disabled
211
214
  />
212
215
  </div>
213
216
 
@@ -4,13 +4,20 @@ import { ReactNode } from "react";
4
4
  import Image from "next/image";
5
5
  import { IconMenu2, IconX } from "@tabler/icons-react";
6
6
  import { ProfilePopover } from "./ProfilePopover";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
7
8
  import { useMobileNavbarSheet } from "../../store/useMobileNavbarSheet";
8
- import { useIsDefaultWhitelabel } from "../../providers/whitelabel.provider";
9
+ import {
10
+ useWhitelabel,
11
+ useIsDefaultWhitelabel,
12
+ } from "../../providers/whitelabel.provider";
13
+ import { cn } from "../../infra/utils/clsx";
9
14
  import type { ProfileMenuItem } from "./ProfilePopover";
10
15
 
11
16
  export interface AppMobileNavBarProps {
12
17
  /** Which app is currently active — controls which icon gets the indicator */
13
18
  activeApp?: "gapps" | "gpages";
19
+ /** Called when the GreatApps logo is clicked */
20
+ onLogoClick?: () => void;
14
21
  /** Called when the app icon (e.g. GreatPages) is clicked */
15
22
  onAppIconClick?: () => void;
16
23
  /** Extra slot rendered after the app icon (e.g. project selector) */
@@ -21,19 +28,165 @@ export interface AppMobileNavBarProps {
21
28
 
22
29
  export function AppMobileNavBar({
23
30
  activeApp,
31
+ onLogoClick,
24
32
  onAppIconClick,
25
33
  extraLeftSlot,
26
34
  menuItems = [],
27
35
  showMenuToggle = true,
28
36
  }: AppMobileNavBarProps) {
29
37
  const { isOpen, toggle } = useMobileNavbarSheet();
38
+ const { whitelabel } = useWhitelabel();
30
39
  const isDefaultWl = useIsDefaultWhitelabel();
40
+ const primaryColor = whitelabel?.general?.primary_color;
41
+ const wlName = whitelabel?.name || "Great";
31
42
 
43
+ const isGappsActive = activeApp === "gapps";
32
44
  const isGpagesActive = activeApp === "gpages";
33
45
 
46
+ const wlIcon = whitelabel?.secondary_logo ? (
47
+ <Image
48
+ className="size-7 object-contain"
49
+ src={`${process.env.NEXT_PUBLIC_STORAGE_URL}/${whitelabel.secondary_logo}`}
50
+ alt={`${wlName} Logo`}
51
+ width={28}
52
+ height={28}
53
+ />
54
+ ) : (
55
+ <div
56
+ className={cn(
57
+ "size-10 rounded-lg flex items-center justify-center transition-opacity duration-300",
58
+ !isGappsActive && "opacity-20",
59
+ )}
60
+ style={{ backgroundColor: primaryColor }}
61
+ >
62
+ <svg
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ width="24"
65
+ height="24"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ stroke="white"
69
+ strokeWidth="2"
70
+ strokeLinecap="round"
71
+ strokeLinejoin="round"
72
+ >
73
+ <path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
74
+ <path d="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
75
+ <path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
76
+ <path d="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
77
+ </svg>
78
+ </div>
79
+ );
80
+
34
81
  return (
35
82
  <div className="fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200">
36
83
  <div className="flex items-center gap-1.5">
84
+ {/* GreatApps logo */}
85
+ <div className="relative">
86
+ <Tooltip>
87
+ <TooltipTrigger asChild>
88
+ <div
89
+ className={cn(
90
+ "flex items-center justify-center",
91
+ onLogoClick ? "cursor-pointer" : "",
92
+ )}
93
+ onClick={onLogoClick}
94
+ >
95
+ {isDefaultWl ? (
96
+ <div className="size-10 flex items-center justify-center rounded-lg">
97
+ <Image
98
+ src="/icons/great-icon.svg"
99
+ alt="Great Icon"
100
+ width={28}
101
+ height={28}
102
+ />
103
+ </div>
104
+ ) : (
105
+ wlIcon
106
+ )}
107
+ </div>
108
+ </TooltipTrigger>
109
+ <TooltipContent side="bottom">{wlName}</TooltipContent>
110
+ </Tooltip>
111
+ <div
112
+ className={cn(
113
+ "absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] rounded-t-xl transition-opacity duration-300",
114
+ isDefaultWl ? "bg-gray-950" : "",
115
+ isGappsActive ? "opacity-100" : "opacity-0",
116
+ )}
117
+ style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
118
+ />
119
+ </div>
120
+
121
+ {/* GreatPages icon */}
122
+ <div className="relative">
123
+ <Tooltip>
124
+ <TooltipTrigger asChild>
125
+ <div
126
+ className={cn(
127
+ "size-10 rounded-lg flex items-center justify-center border-2 ring-2 transition-colors duration-300",
128
+ isGpagesActive
129
+ ? isDefaultWl
130
+ ? "bg-cyan-100 border-white ring-cyan-300"
131
+ : "border-white ring-transparent"
132
+ : isDefaultWl
133
+ ? "hover:bg-gray-100 border-transparent ring-transparent"
134
+ : "border-transparent ring-transparent",
135
+ onAppIconClick ? "cursor-pointer" : "",
136
+ )}
137
+ style={
138
+ !isDefaultWl
139
+ ? {
140
+ backgroundColor: primaryColor,
141
+ boxShadow: isGpagesActive
142
+ ? `0 0 0 2px ${primaryColor}40`
143
+ : undefined,
144
+ opacity: !isGpagesActive ? 0.2 : undefined,
145
+ }
146
+ : undefined
147
+ }
148
+ onClick={onAppIconClick}
149
+ >
150
+ {isDefaultWl ? (
151
+ <Image
152
+ src="/icons/icon-navbar.svg"
153
+ alt="GreatPages"
154
+ width={24}
155
+ height={24}
156
+ />
157
+ ) : (
158
+ <svg
159
+ xmlns="http://www.w3.org/2000/svg"
160
+ width="24"
161
+ height="24"
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="white"
165
+ strokeWidth="2"
166
+ strokeLinecap="round"
167
+ strokeLinejoin="round"
168
+ >
169
+ <path d="M4 8h16" />
170
+ <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" />
171
+ <path d="M8 4v4" />
172
+ </svg>
173
+ )}
174
+ </div>
175
+ </TooltipTrigger>
176
+ <TooltipContent side="bottom">
177
+ {isDefaultWl ? "GreatPages" : "Páginas"}
178
+ </TooltipContent>
179
+ </Tooltip>
180
+ <div
181
+ className={cn(
182
+ "absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] rounded-t-xl transition-opacity duration-300",
183
+ isDefaultWl ? "bg-cyan-300" : "",
184
+ isGpagesActive ? "opacity-100" : "opacity-0",
185
+ )}
186
+ style={!isDefaultWl ? { backgroundColor: primaryColor } : undefined}
187
+ />
188
+ </div>
189
+
37
190
  {extraLeftSlot}
38
191
  </div>
39
192
  <div className="flex items-center gap-1.5">
@@ -195,7 +195,7 @@ export default function BuyCreditsModal() {
195
195
  ),
196
196
  displayValue: translate('common.credits.buy.creditsOptionDisplayValue', { credits: opt.credits.toLocaleString('pt-BR') }),
197
197
  }));
198
- }, []);
198
+ }, [translate]);
199
199
 
200
200
  const onSubmit = useCallback(
201
201
  async (data: BuyCreditFormData) => {
@@ -36,7 +36,7 @@ export default function CircularProgress({
36
36
  aria-valuemax={total}
37
37
  // i18n-TODO: aria-label hardcoded — CircularProgress has no 'use client' directive and
38
38
  // receives no translate prop, so useTranslations() cannot be called here without
39
- // converting it to a client component. Key to use: common.forms.progressAriaLabel ('Progresso').
39
+ // converting it to a client component. Key to use: common.forms.circularProgress.ariaLabel ('Progresso').
40
40
  // Either add 'use client' + useTranslations(), or accept a translate prop from the caller.
41
41
  aria-label="Progresso"
42
42
  >
@@ -489,6 +489,7 @@ const messages = {
489
489
  domainsPlural: '{count} external domains',
490
490
  usersUnlimited: 'Unlimited users',
491
491
  },
492
+ usdNotConfigured: 'USD not configured',
492
493
  },
493
494
 
494
495
  // Periodicidade de cobrança.
@@ -490,6 +490,7 @@ const messages = {
490
490
  domainsPlural: '{count} dominios externos',
491
491
  usersUnlimited: 'Usuarios ilimitados',
492
492
  },
493
+ usdNotConfigured: 'USD no configurado',
493
494
  },
494
495
 
495
496
  // Periodicidade de cobrança.
@@ -412,6 +412,7 @@ const messages = {
412
412
  domainsPlural: '{count} domínios externos',
413
413
  usersUnlimited: 'Usuários ilimitados',
414
414
  },
415
+ usdNotConfigured: 'Internacional não configurado',
415
416
  },
416
417
 
417
418
  // Periodicidade de cobrança.
@@ -14,7 +14,9 @@ export function createListPlansHandler() {
14
14
 
15
15
  if (!apiPlans?.length) return [];
16
16
 
17
- return [...apiPlans].sort((a, b) => a.value - b.value);
17
+ // null/undefined (USD price não configurado) ordena por último — evita aparecer como "o mais barato"
18
+ const sortValue = (v: number | null | undefined) => (v == null ? Number.POSITIVE_INFINITY : v);
19
+ return [...apiPlans].sort((a, b) => sortValue(a.value) - sortValue(b.value));
18
20
  });
19
21
  };
20
22
  }
@@ -32,7 +32,7 @@ class PlansService {
32
32
  const sort = params?.sort ?? "id:ASC";
33
33
  const active = params?.active ?? "";
34
34
  const search = params?.search ?? "";
35
- return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v3`;
35
+ return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v4`;
36
36
  }
37
37
 
38
38
  /**
@@ -91,7 +91,7 @@ class PlansService {
91
91
 
92
92
  async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {
93
93
  const { id_wl, id_account } = await getUserContext();
94
- const cacheKey = this.buildCacheKey(`plan-${idPlan}`, {
94
+ const cacheKey = this.buildCacheKey(`plan-${idPlan}-v2`, {
95
95
  id_wl,
96
96
  id_account: String(id_account),
97
97
  });
@@ -5,7 +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
+ value_usd: z.number().nullish(),
9
9
  });
10
10
 
11
11
  export type PlanItem = z.infer<typeof PlanItemSchema>;
@@ -14,10 +14,13 @@ export const PlanSchema = z.object({
14
14
  id: z.number(),
15
15
  id_plan: z.number().optional(),
16
16
  name: z.string(),
17
- value: z.number().default(0),
17
+ value: z.number().nullable().default(0),
18
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(),
19
+ // sem suporte internacional. Backend agora resolve por moeda da conta (customer),
20
+ // omitindo value_usd para customer requests e adicionando currency. Admin paths
21
+ // ainda recebem value_usd como número (sem .coerce — backend joga agora como Number).
22
+ value_usd: z.number().nullish(),
23
+ currency: z.enum(['brl', 'usd']).optional(),
21
24
  type: z.string().optional(),
22
25
  discount_semester: z.number().default(0),
23
26
  discount_annual: z.number().default(0),
@@ -60,6 +63,7 @@ export type UiPlan = {
60
63
  price: string;
61
64
  pricingByPeriod?: PlanPricingByPeriod;
62
65
  buttonVariant?: 'brand' | 'default';
66
+ priceUnavailable?: boolean;
63
67
  items: PlanItem[];
64
68
  };
65
69
 
@@ -82,9 +82,12 @@ function buildMainFeaturesFromItems(
82
82
  }
83
83
 
84
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;
85
+ // Backend agora resolve value por moeda da conta; mapper apenas lê.
86
+ // value pode ser null (ex.: conta USD, preço USD não cadastrado).
87
+ const monthly = apiPlan.value;
88
+ if (monthly == null) {
89
+ return { monthly: 0, semiannual: 0, annual: 0 };
90
+ }
88
91
  const round2 = (n: number) => Math.round(n * 100) / 100;
89
92
  return {
90
93
  monthly,
@@ -123,9 +126,12 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
123
126
 
124
127
  const currencyAwareItems = plan.items.map((item) => ({
125
128
  ...item,
126
- value: currency === "usd" ? (item.value_usd ?? item.value) : item.value,
129
+ value: item.value ?? 0,
127
130
  }));
128
131
 
132
+ const priceUnavailable = plan.value == null;
133
+ const unavailableLabel = translate ? translate("common.plans.usdNotConfigured") : "USD não configurado";
134
+
129
135
  return {
130
136
  planId: plan.id,
131
137
  name: plan.name,
@@ -133,9 +139,10 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
133
139
  buttonVariant,
134
140
  mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
135
141
  features,
136
- originalPrice: formatCurrencyNumber(monthlyFinal, 2, currency),
137
- price: formatCurrencyNumber(monthlyFinal, 2, currency),
142
+ originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
143
+ price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),
138
144
  pricingByPeriod,
145
+ priceUnavailable,
139
146
  items: currencyAwareItems,
140
147
  };
141
148
  }