@greatapps/common 1.1.439 → 1.1.441

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 (37) hide show
  1. package/dist/components/embeds/CrispEmbed.mjs +51 -21
  2. package/dist/components/embeds/CrispEmbed.mjs.map +1 -1
  3. package/dist/components/embeds/FrillEmbed.mjs +7 -1
  4. package/dist/components/embeds/FrillEmbed.mjs.map +1 -1
  5. package/dist/components/modals/cards/AddCardModal.mjs +2 -5
  6. package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
  7. package/dist/index.mjs +9 -1
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/modules/plans/actions/list-plans.action.mjs +26 -0
  10. package/dist/modules/plans/actions/list-plans.action.mjs.map +1 -0
  11. package/dist/modules/plans/hooks/list-plans.hook.mjs +22 -0
  12. package/dist/modules/plans/hooks/list-plans.hook.mjs.map +1 -0
  13. package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs +18 -2
  14. package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
  15. package/dist/modules/plans/services/plans.service.mjs +60 -4
  16. package/dist/modules/plans/services/plans.service.mjs.map +1 -1
  17. package/dist/modules/plans/types/plan.type.mjs +1 -0
  18. package/dist/modules/plans/types/plan.type.mjs.map +1 -1
  19. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +58 -0
  20. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -0
  21. package/dist/server.mjs +2 -0
  22. package/dist/server.mjs.map +1 -1
  23. package/dist/utils/format/currency.mjs +13 -0
  24. package/dist/utils/format/currency.mjs.map +1 -0
  25. package/package.json +1 -1
  26. package/src/components/embeds/CrispEmbed.tsx +73 -29
  27. package/src/components/embeds/FrillEmbed.tsx +8 -1
  28. package/src/components/modals/cards/AddCardModal.tsx +1 -2
  29. package/src/index.ts +6 -2
  30. package/src/modules/plans/actions/list-plans.action.ts +28 -0
  31. package/src/modules/plans/hooks/list-plans.hook.ts +21 -0
  32. package/src/modules/plans/hooks/use-plan-by-id.hook.ts +26 -5
  33. package/src/modules/plans/services/plans.service.ts +87 -4
  34. package/src/modules/plans/types/plan.type.ts +46 -0
  35. package/src/modules/plans/utils/map-api-plan-to-ui.ts +89 -0
  36. package/src/server.ts +1 -0
  37. package/src/utils/format/currency.ts +15 -0
@@ -1,9 +1,63 @@
1
1
  import "server-only";
2
- import { api } from "../../../infra/api/client";
3
- import { ApiError } from "../../../infra/api/types";
4
- import { PlanSchema } from "../types/plan.type";
2
+ import { api, apiClient } from "@greatapps/common/server";
3
+ import { ApiError, buildQueryParams, PlanSchema } from "@greatapps/common";
4
+ import greatCache from "@greatapps/cache";
5
+ const PLANS_CACHE_TTL = 604800;
5
6
  class PlansService {
7
+ cache = new greatCache({
8
+ service: "plans-service",
9
+ version: "1.0.1",
10
+ domain: "whitelabel-cache.greatapps.com.br",
11
+ ambient: process.env.NODE_ENV || "development"
12
+ });
13
+ buildCacheKey(key, params) {
14
+ const sort = params?.sort ?? "id:ASC";
15
+ const active = params?.active ?? "";
16
+ const search = params?.search ?? "";
17
+ return `${key}-${sort}-${active}-${search}`;
18
+ }
19
+ /**
20
+ * Lista planos do whitelabel.
21
+ * Exemplo:
22
+ * GET /{id_wl}/plans?active=true&search=client&sort=id:ASC
23
+ */
24
+ async listPlans(params) {
25
+ const cacheKey = this.buildCacheKey("plans", params);
26
+ const cachedData = await this.cache.select(cacheKey);
27
+ if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
28
+ const data2 = JSON.parse(cachedData.data);
29
+ return { data: data2, total: data2.length, success: true };
30
+ }
31
+ const query = buildQueryParams({
32
+ sort: params?.sort ?? "id:ASC",
33
+ active: params?.active,
34
+ search: params?.search
35
+ });
36
+ const url = `/plans${query ? `?${query}` : ""}`;
37
+ const response = await apiClient.get(url);
38
+ if (response.status === 0) {
39
+ throw new ApiError(
40
+ response.message || "Erro ao listar planos",
41
+ "LIST_PLANS_FAILED",
42
+ 400
43
+ );
44
+ }
45
+ const rawData = response.data;
46
+ const data = Array.isArray(rawData) ? rawData.map((item) => PlanSchema.parse(item)) : [];
47
+ await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);
48
+ return {
49
+ data,
50
+ total: response.total,
51
+ success: true
52
+ };
53
+ }
6
54
  async findById(idPlan) {
55
+ const cacheKey = this.buildCacheKey(`plan-${idPlan}`);
56
+ const cachedData = await this.cache.select(cacheKey);
57
+ if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
58
+ const data = JSON.parse(cachedData.data);
59
+ return { data, success: true };
60
+ }
7
61
  const response = await api.apps.get(
8
62
  `/plans/${idPlan}?type=client`
9
63
  );
@@ -21,9 +75,11 @@ class PlansService {
21
75
  404
22
76
  );
23
77
  }
78
+ const plan = PlanSchema.parse(response.data[0]);
79
+ await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);
24
80
  return {
25
81
  success: true,
26
- data: PlanSchema.parse(response.data[0])
82
+ data: plan
27
83
  };
28
84
  }
29
85
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/plans/services/plans.service.ts"],"sourcesContent":["import 'server-only';\n\nimport { api } from '../../../infra/api/client';\nimport { ApiError, ApiPaginatedActionResult, SuccessResult } from '../../../infra/api/types';\nimport { Plan, PlanSchema } from '../types/plan.type';\n\nclass PlansService {\n async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {\n const response = await api.apps.get<ApiPaginatedActionResult<Plan>>(\n `/plans/${idPlan}?type=client`\n );\n\n if (response.status === 0) {\n throw new ApiError(\n response.message || 'Erro ao buscar plano',\n 'FIND_PLAN_FAILED',\n 400\n );\n }\n\n if (!response.data?.length) {\n throw new ApiError(\n 'Plano não encontrado',\n 'PLAN_NOT_FOUND',\n 404\n );\n }\n\n return {\n success: true,\n data: PlanSchema.parse(response.data[0]),\n };\n }\n}\n\nexport const plansService = new PlansService();\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,WAAW;AACpB,SAAS,gBAAyD;AAClE,SAAe,kBAAkB;AAEjC,MAAM,aAAa;AAAA,EACjB,MAAM,SAAS,QAAuD;AACpE,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,UAAU,MAAM;AAAA,IAClB;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,MAAM,QAAQ;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,WAAW,MAAM,SAAS,KAAK,CAAC,CAAC;AAAA,IACzC;AAAA,EACF;AACF;AAEO,MAAM,eAAe,IAAI,aAAa;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/plans/services/plans.service.ts"],"sourcesContent":["import 'server-only';\n\nimport { api, apiClient } from '@greatapps/common/server';\nimport { ApiError, buildQueryParams, PlanSchema } from '@greatapps/common';\nimport type { ApiPaginatedActionResult, PaginatedSuccessResult, Plan, SuccessResult } from '@greatapps/common';\nimport greatCache from '@greatapps/cache';\n\nexport type ListPlansParams = {\n sort?: string;\n active?: boolean;\n search?: string;\n};\n\nconst PLANS_CACHE_TTL = 604800;\n\nclass PlansService {\n private cache = new greatCache({\n service: 'plans-service',\n version: '1.0.1',\n domain: 'whitelabel-cache.greatapps.com.br',\n ambient: process.env.NODE_ENV || 'development',\n });\n\n private buildCacheKey(key: string, params?: ListPlansParams): string {\n const sort = params?.sort ?? 'id:ASC';\n const active = params?.active ?? '';\n const search = params?.search ?? '';\n return `${key}-${sort}-${active}-${search}`;\n }\n\n /**\n * Lista planos do whitelabel.\n * Exemplo:\n * GET /{id_wl}/plans?active=true&search=client&sort=id:ASC\n */\n async listPlans(params?: ListPlansParams): Promise<PaginatedSuccessResult<Plan>> {\n const cacheKey = this.buildCacheKey('plans', params);\n\n const cachedData = await this.cache.select(cacheKey);\n\n if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {\n const data = JSON.parse(cachedData.data) as Plan[];\n return { data, total: data.length, success: true };\n }\n\n const query = buildQueryParams({\n sort: params?.sort ?? 'id:ASC',\n active: params?.active,\n search: params?.search,\n });\n const url = `/plans${query ? `?${query}` : ''}`;\n\n const response = await apiClient.get<ApiPaginatedActionResult<Plan[]>>(url);\n\n if (response.status === 0) {\n throw new ApiError(\n (response as { message?: string }).message || 'Erro ao listar planos',\n 'LIST_PLANS_FAILED',\n 400\n );\n }\n\n const rawData = (response as { data?: unknown }).data;\n const data = Array.isArray(rawData)\n ? rawData.map((item) => PlanSchema.parse(item))\n : [];\n\n await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);\n\n return {\n data,\n total: response.total,\n success: true,\n } satisfies PaginatedSuccessResult<Plan>;\n }\n\n\n async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {\n const cacheKey = this.buildCacheKey(`plan-${idPlan}`);\n\n const cachedData = await this.cache.select(cacheKey);\n \n if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {\n const data = JSON.parse(cachedData.data) as Plan;\n return { data, success: true };\n }\n\n const response = await api.apps.get<ApiPaginatedActionResult<Plan>>(\n `/plans/${idPlan}?type=client`\n );\n\n if (response.status === 0) {\n throw new ApiError(\n response.message || 'Erro ao buscar plano',\n 'FIND_PLAN_FAILED',\n 400\n );\n }\n\n if (!response.data?.length) {\n throw new ApiError(\n 'Plano não encontrado',\n 'PLAN_NOT_FOUND',\n 404\n );\n }\n\n const plan = PlanSchema.parse(response.data[0]);\n\n await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);\n\n return {\n success: true,\n data: plan,\n };\n }\n}\n\nexport const plansService = new PlansService();\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,KAAK,iBAAiB;AAC/B,SAAS,UAAU,kBAAkB,kBAAkB;AAEvD,OAAO,gBAAgB;AAQvB,MAAM,kBAAkB;AAExB,MAAM,aAAa;AAAA,EACT,QAAQ,IAAI,WAAW;AAAA,IAC7B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,QAAQ,IAAI,YAAY;AAAA,EACnC,CAAC;AAAA,EAEO,cAAc,KAAa,QAAkC;AACnE,UAAM,OAAO,QAAQ,QAAQ;AAC7B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,SAAS,QAAQ,UAAU;AACjC,WAAO,GAAG,GAAG,IAAI,IAAI,IAAI,MAAM,IAAI,MAAM;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAU,QAAiE;AAC/E,UAAM,WAAW,KAAK,cAAc,SAAS,MAAM;AAEnD,UAAM,aAAa,MAAM,KAAK,MAAM,OAAO,QAAQ;AAEnD,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,YAAMA,QAAO,KAAK,MAAM,WAAW,IAAI;AACvC,aAAO,EAAE,MAAAA,OAAM,OAAOA,MAAK,QAAQ,SAAS,KAAK;AAAA,IACnD;AAEA,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,MAAM,QAAQ,QAAQ;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,UAAM,MAAM,SAAS,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE7C,UAAM,WAAW,MAAM,UAAU,IAAsC,GAAG;AAE1E,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACP,SAAkC,WAAW;AAAA,QAC9C;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAW,SAAgC;AACjD,UAAM,OAAO,MAAM,QAAQ,OAAO,IAC9B,QAAQ,IAAI,CAAC,SAAS,WAAW,MAAM,IAAI,CAAC,IAC5C,CAAC;AAEL,UAAM,KAAK,MAAM,OAAO,UAAU,KAAK,UAAU,IAAI,GAAG,eAAe;AAEvE,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAGA,MAAM,SAAS,QAAuD;AACpE,UAAM,WAAW,KAAK,cAAc,QAAQ,MAAM,EAAE;AAEpD,UAAM,aAAa,MAAM,KAAK,MAAM,OAAO,QAAQ;AAEnD,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,YAAM,OAAO,KAAK,MAAM,WAAW,IAAI;AACvC,aAAO,EAAE,MAAM,SAAS,KAAK;AAAA,IAC/B;AAEA,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,UAAU,MAAM;AAAA,IAClB;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,MAAM,QAAQ;AAC1B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,WAAW,MAAM,SAAS,KAAK,CAAC,CAAC;AAE9C,UAAM,KAAK,MAAM,OAAO,UAAU,KAAK,UAAU,IAAI,GAAG,eAAe;AAEvE,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,MAAM,eAAe,IAAI,aAAa;","names":["data"]}
@@ -6,6 +6,7 @@ const PlanItemSchema = z.object({
6
6
  });
7
7
  const PlanSchema = z.object({
8
8
  id: z.number(),
9
+ id_plan: z.number().optional(),
9
10
  name: z.string(),
10
11
  value: z.number().default(0),
11
12
  type: z.string().optional(),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/plans/types/plan.type.ts"],"sourcesContent":["import z from 'zod';\n\nexport const PlanItemSchema = z.object({\n id_addon: z.number(),\n quantity: z.number(),\n value: z.number(),\n});\n\nexport type PlanItem = z.infer<typeof PlanItemSchema>;\n\nexport const PlanSchema = z.object({\n id: z.number(),\n name: z.string(),\n value: z.number().default(0),\n type: z.string().optional(),\n discount_semester: z.number().default(0),\n discount_annual: z.number().default(0),\n items: z.array(PlanItemSchema).optional().default([]),\n});\n\nexport type Plan = z.infer<typeof PlanSchema>;\n"],"mappings":"AAAA,OAAO,OAAO;AAEP,MAAM,iBAAiB,EAAE,OAAO;AAAA,EACrC,UAAU,EAAE,OAAO;AAAA,EACnB,UAAU,EAAE,OAAO;AAAA,EACnB,OAAO,EAAE,OAAO;AAClB,CAAC;AAIM,MAAM,aAAa,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,OAAO;AAAA,EACb,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EAC3B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,mBAAmB,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EACvC,iBAAiB,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EACrC,OAAO,EAAE,MAAM,cAAc,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/plans/types/plan.type.ts"],"sourcesContent":["import z from 'zod';\nimport type { BillingPeriod } from '../../subscriptions/types/billing-period.type';\n\nexport const PlanItemSchema = z.object({\n id_addon: z.number(),\n quantity: z.number(),\n value: z.number(),\n});\n\nexport type PlanItem = z.infer<typeof PlanItemSchema>;\n\nexport const PlanSchema = z.object({\n id: z.number(),\n id_plan: z.number().optional(),\n name: z.string(),\n value: z.number().default(0),\n type: z.string().optional(),\n discount_semester: z.number().default(0),\n discount_annual: z.number().default(0),\n items: z.array(PlanItemSchema).optional().default([]),\n});\n\nexport type Plan = z.infer<typeof PlanSchema>;\n\n// ── UI Plan types (used by list-plans action and consumers) ──\n\nexport type PlanFeature = {\n icon: 'check' | 'x';\n text: string;\n disabled?: boolean;\n tooltip?: keyof PlanTooltips;\n};\n\nexport type PlanMainFeatures = {\n pages: string;\n domains: string;\n users: string;\n};\n\n/** Preços mensais equivalentes por período de cobrança (já com desconto aplicado). */\nexport type PlanPricingByPeriod = {\n monthly: number;\n semiannual: number;\n annual: number;\n};\n\nexport type UiPlan = {\n idPlan: number;\n name: string;\n isPopular?: boolean;\n isCurrentPlan?: boolean;\n currentPeriodicity?: BillingPeriod | null;\n mainFeatures: PlanMainFeatures;\n features: PlanFeature[];\n originalPrice: string;\n price: string;\n pricingByPeriod?: PlanPricingByPeriod;\n buttonVariant?: 'brand' | 'default';\n items: PlanItem[];\n};\n\nexport type PlanTooltips = {\n domains: string;\n users: string;\n sharePages: string;\n projectManagement: string;\n};\n"],"mappings":"AAAA,OAAO,OAAO;AAGP,MAAM,iBAAiB,EAAE,OAAO;AAAA,EACrC,UAAU,EAAE,OAAO;AAAA,EACnB,UAAU,EAAE,OAAO;AAAA,EACnB,OAAO,EAAE,OAAO;AAClB,CAAC;AAIM,MAAM,aAAa,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,OAAO;AAAA,EACb,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EAC3B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,mBAAmB,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EACvC,iBAAiB,EAAE,OAAO,EAAE,QAAQ,CAAC;AAAA,EACrC,OAAO,EAAE,MAAM,cAAc,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC;","names":[]}
@@ -0,0 +1,58 @@
1
+ import { ADDON_IDS } from "../../subscriptions/constants/addons.constants";
2
+ import { formatCurrencyNumber } from "../../../utils/format/currency";
3
+ const BASE_FEATURES = [
4
+ { icon: "check", text: "Visitas ilimitadas" },
5
+ { icon: "check", text: "Leads ilimitadas" },
6
+ { icon: "check", text: "Compartilhar p\xE1ginas", tooltip: "sharePages" },
7
+ { icon: "check", text: "Hospedagem inclusa" },
8
+ { icon: "check", text: "SSL (HTTPS) + CDN" },
9
+ { icon: "check", text: "Templates Free" }
10
+ ];
11
+ function buildMainFeaturesFromItems(items) {
12
+ const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);
13
+ const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);
14
+ const pagesQty = pagesItem?.quantity ?? 0;
15
+ const domainsQty = domainsItem?.quantity ?? 1;
16
+ return {
17
+ pages: pagesQty === 0 ? "P\xE1ginas ilimitadas" : pagesQty === 1 ? "1 p\xE1gina" : `${pagesQty} p\xE1ginas`,
18
+ domains: domainsQty >= 999 ? "Dom\xEDnios ilimitados" : domainsQty === 1 ? "1 dom\xEDnio externo" : `${domainsQty} dom\xEDnios externos`,
19
+ users: "Usu\xE1rios ilimitados"
20
+ };
21
+ }
22
+ function computePricingByPeriod(apiPlan) {
23
+ const monthly = apiPlan.value;
24
+ const round2 = (n) => Math.round(n * 100) / 100;
25
+ return {
26
+ monthly,
27
+ semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),
28
+ annual: round2(monthly * (1 - apiPlan.discount_annual / 100))
29
+ };
30
+ }
31
+ function mapApiPlanToUiPlan(apiPlan, options) {
32
+ const { planId, isPopular, buttonVariant } = options;
33
+ const pricingByPeriod = computePricingByPeriod(apiPlan);
34
+ const monthlyFinal = pricingByPeriod.monthly;
35
+ const lowerName = apiPlan.name.toLowerCase();
36
+ const hasProjectAndOnboarding = lowerName.includes("ag\xEAncia") || lowerName.includes("agencia");
37
+ const features = [
38
+ ...BASE_FEATURES,
39
+ hasProjectAndOnboarding ? { icon: "check", text: "Reuni\xE3o de Onboarding" } : { icon: "x", text: "Reuni\xE3o de Onboarding", disabled: true },
40
+ hasProjectAndOnboarding ? { icon: "check", text: "Gest\xE3o de projetos", tooltip: "projectManagement" } : { icon: "x", text: "Gest\xE3o de projetos", disabled: true, tooltip: "projectManagement" }
41
+ ];
42
+ return {
43
+ idPlan: planId,
44
+ name: apiPlan.name,
45
+ isPopular,
46
+ buttonVariant,
47
+ mainFeatures: buildMainFeaturesFromItems(apiPlan.items),
48
+ features,
49
+ originalPrice: formatCurrencyNumber(monthlyFinal),
50
+ price: formatCurrencyNumber(monthlyFinal),
51
+ pricingByPeriod,
52
+ items: apiPlan.items
53
+ };
54
+ }
55
+ export {
56
+ mapApiPlanToUiPlan
57
+ };
58
+ //# sourceMappingURL=map-api-plan-to-ui.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type { Plan, PlanFeature, PlanMainFeatures, PlanPricingByPeriod, UiPlan } from '../types/plan.type';\nimport { ADDON_IDS } from '../../subscriptions/constants/addons.constants';\nimport { formatCurrencyNumber } from '../../../utils/format/currency';\n\nexport type PlanBillingPeriod = 'monthly' | 'semiannual' | 'annual';\n\nconst BASE_FEATURES: PlanFeature[] = [\n { icon: 'check', text: 'Visitas ilimitadas' },\n { icon: 'check', text: 'Leads ilimitadas' },\n { icon: 'check', text: 'Compartilhar páginas', tooltip: 'sharePages' },\n { icon: 'check', text: 'Hospedagem inclusa' },\n { icon: 'check', text: 'SSL (HTTPS) + CDN' },\n { icon: 'check', text: 'Templates Free' },\n];\n\nfunction buildMainFeaturesFromItems(items: Plan['items']): 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 return {\n pages:\n pagesQty === 0\n ? 'Páginas ilimitadas'\n : pagesQty === 1\n ? '1 página'\n : `${pagesQty} páginas`,\n domains:\n domainsQty >= 999\n ? 'Domínios ilimitados'\n : domainsQty === 1\n ? '1 domínio externo'\n : `${domainsQty} domínios externos`,\n users: 'Usuários ilimitados',\n };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {\n const monthly = 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\nexport function mapApiPlanToUiPlan(\n apiPlan: Plan,\n options: {\n planId: number;\n isPopular?: boolean;\n buttonVariant?: 'brand' | 'default';\n }\n): UiPlan {\n const { planId, isPopular, buttonVariant } = options;\n\n const pricingByPeriod = computePricingByPeriod(apiPlan);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const lowerName = apiPlan.name.toLowerCase();\n const hasProjectAndOnboarding =\n lowerName.includes('agência') || lowerName.includes('agencia');\n\n const features: PlanFeature[] = [\n ...BASE_FEATURES,\n hasProjectAndOnboarding\n ? { icon: 'check', text: 'Reunião de Onboarding' }\n : { icon: 'x', text: 'Reunião de Onboarding', disabled: true },\n hasProjectAndOnboarding\n ? { icon: 'check', text: 'Gestão de projetos', tooltip: 'projectManagement' }\n : { icon: 'x', text: 'Gestão de projetos', disabled: true, tooltip: 'projectManagement' },\n ];\n\n return {\n idPlan: planId,\n name: apiPlan.name,\n isPopular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(apiPlan.items),\n features,\n originalPrice: formatCurrencyNumber(monthlyFinal),\n price: formatCurrencyNumber(monthlyFinal),\n pricingByPeriod,\n items: apiPlan.items,\n };\n}\n"],"mappings":"AACA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AAIrC,MAAM,gBAA+B;AAAA,EACnC,EAAE,MAAM,SAAS,MAAM,qBAAqB;AAAA,EAC5C,EAAE,MAAM,SAAS,MAAM,mBAAmB;AAAA,EAC1C,EAAE,MAAM,SAAS,MAAM,2BAAwB,SAAS,aAAa;AAAA,EACrE,EAAE,MAAM,SAAS,MAAM,qBAAqB;AAAA,EAC5C,EAAE,MAAM,SAAS,MAAM,oBAAoB;AAAA,EAC3C,EAAE,MAAM,SAAS,MAAM,iBAAiB;AAC1C;AAEA,SAAS,2BAA2B,OAAwC;AAC1E,QAAM,YAAY,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,UAAU,KAAK;AAClE,QAAM,cAAc,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,UAAU,OAAO;AAEtE,QAAM,WAAW,WAAW,YAAY;AACxC,QAAM,aAAa,aAAa,YAAY;AAE5C,SAAO;AAAA,IACL,OACE,aAAa,IACT,0BACA,aAAa,IACX,gBACA,GAAG,QAAQ;AAAA,IACnB,SACE,cAAc,MACV,2BACA,eAAe,IACb,yBACA,GAAG,UAAU;AAAA,IACrB,OAAO;AAAA,EACT;AACF;AAEA,SAAS,uBAAuB,SAAoC;AAClE,QAAM,UAAU,QAAQ;AACxB,QAAM,SAAS,CAAC,MAAc,KAAK,MAAM,IAAI,GAAG,IAAI;AACpD,SAAO;AAAA,IACL;AAAA,IACA,YAAY,OAAO,WAAW,IAAI,QAAQ,oBAAoB,IAAI;AAAA,IAClE,QAAQ,OAAO,WAAW,IAAI,QAAQ,kBAAkB,IAAI;AAAA,EAC9D;AACF;AAEO,SAAS,mBACd,SACA,SAKQ;AACR,QAAM,EAAE,QAAQ,WAAW,cAAc,IAAI;AAE7C,QAAM,kBAAkB,uBAAuB,OAAO;AACtD,QAAM,eAAe,gBAAgB;AAErC,QAAM,YAAY,QAAQ,KAAK,YAAY;AAC3C,QAAM,0BACJ,UAAU,SAAS,YAAS,KAAK,UAAU,SAAS,SAAS;AAE/D,QAAM,WAA0B;AAAA,IAC9B,GAAG;AAAA,IACH,0BACI,EAAE,MAAM,SAAS,MAAM,2BAAwB,IAC/C,EAAE,MAAM,KAAK,MAAM,4BAAyB,UAAU,KAAK;AAAA,IAC/D,0BACI,EAAE,MAAM,SAAS,MAAM,yBAAsB,SAAS,oBAAoB,IAC1E,EAAE,MAAM,KAAK,MAAM,yBAAsB,UAAU,MAAM,SAAS,oBAAoB;AAAA,EAC5F;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,QAAQ;AAAA,IACd;AAAA,IACA;AAAA,IACA,cAAc,2BAA2B,QAAQ,KAAK;AAAA,IACtD;AAAA,IACA,eAAe,qBAAqB,YAAY;AAAA,IAChD,OAAO,qBAAqB,YAAY;AAAA,IACxC;AAAA,IACA,OAAO,QAAQ;AAAA,EACjB;AACF;","names":[]}
package/dist/server.mjs CHANGED
@@ -17,6 +17,7 @@ import { findUserById } from "./modules/users/action/find-user-by-id.action";
17
17
  import { findCurrentAccount } from "./modules/accounts/actions/find-current-account.action";
18
18
  import { listSubscriptionsAction } from "./modules/subscriptions/actions/list-subscriptions.action";
19
19
  import { findPlanByIdAction } from "./modules/plans/actions/find-plan-by-id.action";
20
+ import { listPlansAction } from "./modules/plans/actions/list-plans.action";
20
21
  import { calculateSubscriptionAction } from "./modules/subscriptions/actions/calculate-subscription.action";
21
22
  import { updateSubscriptionPlanAction } from "./modules/subscriptions/actions/update-subscription-plan.action";
22
23
  import { listCardsAction } from "./modules/cards/actions/list-cards.action";
@@ -89,6 +90,7 @@ export {
89
90
  listAvailableUsersAction,
90
91
  listCardsAction,
91
92
  listIaCreditsAction,
93
+ listPlansAction,
92
94
  listProjectUsersAction,
93
95
  listSubscriptionsAction,
94
96
  plansService,
@@ -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 { 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 { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { findPlanByIdAction } from './modules/plans/actions/find-plan-by-id.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { listCardsAction } from './modules/cards/actions/list-cards.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.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 { listAvailableUsersAction } from './modules/projects/actions/list-available-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 { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\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,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,gCAAgC;AACzC,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,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;","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 { 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 { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { findPlanByIdAction } from './modules/plans/actions/find-plan-by-id.action';\r\nexport { listPlansAction } from './modules/plans/actions/list-plans.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { listCardsAction } from './modules/cards/actions/list-cards.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { listIaCreditsAction } from './modules/ia-credits/actions/list-ia-credits.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 { listAvailableUsersAction } from './modules/projects/actions/list-available-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 { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\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,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,gCAAgC;AACzC,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,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;","names":[]}
@@ -0,0 +1,13 @@
1
+ const formatCurrencyNumber = (value, decimals = 2) => {
2
+ const safeValue = value ?? 0;
3
+ return safeValue.toLocaleString("pt-BR", {
4
+ style: "currency",
5
+ currency: "BRL",
6
+ minimumFractionDigits: decimals,
7
+ maximumFractionDigits: decimals
8
+ });
9
+ };
10
+ export {
11
+ formatCurrencyNumber
12
+ };
13
+ //# sourceMappingURL=currency.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/format/currency.ts"],"sourcesContent":["/**\n * Formata um número (opcional) para moeda BRL\n * @param value - Número a ser formatado (pode ser undefined ou null)\n * @param decimals - Casas decimais (padrão: 2)\n * @returns String formatada como \"R$ X.XXX,XX\"\n */\nexport const formatCurrencyNumber = (value?: number | null, decimals = 2): string => {\n const safeValue = value ?? 0;\n return safeValue.toLocaleString('pt-BR', {\n style: 'currency',\n currency: 'BRL',\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n });\n};\n"],"mappings":"AAMO,MAAM,uBAAuB,CAAC,OAAuB,WAAW,MAAc;AACnF,QAAM,YAAY,SAAS;AAC3B,SAAO,UAAU,eAAe,SAAS;AAAA,IACvC,OAAO;AAAA,IACP,UAAU;AAAA,IACV,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,EACzB,CAAC;AACH;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.439",
3
+ "version": "1.1.441",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -7,29 +7,80 @@ import { useActiveSubscription } from '../../modules/subscriptions/hooks/find-ac
7
7
 
8
8
  const V4_WHITELABEL_ID = 367568;
9
9
 
10
+ const PROFILE_LABELS: Record<string, string> = {
11
+ owner: 'Proprietário',
12
+ admin: 'Administrador',
13
+ editor: 'Colaborador',
14
+ viewer: 'Visualizador',
15
+ };
16
+
10
17
  const CRISP_WEBSITE_ID = '3339a3ec-b4c6-4aac-8b86-45668fb7655c';
11
18
  const CRISP_SCRIPT_URL = 'https://client.crisp.chat/l.js';
12
19
  const CRISP_LOAD_DELAY = 800;
13
20
 
14
- function safeCrispPush(args: any[]) {
21
+ // ── Crisp helpers ──────────────────────────────────────────────
22
+
23
+ function safeCrispPush(args: unknown[]) {
15
24
  try {
16
25
  const $crisp = (window as any)?.$crisp;
17
26
  if ($crisp) $crisp.push(args);
18
27
  } catch {}
19
28
  }
20
29
 
30
+ function crispDo(action: string) {
31
+ safeCrispPush(['do', action]);
32
+ }
33
+
34
+ function crispSegment(segment: string) {
35
+ safeCrispPush(['set', 'session:segments', [[segment]]]);
36
+ }
37
+
38
+ function crispSessionData(key: string, value: string) {
39
+ safeCrispPush(['set', 'session:data', [[[key, value]]]]);
40
+ }
41
+
42
+ // ── Public API ─────────────────────────────────────────────────
43
+
21
44
  export function openCrispHelpdesk() {
22
- safeCrispPush(['do', 'helpdesk:search']);
45
+ crispDo('helpdesk:search');
23
46
  }
24
47
 
25
48
  export function hideCrisp() {
26
- safeCrispPush(['do', 'chat:hide']);
49
+ crispDo('chat:hide');
27
50
  }
28
51
 
29
52
  export function showCrisp() {
30
- safeCrispPush(['do', 'chat:show']);
53
+ crispDo('chat:show');
54
+ }
55
+
56
+ export function updateCrispUser(data: {
57
+ email?: string;
58
+ name?: string;
59
+ lastName?: string;
60
+ company?: string;
61
+ ddi?: string;
62
+ phone?: string;
63
+ photo?: string;
64
+ }) {
65
+ try {
66
+ if (data.email) safeCrispPush(['set', 'user:email', data.email]);
67
+
68
+ const fullName = `${data.name ?? ''} ${data.lastName ?? ''}`.trim();
69
+ if (fullName) safeCrispPush(['set', 'user:nickname', fullName]);
70
+
71
+ if (data.company) safeCrispPush(['set', 'user:company', data.company]);
72
+
73
+ const phone = `${data.ddi ?? ''}${data.phone ?? ''}`.trim();
74
+ if (phone) safeCrispPush(['set', 'user:phone', phone]);
75
+
76
+ if (data.photo?.startsWith('http')) {
77
+ safeCrispPush(['set', 'user:avatar', data.photo]);
78
+ }
79
+ } catch {}
31
80
  }
32
81
 
82
+ // ── Component ──────────────────────────────────────────────────
83
+
33
84
  export function CrispEmbed() {
34
85
  const { user, account } = useAuth();
35
86
  const { whitelabel } = useWhitelabel();
@@ -46,15 +97,12 @@ export function CrispEmbed() {
46
97
  try {
47
98
  const win = window as any;
48
99
 
49
- // Only initialize if Crisp hasn't been loaded yet — avoids
50
- // wiping an already-active Crisp instance on component remount
51
100
  if (!win.$crisp) {
52
101
  win.$crisp = [];
53
102
  win.CRISP_WEBSITE_ID = CRISP_WEBSITE_ID;
54
103
  win.$crisp.push(['config', 'container:index', [100]]);
55
104
  }
56
105
 
57
- // Script already injected (previous mount) — just mark as loaded
58
106
  if (document.querySelector(`script[src="${CRISP_SCRIPT_URL}"]`)) {
59
107
  scriptLoadedRef.current = true;
60
108
  return;
@@ -78,32 +126,28 @@ export function CrispEmbed() {
78
126
  // Identify user
79
127
  useEffect(() => {
80
128
  try {
81
- const $crisp = (window as any)?.$crisp;
82
- if (!user || !$crisp) return;
83
-
84
- if (user.email) $crisp.push(['set', 'user:email', user.email]);
85
-
86
- const fullName = `${user.name ?? ''} ${user.last_name ?? ''}`.trim();
87
- if (fullName) $crisp.push(['set', 'user:nickname', fullName]);
88
-
89
- if (account?.name) $crisp.push(['set', 'user:company', account.name]);
90
-
91
- const phone = `${user.ddi ?? ''}${user.phone ?? ''}`.trim();
92
- if (phone) $crisp.push(['set', 'user:phone', phone]);
93
-
94
- if (user.photo && user.photo.startsWith('http')) {
95
- $crisp.push(['set', 'user:avatar', user.photo]);
96
- }
129
+ if (!user || !(window as any)?.$crisp) return;
130
+
131
+ updateCrispUser({
132
+ email: user.email,
133
+ name: user.name,
134
+ lastName: user.last_name,
135
+ company: account?.name,
136
+ ddi: user.ddi,
137
+ phone: user.phone,
138
+ photo: user.photo,
139
+ });
97
140
 
98
141
  if (isV4) {
99
- $crisp.push(['set', 'session:segments', [['company_v4']]]);
100
- $crisp.push(['set', 'session:data', [[['Company', 'V4']]]]);
142
+ crispSegment('company_v4');
143
+ crispSessionData('Company', 'V4');
101
144
  } else {
102
145
  const planName = subscription?.plan_name ?? '';
103
- if (planName) $crisp.push(['set', 'session:segments', [[`plano_${planName}`]]]);
104
- if (user.profile) $crisp.push(['set', 'session:segments', [[`usuario_${user.profile}`]]]);
105
- if (planName) $crisp.push(['set', 'session:data', [[['Plano', planName]]]]);
106
- if (user.profile) $crisp.push(['set', 'session:data', [[['Usuario', user.profile]]]]);
146
+ const profileLabel = PROFILE_LABELS[user.profile ?? ''] ?? user.profile ?? '';
147
+ if (planName) crispSegment(`plano_${planName}`);
148
+ if (profileLabel) crispSegment(`usuario_${profileLabel}`);
149
+ if (planName) crispSessionData('Plano', planName);
150
+ if (profileLabel) crispSessionData('Usuario', profileLabel);
107
151
  }
108
152
  } catch {}
109
153
  }, [user, account, isV4, subscription?.plan_name]);
@@ -7,6 +7,13 @@ import { useActiveSubscription } from '../../modules/subscriptions/hooks/find-ac
7
7
 
8
8
  const V4_WHITELABEL_ID = 367568;
9
9
 
10
+ const PROFILE_LABELS: Record<string, string> = {
11
+ owner: 'Proprietário',
12
+ admin: 'Administrador',
13
+ editor: 'Colaborador',
14
+ viewer: 'Visualizador',
15
+ };
16
+
10
17
  const FRILL_SCRIPT_URL = 'https://widget.frill.co/v2/container.js';
11
18
  const FRILL_KEY = '2f77f232-a120-438e-8ef6-01912ffd1b99';
12
19
 
@@ -91,7 +98,7 @@ export function FrillEmbed() {
91
98
  }
92
99
  userData.attributes = isV4
93
100
  ? { company: 'V4' }
94
- : { profile: user.profile ?? '', plan: subscription?.plan_name ?? '' };
101
+ : { profile: PROFILE_LABELS[user.profile ?? ''] ?? user.profile ?? '', plan: subscription?.plan_name ?? '' };
95
102
 
96
103
  const config: Record<string, any> = { key: FRILL_KEY, user: userData };
97
104
  if (!isV4) {
@@ -9,7 +9,7 @@ import { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';
9
9
  import { useUpdateAccount } from '../../../modules/accounts/hooks/useAccountManagement';
10
10
  import { CardFormFields, BillingFormFields, cardFormSchema } from './CardFormFields';
11
11
  import type { CardFormData, StripeElementsStatus } from './CardFormFields';
12
- import { IconLoader2, IconX } from '@tabler/icons-react';
12
+ import { IconX } from '@tabler/icons-react';
13
13
  import { useForm } from 'react-hook-form';
14
14
  import { zodResolver } from '@hookform/resolvers/zod';
15
15
  import { CardNumberElement, useElements, useStripe } from '@stripe/react-stripe-js';
@@ -248,7 +248,6 @@ export default function AddCardModal() {
248
248
  Voltar
249
249
  </Button>
250
250
  <Button type="submit" className="h-10!" disabled={isSubmitting || !isStep2Valid}>
251
- {isSubmitting && <IconLoader2 className="size-4 animate-spin" />}
252
251
  {isSubmitting ? 'Adicionando cartão...' : 'Adicionar cartão'}
253
252
  </Button>
254
253
  </>
package/src/index.ts CHANGED
@@ -64,8 +64,11 @@ export {
64
64
  } from "./modules/subscriptions/types/subscription.type";
65
65
  export { ADDON_IDS, AI_CREDIT_OPTIONS, AI_CREDIT_VALUES } from "./modules/subscriptions/constants/addons.constants";
66
66
  export type { AddonKindEnum, AiCreditOption } from "./modules/subscriptions/constants/addons.constants";
67
- export type { Plan, PlanItem } from "./modules/plans/types/plan.type";
67
+ export type { Plan, PlanItem, UiPlan, PlanFeature, PlanMainFeatures, PlanPricingByPeriod, PlanTooltips } from "./modules/plans/types/plan.type";
68
68
  export { PlanSchema, PlanItemSchema } from "./modules/plans/types/plan.type";
69
+ export { mapApiPlanToUiPlan } from "./modules/plans/utils/map-api-plan-to-ui";
70
+ export type { PlanBillingPeriod } from "./modules/plans/utils/map-api-plan-to-ui";
71
+ export { usePlans, PLANS_QUERY_KEY } from "./modules/plans/hooks/list-plans.hook";
69
72
  export type { BillingPeriod } from "./modules/subscriptions/types/billing-period.type";
70
73
  export type {
71
74
  CalculateSubscriptionRequest,
@@ -292,7 +295,7 @@ export {
292
295
 
293
296
  // Embeds
294
297
  export { FrillEmbed } from "./components/embeds/FrillEmbed";
295
- export { CrispEmbed, openCrispHelpdesk, hideCrisp, showCrisp } from "./components/embeds/CrispEmbed";
298
+ export { CrispEmbed, openCrispHelpdesk, hideCrisp, showCrisp, updateCrispUser } from "./components/embeds/CrispEmbed";
296
299
  export { EmbedWidgets } from "./components/embeds/EmbedWidgets";
297
300
 
298
301
  // Store
@@ -321,6 +324,7 @@ export {
321
324
  formatCNPJ,
322
325
  formatCardNumber,
323
326
  } from "./utils/format/masks";
327
+ export { formatCurrencyNumber } from "./utils/format/currency";
324
328
  export { BR_STATE_OPTIONS } from "./utils/constants/br-states";
325
329
  export type { TimezoneOption } from "./modules/accounts/services/timezone.service";
326
330
  export {
@@ -0,0 +1,28 @@
1
+ 'use server';
2
+
3
+ import { plansService, safeServerAction } from '@greatapps/common/server';
4
+ import type { UiPlan } from '../types/plan.type';
5
+ import { mapApiPlanToUiPlan } from '../utils/map-api-plan-to-ui';
6
+
7
+ export async function listPlansAction() {
8
+ return safeServerAction(async () => {
9
+ const { data: apiPlans } = await plansService.listPlans({
10
+ active: true,
11
+ search: 'client',
12
+ sort: 'id:ASC',
13
+ });
14
+
15
+ if (!apiPlans?.length) return [] as UiPlan[];
16
+
17
+ const sorted = [...apiPlans].sort((a, b) => a.value - b.value);
18
+
19
+ return sorted.map((plan) => {
20
+ const popular = plan.id === 2 || plan.id_plan === 2;
21
+ return mapApiPlanToUiPlan(plan, {
22
+ planId: plan.id_plan ?? plan.id,
23
+ isPopular: popular,
24
+ buttonVariant: popular ? 'brand' : undefined,
25
+ });
26
+ });
27
+ });
28
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { withAction } from '@greatapps/common';
5
+ import { listPlansAction } from '../actions/list-plans.action';
6
+ import type { UiPlan } from '../types/plan.type';
7
+
8
+ export const PLANS_QUERY_KEY = ['plans'];
9
+
10
+ export function usePlans() {
11
+ return useQuery<UiPlan[]>({
12
+ queryKey: [...PLANS_QUERY_KEY],
13
+ queryFn: withAction(listPlansAction),
14
+ staleTime: Infinity,
15
+ gcTime: Infinity,
16
+ refetchOnWindowFocus: false,
17
+ refetchOnReconnect: false,
18
+ refetchOnMount: false,
19
+ retry: 1,
20
+ });
21
+ }
@@ -1,13 +1,34 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useQuery } from '@tanstack/react-query';
4
- import { withAction } from '../../../utils/withAction';
5
- import { findPlanByIdAction } from '../actions/find-plan-by-id.action';
3
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { withAction } from "../../../utils/withAction";
5
+ import { findPlanByIdAction } from "../actions/find-plan-by-id.action";
6
+ import { Plan } from "../types/plan.type";
7
+ import { PLANS_QUERY_KEY } from "./list-plans.hook";
8
+ import { PaginatedSuccessResult } from "../../../infra/api/types";
6
9
 
7
10
  export function usePlanById(idPlan: number | string | undefined) {
11
+ const queryClient = useQueryClient();
12
+
8
13
  return useQuery({
9
- queryKey: ['plan', idPlan],
14
+ queryKey: [...PLANS_QUERY_KEY, idPlan],
10
15
  queryFn: withAction(() => findPlanByIdAction(idPlan!)),
16
+ initialData: () => {
17
+ const queries = queryClient.getQueriesData<PaginatedSuccessResult<Plan>>({
18
+ queryKey: [...PLANS_QUERY_KEY],
19
+ });
20
+
21
+ for (const [, data] of queries) {
22
+ if (!data) continue;
23
+ const plan = (Array.isArray(data) ? data : data.data)?.find((p) => p.idPlan === idPlan);
24
+ if (plan) {
25
+ return plan;
26
+ }
27
+ }
28
+
29
+ return undefined;
30
+ },
31
+ initialDataUpdatedAt: 0,
11
32
  enabled: !!idPlan,
12
33
  });
13
34
  }