@vtex/faststore-plugin-buyer-portal 1.1.91 → 1.1.93

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 (55) hide show
  1. package/package.json +1 -1
  2. package/public/buyer-portal-icons.svg +1 -1
  3. package/src/features/addresses/services/get-addresses-by-unit-id.service.ts +13 -1
  4. package/src/features/addresses/services/get-addresses.service.ts +15 -2
  5. package/src/features/buying-policies/services/get-buying-policies.service.ts +19 -21
  6. package/src/features/contracts/services/get-contract-details.service.ts +13 -1
  7. package/src/features/contracts/services/get-contracts-org-by-unit-id.service.ts +18 -11
  8. package/src/features/contracts/services/update-contract-status.service.ts +18 -11
  9. package/src/features/org-units/components/AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown.tsx +3 -1
  10. package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumb.tsx +2 -0
  11. package/src/features/org-units/components/OrgUnitBreadcrumb/OrgUnitBreadcrumbPath.tsx +12 -12
  12. package/src/features/org-units/components/OrgUnitBreadcrumb/org-unit-breadcrumb.scss +47 -36
  13. package/src/features/org-units/types/OrgUnitBreadcrumbTypes.ts +2 -0
  14. package/src/features/profile/layouts/ProfileLayout/profile-layout.scss +1 -0
  15. package/src/features/shared/clients/Client.ts +84 -8
  16. package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +4 -1
  17. package/src/features/shared/components/Error/Error.tsx +31 -0
  18. package/src/features/shared/components/Error/error.scss +71 -0
  19. package/src/features/shared/components/ErrorBoundary/ErrorBoundary.tsx +63 -0
  20. package/src/features/shared/components/ErrorBoundary/types.ts +14 -0
  21. package/src/features/shared/components/index.ts +3 -0
  22. package/src/features/shared/components/withErrorBoundary/withErrorBoundary.tsx +35 -0
  23. package/src/features/shared/layouts/BaseTabsLayout/Navbar.tsx +1 -1
  24. package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +9 -2
  25. package/src/features/shared/layouts/BaseTabsLayout/base-tabs-layout.scss +9 -6
  26. package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +4 -2
  27. package/src/features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout.tsx +119 -0
  28. package/src/features/shared/layouts/ErrorTabsLayout/error-tabs-layout.scss +1 -0
  29. package/src/features/shared/utils/environment.ts +41 -0
  30. package/src/features/shared/utils/extractErrorMessage.ts +22 -0
  31. package/src/features/shared/utils/getHome.tsx +5 -0
  32. package/src/features/shared/utils/index.ts +2 -0
  33. package/src/features/shared/utils/withClientErrorBoundary.ts +61 -0
  34. package/src/features/shared/utils/withLoaderErrorBoundary.ts +46 -0
  35. package/src/features/users/clients/UsersClient.ts +0 -1
  36. package/src/pages/address-details.tsx +38 -11
  37. package/src/pages/addresses.tsx +35 -6
  38. package/src/pages/budgets-details.tsx +38 -8
  39. package/src/pages/budgets.tsx +33 -8
  40. package/src/pages/buying-policies.tsx +36 -12
  41. package/src/pages/buying-policy-details.tsx +38 -8
  42. package/src/pages/collections.tsx +36 -12
  43. package/src/pages/cost-centers.tsx +38 -8
  44. package/src/pages/credit-cards.tsx +38 -8
  45. package/src/pages/home.tsx +22 -5
  46. package/src/pages/org-unit-details.tsx +43 -7
  47. package/src/pages/org-units.tsx +39 -8
  48. package/src/pages/payment-methods.tsx +38 -8
  49. package/src/pages/po-numbers.tsx +38 -8
  50. package/src/pages/profile.tsx +31 -6
  51. package/src/pages/releases.tsx +33 -8
  52. package/src/pages/role-details.tsx +39 -7
  53. package/src/pages/roles.tsx +28 -7
  54. package/src/pages/user-details.tsx +39 -8
  55. package/src/pages/users.tsx +25 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.1.91",
3
+ "version": "1.1.93",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -537,4 +537,4 @@
537
537
  fill="currentColor"
538
538
  />
539
539
  </symbol>
540
- </svg>
540
+ </svg>
@@ -1,4 +1,8 @@
1
1
  import { compareItems, statusFilters } from "../../shared/utils";
2
+ import {
3
+ handleUnauthorizedRedirect,
4
+ withClientErrorBoundary,
5
+ } from "../../shared/utils/withClientErrorBoundary";
2
6
  import { addressesClient } from "../clients/AddressesClient";
3
7
 
4
8
  export type GetAddressesByUnitIdServiceProps = Partial<{
@@ -13,7 +17,7 @@ export type GetAddressesByUnitIdServiceProps = Partial<{
13
17
  cookie: string;
14
18
  };
15
19
 
16
- export const getAddressesByUnitIdService = async ({
20
+ export const getAddressesByUnitIdServiceFn = async ({
17
21
  orgUnitId,
18
22
  status,
19
23
  type,
@@ -46,3 +50,11 @@ export const getAddressesByUnitIdService = async ({
46
50
 
47
51
  return { data: formattedAddress, total };
48
52
  };
53
+
54
+ export const getAddressesByUnitIdService = withClientErrorBoundary(
55
+ getAddressesByUnitIdServiceFn,
56
+ {
57
+ componentName: "AddressesByUnitId",
58
+ onError: handleUnauthorizedRedirect,
59
+ }
60
+ );
@@ -1,4 +1,9 @@
1
- import { compareItems, statusFilters } from "../../shared/utils";
1
+ import {
2
+ compareItems,
3
+ statusFilters,
4
+ withClientErrorBoundary,
5
+ } from "../../shared/utils";
6
+ import { handleUnauthorizedRedirect } from "../../shared/utils/withClientErrorBoundary";
2
7
  import { addressesClient } from "../clients/AddressesClient";
3
8
 
4
9
  import type { AddressData } from "../types/AddressData";
@@ -15,7 +20,7 @@ export type GetAddressesServiceProps = Partial<{
15
20
  };
16
21
 
17
22
  //TODO: Check if deprecated so we can remove this
18
- export const getAddressesService = async ({
23
+ export const getAddressesServiceFn = async ({
19
24
  search = "",
20
25
  customerId,
21
26
  unitId,
@@ -54,3 +59,11 @@ export const getAddressesService = async ({
54
59
  })
55
60
  .sort((a, b) => compareItems(a, b, sort));
56
61
  };
62
+
63
+ export const getAddressesService = withClientErrorBoundary(
64
+ getAddressesServiceFn,
65
+ {
66
+ componentName: "AddressesService",
67
+ onError: handleUnauthorizedRedirect,
68
+ }
69
+ );
@@ -1,3 +1,7 @@
1
+ import {
2
+ withClientErrorBoundary,
3
+ handleUnauthorizedRedirect,
4
+ } from "../../shared/utils/withClientErrorBoundary";
1
5
  import { buyingPoliciesClient } from "../clients/BuyingPoliciesClient";
2
6
  import { BuyingPolicy } from "../types";
3
7
 
@@ -9,7 +13,7 @@ export type GetBuyingPoliciesServiceProps = {
9
13
  page?: number;
10
14
  };
11
15
 
12
- export const getBuyingPoliciesService = async ({
16
+ const getBuyingPoliciesFunction = async ({
13
17
  search,
14
18
  page = 1,
15
19
  contractId,
@@ -19,25 +23,19 @@ export const getBuyingPoliciesService = async ({
19
23
  data: BuyingPolicy[];
20
24
  total: number;
21
25
  }> => {
22
- try {
23
- return await buyingPoliciesClient.getBuyingPoliciesByOrgUnitId(
24
- contractId,
25
- orgUnitId,
26
- cookie,
27
- search,
28
- page
29
- );
30
- } catch (error) {
31
- console.error("Error in getBuyingPoliciesService:", {
32
- error,
33
- orgUnitId,
34
- contractId,
35
- errorMessage: (error as { message?: string })?.message,
36
- });
26
+ return await buyingPoliciesClient.getBuyingPoliciesByOrgUnitId(
27
+ contractId,
28
+ orgUnitId,
29
+ cookie,
30
+ search,
31
+ page
32
+ );
33
+ };
37
34
 
38
- return {
39
- data: [],
40
- total: 0,
41
- };
35
+ export const getBuyingPoliciesService = withClientErrorBoundary(
36
+ getBuyingPoliciesFunction,
37
+ {
38
+ componentName: "BuyingPoliciesService",
39
+ onError: handleUnauthorizedRedirect,
42
40
  }
43
- };
41
+ );
@@ -1,3 +1,7 @@
1
+ import {
2
+ withClientErrorBoundary,
3
+ handleUnauthorizedRedirect,
4
+ } from "../../shared/utils/withClientErrorBoundary";
1
5
  import { contractsClient } from "../clients/ContractsClient";
2
6
 
3
7
  type GetContractDetailsServiceProps = {
@@ -6,7 +10,7 @@ type GetContractDetailsServiceProps = {
6
10
  unitId: string;
7
11
  };
8
12
 
9
- export const getContractDetailsService = async ({
13
+ const getContractDetailsFunction = async ({
10
14
  contractId,
11
15
  cookie,
12
16
  unitId,
@@ -19,3 +23,11 @@ export const getContractDetailsService = async ({
19
23
 
20
24
  return contract;
21
25
  };
26
+
27
+ export const getContractDetailsService = withClientErrorBoundary(
28
+ getContractDetailsFunction,
29
+ {
30
+ componentName: "ContractDetails",
31
+ onError: handleUnauthorizedRedirect,
32
+ }
33
+ );
@@ -1,3 +1,7 @@
1
+ import {
2
+ withClientErrorBoundary,
3
+ handleUnauthorizedRedirect,
4
+ } from "../../shared/utils/withClientErrorBoundary";
1
5
  import { contractsClient } from "../clients/ContractsClient";
2
6
 
3
7
  import type { ContractData } from "../types";
@@ -10,7 +14,7 @@ export type GetContractsByOrgUnitIdServiceProps = {
10
14
  cookie: string;
11
15
  };
12
16
 
13
- export const getContractsByOrgUnitIdService = async ({
17
+ const getContractsByOrgUnitIdFunction = async ({
14
18
  orgUnitId,
15
19
  cookie,
16
20
  }: GetContractsByOrgUnitIdServiceProps): Promise<ContractData[]> => {
@@ -20,16 +24,19 @@ export const getContractsByOrgUnitIdService = async ({
20
24
  return contractsData;
21
25
  }
22
26
 
23
- try {
24
- const { contracts } = await contractsClient.getContractsByOrgUnitId(
25
- orgUnitId,
26
- cookie
27
- );
28
- contractsData.push(...contracts);
29
- } catch (err) {
30
- console.error("Failed to fetch contracts", err);
31
- return contractsData;
32
- }
27
+ const { contracts } = await contractsClient.getContractsByOrgUnitId(
28
+ orgUnitId,
29
+ cookie
30
+ );
31
+ contractsData.push(...contracts);
33
32
 
34
33
  return contractsData;
35
34
  };
35
+
36
+ export const getContractsByOrgUnitIdService = withClientErrorBoundary(
37
+ getContractsByOrgUnitIdFunction,
38
+ {
39
+ componentName: "GetContractsByOrgUnitId",
40
+ onError: handleUnauthorizedRedirect,
41
+ }
42
+ );
@@ -1,3 +1,7 @@
1
+ import {
2
+ withClientErrorBoundary,
3
+ handleUnauthorizedRedirect,
4
+ } from "../../shared/utils/withClientErrorBoundary";
1
5
  import { contractsClient } from "../clients/ContractsClient";
2
6
 
3
7
  export type UpdateContractStatusServiceProps = {
@@ -7,21 +11,24 @@ export type UpdateContractStatusServiceProps = {
7
11
  unitId: string;
8
12
  };
9
13
 
10
- export const updateContractStatusService = async ({
14
+ const updateContractStatusFunction = async ({
11
15
  contractId,
12
16
  isActive,
13
17
  cookie,
14
18
  unitId,
15
19
  }: UpdateContractStatusServiceProps): Promise<{ isActive: boolean } | null> => {
16
- try {
17
- const data = await contractsClient.updateContractStatus(
18
- { unitId: unitId, contractId, isActive },
19
- cookie
20
- );
20
+ const data = await contractsClient.updateContractStatus(
21
+ { unitId: unitId, contractId, isActive },
22
+ cookie
23
+ );
21
24
 
22
- return data;
23
- } catch (err) {
24
- console.error("Failed to update contract status", err);
25
- return null;
26
- }
25
+ return data;
27
26
  };
27
+
28
+ export const updateContractStatusService = withClientErrorBoundary(
29
+ updateContractStatusFunction,
30
+ {
31
+ componentName: "UpdateContractStatus",
32
+ onError: handleUnauthorizedRedirect,
33
+ }
34
+ );
@@ -21,6 +21,7 @@ export type AddAllToOrgUnitDropdownProps = {
21
21
  unitId: string;
22
22
  unitName: string;
23
23
  contractId: string;
24
+ align?: "left" | "right";
24
25
  };
25
26
 
26
27
  export const AddAllToOrgUnitDropdown = ({
@@ -28,6 +29,7 @@ export const AddAllToOrgUnitDropdown = ({
28
29
  unitId,
29
30
  unitName,
30
31
  contractId,
32
+ align = "right",
31
33
  }: AddAllToOrgUnitDropdownProps) => {
32
34
  const sizeProps = { width: 20, height: 20 };
33
35
  const [customField, setCustomField] = useState<CustomFieldType | null>(null);
@@ -84,7 +86,7 @@ export const AddAllToOrgUnitDropdown = ({
84
86
 
85
87
  return (
86
88
  <>
87
- <BasicDropdownMenu>
89
+ <BasicDropdownMenu align={align}>
88
90
  {/* TODO: Liberar quando adição de contrato estiver finalizada
89
91
  levando em consideração as integrações das outras etapas de desenvolviemento
90
92
  de Contratos */}
@@ -8,6 +8,7 @@ import { OrgUnitBreadcrumbPath } from "./OrgUnitBreadcrumbPath";
8
8
  export const OrgUnitBreadcrumb = ({
9
9
  items,
10
10
  maxItems,
11
+ hasDropdown = false,
11
12
  }: OrgUnitBreadcrumbProps) => {
12
13
  const { currentContract } = useBuyerPortal();
13
14
  const sortedByPosition = [...items].sort((a, b) => a.position - b.position);
@@ -34,6 +35,7 @@ export const OrgUnitBreadcrumb = ({
34
35
  contractId={currentContract?.id ?? ""}
35
36
  items={sortedByPosition.slice(offset)}
36
37
  divider={divider}
38
+ hasDropdown={hasDropdown}
37
39
  />
38
40
  </div>
39
41
  );
@@ -1,11 +1,10 @@
1
1
  import { Fragment } from "react";
2
2
 
3
- import { Dropdown } from "@faststore/ui";
3
+ import { Dropdown, DropdownButton } from "@faststore/ui";
4
4
 
5
- import { BasicDropdownMenu } from "../../../shared/components";
5
+ import { Icon } from "../../../shared/components";
6
6
  import { OrgUnitBreadcrumbPathProps } from "../../types";
7
7
  import { AddAllToOrgUnitDropdown } from "../AddAllToOrgUnitDropdown/AddAllToOrgUnitDropdown";
8
- import { OrgUnitsDropdownMenu } from "../OrgUnitsDropdownMenu/OrgUnitsDropdownMenu";
9
8
 
10
9
  import { OrgUnitBreadcrumbLink } from "./OrgUnitBreadcrumbLink";
11
10
 
@@ -13,27 +12,28 @@ export const OrgUnitBreadcrumbPath = ({
13
12
  items,
14
13
  contractId,
15
14
  divider,
15
+ hasDropdown = false,
16
16
  }: OrgUnitBreadcrumbPathProps) =>
17
17
  items.map((item, index) => {
18
18
  const isLast = index === items.length - 1;
19
19
 
20
- if (isLast) {
20
+ if (isLast && hasDropdown) {
21
21
  return (
22
22
  <Fragment key={item.item}>
23
- <OrgUnitBreadcrumbLink
24
- item={item.item}
25
- name={item.name}
26
- isLast={isLast}
27
- />
28
23
  <Dropdown>
29
- <BasicDropdownMenu.Trigger iconName="ArrowDropDown" />
24
+ <DropdownButton asChild>
25
+ <button data-fs-bp-breadcrumb-dropdown-trigger>
26
+ {item.name}
27
+ <Icon name="ArrowDropDown" />
28
+ </button>
29
+ </DropdownButton>
30
30
 
31
- <OrgUnitsDropdownMenu id={item.item} name={item.name} />
32
31
  <AddAllToOrgUnitDropdown
33
32
  isSingleContract
34
33
  contractId={contractId}
35
34
  unitId={item.unitId}
36
35
  unitName={item.name}
36
+ align="left"
37
37
  />
38
38
  </Dropdown>
39
39
  </Fragment>
@@ -47,7 +47,7 @@ export const OrgUnitBreadcrumbPath = ({
47
47
  name={item.name}
48
48
  isLast={isLast}
49
49
  />
50
- {divider}
50
+ {!isLast && divider}
51
51
  </Fragment>
52
52
  );
53
53
  });
@@ -1,7 +1,8 @@
1
- @import "@faststore/ui/src/components/molecules/Dropdown/styles.scss";
2
- @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
3
-
4
1
  [data-fs-bp-breadcrumb-nav] {
2
+ @import "@faststore/ui/src/components/molecules/Dropdown/styles.scss";
3
+ @import "@faststore/ui/src/components/molecules/Tooltip/styles.scss";
4
+
5
+ --fs-dropdown-item-icon-margin-top: var(--fs-spacing-0);
5
6
  flex: 1;
6
7
  display: flex;
7
8
  align-items: center;
@@ -15,46 +16,56 @@
15
16
  flex-wrap: wrap;
16
17
  }
17
18
 
18
- [data-fs-bp-basic-dropdown-menu-trigger] {
19
- color: var(--fs-color-neutral-6);
20
- padding: 0;
21
- width: calc(var(--fs-spacing-0) + var(--fs-spacing-3));
22
- height: calc(var(--fs-spacing-0) + var(--fs-spacing-3));
19
+ [data-fs-bp-breadcrumb-link] {
20
+ flex: 0 0 max-content;
21
+ max-width: 28.125rem; // 450px
22
+
23
+ p,
24
+ a {
25
+ font-size: var(--fs-text-size-4);
26
+ line-height: var(--fs-text-size-6);
27
+ font-weight: var(--fs-text-weight-semibold);
28
+ color: #5c5c5c;
29
+ text-decoration: none;
30
+ cursor: pointer;
31
+ transition: all 0.2s;
32
+ letter-spacing: -0.04em;
33
+ display: block;
34
+
35
+ white-space: nowrap;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ }
39
+
40
+ [data-fs-bp-breadcrumb-current] {
41
+ color: #000;
42
+ cursor: inherit;
43
+ }
44
+
45
+ a:hover {
46
+ color: #565656;
47
+ }
23
48
  }
24
- }
25
49
 
26
- [data-fs-bp-breadcrumb-link] {
27
- flex: 0 0 max-content;
28
- max-width: 28.125rem; // 450px
50
+ [data-fs-bp-breadcrumb-divider] {
51
+ color: #858585;
52
+ flex: 0 0 auto;
53
+ }
29
54
 
30
- p,
31
- a {
55
+ [data-fs-bp-breadcrumb-dropdown-trigger] {
56
+ display: inline-flex;
57
+ align-items: center;
32
58
  font-size: var(--fs-text-size-4);
33
59
  line-height: var(--fs-text-size-6);
34
60
  font-weight: var(--fs-text-weight-semibold);
35
- color: #5c5c5c;
36
- text-decoration: none;
37
- cursor: pointer;
38
- transition: all 0.2s;
39
- letter-spacing: -0.04em;
40
- display: block;
41
-
42
- white-space: nowrap;
43
- overflow: hidden;
44
- text-overflow: ellipsis;
45
- }
46
-
47
- [data-fs-bp-breadcrumb-current] {
48
61
  color: #000;
49
- cursor: inherit;
50
- }
62
+ cursor: pointer;
63
+ padding: 0 var(--fs-spacing-0) 0 var(--fs-spacing-1);
64
+ margin-left: calc(var(--fs-spacing-1) * -1);
65
+ border-radius: var(--fs-border-radius-pill);
51
66
 
52
- a:hover {
53
- color: #565656;
67
+ &:hover {
68
+ background-color: #F5F5F5;
69
+ }
54
70
  }
55
71
  }
56
-
57
- [data-fs-bp-breadcrumb-divider] {
58
- color: #858585;
59
- flex: 0 0 auto;
60
- }
@@ -10,6 +10,7 @@ export type OrgUnitBreadcrumbItem = {
10
10
  export type OrgUnitBreadcrumbProps = {
11
11
  items: OrgUnitBreadcrumbItem[];
12
12
  maxItems: number;
13
+ hasDropdown?: boolean;
13
14
  };
14
15
 
15
16
  export interface OrgUnitBreadcrumbItemProps
@@ -21,4 +22,5 @@ export interface OrgUnitBreadcrumbPathProps {
21
22
  divider: ReactNode;
22
23
  items: OrgUnitBreadcrumbItem[];
23
24
  contractId: string;
25
+ hasDropdown?: boolean;
24
26
  }
@@ -5,6 +5,7 @@
5
5
  @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
6
6
  @import "../../../shared/components/InternalTopbar/internal-top-bar.scss";
7
7
  @import "../../../shared/components/HeaderInside/header-inside.scss";
8
+ @import "../../../shared/components/Error/error.scss";
8
9
 
9
10
  width: 100%;
10
11
 
@@ -3,8 +3,9 @@ import {
3
3
  getAuthFromCookie,
4
4
  getCookieWithoutSessionObjects,
5
5
  } from "../utils/cookie";
6
+ import { extractErrorMessage } from "../utils/extractErrorMessage";
6
7
 
7
- interface RequestConfig<Input = never> {
8
+ interface RequestConfig<Input = unknown> {
8
9
  url: string;
9
10
  method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
10
11
  data?: Input;
@@ -12,6 +13,37 @@ interface RequestConfig<Input = never> {
12
13
  params?: Record<string, string | number | boolean>;
13
14
  ignoreContentType?: boolean;
14
15
  cache?: RequestCache;
16
+ componentName?: string; // For error tracking
17
+ }
18
+
19
+ export class ClientError extends Error {
20
+ public status?: number;
21
+ public statusText?: string;
22
+ public url?: string;
23
+ public method?: string;
24
+ public responseData?: unknown;
25
+ public componentName?: string;
26
+
27
+ constructor(
28
+ message: string,
29
+ options: {
30
+ status?: number;
31
+ statusText?: string;
32
+ url?: string;
33
+ method?: string;
34
+ responseData?: unknown;
35
+ componentName?: string;
36
+ } = {}
37
+ ) {
38
+ super(message);
39
+ this.name = "ClientError";
40
+ this.status = options.status;
41
+ this.statusText = options.statusText;
42
+ this.url = options.url;
43
+ this.method = options.method;
44
+ this.responseData = options.responseData;
45
+ this.componentName = options.componentName;
46
+ }
15
47
  }
16
48
 
17
49
  class Client {
@@ -21,6 +53,22 @@ class Client {
21
53
  this.baseURL = baseURL;
22
54
  }
23
55
 
56
+ private createClientError(
57
+ message: string,
58
+ response: Response,
59
+ responseData: unknown,
60
+ requestConfig: RequestConfig
61
+ ): ClientError {
62
+ return new ClientError(message, {
63
+ status: response.status,
64
+ statusText: response.statusText,
65
+ url: this.baseURL + requestConfig.url,
66
+ method: requestConfig.method,
67
+ responseData,
68
+ componentName: requestConfig.componentName,
69
+ });
70
+ }
71
+
24
72
  async request<Return, Input = never>({
25
73
  url,
26
74
  method,
@@ -29,6 +77,7 @@ class Client {
29
77
  params = {},
30
78
  ignoreContentType = false,
31
79
  cache = "no-cache",
80
+ componentName,
32
81
  }: RequestConfig<Input>): Promise<Return> {
33
82
  const config: RequestInit = {
34
83
  method,
@@ -59,24 +108,51 @@ class Client {
59
108
  }
60
109
 
61
110
  if (!response.ok) {
62
- console.error(`Request to ${url} failed:`, responseData);
63
- console.error(response.status, response.statusText);
64
- throw new Error(
65
- responseData ? JSON.stringify(responseData) : "Request failed"
111
+ const errorMessage = extractErrorMessage(response, responseData);
112
+
113
+ const clientError = this.createClientError(
114
+ errorMessage,
115
+ response,
116
+ responseData,
117
+ {
118
+ url,
119
+ method,
120
+ data,
121
+ headers,
122
+ params,
123
+ ignoreContentType,
124
+ cache,
125
+ componentName,
126
+ }
66
127
  );
128
+
129
+ throw clientError;
67
130
  }
68
131
 
69
132
  return responseData as Return;
70
133
  } catch (error) {
71
- return Promise.reject(error);
134
+ if (error instanceof ClientError) {
135
+ throw error;
136
+ }
137
+
138
+ const clientError = new ClientError(
139
+ error instanceof Error ? error.message : "Network error",
140
+ {
141
+ url: this.baseURL + url,
142
+ method,
143
+ componentName,
144
+ }
145
+ );
146
+
147
+ throw clientError;
72
148
  }
73
149
  }
74
150
 
75
151
  get<Return>(
76
152
  url: string,
77
- config: Partial<RequestConfig> = {}
153
+ config: Partial<RequestConfig<never>> = {}
78
154
  ): Promise<Return> {
79
- return this.request<Return>({ url, method: "GET", ...config });
155
+ return this.request<Return, never>({ url, method: "GET", ...config });
80
156
  }
81
157
 
82
158
  post<Return, Input>(
@@ -1,6 +1,7 @@
1
1
  import { createContext, type ReactNode } from "react";
2
2
 
3
3
  import { LoadingTabsLayout } from "../../layouts";
4
+ import { ErrorBoundary } from "../ErrorBoundary/ErrorBoundary";
4
5
 
5
6
  import type { ContractData } from "../../../contracts/types";
6
7
  import type { OrgUnitBasicData } from "../../../org-units/types";
@@ -31,7 +32,9 @@ export const BuyerPortalProvider = ({
31
32
  }: BuyerPortalProviderProps) => {
32
33
  return (
33
34
  <BuyerPortalContext.Provider value={value}>
34
- <LoadingTabsLayout>{children}</LoadingTabsLayout>
35
+ <ErrorBoundary>
36
+ <LoadingTabsLayout>{children}</LoadingTabsLayout>
37
+ </ErrorBoundary>
35
38
  </BuyerPortalContext.Provider>
36
39
  );
37
40
  };
@@ -0,0 +1,31 @@
1
+ import { isDevelopment } from "../../utils/environment";
2
+ import { Icon } from "../Icon";
3
+
4
+ export type ErrorProps = {
5
+ error?: Error;
6
+ tags?: {
7
+ component: string;
8
+ errorType: string;
9
+ };
10
+ };
11
+
12
+ export default function Error({ error, tags }: ErrorProps) {
13
+ return (
14
+ <div data-fs-bp-error>
15
+ <Icon name="Warning" width={48} height={48} data-fs-icon-loading="true" />
16
+ <h2 data-fs-bp-error-title>Something went wrong!</h2>
17
+ <button data-fs-bp-error-button onClick={() => window.location.reload()}>
18
+ Try again
19
+ </button>
20
+ {isDevelopment() ? (
21
+ <div data-fs-bp-error-details>
22
+ <span data-fs-bp-error-details-type>{tags?.errorType}</span>
23
+ <h2 data-fs-bp-error-details-title>Error Details</h2>
24
+ <p data-fs-bp-error-details-message>{error?.message}</p>
25
+ <p data-fs-bp-error-details-stack>Stack: {error?.stack}</p>
26
+ <p data-fs-bp-error-details-component>Component: {tags?.component}</p>
27
+ </div>
28
+ ) : null}
29
+ </div>
30
+ );
31
+ }