@vtex/faststore-plugin-buyer-portal 2.0.24 → 2.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "2.0.24",
3
+ "version": "2.0.25",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -8,4 +8,9 @@ export type ContractData = {
8
8
  salesRepresentative?: string;
9
9
  imageUrl?: string;
10
10
  assortment?: unknown[];
11
+ corporateName?: string | null;
12
+ tradeName?: string | null;
13
+ corporateDocument?: string | null;
14
+ documentType?: string | null;
15
+ localeDefault?: string | null;
11
16
  };
@@ -1,8 +1,12 @@
1
+ import { Fragment } from "react";
2
+
1
3
  import { HeaderInside } from "../../../shared/components";
2
4
  import { useBuyerPortal } from "../../../shared/hooks";
3
5
  import { GlobalLayout, ContractTabsLayout } from "../../../shared/layouts";
4
6
  import { useLocalization } from "../../../shared/localization/LocalizationContext";
5
7
 
8
+ import { getProfileDetailRows } from "./utils";
9
+
6
10
  import type { ContractData } from "../../../contracts/types";
7
11
 
8
12
  export type ProfileLayoutProps = {
@@ -13,13 +17,7 @@ export const ProfileLayout = ({ data }: ProfileLayoutProps) => {
13
17
  const { t } = useLocalization();
14
18
  const { currentOrgUnit: orgUnit } = useBuyerPortal();
15
19
 
16
- const creationDate = data
17
- ? new Date(data.creationDate).toLocaleDateString("en-US", {
18
- year: "numeric",
19
- month: "long",
20
- day: "numeric",
21
- })
22
- : null;
20
+ const rows = getProfileDetailRows(data);
23
21
 
24
22
  return (
25
23
  <GlobalLayout>
@@ -39,41 +37,25 @@ export const ProfileLayout = ({ data }: ProfileLayoutProps) => {
39
37
  {t("profile.titles.details")}
40
38
  </span>
41
39
 
42
- {!!data.name && (
43
- <>
40
+ {rows.map((row) => (
41
+ <Fragment key={row.key}>
44
42
  <hr data-fs-bp-profile-divider />
45
43
  <div data-fs-bp-profile-details-row>
46
44
  <span data-fs-bp-profile-details-row-label>
47
- {t("shared.labels.name")}
45
+ {t(row.labelKey)}
48
46
  </span>
49
47
  <span data-fs-bp-profile-details-row-value>
50
- {data.name}
48
+ {row.key === "email" ? (
49
+ <a href={`mailto:${row.value}`}>{row.value}</a>
50
+ ) : (
51
+ row.value
52
+ )}
51
53
  </span>
52
54
  </div>
53
- </>
54
- )}
55
-
56
- <hr data-fs-bp-profile-divider />
57
-
58
- <div data-fs-bp-profile-details-row>
59
- <span data-fs-bp-profile-details-row-label>
60
- {t("shared.labels.email")}
61
- </span>
62
- <span data-fs-bp-profile-details-row-value>
63
- <a href={`mailto:${data.email}`}>{data.email}</a>
64
- </span>
65
- </div>
66
-
67
- <hr data-fs-bp-profile-divider />
68
-
69
- <div data-fs-bp-profile-details-row>
70
- <span data-fs-bp-profile-details-row-label>
71
- {t("profile.labels.creationDate")}
72
- </span>
73
- <span data-fs-bp-profile-details-row-value>{creationDate}</span>
74
- </div>
55
+ </Fragment>
56
+ ))}
75
57
 
76
- <hr data-fs-bp-profile-divider />
58
+ {rows.length > 0 && <hr data-fs-bp-profile-divider />}
77
59
  </div>
78
60
  )}
79
61
  </section>
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getProfileDetailRows, hasContent } from "../utils";
4
+
5
+ import type { ContractData } from "../../../../contracts/types";
6
+
7
+ const baseContract: ContractData = {
8
+ id: "contract-a",
9
+ name: "Acme Corp",
10
+ email: "buyer@acme.com",
11
+ isActive: true,
12
+ // Noon UTC keeps the local calendar day stable across the timezones CI
13
+ // and local machines run in, so the formatted date assertion isn't flaky.
14
+ creationDate: "2026-01-15T12:00:00.000Z",
15
+ };
16
+
17
+ describe("hasContent", () => {
18
+ it("is true for a string with at least one non-whitespace character", () => {
19
+ expect(hasContent("Acme Corp")).toBe(true);
20
+ });
21
+
22
+ it.each([null, undefined, "", " ", 42, {}])("is false for %p", (value) => {
23
+ expect(hasContent(value)).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("getProfileDetailRows", () => {
28
+ it("returns an empty list for a null contract", () => {
29
+ expect(getProfileDetailRows(null)).toEqual([]);
30
+ });
31
+
32
+ it("renders every row, in order, when all five new fields have content", () => {
33
+ const contract: ContractData = {
34
+ ...baseContract,
35
+ corporateName: "Acme Corporation",
36
+ tradeName: "Acme",
37
+ corporateDocument: "12.345.678/0001-90",
38
+ documentType: "CNPJ",
39
+ localeDefault: "pt-BR",
40
+ };
41
+
42
+ expect(getProfileDetailRows(contract).map((row) => row.key)).toEqual([
43
+ "name",
44
+ "email",
45
+ "creationDate",
46
+ "corporateName",
47
+ "tradeName",
48
+ "corporateDocument",
49
+ "documentType",
50
+ "localeDefault",
51
+ ]);
52
+ });
53
+
54
+ it("renders only the rows with content, keeping the fixed order", () => {
55
+ const contract: ContractData = {
56
+ ...baseContract,
57
+ documentType: "CNPJ",
58
+ };
59
+
60
+ expect(getProfileDetailRows(contract).map((row) => row.key)).toEqual([
61
+ "name",
62
+ "email",
63
+ "creationDate",
64
+ "documentType",
65
+ ]);
66
+ });
67
+
68
+ it.each([null, undefined, "", " "])(
69
+ "omits a new field when it resolves to %p",
70
+ (value) => {
71
+ const contract: ContractData = {
72
+ ...baseContract,
73
+ corporateName: value as string | null | undefined,
74
+ };
75
+
76
+ expect(
77
+ getProfileDetailRows(contract).some(
78
+ (row) => row.key === "corporateName"
79
+ )
80
+ ).toBe(false);
81
+ }
82
+ );
83
+
84
+ it("omits a new field that is absent from the payload altogether", () => {
85
+ expect(
86
+ getProfileDetailRows(baseContract).some(
87
+ (row) => row.key === "corporateDocument"
88
+ )
89
+ ).toBe(false);
90
+ });
91
+
92
+ it("returns the three legacy rows, byte-equivalent to today's page, when none of the five new fields have content", () => {
93
+ expect(getProfileDetailRows(baseContract).map((row) => row.key)).toEqual([
94
+ "name",
95
+ "email",
96
+ "creationDate",
97
+ ]);
98
+ });
99
+
100
+ it("formats creationDate with toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })", () => {
101
+ const rows = getProfileDetailRows(baseContract);
102
+ const creationDateRow = rows.find((row) => row.key === "creationDate");
103
+
104
+ expect(creationDateRow?.value).toBe("January 15, 2026");
105
+ });
106
+
107
+ it("omits the creationDate row instead of rendering 'Invalid Date' when the date can't be parsed", () => {
108
+ const contract: ContractData = {
109
+ ...baseContract,
110
+ creationDate: "not-a-date",
111
+ };
112
+
113
+ expect(
114
+ getProfileDetailRows(contract).some((row) => row.key === "creationDate")
115
+ ).toBe(false);
116
+ });
117
+
118
+ it("carries the correct labelKey for each of the five new rows", () => {
119
+ const contract: ContractData = {
120
+ ...baseContract,
121
+ corporateName: "Acme Corporation",
122
+ tradeName: "Acme",
123
+ corporateDocument: "12.345.678/0001-90",
124
+ documentType: "CNPJ",
125
+ localeDefault: "pt-BR",
126
+ };
127
+
128
+ const labelKeysByKey = Object.fromEntries(
129
+ getProfileDetailRows(contract).map((row) => [row.key, row.labelKey])
130
+ );
131
+
132
+ expect(labelKeysByKey).toEqual({
133
+ name: "shared.labels.name",
134
+ email: "shared.labels.email",
135
+ creationDate: "profile.labels.creationDate",
136
+ corporateName: "shared.labels.corporateName",
137
+ tradeName: "shared.labels.tradeName",
138
+ corporateDocument: "shared.labels.corporateDocument",
139
+ documentType: "shared.labels.documentType",
140
+ localeDefault: "shared.labels.localeDefault",
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,106 @@
1
+ import type { ContractData } from "../../../contracts/types";
2
+
3
+ export type ProfileDetailFieldKey =
4
+ | "name"
5
+ | "email"
6
+ | "creationDate"
7
+ | "corporateName"
8
+ | "tradeName"
9
+ | "corporateDocument"
10
+ | "documentType"
11
+ | "localeDefault";
12
+
13
+ /** A row the layout will render. Only rows with content are ever produced. */
14
+ export type ProfileDetailRow = {
15
+ key: ProfileDetailFieldKey;
16
+ labelKey: string;
17
+ value: string;
18
+ };
19
+
20
+ type ProfileDetailField = {
21
+ key: ProfileDetailFieldKey;
22
+ labelKey: string;
23
+ /** Reads the display value off the contract. May return no content. */
24
+ resolve: (contract: ContractData) => string | null | undefined;
25
+ };
26
+
27
+ /** A value is renderable only if it is a string with at least one non-whitespace char. */
28
+ export const hasContent = (value: unknown): value is string =>
29
+ typeof value === "string" && value.trim().length > 0;
30
+
31
+ const resolveCreationDate = (contract: ContractData) => {
32
+ const date = new Date(contract.creationDate);
33
+
34
+ if (Number.isNaN(date.getTime())) {
35
+ return null;
36
+ }
37
+
38
+ return date.toLocaleDateString("en-US", {
39
+ year: "numeric",
40
+ month: "long",
41
+ day: "numeric",
42
+ });
43
+ };
44
+
45
+ /** Ordered. This array is the single source of truth for which rows exist and in what order. */
46
+ export const PROFILE_DETAIL_FIELDS: readonly ProfileDetailField[] = [
47
+ {
48
+ key: "name",
49
+ labelKey: "shared.labels.name",
50
+ resolve: (contract) => contract.name,
51
+ },
52
+ {
53
+ key: "email",
54
+ labelKey: "shared.labels.email",
55
+ resolve: (contract) => contract.email,
56
+ },
57
+ {
58
+ key: "creationDate",
59
+ labelKey: "profile.labels.creationDate",
60
+ resolve: resolveCreationDate,
61
+ },
62
+ {
63
+ key: "corporateName",
64
+ labelKey: "shared.labels.corporateName",
65
+ resolve: (contract) => contract.corporateName,
66
+ },
67
+ {
68
+ key: "tradeName",
69
+ labelKey: "shared.labels.tradeName",
70
+ resolve: (contract) => contract.tradeName,
71
+ },
72
+ {
73
+ key: "corporateDocument",
74
+ labelKey: "shared.labels.corporateDocument",
75
+ resolve: (contract) => contract.corporateDocument,
76
+ },
77
+ {
78
+ key: "documentType",
79
+ labelKey: "shared.labels.documentType",
80
+ resolve: (contract) => contract.documentType,
81
+ },
82
+ {
83
+ key: "localeDefault",
84
+ labelKey: "shared.labels.localeDefault",
85
+ resolve: (contract) => contract.localeDefault,
86
+ },
87
+ ];
88
+
89
+ /** Maps the descriptors over the contract and drops every row without content. */
90
+ export const getProfileDetailRows = (
91
+ contract: ContractData | null
92
+ ): ProfileDetailRow[] => {
93
+ if (!contract) {
94
+ return [];
95
+ }
96
+
97
+ return PROFILE_DETAIL_FIELDS.reduce<ProfileDetailRow[]>((rows, field) => {
98
+ const value = field.resolve(contract);
99
+
100
+ if (hasContent(value)) {
101
+ rows.push({ key: field.key, labelKey: field.labelKey, value });
102
+ }
103
+
104
+ return rows;
105
+ }, []);
106
+ };
@@ -22,4 +22,4 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "2.0.24";
25
+ export const CURRENT_VERSION = "2.0.25";