@vtex/faststore-plugin-buyer-portal 1.3.41 → 1.3.43

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/CHANGELOG.md +26 -1
  2. package/package.json +1 -1
  3. package/src/features/addresses/components/AddressRecipientsList/AddressRecipientsList.tsx +3 -2
  4. package/src/features/addresses/components/CreateAddressSettingsDrawer/CreateAddressSettingsDrawer.tsx +64 -25
  5. package/src/features/addresses/components/CreateAddressSettingsDrawer/create-address-settings-drawer.scss +22 -22
  6. package/src/features/addresses/components/RecipientsForm/RecipientItem/RecipientItem.tsx +6 -3
  7. package/src/features/addresses/data/countries.ts +3 -1
  8. package/src/features/addresses/services/default-values/get-default-address.service.ts +1 -1
  9. package/src/features/addresses/services/recipients/get-address-recipients.service.ts +8 -1
  10. package/src/features/addresses/types/AddressData.ts +1 -0
  11. package/src/features/payment-methods/layouts/PaymentMethodsLayout/PaymentMethodsLayout.tsx +8 -6
  12. package/src/features/shared/clients/ScopeClient.ts +38 -2
  13. package/src/features/shared/components/Error/Error.tsx +15 -13
  14. package/src/features/shared/components/SettingsDrawer/SettingsDrawer.tsx +106 -0
  15. package/src/features/shared/components/SettingsDrawer/SettingsDrawerContext.tsx +17 -0
  16. package/src/features/shared/components/SettingsDrawer/SettingsDrawerListType.tsx +100 -0
  17. package/src/features/shared/components/SettingsDrawer/settings-drawer.scss +61 -0
  18. package/src/features/shared/components/index.ts +7 -0
  19. package/src/features/shared/hooks/index.ts +2 -0
  20. package/src/features/shared/hooks/useGetScopeConfig.ts +35 -0
  21. package/src/features/shared/hooks/useSetScopeConfig.ts +30 -0
  22. package/src/features/shared/services/get-scope-config.service.ts +19 -0
  23. package/src/features/shared/services/index.ts +9 -0
  24. package/src/features/shared/services/set-scope-config.service.ts +27 -0
  25. package/src/features/shared/types/index.ts +1 -0
  26. package/src/features/shared/utils/constants.ts +10 -1
  27. package/src/features/shared/utils/index.ts +6 -1
  28. package/src/features/shared/utils/phoneNumber.ts +30 -14
  29. package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +6 -3
  30. package/src/features/users/components/UpdateUserDrawer/UpdateUserDrawer.tsx +7 -4
  31. package/src/features/users/layouts/UserDetailsLayout/UserDetailsLayout.tsx +4 -1
  32. package/src/features/users/services/get-user-by-id.service.ts +2 -1
  33. package/src/pages/payment-methods.tsx +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.43] - 2025-12-16
11
+
12
+ ### Fixed
13
+ - Update maskPhoneNumber to include country DDI
14
+ - Send only phone digits to API in Users and Recipients pages
15
+ - Show masked phone in layout and forms in Users and Recipients pages
16
+
17
+ ## [1.3.42] - 2025-12-11
18
+
19
+ ### Fixed
20
+
21
+ - Improve error handling in the payment methods
22
+ - Show error details on error boundary only in development environment
23
+
10
24
  ## [1.3.41] - 2025-12-09
11
25
 
12
26
  ### Changed
@@ -50,10 +64,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
64
 
51
65
  ## [1.3.34] - 2025-12-01
52
66
 
67
+ - Add generic `SettingsDrawer` component with `ListType` subcomponent for scope configuration
68
+ - Add Scope Config API integration (`useGetScopeConfig`, `useSetScopeConfig` hooks)
69
+ - Integrate Settings Drawer with Address Settings page
70
+
53
71
  ### Added
54
72
 
55
73
  - Add DK Docs
56
74
 
75
+ ### Fixed
76
+
77
+ - Fix address ID mapping in default address service to use correct `id` field
78
+
57
79
  ## [1.3.33] - 2025-11-25
58
80
 
59
81
  ### Changed
@@ -362,7 +384,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
362
384
  - Add CHANGELOG file
363
385
  - Add README file
364
386
 
365
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.41...HEAD
387
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.43...HEAD
366
388
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.2.2...1.2.3
367
389
  [1.2.3]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.3
368
390
  [1.2.4]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.2.4
@@ -411,3 +433,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
411
433
  [1.3.37]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.36...v1.3.37
412
434
  [1.3.36]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.35...v1.3.36
413
435
  [1.3.35]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.35
436
+
437
+ [1.3.43]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.42...v1.3.43
438
+ [1.3.42]: https://github.com/vtex/faststore-plugin-buyer-portal/releases/tag/1.3.42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.41",
3
+ "version": "1.3.43",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -19,6 +19,7 @@ import {
19
19
  usePageItems,
20
20
  } from "../../../shared/hooks/usePageItems";
21
21
  import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
22
+ import { type CountryCodes } from "../../data/countries";
22
23
  import { useSearchAddressRecipients } from "../../hooks/useSearchAddressRecipients";
23
24
  import { RecipientData } from "../../types";
24
25
  import { DeleteRecipientAddressDrawer } from "../DeleteRecipientAddressDrawer/DeleteRecipientAddressDrawer";
@@ -33,7 +34,7 @@ export type AddressRecipientsListProps = {
33
34
  total: number;
34
35
  search: string;
35
36
  page: number;
36
- countryCode?: string;
37
+ countryCode?: CountryCodes;
37
38
  };
38
39
 
39
40
  export const AddressRecipientsList = forwardRef<
@@ -46,7 +47,7 @@ export const AddressRecipientsList = forwardRef<
46
47
  search,
47
48
  total,
48
49
  page = 1,
49
- countryCode = "",
50
+ countryCode,
50
51
  }: AddressRecipientsListProps,
51
52
  ref
52
53
  ) => {
@@ -7,11 +7,14 @@ import { useUI, Skeleton } from "@faststore/ui";
7
7
  import {
8
8
  type BasicDrawerProps,
9
9
  AutocompleteDropdown,
10
- BasicDrawer,
10
+ DEFAULT_LIST_TYPE_OPTIONS,
11
11
  Icon,
12
+ ListTypeOption,
13
+ SettingsDrawer,
12
14
  } from "../../../shared/components";
13
15
  import { OptionSelected } from "../../../shared/components/OptionSelected/OptionSelected";
14
16
  import { SearchHighlight } from "../../../shared/components/SearchHighlight/SearchHighlight";
17
+ import { useSetScopeConfig, SCOPE_KEYS } from "../../../shared/hooks";
15
18
  import { ADDRESS_MESSAGES } from "../../constants/messages";
16
19
  import { useDebouncedSearchAddressByUnitId } from "../../hooks/useDebouncedSearchAddressByUnitId";
17
20
  import { useGetDefaultAddress } from "../../hooks/useGetDefaultAddresses";
@@ -28,14 +31,27 @@ export type CreateAddressSettingsDrawerProps = Omit<
28
31
  onUpdate?: () => void;
29
32
  };
30
33
 
34
+ export const ADDRESS_LIST_TYPE_OPTIONS: ListTypeOption[] = [
35
+ {
36
+ ...DEFAULT_LIST_TYPE_OPTIONS[0],
37
+ description: "Manage a unique list of addresses for this organization.",
38
+ },
39
+ {
40
+ ...DEFAULT_LIST_TYPE_OPTIONS[1],
41
+ description:
42
+ "Use the shared list of addresses defined by the contract. Updates are automatic.",
43
+ },
44
+ ];
45
+
31
46
  export const CreateAddressSettingsDrawer = ({
32
47
  close,
33
48
  onUpdate,
34
- ...props
49
+ ...otherProps
35
50
  }: CreateAddressSettingsDrawerProps) => {
36
51
  const { pushToast } = useUI();
37
52
  const router = useRouter();
38
53
 
54
+ const [listType, setListType] = useState<"sync" | "custom">("custom");
39
55
  const [searchValue, setSearchValue] = useState("");
40
56
  const [activeField, setActiveField] = useState<"Shipping" | "Billing" | null>(
41
57
  "Shipping"
@@ -103,6 +119,21 @@ export const CreateAddressSettingsDrawer = ({
103
119
  },
104
120
  });
105
121
 
122
+ const { setScopeConfig, isSetScopeConfigLoading } = useSetScopeConfig({
123
+ onSuccess: () => {
124
+ pushToast({
125
+ message: "Scope configuration updated successfully",
126
+ status: "INFO",
127
+ });
128
+ },
129
+ onError: () => {
130
+ pushToast({
131
+ message: "Failed to update scope configuration",
132
+ status: "ERROR",
133
+ });
134
+ },
135
+ });
136
+
106
137
  const isConfirmButtonEnabled = Boolean(
107
138
  shippingAddress?.id || billingAddress?.id
108
139
  );
@@ -125,6 +156,15 @@ export const CreateAddressSettingsDrawer = ({
125
156
  }
126
157
 
127
158
  const handleConfirmClick = () => {
159
+ // Update scope config if listType changed
160
+ setScopeConfig({
161
+ customerId: router.query.contractId as string,
162
+ unitId: router.query.orgUnitId as string,
163
+ scopeName: SCOPE_KEYS.ADDRESSES,
164
+ type: listType,
165
+ });
166
+
167
+ // Set default addresses
128
168
  setDefaultAddresses({
129
169
  orgUnitId: router.query.orgUnitId as string,
130
170
  customerId: router.query.contractId as string,
@@ -133,16 +173,29 @@ export const CreateAddressSettingsDrawer = ({
133
173
  };
134
174
 
135
175
  return (
136
- <BasicDrawer
137
- data-fs-bp-create-address-settings-drawer
176
+ <SettingsDrawer
177
+ title="Address settings"
178
+ {...otherProps}
138
179
  close={close}
139
- {...props}
180
+ onPrimaryAction={handleConfirmClick}
181
+ isPrimaryButtonLoading={
182
+ isSetDefaultAddressesLoading || isSetScopeConfigLoading
183
+ }
184
+ isPrimaryButtonDisabled={!isConfirmButtonEnabled}
185
+ scopeName={SCOPE_KEYS.ADDRESSES}
186
+ onDismiss={close}
187
+ data-fs-bp-create-address-settings-drawer
140
188
  >
141
- <BasicDrawer.Heading title="Address settings" onClose={close} />
189
+ <SettingsDrawer.ListType
190
+ title="List type"
191
+ name="listType"
192
+ value={listType}
193
+ onChange={setListType}
194
+ options={ADDRESS_LIST_TYPE_OPTIONS}
195
+ />
142
196
 
143
- <BasicDrawer.Body>
144
- <h3>Default addresses</h3>
145
- <h4>Select the default addresses for this unit</h4>
197
+ <div data-fs-bp-default-addresses-section>
198
+ <h4 data-fs-bp-default-addresses-title>Default addresses</h4>
146
199
 
147
200
  <p data-fs-bp-default-address-label>
148
201
  Default shipping address (optional)
@@ -248,21 +301,7 @@ export const CreateAddressSettingsDrawer = ({
248
301
  )}
249
302
  />
250
303
  )}
251
- </BasicDrawer.Body>
252
-
253
- <BasicDrawer.Footer>
254
- <BasicDrawer.Button variant="ghost" onClick={close}>
255
- Cancel
256
- </BasicDrawer.Button>
257
- <BasicDrawer.Button
258
- variant="confirm"
259
- disabled={!isConfirmButtonEnabled}
260
- onClick={handleConfirmClick}
261
- isLoading={isSetDefaultAddressesLoading}
262
- >
263
- Save
264
- </BasicDrawer.Button>
265
- </BasicDrawer.Footer>
266
- </BasicDrawer>
304
+ </div>
305
+ </SettingsDrawer>
267
306
  );
268
307
  };
@@ -1,9 +1,10 @@
1
- @import "../../../shared/components/BasicDrawer/basic-drawer.scss";
1
+ @import "../../../shared/components/SettingsDrawer/settings-drawer.scss";
2
2
 
3
3
  [data-fs-bp-create-address-settings-drawer] {
4
4
  @import "../../../shared/components/InputText/input-text.scss";
5
5
  @import "../../../shared/components/ErrorMessage/error-message.scss";
6
6
  @import "../../../shared/components/SearchHighlight/search-highlight.scss";
7
+
7
8
  @import "../ExistingAddress/existing-address.scss";
8
9
  @import "../LocationForm/location-form.scss";
9
10
  @import "../RecipientsForm/recipients-form.scss";
@@ -12,33 +13,32 @@
12
13
  @import "@faststore/ui/src/components/atoms/Button/styles.scss";
13
14
  @import "@faststore/ui/src/components/molecules/Alert/styles.scss";
14
15
 
15
- [data-fs-bp-basic-drawer-body] {
16
+ [data-fs-bp-settings-drawer-body] {
16
17
  padding-bottom: 90px;
17
18
 
18
- h3 {
19
- font-weight: var(--fs-text-weight-semibold);
20
- font-size: var(--fs-text-size-2);
21
- line-height: var(--fs-spacing-4);
22
- margin-top: var(--fs-spacing-6);
23
-
24
- &:first-of-type {
19
+ [data-fs-bp-default-addresses-section] {
20
+ [data-fs-bp-default-addresses-title] {
21
+ font-weight: var(--fs-text-weight-semibold);
22
+ font-size: var(--fs-text-size-1);
23
+ line-height: 1.25rem;
24
+ letter-spacing: -0.01em;
25
+ margin-bottom: var(--fs-spacing-4);
25
26
  margin-top: 0;
27
+ color: #000000;
26
28
  }
27
- }
28
29
 
29
- h4 {
30
- font-weight: var(--fs-text-weight-regular);
31
- font-size: var(--fs-text-size-1);
32
- line-height: calc(var(--fs-spacing-0) + var(--fs-spacing-3));
33
- margin-bottom: calc(var(--fs-spacing-0) + var(--fs-spacing-3));
34
- }
30
+ [data-fs-bp-default-address-label] {
31
+ font-weight: var(--fs-text-weight-regular);
32
+ font-size: var(--fs-text-size-1);
33
+ line-height: 1.5;
34
+ color: #1f1f1f;
35
+ margin-bottom: var(--fs-spacing-1);
36
+ margin-top: var(--fs-spacing-4);
35
37
 
36
- [data-fs-bp-default-address-label] {
37
- font-weight: var(--fs-text-weight-regular);
38
- font-size: var(--fs-text-size-0);
39
- line-height: calc(var(--fs-spacing-0) + var(--fs-spacing-3));
40
- color: #1f1f1f;
41
- margin-bottom: var(--fs-spacing-0);
38
+ &:first-of-type {
39
+ margin-top: 0;
40
+ }
41
+ }
42
42
  }
43
43
  }
44
44
 
@@ -1,5 +1,8 @@
1
1
  import { Icon, InputText } from "../../../../shared/components";
2
- import { maskPhoneNumber } from "../../../../shared/utils/phoneNumber";
2
+ import {
3
+ maskPhoneNumber,
4
+ normalizePhoneNumber,
5
+ } from "../../../../shared/utils/phoneNumber";
3
6
 
4
7
  import type { RecipientInput } from "../../../types";
5
8
 
@@ -48,7 +51,7 @@ export const RecipientItem = ({
48
51
  <InputText
49
52
  label="Phone Number"
50
53
  className="recipients-phone"
51
- value={recipient.recipientPhone}
54
+ value={maskPhoneNumber(recipient.recipientPhone, "USA")}
52
55
  wrapperProps={{
53
56
  style: {
54
57
  borderTopLeftRadius: 0,
@@ -59,7 +62,7 @@ export const RecipientItem = ({
59
62
  onChange(
60
63
  index,
61
64
  "recipientPhone",
62
- maskPhoneNumber(event.target.value, "USA")
65
+ normalizePhoneNumber(event.target.value)
63
66
  )
64
67
  }
65
68
  />
@@ -1,5 +1,5 @@
1
1
  export type CountryOption = {
2
- id: string;
2
+ id: CountryCodes;
3
3
  name: string;
4
4
  };
5
5
 
@@ -9,3 +9,5 @@ export const CountryOptions: CountryOption[] = [
9
9
  { id: "CAN", name: "Canada" },
10
10
  { id: "MEX", name: "Mexico" },
11
11
  ];
12
+
13
+ export type CountryCodes = "USA" | "MEX" | "BRA" | "CAN";
@@ -13,7 +13,7 @@ const mapDefaultAddressToAddressData = (
13
13
  response: DefaultAddressResponse
14
14
  ): AddressData => {
15
15
  return {
16
- id: response.userId || "",
16
+ id: response.id,
17
17
  name: response.addressLabel,
18
18
  types: [response.addressType],
19
19
  isActive: response.isActive,
@@ -1,3 +1,4 @@
1
+ import { normalizePhoneNumber } from "../../../shared/utils/phoneNumber";
1
2
  import { recipientsClient } from "../../clients/RecipientsClient";
2
3
 
3
4
  import type { RecipientsResponse } from "../../types/AddressData";
@@ -34,7 +35,13 @@ export const getAddressRecipientsService = async ({
34
35
  page
35
36
  );
36
37
 
37
- return recipients;
38
+ return {
39
+ ...recipients,
40
+ data: recipients.data.map((recipient) => ({
41
+ ...recipient,
42
+ phone: normalizePhoneNumber(recipient.phone),
43
+ })),
44
+ };
38
45
  } catch (err) {
39
46
  console.error("Failed to get address recipients", err);
40
47
  return { data: [], total: 0 };
@@ -184,4 +184,5 @@ export type DefaultAddressResponse = Pick<
184
184
  geoCoordinate: string | null;
185
185
  isActive: boolean;
186
186
  userId?: string;
187
+ id: string;
187
188
  };
@@ -45,7 +45,7 @@ export const PaymentMethodsLayout = ({
45
45
  setSelectedMethod(undefined);
46
46
  });
47
47
 
48
- const isLastPage = data.paging.pages === page || data.paging.pages === 0;
48
+ const isLastPage = data.paging?.pages === page || data.paging?.pages === 0;
49
49
 
50
50
  const {
51
51
  isLoading,
@@ -171,9 +171,9 @@ export const PaymentMethodsLayout = ({
171
171
  textSearch={setSearchTerm}
172
172
  />
173
173
  <Paginator.Counter
174
- total={data.paging.total}
174
+ total={data.paging?.total ?? 0}
175
175
  itemsLength={
176
- isLastPage ? data.paging.total : page * data.paging.perPage
176
+ isLastPage ? data.paging?.total : page * data.paging?.perPage
177
177
  }
178
178
  />
179
179
  </div>
@@ -182,7 +182,7 @@ export const PaymentMethodsLayout = ({
182
182
 
183
183
  {!isLoading && paymentMethods.length > 0 && (
184
184
  <div data-fs-bp-payment-methods-paginator>
185
- {data.paging.page > 1 ? (
185
+ {(data.paging?.page ?? 1) > 1 ? (
186
186
  <Paginator.NextPageButton
187
187
  onClick={decreasePage}
188
188
  disabled={isLoading}
@@ -204,9 +204,11 @@ export const PaymentMethodsLayout = ({
204
204
  )}
205
205
 
206
206
  <Paginator.Counter
207
- total={data.paging.total}
207
+ total={data.paging?.total ?? 0}
208
208
  itemsLength={
209
- isLastPage ? data.paging.total : page * data.paging.perPage
209
+ isLastPage
210
+ ? data.paging?.total ?? 0
211
+ : page * (data.paging?.perPage ?? 0)
210
212
  }
211
213
  />
212
214
  </div>
@@ -1,4 +1,4 @@
1
- import { getApiUrl } from "../../shared/utils";
1
+ import { getApiUrl, SCOPE_KEYS } from "../../shared/utils";
2
2
 
3
3
  import { Client } from "./Client";
4
4
 
@@ -32,8 +32,44 @@ export default class ScopeClient extends Client {
32
32
  }
33
33
  );
34
34
  }
35
+
36
+ getScopeConfig(
37
+ customerId: string,
38
+ unitId: string,
39
+ scopeName: string,
40
+ cookie: string
41
+ ) {
42
+ return this.get<{ type: "sync" | "custom" }>(
43
+ `customers/${customerId}/units/${unitId}/scopes/configs`,
44
+ {
45
+ params: { scopeName },
46
+ headers: {
47
+ Cookie: cookie,
48
+ },
49
+ }
50
+ );
51
+ }
52
+
53
+ setScopeConfig(
54
+ customerId: string,
55
+ unitId: string,
56
+ scopeName: string,
57
+ type: "sync" | "custom",
58
+ cookie: string
59
+ ) {
60
+ return this.post<{ message: string }, { type: "sync" | "custom" }>(
61
+ `customers/${customerId}/units/${unitId}/scopes/configs`,
62
+ { type },
63
+ {
64
+ params: { scopeName },
65
+ headers: {
66
+ Cookie: cookie,
67
+ },
68
+ }
69
+ );
70
+ }
35
71
  }
36
72
 
37
73
  const scopesClient = new ScopeClient();
38
74
 
39
- export { scopesClient };
75
+ export { scopesClient, SCOPE_KEYS };
@@ -1,4 +1,4 @@
1
- // import { isDevelopment } from "../../utils/environment";
1
+ import { isDevelopment } from "../../utils/environment";
2
2
  import { Icon } from "../Icon";
3
3
 
4
4
  export type ErrorProps = {
@@ -20,18 +20,20 @@ export default function Error({ error }: ErrorProps) {
20
20
  <button data-fs-bp-error-button onClick={() => window.location.reload()}>
21
21
  Try again
22
22
  </button>
23
- <div data-fs-bp-error-details>
24
- <span data-fs-bp-error-details-type>{error?.tags?.errorType}</span>
25
- <h2 data-fs-bp-error-details-title>Error Details</h2>
26
- <p data-fs-bp-error-details-message>{error?.error.message}</p>
27
- <p data-fs-bp-error-details-stack>Stack: {error?.error.stack}</p>
28
- <p data-fs-bp-error-details-component>
29
- Component: {error?.tags?.component}
30
- </p>
31
- <p data-fs-bp-error-details-query>
32
- Query: {JSON.stringify(error?.query)}
33
- </p>
34
- </div>
23
+ {isDevelopment() && (
24
+ <div data-fs-bp-error-details>
25
+ <span data-fs-bp-error-details-type>{error?.tags?.errorType}</span>
26
+ <h2 data-fs-bp-error-details-title>Error Details</h2>
27
+ <p data-fs-bp-error-details-message>{error?.error.message}</p>
28
+ <p data-fs-bp-error-details-stack>Stack: {error?.error.stack}</p>
29
+ <p data-fs-bp-error-details-component>
30
+ Component: {error?.tags?.component}
31
+ </p>
32
+ <p data-fs-bp-error-details-query>
33
+ Query: {JSON.stringify(error?.query)}
34
+ </p>
35
+ </div>
36
+ )}
35
37
  </div>
36
38
  );
37
39
  }
@@ -0,0 +1,106 @@
1
+ import React from "react";
2
+
3
+ import { useBuyerPortal } from "../../hooks";
4
+ import { BasicDrawer, type BasicDrawerProps } from "../BasicDrawer/BasicDrawer";
5
+
6
+ import {
7
+ SettingsDrawerContext,
8
+ type SettingsDrawerContextType,
9
+ } from "./SettingsDrawerContext";
10
+ import {
11
+ SettingsDrawerListType,
12
+ type ListTypeOption,
13
+ type SettingsDrawerListTypeProps,
14
+ DEFAULT_LIST_TYPE_OPTIONS,
15
+ } from "./SettingsDrawerListType";
16
+
17
+ export type { ListTypeOption, SettingsDrawerListTypeProps };
18
+ export { DEFAULT_LIST_TYPE_OPTIONS };
19
+
20
+ export type SettingsDrawerProps = Omit<BasicDrawerProps, "children"> & {
21
+ title: string;
22
+ subtitle?: string;
23
+ children: React.ReactNode;
24
+ primaryButtonLabel?: string;
25
+ secondaryButtonLabel?: string;
26
+ onPrimaryAction?: () => void;
27
+ onSecondaryAction?: () => void;
28
+ isPrimaryButtonLoading?: boolean;
29
+ isPrimaryButtonDisabled?: boolean;
30
+ isSecondaryButtonDisabled?: boolean;
31
+ scopeName?: string;
32
+ customerId?: string;
33
+ unitId?: string;
34
+ onScopeConfigChange?: (type: "sync" | "custom") => void;
35
+ };
36
+
37
+ export const SettingsDrawer = ({
38
+ title,
39
+ subtitle,
40
+ children,
41
+ primaryButtonLabel = "Save",
42
+ secondaryButtonLabel = "Cancel",
43
+ onPrimaryAction,
44
+ onSecondaryAction,
45
+ isPrimaryButtonLoading = false,
46
+ isPrimaryButtonDisabled = false,
47
+ isSecondaryButtonDisabled = false,
48
+ scopeName,
49
+ customerId,
50
+ unitId,
51
+ onScopeConfigChange,
52
+ onDismiss,
53
+ ...otherProps
54
+ }: SettingsDrawerProps) => {
55
+ const { clientContext, currentOrgUnit, currentContract } = useBuyerPortal();
56
+
57
+ const resolvedCustomerId =
58
+ customerId || currentContract?.id || clientContext.customerId;
59
+ const resolvedUnitId = unitId || currentOrgUnit?.id || "";
60
+
61
+ const handleClose = () => {
62
+ onDismiss?.();
63
+ };
64
+
65
+ const contextValue: SettingsDrawerContextType = {
66
+ scopeName,
67
+ customerId: resolvedCustomerId,
68
+ unitId: resolvedUnitId,
69
+ onScopeConfigChange,
70
+ };
71
+
72
+ return (
73
+ <SettingsDrawerContext.Provider value={contextValue}>
74
+ <BasicDrawer
75
+ data-fs-bp-settings-drawer
76
+ onDismiss={handleClose}
77
+ {...otherProps}
78
+ >
79
+ <BasicDrawer.Heading title={title} onClose={handleClose} />
80
+ <BasicDrawer.Body data-fs-bp-settings-drawer-body>
81
+ {subtitle && <p data-fs-bp-settings-drawer-subtitle>{subtitle}</p>}
82
+ {children}
83
+ </BasicDrawer.Body>
84
+ <BasicDrawer.Footer>
85
+ <BasicDrawer.Button
86
+ variant="ghost"
87
+ onClick={onSecondaryAction || handleClose}
88
+ disabled={isSecondaryButtonDisabled}
89
+ >
90
+ {secondaryButtonLabel}
91
+ </BasicDrawer.Button>
92
+ <BasicDrawer.Button
93
+ variant="confirm"
94
+ onClick={onPrimaryAction}
95
+ isLoading={isPrimaryButtonLoading}
96
+ disabled={isPrimaryButtonDisabled}
97
+ >
98
+ {primaryButtonLabel}
99
+ </BasicDrawer.Button>
100
+ </BasicDrawer.Footer>
101
+ </BasicDrawer>
102
+ </SettingsDrawerContext.Provider>
103
+ );
104
+ };
105
+
106
+ SettingsDrawer.ListType = SettingsDrawerListType;
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ export type SettingsDrawerContextType = {
4
+ scopeName?: string;
5
+ customerId?: string;
6
+ unitId?: string;
7
+ onScopeConfigChange?: (type: "sync" | "custom") => void;
8
+ };
9
+
10
+ export const SettingsDrawerContext = createContext<
11
+ SettingsDrawerContextType | undefined
12
+ >(undefined);
13
+
14
+ export const useSettingsDrawerContext = (): SettingsDrawerContextType => {
15
+ const context = useContext(SettingsDrawerContext);
16
+ return context || {};
17
+ };
@@ -0,0 +1,100 @@
1
+ import React, { useState } from "react";
2
+
3
+ import { RadioGroup, RadioOption } from "@faststore/ui";
4
+
5
+ import { useGetScopeConfig } from "../../hooks";
6
+
7
+ import { useSettingsDrawerContext } from "./SettingsDrawerContext";
8
+
9
+ export type ListTypeOption = {
10
+ value: string;
11
+ label: string;
12
+ description: string;
13
+ };
14
+
15
+ export const DEFAULT_LIST_TYPE_OPTIONS: ListTypeOption[] = [
16
+ {
17
+ value: "custom",
18
+ label: "Custom list",
19
+ description: "Manage a unique for this organization.",
20
+ },
21
+ {
22
+ value: "sync",
23
+ label: "Synchronized List",
24
+ description:
25
+ "Use the shared list defined by the contract. Updates are automatic.",
26
+ },
27
+ ];
28
+
29
+ export type SettingsDrawerListTypeProps = {
30
+ title: string;
31
+ name: string;
32
+ value?: "sync" | "custom";
33
+ options?: ListTypeOption[];
34
+ onChange?: (value: "sync" | "custom") => void;
35
+ disabled?: boolean;
36
+ };
37
+
38
+ export const SettingsDrawerListType = ({
39
+ title,
40
+ name,
41
+ value: controlledValue,
42
+ options = DEFAULT_LIST_TYPE_OPTIONS,
43
+ onChange: controlledOnChange,
44
+ disabled = false,
45
+ }: SettingsDrawerListTypeProps) => {
46
+ const { scopeName, customerId, unitId } = useSettingsDrawerContext();
47
+ const [internalValue, setInternalValue] = useState<"sync" | "custom">(
48
+ "custom"
49
+ );
50
+
51
+ const { isScopeConfigLoading } = useGetScopeConfig(
52
+ {
53
+ customerId: customerId ?? "",
54
+ unitId: unitId ?? "",
55
+ scopeName: scopeName ?? "",
56
+ },
57
+ {
58
+ lazy: !scopeName || !customerId || !unitId,
59
+ onSuccess: (data) => {
60
+ if (data?.type) {
61
+ setInternalValue(data.type);
62
+ controlledOnChange?.(data.type);
63
+ }
64
+ },
65
+ }
66
+ );
67
+
68
+ const value = controlledValue ?? internalValue;
69
+ const isLoading = isScopeConfigLoading;
70
+
71
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
72
+ const newValue = e.target.value as "sync" | "custom";
73
+ setInternalValue(newValue);
74
+
75
+ if (controlledOnChange) {
76
+ controlledOnChange(newValue);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div data-fs-bp-settings-drawer-list-type>
82
+ <h4 data-fs-bp-settings-drawer-list-type-title>{title}</h4>
83
+ <RadioGroup name={name} selectedValue={value} onChange={handleChange}>
84
+ {options.map((option) => (
85
+ <div key={option.value} data-fs-bp-settings-drawer-list-type-option>
86
+ <RadioOption
87
+ value={option.value}
88
+ label={option.label}
89
+ name={name}
90
+ disabled={disabled || isLoading}
91
+ />
92
+ <p data-fs-bp-settings-drawer-list-type-description>
93
+ {option.description}
94
+ </p>
95
+ </div>
96
+ ))}
97
+ </RadioGroup>
98
+ </div>
99
+ );
100
+ };
@@ -0,0 +1,61 @@
1
+ [data-fs-bp-settings-drawer] {
2
+ @import "@faststore/ui/src/components/atoms/Radio/styles.scss";
3
+
4
+ [data-fs-bp-settings-drawer-body] {
5
+ color: #1f1f1f;
6
+ font-size: var(--fs-text-size-1);
7
+
8
+ [data-fs-bp-settings-drawer-subtitle] {
9
+ margin-bottom: var(--fs-spacing-5);
10
+ color: #1f1f1f;
11
+ }
12
+
13
+ [data-fs-bp-settings-drawer-list-type] {
14
+ display: flex;
15
+ flex-direction: column;
16
+ margin-bottom: var(--fs-spacing-4);
17
+
18
+ h4[data-fs-bp-settings-drawer-list-type-title] {
19
+ color: #000000;
20
+ font-weight: var(--fs-text-weight-semibold);
21
+ font-size: var(--fs-text-size-1);
22
+ line-height: 1.25rem;
23
+ letter-spacing: -0.01em;
24
+ margin-bottom: var(--fs-spacing-4);
25
+ margin-top: 0;
26
+ }
27
+
28
+ [data-fs-bp-settings-drawer-list-type-option] {
29
+ display: flex;
30
+ flex-direction: column;
31
+ margin-bottom: var(--fs-spacing-4);
32
+
33
+ &:last-child {
34
+ margin-bottom: 0;
35
+ }
36
+
37
+ [data-fs-radio-group-option] {
38
+ display: flex;
39
+ align-items: flex-start;
40
+ gap: var(--fs-spacing-2);
41
+
42
+ label {
43
+ font-weight: var(--fs-text-weight-regular);
44
+ font-size: var(--fs-text-size-1);
45
+ color: #1f1f1f;
46
+ }
47
+ }
48
+
49
+ [data-fs-bp-settings-drawer-list-type-description] {
50
+ margin-left: var(--fs-spacing-5);
51
+ margin-bottom: 0;
52
+ color: #707070;
53
+ font-weight: var(--fs-text-weight-regular);
54
+ font-size: var(--fs-bp-text-size-0);
55
+ line-height: var(--fs-bp-text-size-2);
56
+ letter-spacing: 0;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
@@ -7,6 +7,13 @@ export {
7
7
  export { useAutocompletePosition } from "./AutocompleteDropdown/useAutocompletePosition";
8
8
  export { BasicCard, type BasicCardProps } from "./BasicCard/BasicCard";
9
9
  export { BasicDrawer, type BasicDrawerProps } from "./BasicDrawer/BasicDrawer";
10
+ export {
11
+ SettingsDrawer,
12
+ type SettingsDrawerProps,
13
+ type ListTypeOption,
14
+ type SettingsDrawerListTypeProps,
15
+ DEFAULT_LIST_TYPE_OPTIONS,
16
+ } from "./SettingsDrawer/SettingsDrawer";
10
17
  export {
11
18
  BasicDropdownMenu,
12
19
  type BasicDropdownMenuProps,
@@ -15,3 +15,5 @@ export { useRouterLoading } from "./useRouterLoading";
15
15
  export { useLogger } from "./useLogger";
16
16
  export { useGetDependenciesVersion } from "./useGetDependenciesVersion";
17
17
  export { useAnalytics } from "./analytics/useAnalytics";
18
+ export { useGetScopeConfig, SCOPE_KEYS } from "./useGetScopeConfig";
19
+ export { useSetScopeConfig } from "./useSetScopeConfig";
@@ -0,0 +1,35 @@
1
+ import {
2
+ getScopeConfigService,
3
+ type GetScopeConfigServiceProps,
4
+ SCOPE_KEYS,
5
+ } from "../services";
6
+
7
+ import { useQuery, type QueryOptions } from "./useQuery";
8
+
9
+ import type { AwaitedType } from "../types";
10
+
11
+ export const useGetScopeConfig = (
12
+ props: Omit<GetScopeConfigServiceProps, "cookie">,
13
+ options?: QueryOptions<AwaitedType<typeof getScopeConfigService>>
14
+ ) => {
15
+ const { data, error, isLoading, refetch } = useQuery<
16
+ AwaitedType<typeof getScopeConfigService>
17
+ >(
18
+ `scope-config-${props.customerId}-${props.unitId}-${props.scopeName}`,
19
+ (clientContext) =>
20
+ getScopeConfigService({
21
+ ...props,
22
+ cookie: clientContext.cookie,
23
+ }),
24
+ options
25
+ );
26
+
27
+ return {
28
+ scopeConfig: data,
29
+ isScopeConfigLoading: isLoading,
30
+ hasScopeConfigError: error,
31
+ refetchScopeConfig: refetch,
32
+ };
33
+ };
34
+
35
+ export { SCOPE_KEYS };
@@ -0,0 +1,30 @@
1
+ import {
2
+ setScopeConfigService,
3
+ type SetScopeConfigServiceProps,
4
+ SCOPE_KEYS,
5
+ } from "../services";
6
+
7
+ import { type MutationOptions, useMutation } from "./useMutation";
8
+
9
+ import type { AwaitedType } from "../types";
10
+
11
+ export const useSetScopeConfig = (
12
+ options?: MutationOptions<AwaitedType<typeof setScopeConfigService>>
13
+ ) => {
14
+ const { mutate, isLoading, error } = useMutation<
15
+ AwaitedType<typeof setScopeConfigService>,
16
+ Omit<SetScopeConfigServiceProps, "cookie">
17
+ >(
18
+ (variables, clientContext) =>
19
+ setScopeConfigService({ ...variables, cookie: clientContext.cookie }),
20
+ options
21
+ );
22
+
23
+ return {
24
+ setScopeConfig: mutate,
25
+ isSetScopeConfigLoading: isLoading,
26
+ hasSetScopeConfigError: error,
27
+ };
28
+ };
29
+
30
+ export { SCOPE_KEYS };
@@ -0,0 +1,19 @@
1
+ import { scopesClient, SCOPE_KEYS } from "../clients/ScopeClient";
2
+
3
+ export type GetScopeConfigServiceProps = {
4
+ customerId: string;
5
+ unitId: string;
6
+ scopeName: string;
7
+ cookie: string;
8
+ };
9
+
10
+ export const getScopeConfigService = async ({
11
+ customerId,
12
+ unitId,
13
+ scopeName,
14
+ cookie,
15
+ }: GetScopeConfigServiceProps) => {
16
+ return scopesClient.getScopeConfig(customerId, unitId, scopeName, cookie);
17
+ };
18
+
19
+ export { SCOPE_KEYS };
@@ -15,3 +15,12 @@ export {
15
15
  getDependenciesVersionService,
16
16
  type GetDependenciesVersionProps,
17
17
  } from "./get-dependencies-version.service";
18
+ export {
19
+ getScopeConfigService,
20
+ type GetScopeConfigServiceProps,
21
+ SCOPE_KEYS,
22
+ } from "./get-scope-config.service";
23
+ export {
24
+ setScopeConfigService,
25
+ type SetScopeConfigServiceProps,
26
+ } from "./set-scope-config.service";
@@ -0,0 +1,27 @@
1
+ import { scopesClient, SCOPE_KEYS } from "../clients/ScopeClient";
2
+
3
+ export type SetScopeConfigServiceProps = {
4
+ customerId: string;
5
+ unitId: string;
6
+ scopeName: string;
7
+ type: "sync" | "custom";
8
+ cookie: string;
9
+ };
10
+
11
+ export const setScopeConfigService = async ({
12
+ customerId,
13
+ unitId,
14
+ scopeName,
15
+ type,
16
+ cookie,
17
+ }: SetScopeConfigServiceProps) => {
18
+ return scopesClient.setScopeConfig(
19
+ customerId,
20
+ unitId,
21
+ scopeName,
22
+ type,
23
+ cookie
24
+ );
25
+ };
26
+
27
+ export { SCOPE_KEYS };
@@ -9,3 +9,4 @@ export type {
9
9
  PaymentMethodsReqCommonParams,
10
10
  } from "./PaymentMethodsClientTypes";
11
11
  export type { ScopeInput } from "./ScopeInput";
12
+ export type { AwaitedType } from "./AwaitedType";
@@ -13,4 +13,13 @@ export const LOCAL_STORAGE_LOCATION_EDIT_KEY = "bp_hide_edit_location_confirm";
13
13
  export const LOCAL_STORAGE_RECIPIENT_EDIT_KEY =
14
14
  "bp_hide_edit_recipient_confirm";
15
15
 
16
- export const CURRENT_VERSION = "1.3.41";
16
+ export const SCOPE_KEYS = {
17
+ CONTRACTS: "contractIds",
18
+ ADDRESSES: "addresses",
19
+ CUSTOM_FIELDS: "customFields",
20
+ COLLECTIONS: "collectionIds",
21
+ PAYMENT_SYSTEMS: "paymentSystemIds",
22
+ CREDIT_CARDS: "creditCards",
23
+ } as const;
24
+
25
+ export const CURRENT_VERSION = "1.3.43";
@@ -1,7 +1,12 @@
1
1
  export { addressLabelToPropMapping } from "./addresLabelToPropMapping";
2
2
  export { getApiUrl, getPostalCodeApiUrl, getTokenizationUrl } from "./api";
3
3
  export { compareItems } from "./compareItems";
4
- export { API_URL, AUT_COOKIE_KEY, DEBOUNCE_TIMEOUT } from "./constants";
4
+ export {
5
+ API_URL,
6
+ AUT_COOKIE_KEY,
7
+ DEBOUNCE_TIMEOUT,
8
+ SCOPE_KEYS,
9
+ } from "./constants";
5
10
  export {
6
11
  getAuthCookie,
7
12
  getCookieAsString,
@@ -1,18 +1,34 @@
1
+ import { type CountryCodes } from "../../addresses/data/countries";
2
+
1
3
  import mask from "./mask";
2
4
 
3
- export function maskPhoneNumber(phoneNumber: string, country?: string) {
4
- if (!phoneNumber) return phoneNumber;
5
+ type MaskType = {
6
+ mask: string;
7
+ length: number;
8
+ ddi: string;
9
+ };
10
+
11
+ const MASK_MAP: Record<CountryCodes, MaskType> = {
12
+ USA: { mask: "(999) 999-9999", length: 14, ddi: "1" },
13
+ MEX: { mask: "(999) 999-9999", length: 14, ddi: "52" },
14
+ BRA: { mask: "(99) 99999-9999", length: 15, ddi: "55" },
15
+ CAN: { mask: "(999) 999-9999", length: 14, ddi: "1" },
16
+ };
17
+
18
+ export function maskPhoneNumber(phoneNumber: string, country?: CountryCodes) {
19
+ if (!phoneNumber || !country || !MASK_MAP[country]) return phoneNumber;
20
+
21
+ const { mask: phoneMask, length, ddi } = MASK_MAP[country];
5
22
 
6
- switch (country) {
7
- case "USA":
8
- return mask(phoneNumber, "(999) 999-9999").substring(0, 14);
9
- case "MEX":
10
- return mask(phoneNumber, "(999) 999-9999").substring(0, 14);
11
- case "BRA":
12
- return mask(phoneNumber, "(99) 99999-9999").substring(0, 15);
13
- case "CAN":
14
- return mask(phoneNumber, "(999) 999-9999").substring(0, 14);
15
- default:
16
- return phoneNumber;
17
- }
23
+ const normalizedPhone = normalizePhoneNumber(phoneNumber);
24
+ const phoneWithoutDdi = normalizedPhone.startsWith(ddi)
25
+ ? normalizedPhone.slice(ddi.length)
26
+ : normalizedPhone;
27
+
28
+ return `+${ddi} ${mask(phoneWithoutDdi, phoneMask).substring(0, length)}`;
29
+ }
30
+
31
+ export function normalizePhoneNumber(phoneNumber: string) {
32
+ if (!phoneNumber) return phoneNumber;
33
+ return phoneNumber.replace(/\D/g, "");
18
34
  }
@@ -15,7 +15,10 @@ import {
15
15
  import { useAnalytics } from "../../../shared/hooks";
16
16
  import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
17
17
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
18
- import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
18
+ import {
19
+ maskPhoneNumber,
20
+ normalizePhoneNumber,
21
+ } from "../../../shared/utils/phoneNumber";
19
22
  import { useAddUserToOrgUnit } from "../../hooks";
20
23
 
21
24
  export type CreateUserDrawerProps = Omit<BasicDrawerProps, "children"> & {
@@ -256,12 +259,12 @@ export const CreateUserDrawer = ({
256
259
 
257
260
  <InputText
258
261
  label="Phone number (optional)"
259
- value={phone}
262
+ value={maskPhoneNumber(phone, "USA")}
260
263
  onChange={(event) =>
261
264
  // TODO: Update this when implementing i18n
262
265
  updateField(
263
266
  "phone",
264
- maskPhoneNumber(event.target.value, "USA")
267
+ normalizePhoneNumber(event.target.value)
265
268
  )
266
269
  }
267
270
  />
@@ -12,7 +12,10 @@ import {
12
12
  } from "../../../shared/components";
13
13
  import { useAnalytics } from "../../../shared/hooks";
14
14
  import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
15
- import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
15
+ import {
16
+ maskPhoneNumber,
17
+ normalizePhoneNumber,
18
+ } from "../../../shared/utils/phoneNumber";
16
19
  import { useGetUserById, useUpdateUser } from "../../hooks";
17
20
 
18
21
  export type UpdateUserDrawerProps = Omit<BasicDrawerProps, "children"> & {
@@ -174,7 +177,7 @@ export const UpdateUserDrawer = ({
174
177
  updateUser({
175
178
  userId,
176
179
  name,
177
- phone,
180
+ phone: phone ?? "",
178
181
  roles,
179
182
  orgUnitId,
180
183
  });
@@ -245,10 +248,10 @@ export const UpdateUserDrawer = ({
245
248
  ) : (
246
249
  <InputText
247
250
  label="Phone number (optional)"
248
- value={phone}
251
+ value={maskPhoneNumber(phone, "USA")}
249
252
  onChange={(event) =>
250
253
  // TODO: Update this when implementing i18n
251
- updateField("phone", maskPhoneNumber(event.target.value, "USA"))
254
+ updateField("phone", normalizePhoneNumber(event.target.value))
252
255
  }
253
256
  />
254
257
  )}
@@ -9,6 +9,7 @@ import { useBuyerPortal, useDrawerProps } from "../../../shared/hooks";
9
9
  import { GlobalLayout } from "../../../shared/layouts";
10
10
  import { OrgUnitTabsLayout } from "../../../shared/layouts/OrgUnitTabsLayout/OrgUnitTabLayout";
11
11
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
12
+ import { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
12
13
  import { ReassignOrgUnitDrawer, UpdateUserDrawer } from "../../components";
13
14
  import { UserDropdownMenu } from "../../components/UserDropdownMenu/UserDropdownMenu";
14
15
 
@@ -82,7 +83,9 @@ export const UserDetailsLayout = ({
82
83
 
83
84
  <div data-fs-user-details-row>
84
85
  <span data-fs-user-details-row-label>Phone number</span>
85
- <span data-fs-user-details-row-value>{user?.phone}</span>
86
+ <span data-fs-user-details-row-value>
87
+ {maskPhoneNumber(user?.phone ?? "", "USA")}
88
+ </span>
86
89
  </div>
87
90
 
88
91
  <hr data-fs-user-details-divider />
@@ -1,3 +1,4 @@
1
+ import { normalizePhoneNumber } from "../../shared/utils/phoneNumber";
1
2
  import { usersClient } from "../clients/UsersClient";
2
3
 
3
4
  import type { UserData } from "../types";
@@ -23,7 +24,7 @@ export const getUserByIdService = async ({
23
24
  roles: role ? role : [],
24
25
  id: userId,
25
26
  email: email ?? "",
26
- phone: phone ?? "",
27
+ phone: normalizePhoneNumber(phone ?? ""),
27
28
  orgUnit: {
28
29
  name: orgUnit,
29
30
  },
@@ -82,7 +82,7 @@ const loaderFunction = async (
82
82
  return {
83
83
  data: paymentMethods,
84
84
  search: search ?? "",
85
- totalPaymentMethods: contractPaymentMethods.paging.total,
85
+ totalPaymentMethods: contractPaymentMethods?.paging?.total ?? 0,
86
86
  context: {
87
87
  clientContext: { cookie, userId, ...clientContext },
88
88
  currentOrgUnit,