@vtex/faststore-plugin-buyer-portal 1.1.116-poc → 1.1.116-poc-1

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,44 +1,44 @@
1
1
  {
2
- "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.1.116-poc",
4
- "description": "A plugin for faststore with buyer portal",
5
- "main": "index.js",
6
- "scripts": {
7
- "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
8
- "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
9
- "prepare": "husky"
10
- },
11
- "lint-staged": {
12
- "*.{js,jsx,ts,tsx}": [
13
- "eslint --fix"
14
- ]
15
- },
16
- "dependencies": {
17
- "@types/react-dom": "^19.0.3",
18
- "react-dom": "^19.0.0"
19
- },
20
- "devDependencies": {
21
- "@eslint/js": "^9.29.0",
22
- "@faststore/core": "^3.41.5",
23
- "@faststore/ui": "^3.41.5",
24
- "@types/react": "^18.2.42",
25
- "cypress": "13",
26
- "eslint": "^9.29.0",
27
- "eslint-config-prettier": "^10.1.5",
28
- "eslint-plugin-import": "^2.32.0",
29
- "eslint-plugin-prettier": "^5.5.1",
30
- "eslint-plugin-react": "^7.37.5",
31
- "globals": "^16.2.0",
32
- "husky": "^9.1.7",
33
- "lint-staged": "^16.1.2",
34
- "next": "13.5.7",
35
- "typescript": "4.7.3",
36
- "typescript-eslint": "^8.35.0"
37
- },
38
- "peerDependencies": {
39
- "react": "^18.2.0",
40
- "react-dom": "^18.2.0"
41
- },
42
- "license": "MIT",
43
- "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
2
+ "name": "@vtex/faststore-plugin-buyer-portal",
3
+ "version": "1.1.116-poc-1",
4
+ "description": "A plugin for faststore with buyer portal",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
8
+ "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
9
+ "prepare": "husky"
10
+ },
11
+ "lint-staged": {
12
+ "*.{js,jsx,ts,tsx}": [
13
+ "eslint --fix"
14
+ ]
15
+ },
16
+ "dependencies": {
17
+ "@types/react-dom": "^19.0.3",
18
+ "react-dom": "^19.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.29.0",
22
+ "@faststore/core": "^3.41.5",
23
+ "@faststore/ui": "^3.41.5",
24
+ "@types/react": "^18.2.42",
25
+ "cypress": "13",
26
+ "eslint": "^9.29.0",
27
+ "eslint-config-prettier": "^10.1.5",
28
+ "eslint-plugin-import": "^2.32.0",
29
+ "eslint-plugin-prettier": "^5.5.1",
30
+ "eslint-plugin-react": "^7.37.5",
31
+ "globals": "^16.2.0",
32
+ "husky": "^9.1.7",
33
+ "lint-staged": "^16.1.2",
34
+ "next": "13.5.7",
35
+ "typescript": "4.7.3",
36
+ "typescript-eslint": "^8.35.0"
37
+ },
38
+ "peerDependencies": {
39
+ "react": "^18.2.0",
40
+ "react-dom": "^18.2.0"
41
+ },
42
+ "license": "MIT",
43
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
44
44
  }
@@ -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,17 @@ 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
+ </div>
33
+ ) : null}
35
34
  </div>
36
35
  );
37
36
  }
@@ -1,22 +1,47 @@
1
1
  import { Skeleton } from "@faststore/ui";
2
2
 
3
+ import { useBuyerPortal } from "../../hooks";
4
+ import { BaseTabsLayout } from "../../layouts";
5
+
3
6
  /**
4
7
  * PageLoader component displays skeleton loaders while page data is being fetched.
5
8
  * Used when cookies are not yet available during pre-fetch scenarios.
6
9
  */
7
10
  export const PageLoader = () => {
11
+ const { currentOrgUnit, currentUser } = useBuyerPortal();
8
12
  return (
9
- <div data-fs-bp-page-loader>
10
- <div data-fs-bp-page-loader-header>
11
- <Skeleton size={{ width: "12rem", height: "2rem" }} />
12
- <Skeleton size={{ width: "8rem", height: "1.5rem" }} />
13
- </div>
13
+ <BaseTabsLayout data-fs-bp-page-loader>
14
+ <BaseTabsLayout.Navbar
15
+ orgUnit={currentOrgUnit}
16
+ pageName="Loading"
17
+ person={{
18
+ name: currentUser?.name ?? "",
19
+ role: currentUser?.role ?? "",
20
+ id: currentUser?.id ?? "",
21
+ }}
22
+ loading={true}
23
+ />
24
+ <BaseTabsLayout.Container>
25
+ <BaseTabsLayout.SidebarMenu
26
+ verticalLinks={[]}
27
+ showLetterHighlight={false}
28
+ loading={true}
29
+ />
30
+ <BaseTabsLayout.Content>
31
+ <div data-fs-bp-page-loader>
32
+ <div data-fs-bp-page-loader-header>
33
+ <Skeleton size={{ width: "12rem", height: "2rem" }} />
34
+ <Skeleton size={{ width: "8rem", height: "1.5rem" }} />
35
+ </div>
14
36
 
15
- <div data-fs-bp-page-loader-content>
16
- <Skeleton size={{ width: "100%", height: "4rem" }} />
17
- <Skeleton size={{ width: "100%", height: "8rem" }} />
18
- <Skeleton size={{ width: "100%", height: "8rem" }} />
19
- </div>
20
- </div>
37
+ <div data-fs-bp-page-loader-content>
38
+ <Skeleton size={{ width: "100%", height: "4rem" }} />
39
+ <Skeleton size={{ width: "100%", height: "8rem" }} />
40
+ <Skeleton size={{ width: "100%", height: "8rem" }} />
41
+ </div>
42
+ </div>
43
+ </BaseTabsLayout.Content>
44
+ </BaseTabsLayout.Container>
45
+ </BaseTabsLayout>
21
46
  );
22
47
  };
@@ -13,3 +13,4 @@ export { useAddToScope } from "./useAddToScope";
13
13
  export { useRemoveFromScope } from "./useRemoveFromScope";
14
14
  export { usePageItems, type UsePageItemsProps } from "./usePageItems";
15
15
  export { useRouterLoading } from "./useRouterLoading";
16
+ export { useAuth, type UseAuthResult } from "./useAuth";
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { useRouter } from "next/router";
4
+
5
+ import { authClient } from "../clients/Auth";
6
+
7
+ import { useBuyerPortal } from "./useBuyerPortal";
8
+
9
+ export type UseAuthResult = {
10
+ isAuthenticated: boolean | null;
11
+ isLoading: boolean;
12
+ };
13
+
14
+ /**
15
+ * Client-side hook to validate user authentication.
16
+ * Checks access with the server and redirects to home if unauthorized.
17
+ *
18
+ * @returns Object containing authentication status and loading state
19
+ */
20
+ export const useAuth = (): UseAuthResult => {
21
+ const { clientContext } = useBuyerPortal();
22
+ const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const router = useRouter();
25
+
26
+ useEffect(() => {
27
+ const validateAccess = async () => {
28
+ setIsLoading(true);
29
+
30
+ try {
31
+ const hasAccess = await authClient.validateAccess(clientContext);
32
+
33
+ if (!hasAccess) {
34
+ router.push("/");
35
+ setIsAuthenticated(false);
36
+ } else {
37
+ setIsAuthenticated(true);
38
+ }
39
+ } catch {
40
+ router.push("/");
41
+ setIsAuthenticated(false);
42
+ } finally {
43
+ setIsLoading(false);
44
+ }
45
+ };
46
+
47
+ validateAccess();
48
+ }, [router]);
49
+
50
+ return {
51
+ isAuthenticated,
52
+ isLoading,
53
+ };
54
+ };
@@ -1,4 +1,4 @@
1
- // import { getClientContext } from "./getClientContext";
1
+ import { getClientContext } from "./getClientContext";
2
2
  import { serializeLoaderData } from "./serializeLoaderData";
3
3
 
4
4
  import type { LoaderData } from "../types";
@@ -11,85 +11,77 @@ interface WithLoaderErrorBoundaryOptions {
11
11
 
12
12
  export function withLoaderErrorBoundary<TQuery, TReturn>(
13
13
  loaderFn: (data: LoaderData<TQuery>) => Promise<TReturn>,
14
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
- _options: WithLoaderErrorBoundaryOptions = {}
14
+ options: WithLoaderErrorBoundaryOptions = {}
16
15
  ) {
17
- // Bypass: Just call the loader function and serialize the result
16
+ const { componentName, onError, redirectToError = true } = options;
17
+
18
18
  return async (data: LoaderData<TQuery>): Promise<TReturn> => {
19
- const result = await loaderFn(data);
20
- return serializeLoaderData(result);
21
- };
19
+ try {
20
+ const result = await loaderFn(data);
21
+ return serializeLoaderData(result);
22
+ } catch (error) {
23
+ console.error(`[${componentName || "Loader"}] Error:`, {
24
+ message: (error as Error).message,
25
+ query: data.query,
26
+ });
27
+
28
+ if (onError) {
29
+ onError(error as Error, data.query);
30
+ }
31
+
32
+ if (redirectToError) {
33
+ let clientContext;
34
+ try {
35
+ clientContext = await getClientContext(data);
36
+ } catch (contextError) {
37
+ console.error(
38
+ `[${componentName || "Loader"}] Failed to get client context:`,
39
+ contextError
40
+ );
41
+ clientContext = {
42
+ cookie: "",
43
+ customerId: "",
44
+ userId: "",
45
+ vtexIdclientAutCookie: "",
46
+ };
47
+ }
22
48
 
23
- // Old error handling code - commented out for bypass
24
- // const { componentName, onError, redirectToError = true } = options;
25
- //
26
- // return async (data: LoaderData<TQuery>): Promise<TReturn> => {
27
- // try {
28
- // const result = await loaderFn(data);
29
- // return serializeLoaderData(result);
30
- // } catch (error) {
31
- // console.error(`[${componentName || "Loader"}] Error:`, {
32
- // message: (error as Error).message,
33
- // query: data.query,
34
- // });
35
- //
36
- // if (onError) {
37
- // onError(error as Error, data.query);
38
- // }
39
- //
40
- // if (redirectToError) {
41
- // let clientContext;
42
- // try {
43
- // clientContext = await getClientContext(data);
44
- // } catch (contextError) {
45
- // console.error(
46
- // `[${componentName || "Loader"}] Failed to get client context:`,
47
- // contextError
48
- // );
49
- // clientContext = {
50
- // cookie: "",
51
- // customerId: "",
52
- // userId: "",
53
- // vtexIdclientAutCookie: "",
54
- // };
55
- // }
56
- //
57
- // // Serialize query to ensure it's JSON-safe (use null instead of undefined)
58
- // const serializedQuery = data.query
59
- // ? JSON.parse(JSON.stringify(data.query))
60
- // : null;
61
- //
62
- // const errorResponse = {
63
- // error: {
64
- // error: {
65
- // message: (error as Error).message,
66
- // name: (error as Error).name,
67
- // stack: (error as Error).stack || null,
68
- // query: serializedQuery,
69
- // },
70
- // tags: {
71
- // component: componentName || "Unknown",
72
- // errorType: "loader_error",
73
- // },
74
- // extra: {
75
- // query: serializedQuery,
76
- // },
77
- // },
78
- // hasError: true,
79
- // context: {
80
- // clientContext,
81
- // currentOrgUnit: null,
82
- // currentContract: null,
83
- // currentUser: null,
84
- // },
85
- // data: null,
86
- // };
87
- //
88
- // // Ensure error response is also JSON-serializable
89
- // return serializeLoaderData(errorResponse) as TReturn;
90
- // }
91
- //
92
- // throw error;
93
- // }
94
- // };
49
+ // Serialize query to ensure it's JSON-safe (use null instead of undefined)
50
+ const serializedQuery = data.query
51
+ ? JSON.parse(JSON.stringify(data.query))
52
+ : null;
53
+
54
+ const errorResponse = {
55
+ error: {
56
+ error: {
57
+ message: (error as Error).message,
58
+ name: (error as Error).name,
59
+ stack: (error as Error).stack || null,
60
+ query: serializedQuery,
61
+ },
62
+ tags: {
63
+ component: componentName || "Unknown",
64
+ errorType: "loader_error",
65
+ },
66
+ extra: {
67
+ query: serializedQuery,
68
+ },
69
+ },
70
+ hasError: true,
71
+ context: {
72
+ clientContext,
73
+ currentOrgUnit: null,
74
+ currentContract: null,
75
+ currentUser: null,
76
+ },
77
+ data: null,
78
+ };
79
+
80
+ // Ensure error response is also JSON-serializable
81
+ return serializeLoaderData(errorResponse) as TReturn;
82
+ }
83
+
84
+ throw error;
85
+ }
86
+ };
95
87
  }
@@ -1,6 +1,5 @@
1
1
  import { type ComponentType } from "react";
2
2
 
3
- import { withAuth } from "./withAuth";
4
3
  import { withBuyerPortal } from "./withBuyerPortal";
5
4
 
6
5
  /**
@@ -14,7 +13,7 @@ type ProviderHOC = (Component: ComponentType<any>) => ComponentType<any>;
14
13
  * Default project providers applied to all pages.
15
14
  * Order: right to left (like pipe/compose)
16
15
  */
17
- const projectProviders: ProviderHOC[] = [withAuth, withBuyerPortal];
16
+ const projectProviders: ProviderHOC[] = [withBuyerPortal];
18
17
 
19
18
  /**
20
19
  * HOC that applies all default project providers.
@@ -1,9 +1,18 @@
1
1
  import { OrgUnitsDetailsLayout } from "../features/org-units/layouts";
2
- import { withErrorBoundary } from "../features/shared/components";
3
- import { withAuthLoader, withProviders } from "../features/shared/utils";
4
- import { ClientContext } from "../features/shared/utils/getClientContext";
2
+ import { PageLoader, withErrorBoundary } from "../features/shared/components";
3
+ import { ErrorBoundaryProps } from "../features/shared/components/ErrorBoundary/types";
4
+ import { useAuth } from "../features/shared/hooks";
5
+ import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
6
+ import {
7
+ withLoaderErrorBoundary,
8
+ withProviders,
9
+ } from "../features/shared/utils";
10
+ import {
11
+ ClientContext,
12
+ getClientContext,
13
+ } from "../features/shared/utils/getClientContext";
5
14
 
6
- import type { AuthRouteProps, LoaderData } from "../features/shared/types";
15
+ import type { LoaderData } from "../features/shared/types";
7
16
 
8
17
  export type OrgUnitDetailsPageData = {
9
18
  data: {
@@ -13,35 +22,52 @@ export type OrgUnitDetailsPageData = {
13
22
  context: {
14
23
  clientContext: ClientContext;
15
24
  };
25
+ hasError?: boolean;
26
+ error?: ErrorBoundaryProps;
16
27
  };
17
28
 
18
29
  type OrgUnitDetailsPageQuery = {
19
30
  orgUnitId: string;
20
31
  };
21
32
 
22
- export const loader = async (
33
+ const loaderFunction = async (
23
34
  data: LoaderData<OrgUnitDetailsPageQuery>
24
- ): Promise<AuthRouteProps<OrgUnitDetailsPageData>> => {
35
+ ): Promise<OrgUnitDetailsPageData> => {
25
36
  const { orgUnitId } = data.query;
26
37
 
27
- if (!orgUnitId) {
28
- throw new Error(`Missing required query param: orgUnitId=${orgUnitId}`);
29
- }
38
+ const { cookie, userId, ...clientContext } = await getClientContext(data);
30
39
 
31
- return withAuthLoader(data, async ({ cookie, userId, ...clientContext }) => {
32
- return {
33
- data: {
34
- orgUnitId,
35
- userId,
36
- },
37
- context: {
38
- clientContext: { cookie, userId, ...clientContext },
39
- },
40
- };
41
- });
40
+ return {
41
+ data: {
42
+ orgUnitId,
43
+ userId,
44
+ },
45
+ context: {
46
+ clientContext: { cookie, userId, ...clientContext },
47
+ },
48
+ };
42
49
  };
43
50
 
44
- const OrgUnitDetailsPage = ({ data }: OrgUnitDetailsPageData) => {
51
+ export const loader = withLoaderErrorBoundary(loaderFunction, {
52
+ componentName: "OrgUnitDetailsPage",
53
+ redirectToError: true,
54
+ });
55
+
56
+ const OrgUnitDetailsPage = ({
57
+ data,
58
+ hasError,
59
+ error,
60
+ }: OrgUnitDetailsPageData) => {
61
+ if (hasError) {
62
+ return <ErrorTabsLayout error={error} />;
63
+ }
64
+
65
+ const { isAuthenticated, isLoading } = useAuth();
66
+
67
+ if (isLoading || isAuthenticated === null) {
68
+ return <PageLoader />;
69
+ }
70
+
45
71
  return (
46
72
  <OrgUnitsDetailsLayout orgUnitId={data.orgUnitId} userId={data.userId} />
47
73
  );
@@ -1,12 +1,16 @@
1
1
  import { ProfileLayout } from "../features/profile/layouts";
2
- import { withErrorBoundary } from "../features/shared/components";
2
+ import { PageLoader, withErrorBoundary } from "../features/shared/components";
3
+ import { ErrorBoundaryProps } from "../features/shared/components/ErrorBoundary/types";
4
+ import { useAuth } from "../features/shared/hooks";
5
+ import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
3
6
  import {
4
7
  type ClientContext,
5
8
  withProviders,
6
- withAuthLoader,
9
+ getClientContext,
10
+ withLoaderErrorBoundary,
7
11
  } from "../features/shared/utils";
8
12
 
9
- import type { AuthRouteProps, LoaderData } from "../features/shared/types";
13
+ import type { LoaderData } from "../features/shared/types";
10
14
 
11
15
  export type ProfilePageData = {
12
16
  data: {
@@ -17,6 +21,8 @@ export type ProfilePageData = {
17
21
  context: {
18
22
  clientContext: ClientContext;
19
23
  };
24
+ hasError?: boolean;
25
+ error?: ErrorBoundaryProps;
20
26
  };
21
27
 
22
28
  export type ProfilePageQuery = {
@@ -25,32 +31,51 @@ export type ProfilePageQuery = {
25
31
  userId: string;
26
32
  };
27
33
 
28
- export const loader = async (
34
+ const loaderFunction = async (
29
35
  data: LoaderData<ProfilePageQuery>
30
- ): Promise<AuthRouteProps<ProfilePageData>> => {
36
+ ): Promise<ProfilePageData> => {
31
37
  const { contractId, orgUnitId } = data.query;
32
38
 
33
- return withAuthLoader(data, async ({ cookie, userId, ...clientContext }) => {
34
- return {
35
- data: {
36
- contractId,
37
- orgUnitId,
38
- userId,
39
- },
40
- context: {
41
- clientContext: { cookie, userId, ...clientContext },
42
- },
43
- };
44
- });
39
+ const { cookie, userId, ...clientContext } = await getClientContext(data);
40
+
41
+ return {
42
+ data: {
43
+ contractId,
44
+ orgUnitId,
45
+ userId,
46
+ },
47
+ context: {
48
+ clientContext: { cookie, userId, ...clientContext },
49
+ },
50
+ hasError: false,
51
+ error: undefined,
52
+ };
45
53
  };
46
54
 
47
- const ProfilePage = ({ data }: ProfilePageData) => (
48
- <ProfileLayout
49
- orgUnitId={data.orgUnitId}
50
- contractId={data.contractId}
51
- userId={data.userId}
52
- />
53
- );
55
+ export const loader = withLoaderErrorBoundary(loaderFunction, {
56
+ componentName: "ProfilePage",
57
+ redirectToError: true,
58
+ });
59
+
60
+ const ProfilePage = ({ data, hasError, error }: ProfilePageData) => {
61
+ if (hasError) {
62
+ return <ErrorTabsLayout error={error} />;
63
+ }
64
+
65
+ const { isAuthenticated, isLoading } = useAuth();
66
+
67
+ if (isLoading || isAuthenticated === null) {
68
+ return <PageLoader />;
69
+ }
70
+
71
+ return (
72
+ <ProfileLayout
73
+ orgUnitId={data.orgUnitId}
74
+ contractId={data.contractId}
75
+ userId={data.userId}
76
+ />
77
+ );
78
+ };
54
79
 
55
80
  export default withProviders(
56
81
  withErrorBoundary(ProfilePage, {