@vtex/faststore-plugin-buyer-portal 1.3.53 → 1.3.54

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/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.54] - 2026-01-07
11
+
12
+ ### Added
13
+
14
+ - Alternative Login Keys:
15
+ - Edit User to support username
16
+
10
17
  ## [1.3.53] - 2026-01-06
11
18
 
12
19
  ### Fixed
@@ -451,7 +458,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
451
458
  - Add CHANGELOG file
452
459
  - Add README file
453
460
 
454
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...HEAD
461
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...HEAD
462
+ [1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
455
463
  [1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
456
464
  [1.3.52]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.51...v1.3.52
457
465
  [1.3.51]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.50...v1.3.51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.53",
3
+ "version": "1.3.54",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,4 +22,4 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "1.3.53";
25
+ export const CURRENT_VERSION = "1.3.54";
@@ -194,7 +194,9 @@ class UsersClient extends Client {
194
194
  userId: string;
195
195
  phone?: string;
196
196
  name?: string;
197
+ email?: string;
197
198
  role?: string;
199
+ userName?: string;
198
200
  },
199
201
  cookie: string
200
202
  ) {
@@ -204,8 +206,10 @@ class UsersClient extends Client {
204
206
  unknown,
205
207
  {
206
208
  name?: string;
209
+ email?: string;
207
210
  phone?: string;
208
211
  role?: string;
212
+ userName?: string;
209
213
  }
210
214
  >(
211
215
  `units/${orgUnitId}/users/${userId}`,
@@ -299,6 +299,8 @@ export const CreateUserDrawerWithUsername = ({
299
299
  const isFormValid = () => {
300
300
  if (!userName?.trim()) return false;
301
301
 
302
+ if (!email?.trim()) return false;
303
+
302
304
  if (roles.length === 0) return false;
303
305
 
304
306
  if (isUsernameValid === false) return false;
@@ -457,13 +459,18 @@ export const CreateUserDrawerWithUsername = ({
457
459
  <InputText
458
460
  // TODO[2FA]: Uncomment when 2FA settings are ready
459
461
  // label={is2FAEnabled ? "Email" : "Email (optional)"}
460
- // hasError={is2FAEnabled && isTouched && !email?.trim() && !phone?.trim()}
461
- label="Email (optional)"
462
+ hasError={isTouched && !email?.trim()}
463
+ label="Email"
462
464
  value={email}
463
465
  wrapperProps={{ style: { marginTop: 16 } }}
464
466
  onChange={(event) => updateField("email", event.target.value)}
465
467
  />
466
468
 
469
+ <ErrorMessage
470
+ show={isTouched && !email?.trim()}
471
+ message="Email is required"
472
+ />
473
+
467
474
  <InputText
468
475
  // TODO[2FA]: Uncomment when 2FA settings are ready
469
476
  // label={is2FAEnabled ? "Phone number" : "Phone number (optional)"}
@@ -0,0 +1,20 @@
1
+ import { useBuyerPortal } from "../../../shared/hooks";
2
+ import {
3
+ UpdateUserDrawer,
4
+ type UpdateUserDrawerProps,
5
+ } from "../UpdateUserDrawer/UpdateUserDrawer";
6
+ import { UpdateUserDrawerWithUsername } from "../UpdateUserDrawerWithUsername/UpdateUserDrawerWithUsername";
7
+
8
+ export type UpdateUserDrawerSelectorProps = UpdateUserDrawerProps;
9
+
10
+ export const UpdateUserDrawerSelector = (
11
+ props: UpdateUserDrawerSelectorProps
12
+ ) => {
13
+ const { featureFlags } = useBuyerPortal();
14
+
15
+ if (featureFlags?.enableUsernameCreation) {
16
+ return <UpdateUserDrawerWithUsername {...props} />;
17
+ }
18
+
19
+ return <UpdateUserDrawer {...props} />;
20
+ };
@@ -0,0 +1,342 @@
1
+ import { useRef, useState } from "react";
2
+
3
+ import { Skeleton, useUI } from "@faststore/ui";
4
+
5
+ import { useUpdateRoles } from "../../../roles/hooks";
6
+ import { RoleData, UserRoles } from "../../../roles/types";
7
+ import {
8
+ type BasicDrawerProps,
9
+ BasicDrawer,
10
+ ErrorMessage,
11
+ InputText,
12
+ } from "../../../shared/components";
13
+ import { useAnalytics } from "../../../shared/hooks";
14
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
15
+ import {
16
+ maskPhoneNumber,
17
+ normalizePhoneNumber,
18
+ } from "../../../shared/utils/phoneNumber";
19
+ import { useGetUserById, useUpdateUser } from "../../hooks";
20
+
21
+ export type UpdateUserDrawerWithUsernameProps = Omit<
22
+ BasicDrawerProps,
23
+ "children"
24
+ > & {
25
+ onUpdate?: () => void;
26
+ orgUnitId: string;
27
+ userId: string;
28
+ rolesOptions: RoleData[] | null;
29
+ };
30
+
31
+ export const UpdateUserDrawerWithUsername = ({
32
+ close,
33
+ onUpdate,
34
+ userId,
35
+ orgUnitId,
36
+ rolesOptions,
37
+ ...props
38
+ }: UpdateUserDrawerWithUsernameProps) => {
39
+ const { pushToast } = useUI();
40
+ const { trackEntityEdited, trackEntityEditError } = useAnalytics({
41
+ entityType: "user",
42
+ entityId: userId,
43
+ defaultTimerName: "user_edit",
44
+ shouldTrackDefaultTimer: true,
45
+ });
46
+ const initialRolesRef = useRef<string[]>([]);
47
+ const initialUsernameRef = useRef<string>("");
48
+
49
+ const { isUserLoading } = useGetUserById(
50
+ { orgUnitId, userId },
51
+ {
52
+ onSuccess: (data) => {
53
+ const userRoles = data?.roles ? data.roles : [];
54
+ initialRolesRef.current = userRoles;
55
+ initialUsernameRef.current = data?.userName ?? "";
56
+ setForm({
57
+ name: data?.name ?? "",
58
+ email: data?.email ?? "",
59
+ phone: data?.phone ?? "",
60
+ roles: userRoles,
61
+ username: data?.userName ?? "",
62
+ });
63
+ },
64
+ onError: () => {
65
+ pushToast({
66
+ message: "Failed to fetch user details",
67
+ status: "ERROR",
68
+ });
69
+ },
70
+ }
71
+ );
72
+
73
+ const [form, setForm] = useState<{
74
+ name: string;
75
+ email: string;
76
+ phone: string;
77
+ roles: string[];
78
+ username: string;
79
+ }>({
80
+ name: "",
81
+ email: "",
82
+ phone: "",
83
+ roles: [],
84
+ username: "",
85
+ });
86
+
87
+ const [isTouched, setIsTouched] = useState(false);
88
+
89
+ const { name, email, roles, phone, username } = form;
90
+
91
+ const updateField = (field: keyof typeof form, value: string | string[]) => {
92
+ setForm((prev) => ({
93
+ ...prev,
94
+ [field]: value,
95
+ }));
96
+ };
97
+
98
+ const handleUpdateUserSuccess = () => {
99
+ trackEntityEdited(ANALYTICS_EVENTS.USER_EDITED, {
100
+ user_id: userId,
101
+ user_name: name,
102
+ user_phone: phone,
103
+ old_roles: initialRolesRef.current,
104
+ new_roles: roles,
105
+ roles_count: roles.length,
106
+ org_unit_id: orgUnitId,
107
+ });
108
+
109
+ pushToast({
110
+ message: "User successfully updated",
111
+ status: "INFO",
112
+ });
113
+ onUpdate?.();
114
+ close();
115
+ };
116
+
117
+ const { updateUser, isUpdateUserLoading } = useUpdateUser({
118
+ onSuccess: handleUpdateUserSuccess,
119
+ onError: (error) => {
120
+ trackEntityEditError(
121
+ ANALYTICS_EVENTS.USER_EDIT_ERROR,
122
+ "user",
123
+ userId,
124
+ error,
125
+ {
126
+ org_unit_id: orgUnitId,
127
+ }
128
+ );
129
+
130
+ pushToast({
131
+ message: "Failed to update user",
132
+ status: "ERROR",
133
+ });
134
+ },
135
+ });
136
+
137
+ const { updateRoles, isUpdateRolesLoading } = useUpdateRoles({
138
+ onSuccess: handleUpdateUserSuccess,
139
+ onError: (error) => {
140
+ trackEntityEditError(
141
+ ANALYTICS_EVENTS.USER_EDIT_ERROR,
142
+ "user",
143
+ userId,
144
+ error,
145
+ {
146
+ org_unit_id: orgUnitId,
147
+ }
148
+ );
149
+
150
+ pushToast({
151
+ message: "Failed to update user",
152
+ status: "ERROR",
153
+ });
154
+ },
155
+ });
156
+
157
+ const isAllFieldsFilled = Object.keys(form).every((key) => {
158
+ if (key === "roles" || key === "phone") {
159
+ return true; // allow empty roles
160
+ }
161
+
162
+ return (form[key as keyof typeof form] as string)?.trim();
163
+ });
164
+
165
+ const handleConfirmClick = () => {
166
+ setIsTouched(true);
167
+
168
+ if (!isAllFieldsFilled) {
169
+ return;
170
+ }
171
+
172
+ const updatedRoles = rolesOptions?.map((option) => ({
173
+ roleName: option.roleName,
174
+ isActive: roles.includes(option.roleName),
175
+ }));
176
+
177
+ updateRoles({
178
+ data: {
179
+ userId,
180
+ roles: updatedRoles ? updatedRoles : ([] as UserRoles[]),
181
+ },
182
+ orgUnitId,
183
+ });
184
+
185
+ updateUser({
186
+ userId,
187
+ name,
188
+ email,
189
+ phone: phone ?? "",
190
+ roles,
191
+ orgUnitId,
192
+ userName: username,
193
+ });
194
+ };
195
+
196
+ const isLoading = isUpdateUserLoading || isUpdateRolesLoading;
197
+
198
+ const isConfirmButtonEnabled = isAllFieldsFilled && !isLoading;
199
+
200
+ return (
201
+ <BasicDrawer
202
+ data-fs-bp-update-user-drawer-with-username
203
+ close={close}
204
+ {...props}
205
+ >
206
+ <BasicDrawer.Heading title="Edit user details" onClose={close} />
207
+
208
+ <BasicDrawer.Body>
209
+ {isUserLoading ? (
210
+ <Skeleton size={{ width: "100%", height: "3.5rem" }} />
211
+ ) : (
212
+ <InputText
213
+ label="Username"
214
+ value={username}
215
+ hasError={isTouched && !username?.trim()}
216
+ onChange={(event) => updateField("username", event.target.value)}
217
+ readOnly={!!initialUsernameRef.current}
218
+ disabled={!!initialUsernameRef.current}
219
+ />
220
+ )}
221
+
222
+ <ErrorMessage
223
+ show={isTouched && !username?.trim()}
224
+ message="Username is required"
225
+ />
226
+
227
+ {isUserLoading ? (
228
+ <Skeleton size={{ width: "100%", height: "3.5rem" }} />
229
+ ) : (
230
+ <InputText
231
+ label="Full Name"
232
+ wrapperProps={{ style: { marginTop: 16, marginBottom: 16 } }}
233
+ value={name}
234
+ hasError={isTouched && !name?.trim()}
235
+ onChange={(event) => updateField("name", event.target.value)}
236
+ />
237
+ )}
238
+
239
+ <ErrorMessage
240
+ show={isTouched && !name?.trim()}
241
+ message="Full name is required"
242
+ />
243
+
244
+ {isUserLoading ? (
245
+ <Skeleton
246
+ size={{ width: "100%", height: "3.5rem" }}
247
+ style={{
248
+ marginTop: "1rem",
249
+ marginBottom: "1rem",
250
+ width: "100%",
251
+ height: "3.5rem",
252
+ }}
253
+ />
254
+ ) : (
255
+ <InputText
256
+ label="Email"
257
+ value={email}
258
+ wrapperProps={{ style: { marginBottom: 16 } }}
259
+ hasError={isTouched && !email?.trim()}
260
+ onChange={(event) => updateField("email", event.target.value)}
261
+ />
262
+ )}
263
+
264
+ <ErrorMessage
265
+ show={isTouched && !email?.trim()}
266
+ message="Email is required"
267
+ />
268
+
269
+ {isUserLoading ? (
270
+ <Skeleton
271
+ size={{ width: "100%", height: "3.5rem" }}
272
+ style={{
273
+ marginTop: "1rem",
274
+ marginBottom: "1rem",
275
+ width: "100%",
276
+ height: "3.5rem",
277
+ }}
278
+ />
279
+ ) : (
280
+ <InputText
281
+ label="Phone number (optional)"
282
+ value={maskPhoneNumber(phone, "USA")}
283
+ onChange={(event) =>
284
+ // TODO: Update this when implementing i18n
285
+ updateField("phone", normalizePhoneNumber(event.target.value))
286
+ }
287
+ />
288
+ )}
289
+
290
+ <div data-fs-bp-update-user-roles>
291
+ <span data-fs-bp-update-user-roles-label>Roles</span>
292
+ {rolesOptions &&
293
+ rolesOptions.map((role) => {
294
+ if (isUserLoading) {
295
+ return (
296
+ <Skeleton
297
+ key={role.roleId}
298
+ size={{ width: "100%", height: "1.25rem" }}
299
+ />
300
+ );
301
+ }
302
+
303
+ const id = `role-${role.roleName
304
+ .toLowerCase()
305
+ .replace(" ", "-")}`;
306
+ return (
307
+ <span data-fs-bp-update-user-role-wrapper key={role.roleName}>
308
+ <input
309
+ type="checkbox"
310
+ key={role.roleId}
311
+ value={role.roleId}
312
+ id={id}
313
+ checked={roles?.includes(role.roleName)}
314
+ onChange={(event) => {
315
+ const newRoles = event.target.checked
316
+ ? [...(roles ?? []), role.roleName]
317
+ : roles?.filter((r) => r !== role.roleName) ?? [];
318
+ updateField("roles", newRoles);
319
+ }}
320
+ />
321
+ <label htmlFor={id}>{role.roleName}</label>
322
+ </span>
323
+ );
324
+ })}
325
+ </div>
326
+ </BasicDrawer.Body>
327
+ <BasicDrawer.Footer>
328
+ <BasicDrawer.Button variant="ghost" onClick={close}>
329
+ Cancel
330
+ </BasicDrawer.Button>
331
+ <BasicDrawer.Button
332
+ variant="confirm"
333
+ disabled={!isConfirmButtonEnabled}
334
+ onClick={handleConfirmClick}
335
+ isLoading={isLoading}
336
+ >
337
+ Save
338
+ </BasicDrawer.Button>
339
+ </BasicDrawer.Footer>
340
+ </BasicDrawer>
341
+ );
342
+ };
@@ -0,0 +1,31 @@
1
+ @import "../../../shared/components/BasicDrawer/basic-drawer.scss";
2
+
3
+ [data-fs-bp-update-user-drawer-with-username] {
4
+ @import "../UpdateUserDrawer/update-user-drawer.scss";
5
+
6
+ [data-fs-bp-update-user-roles] {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--fs-spacing-2);
10
+ margin-bottom: var(--fs-spacing-6);
11
+ margin-top: var(--fs-spacing-5);
12
+
13
+ [data-fs-bp-update-user-roles-label] {
14
+ font-weight: var(--fs-text-weight-regular);
15
+ font-size: var(--fs-text-size-1);
16
+ line-height: var(--fs-text-size-3);
17
+ }
18
+
19
+ [data-fs-bp-update-user-role-wrapper] {
20
+ input {
21
+ margin-right: var(--fs-spacing-1);
22
+ }
23
+
24
+ label {
25
+ font-weight: var(--fs-text-weight-regular);
26
+ font-size: var(--fs-text-size-1);
27
+ line-height: var(--fs-text-size-3);
28
+ }
29
+ }
30
+ }
31
+ }
@@ -7,7 +7,7 @@ import { useBuyerPortal, useDrawerProps } from "../../../shared/hooks";
7
7
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
8
8
  import { DeleteUserDrawer } from "../DeleteUserDrawer/DeleteUserDrawer";
9
9
  import { ReassignOrgUnitDrawer } from "../ReassignOrgUnitDrawer/ReassignOrgUnitDrawer";
10
- import { UpdateUserDrawer } from "../UpdateUserDrawer/UpdateUserDrawer";
10
+ import { UpdateUserDrawerSelector } from "../UpdateUserDrawerSelector/UpdateUserDrawerSelector";
11
11
 
12
12
  import type { RoleData } from "../../../roles/types";
13
13
 
@@ -99,7 +99,7 @@ export const UserDropdownMenu = ({
99
99
  />
100
100
  )}
101
101
  {isUpdateUserDrawerOpen && (
102
- <UpdateUserDrawer
102
+ <UpdateUserDrawerSelector
103
103
  rolesOptions={rolesOptions}
104
104
  userId={user.id}
105
105
  orgUnitId={currentOrgUnit?.id ?? ""}
@@ -4,3 +4,4 @@
4
4
  @import "../CreateUserDrawer/create-user-drawer.scss";
5
5
  @import "../CreateUserDrawerWithUsername/create-user-drawer-with-username.scss";
6
6
  @import "../UpdateUserDrawer/update-user-drawer.scss";
7
+ @import "../UpdateUserDrawerWithUsername/update-user-drawer-with-username.scss";
@@ -16,3 +16,8 @@ export {
16
16
  UpdateUserDrawer,
17
17
  type UpdateUserDrawerProps,
18
18
  } from "./UpdateUserDrawer/UpdateUserDrawer";
19
+ export { UpdateUserDrawerWithUsername } from "./UpdateUserDrawerWithUsername/UpdateUserDrawerWithUsername";
20
+ export {
21
+ UpdateUserDrawerSelector,
22
+ type UpdateUserDrawerSelectorProps,
23
+ } from "./UpdateUserDrawerSelector/UpdateUserDrawerSelector";
@@ -10,7 +10,8 @@ import { GlobalLayout } from "../../../shared/layouts";
10
10
  import { OrgUnitTabsLayout } from "../../../shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout";
11
11
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
12
12
  import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
13
- import { ReassignOrgUnitDrawer, UpdateUserDrawer } from "../../components";
13
+ import { ReassignOrgUnitDrawer } from "../../components";
14
+ import { UpdateUserDrawerSelector } from "../../components/UpdateUserDrawerSelector/UpdateUserDrawerSelector";
14
15
  import { UserDropdownMenu } from "../../components/UserDropdownMenu/UserDropdownMenu";
15
16
 
16
17
  import type { RoleData } from "../../../roles/types";
@@ -148,7 +149,7 @@ export const UserDetailsLayout = ({
148
149
  />
149
150
  )}
150
151
  {isUpdateUserDrawerOpen && (
151
- <UpdateUserDrawer
152
+ <UpdateUserDrawerSelector
152
153
  rolesOptions={rolesOptions ?? []}
153
154
  userId={user?.id ?? ""}
154
155
  orgUnitId={currentOrgUnit?.id ?? ""}
@@ -1,6 +1,7 @@
1
1
  @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
2
2
  @import "../../components/ReassignOrgUnitDrawer/reassign-org-unit-drawer.scss";
3
3
  @import "../../components/UpdateUserDrawer/update-user-drawer.scss";
4
+ @import "../../components/UpdateUserDrawerWithUsername/update-user-drawer-with-username.scss";
4
5
  @import "../../components/DeleteUserDrawer/delete-user-drawer.scss";
5
6
  @import "../../components/UserDropdownMenu/user-dropdown-menu.scss";
6
7
 
@@ -13,14 +13,12 @@ export const getUserByIdService = async ({
13
13
  cookie: string;
14
14
  }): Promise<UserData | null> => {
15
15
  try {
16
- const { email, name, phone, orgUnit, role } = await usersClient.getUserById(
17
- orgUnitId,
18
- userId,
19
- cookie
20
- );
16
+ const { email, name, phone, orgUnit, role, userName } =
17
+ await usersClient.getUserById(orgUnitId, userId, cookie);
21
18
 
22
19
  return {
23
20
  name,
21
+ userName: userName ?? "",
24
22
  roles: role ? role : [],
25
23
  id: userId,
26
24
  email: email ?? "",
@@ -4,8 +4,10 @@ export type UpdateUserServiceProps = {
4
4
  orgUnitId: string;
5
5
  userId: string;
6
6
  name?: string;
7
+ email?: string;
7
8
  phone?: string;
8
9
  roles?: string[];
10
+ userName?: string;
9
11
  cookie: string;
10
12
  };
11
13
 
@@ -14,16 +16,20 @@ export const updateUserService = async ({
14
16
  userId,
15
17
  cookie,
16
18
  name,
19
+ email,
17
20
  phone = "",
18
21
  roles,
22
+ userName,
19
23
  }: UpdateUserServiceProps) => {
20
24
  return usersClient.updateUser(
21
25
  {
22
26
  orgUnitId,
23
27
  userId,
24
28
  name,
29
+ email,
25
30
  phone,
26
31
  role: roles?.join(",") || undefined,
32
+ userName,
27
33
  },
28
34
  cookie
29
35
  );