@vtex/faststore-plugin-buyer-portal 1.1.107 → 1.1.108

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/features/custom-fields/layouts/CustomFieldsLayout/CustomFieldsLayout.tsx +1 -0
  3. package/src/features/shared/clients/Auth.ts +25 -0
  4. package/src/features/shared/clients/Client.ts +11 -1
  5. package/src/features/shared/services/index.ts +4 -0
  6. package/src/features/shared/services/validate-access.service.ts +11 -0
  7. package/src/features/shared/types/AuthRouteProps.ts +5 -0
  8. package/src/features/shared/types/index.ts +1 -0
  9. package/src/features/shared/utils/constants.ts +4 -4
  10. package/src/features/shared/utils/index.ts +5 -0
  11. package/src/features/shared/utils/withAuth.tsx +31 -0
  12. package/src/features/shared/utils/withAuthLoader.ts +47 -0
  13. package/src/features/shared/utils/withAuthProvider.tsx +38 -0
  14. package/src/features/shared/utils/withBuyerPortal.tsx +24 -0
  15. package/src/features/shared/utils/withProviders.tsx +35 -0
  16. package/src/pages/address-details.tsx +95 -91
  17. package/src/pages/addresses.tsx +63 -63
  18. package/src/pages/budgets-details.tsx +44 -43
  19. package/src/pages/budgets.tsx +58 -56
  20. package/src/pages/buying-policies.tsx +84 -78
  21. package/src/pages/buying-policy-details.tsx +43 -44
  22. package/src/pages/collections.tsx +59 -60
  23. package/src/pages/cost-centers.tsx +48 -51
  24. package/src/pages/credit-cards.tsx +44 -49
  25. package/src/pages/home.tsx +28 -23
  26. package/src/pages/org-unit-details.tsx +39 -36
  27. package/src/pages/org-units.tsx +86 -90
  28. package/src/pages/payment-methods.tsx +41 -42
  29. package/src/pages/po-numbers.tsx +43 -46
  30. package/src/pages/profile.tsx +54 -52
  31. package/src/pages/releases.tsx +43 -41
  32. package/src/pages/role-details.tsx +57 -60
  33. package/src/pages/roles.tsx +42 -39
  34. package/src/pages/user-details.tsx +58 -58
  35. package/src/pages/users.tsx +83 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.1.107",
3
+ "version": "1.1.108",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -43,6 +43,7 @@ export const CustomFieldsLayout = ({
43
43
  currentContract: contract,
44
44
  clientContext,
45
45
  } = useBuyerPortal();
46
+
46
47
  const searchParams = useSearchParams();
47
48
  const [currentSelectedContent, setCurrentSelectedContent] =
48
49
  useState<CustomFieldData>();
@@ -0,0 +1,25 @@
1
+ import { getApiUrl } from "../../shared/utils";
2
+
3
+ import { Client } from "./Client";
4
+
5
+ type AuthContextProps = {
6
+ cookie: string;
7
+ };
8
+
9
+ export default class AuthClient extends Client {
10
+ constructor() {
11
+ super(getApiUrl());
12
+ }
13
+
14
+ validateAccess(context: AuthContextProps) {
15
+ return this.get<boolean>("granted", {
16
+ headers: {
17
+ Cookie: context.cookie,
18
+ "Content-Type": "text/plain",
19
+ },
20
+ });
21
+ }
22
+ }
23
+
24
+ const authClient = new AuthClient();
25
+ export { authClient };
@@ -1,8 +1,8 @@
1
- import { toQueryParams } from "../utils";
2
1
  import {
3
2
  getAuthFromCookie,
4
3
  getCookieWithoutSessionObjects,
5
4
  } from "../utils/cookie";
5
+ import { toQueryParams } from "../utils/toQueryParams";
6
6
 
7
7
  interface RequestConfig<Input = unknown> {
8
8
  url: string;
@@ -87,6 +87,7 @@ class Client {
87
87
  ? getCookieWithoutSessionObjects(headers.Cookie ?? "")
88
88
  : {}),
89
89
  ...(headers.Cookie ? getAuthFromCookie(headers.Cookie ?? "") : {}),
90
+ ...headers,
90
91
  },
91
92
  ...(cache ? { cache, next: { revalidate: 300 } } : {}),
92
93
  };
@@ -106,6 +107,15 @@ class Client {
106
107
  responseData = await response.json();
107
108
  }
108
109
 
110
+ if (contentType?.includes("text/plain")) {
111
+ const text = await response.text();
112
+ try {
113
+ responseData = JSON.parse(text);
114
+ } catch {
115
+ responseData = text as unknown as Return;
116
+ }
117
+ }
118
+
109
119
  if (!response.ok) {
110
120
  console.error(`Request to ${url} failed:`, responseData);
111
121
  console.error(response.status, response.statusText);
@@ -6,3 +6,7 @@ export {
6
6
  removeFromScopeService,
7
7
  type RemoveFromScopeServiceProps,
8
8
  } from "./remove-from-scope.service";
9
+ export {
10
+ validateAccessService,
11
+ type ValidateAccessServiceProps,
12
+ } from "./validate-access.service";
@@ -0,0 +1,11 @@
1
+ import { authClient } from "../clients/Auth";
2
+
3
+ export type ValidateAccessServiceProps = {
4
+ cookie: string;
5
+ };
6
+
7
+ export const validateAccessService = async ({
8
+ cookie,
9
+ }: ValidateAccessServiceProps) => {
10
+ return authClient.validateAccess({ cookie });
11
+ };
@@ -0,0 +1,5 @@
1
+ import { ClientContext } from "../utils";
2
+
3
+ export type AuthRouteProps<T> =
4
+ | { authorized: true; data: T; clientContext: ClientContext }
5
+ | { authorized: false };
@@ -7,3 +7,4 @@ export type {
7
7
  PaymentMethodResponse,
8
8
  PaymentMethodsReqCommonParams,
9
9
  } from "./PaymentMethodsClientTypes";
10
+ export { type AuthRouteProps } from "./AuthRouteProps";
@@ -1,10 +1,10 @@
1
1
  export const AUT_COOKIE_KEY = "VtexIdclientAutCookie";
2
2
  // PROD URL
3
- export const API_URL = (checkoutUrl: string, operation?: string) =>
4
- `${checkoutUrl}/_v/store-front/${operation}`;
3
+ // export const API_URL = (checkoutUrl: string, operation?: string) =>
4
+ // `${checkoutUrl}/_v/store-front/${operation}`;
5
5
  // DEV URL - CHANGE BEFORE MERGE
6
- // export const API_URL = (checkoutUrl?: string, operation?: string) =>
7
- // `https://everton--b2bfaststoredev.myvtex.com/_v/store-front/${operation}`;
6
+ export const API_URL = (checkoutUrl?: string, operation?: string) =>
7
+ `https://permission--b2bfaststoredev.myvtex.com/_v/store-front/${operation}`;
8
8
 
9
9
  export const DEBOUNCE_TIMEOUT = 500;
10
10
 
@@ -37,3 +37,8 @@ export { isPluginError } from "./isPluginError";
37
37
  export { serializeLoaderData } from "./serializeLoaderData";
38
38
  export { withLoaderErrorBoundary } from "./withLoaderErrorBoundary";
39
39
  export { withClientErrorBoundary } from "./withClientErrorBoundary";
40
+ export { withAuthProvider } from "./withAuthProvider";
41
+ export { withAuth } from "./withAuth";
42
+ export { withBuyerPortal } from "./withBuyerPortal";
43
+ export { withProviders } from "./withProviders";
44
+ export { withAuthLoader } from "./withAuthLoader";
@@ -0,0 +1,31 @@
1
+ import { useEffect, type ComponentType } from "react";
2
+
3
+ import { useRouter } from "next/router";
4
+
5
+ import type { AuthRouteProps } from "../types";
6
+
7
+ /**
8
+ * HOC that checks only for authorization
9
+ */
10
+ export const withAuth = <T extends Record<string, unknown>>(
11
+ Component: ComponentType<T>
12
+ ) => {
13
+ return function AuthenticatedComponent(props: AuthRouteProps<T>) {
14
+ const router = useRouter();
15
+
16
+ useEffect(() => {
17
+ // If not authorized, reload the page
18
+ if (!props?.authorized) {
19
+ router.reload();
20
+ }
21
+ }, [props?.authorized, router]);
22
+
23
+ // If not authorized, render nothing
24
+ if (!props?.authorized) {
25
+ return null;
26
+ }
27
+
28
+ // If authorized, render the component with the data
29
+ return <Component {...(props.data as T)} />;
30
+ };
31
+ };
@@ -0,0 +1,47 @@
1
+ import { validateAccessService } from "../services";
2
+
3
+ import { ClientContext, getClientContext } from "./getClientContext";
4
+
5
+ import type { AuthRouteProps } from "../types";
6
+ import type { LoaderData } from "../types";
7
+
8
+ /**
9
+ * Utility function to encapsulate access validation in loaders.
10
+ * Similar to withAuthProvider but for server-side usage.
11
+ */
12
+ export const withAuthLoader = async <T>(
13
+ data: LoaderData,
14
+ dataLoader: (clientContext: ClientContext) => Promise<T>
15
+ ): Promise<AuthRouteProps<T>> => {
16
+ try {
17
+ const { cookie, userId, ...clientContext } = await getClientContext(data);
18
+
19
+ const hasAccess = await validateAccessService({ cookie });
20
+
21
+ if (!hasAccess) {
22
+ data.res?.writeHead(302, { Location: "/" });
23
+ data.res?.end();
24
+
25
+ return {
26
+ authorized: false,
27
+ };
28
+ }
29
+
30
+ const pageData = await dataLoader({ cookie, userId, ...clientContext });
31
+
32
+ return {
33
+ authorized: true,
34
+ data: pageData,
35
+ clientContext: { cookie, userId, ...clientContext },
36
+ };
37
+ } catch (error) {
38
+ console.error("Auth validation failed:", error);
39
+
40
+ data.res?.writeHead(302, { Location: "/" });
41
+ data.res?.end();
42
+
43
+ return {
44
+ authorized: false,
45
+ };
46
+ }
47
+ };
@@ -0,0 +1,38 @@
1
+ import { useEffect, type ComponentType } from "react";
2
+
3
+ import { useRouter } from "next/router";
4
+
5
+ import { BuyerPortalProvider } from "../components";
6
+
7
+ import type { AuthRouteProps } from "../types";
8
+ import type { ClientContext } from "./getClientContext";
9
+
10
+ /**
11
+ * HOC que encapsula lógica de autenticação e provider
12
+ */
13
+ export const withAuthProvider = <T,>(
14
+ Component: ComponentType<T & { clientContext: ClientContext }>
15
+ ) => {
16
+ return function WrappedPage(props: AuthRouteProps<T>) {
17
+ const router = useRouter();
18
+
19
+ useEffect(() => {
20
+ // Se não está autorizado, recarrega a página
21
+ if (!props?.authorized) {
22
+ router.reload();
23
+ }
24
+ }, [props?.authorized, router]);
25
+
26
+ // Se não está autorizado, não renderiza nada
27
+ if (!props?.authorized) {
28
+ return null;
29
+ }
30
+
31
+ // Se autorizado, renderiza o componente envolvido no provider
32
+ return (
33
+ <BuyerPortalProvider clientContext={props?.clientContext}>
34
+ <Component {...props.data} clientContext={props?.clientContext} />
35
+ </BuyerPortalProvider>
36
+ );
37
+ };
38
+ };
@@ -0,0 +1,24 @@
1
+ import { type ComponentType } from "react";
2
+
3
+ import { BuyerPortalProvider } from "../components";
4
+
5
+ import type { ClientContext } from "./getClientContext";
6
+
7
+ /**
8
+ * HOC que adiciona BuyerPortalProvider e context
9
+ */
10
+ export const withBuyerPortal = <T extends Record<string, unknown>>(
11
+ Component: ComponentType<T>
12
+ ) => {
13
+ return function WrappedWithProvider(
14
+ props: T & { context: { clientContext: ClientContext } }
15
+ ) {
16
+ const { context, ...componentProps } = props;
17
+
18
+ return (
19
+ <BuyerPortalProvider {...context}>
20
+ <Component {...(componentProps as unknown as T)} />
21
+ </BuyerPortalProvider>
22
+ );
23
+ };
24
+ };
@@ -0,0 +1,35 @@
1
+ import { type ComponentType } from "react";
2
+
3
+ import { withAuth } from "./withAuth";
4
+ import { withBuyerPortal } from "./withBuyerPortal";
5
+
6
+ /**
7
+ * Type for HOCs that can be applied in sequence
8
+ * Using any for flexibility in HOC composition
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type ProviderHOC = (Component: ComponentType<any>) => ComponentType<any>;
12
+
13
+ /**
14
+ * Default project providers applied to all pages.
15
+ * Order: right to left (like pipe/compose)
16
+ */
17
+ const projectProviders: ProviderHOC[] = [withAuth, withBuyerPortal];
18
+
19
+ /**
20
+ * HOC that applies all default project providers.
21
+ * To add new providers, just include them in the projectProviders array.
22
+ *
23
+ * @example
24
+ * export default withProviders(MyComponent);
25
+ */
26
+ export const withProviders = <T extends Record<string, unknown>>(
27
+ Component: ComponentType<T>
28
+ ): ComponentType<T> => {
29
+ // Apply providers in sequence (right to left)
30
+ return projectProviders.reduceRight(
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ (WrappedComponent, provider) => provider(WrappedComponent) as any,
33
+ Component
34
+ ) as ComponentType<T>;
35
+ };
@@ -4,10 +4,7 @@ import { getAddressLocationService } from "../features/addresses/services/locati
4
4
  import { getAddressRecipientsService } from "../features/addresses/services/recipients/get-address-recipients.service";
5
5
  import { getContractDetailsService } from "../features/contracts/services";
6
6
  import { getOrgUnitBasicDataService } from "../features/org-units/services";
7
- import {
8
- BuyerPortalProvider,
9
- withErrorBoundary,
10
- } from "../features/shared/components";
7
+ import { withErrorBoundary } from "../features/shared/components";
11
8
  import { ErrorBoundaryProps } from "../features/shared/components/ErrorBoundary/types";
12
9
  import {
13
10
  PAGE_PARAMS,
@@ -16,9 +13,10 @@ import {
16
13
  import { ErrorTabsLayout } from "../features/shared/layouts/ErrorTabsLayout/ErrorTabsLayout";
17
14
  import {
18
15
  type ClientContext,
19
- getClientContext,
20
16
  getValidPage,
21
17
  withLoaderErrorBoundary,
18
+ withAuthLoader,
19
+ withProviders,
22
20
  } from "../features/shared/utils";
23
21
  import { getUserByIdService } from "../features/users/services";
24
22
 
@@ -29,7 +27,7 @@ import type {
29
27
  } from "../features/addresses/types/AddressData";
30
28
  import type { ContractData } from "../features/contracts/types";
31
29
  import type { OrgUnitBasicData } from "../features/org-units/types";
32
- import type { LoaderData } from "../features/shared/types";
30
+ import type { AuthRouteProps, LoaderData } from "../features/shared/types";
33
31
  import type { UserData } from "../features/users/types";
34
32
 
35
33
  const DEFAULT_ADDRESS_PAGINATED_DATA = {
@@ -69,7 +67,7 @@ export type AddressDetailsPageData = {
69
67
 
70
68
  const loaderFunction = async (
71
69
  data: LoaderData<AddressDetailsPageQuery>
72
- ): Promise<AddressDetailsPageData> => {
70
+ ): Promise<AuthRouteProps<AddressDetailsPageData>> => {
73
71
  const {
74
72
  contractId,
75
73
  orgUnitId,
@@ -82,83 +80,88 @@ const loaderFunction = async (
82
80
  const pageRecipients = getValidPage(pageRecipientsString);
83
81
  const pageLocation = getValidPage(pageLocationString);
84
82
 
85
- const { customerId, cookie, userId, ...clientContext } =
86
- await getClientContext(data);
87
-
88
- if (!contractId || !orgUnitId) {
89
- return {
90
- data: {
91
- addressDetails: null,
92
- recipientsData: null,
93
- locationsData: null,
94
- },
95
- context: {
96
- clientContext: { customerId, cookie, userId, ...clientContext },
97
- currentOrgUnit: null,
98
- currentContract: null,
99
- currentUser: null,
100
- },
101
- };
102
- }
103
-
104
- const orgUnit = await getOrgUnitBasicDataService({ cookie, id: orgUnitId });
105
-
106
- const contract = await getContractDetailsService({
107
- contractId,
108
- cookie,
109
- unitId: orgUnitId,
110
- });
111
-
112
- const user = await getUserByIdService({ orgUnitId, userId, cookie });
113
-
114
- const { data: recipients = [], total = 0 } =
115
- await getAddressRecipientsService({
116
- unitId: orgUnitId,
117
- customerId: contractId,
118
- addressId,
119
- cookie,
120
- search: searchRecipients,
121
- page: pageRecipients,
122
- });
123
-
124
- const { data: locations, total: totalLocations } =
125
- await getAddressLocationService({
126
- contractId,
127
- addressId,
128
- cookie,
129
- unitId: orgUnitId,
130
- search: searchLocations,
131
- page: pageLocation,
132
- });
133
-
134
- return {
135
- data: {
136
- addressDetails: await getAddressDetailsService(
137
- addressId,
83
+ return withAuthLoader(
84
+ data,
85
+ async ({ customerId, cookie, userId, ...clientContext }) => {
86
+ if (!contractId || !orgUnitId) {
87
+ return {
88
+ data: {
89
+ addressDetails: null,
90
+ recipientsData: null,
91
+ locationsData: null,
92
+ },
93
+ context: {
94
+ clientContext: { customerId, cookie, userId, ...clientContext },
95
+ currentOrgUnit: null,
96
+ currentContract: null,
97
+ currentUser: null,
98
+ },
99
+ };
100
+ }
101
+
102
+ const orgUnit = await getOrgUnitBasicDataService({
138
103
  cookie,
104
+ id: orgUnitId,
105
+ });
106
+
107
+ const contract = await getContractDetailsService({
139
108
  contractId,
140
- orgUnitId
141
- ),
142
- recipientsData: {
143
- data: recipients,
144
- total,
145
- search: searchRecipients ?? "",
146
- page: pageRecipients ?? 1,
147
- },
148
- locationsData: {
149
- data: locations,
150
- total: totalLocations,
151
- search: searchLocations ?? "",
152
- page: pageLocation ?? 1,
153
- },
154
- },
155
- context: {
156
- clientContext: { customerId, cookie, userId, ...clientContext },
157
- currentOrgUnit: orgUnit,
158
- currentContract: contract,
159
- currentUser: user,
160
- },
161
- };
109
+ cookie,
110
+ unitId: orgUnitId,
111
+ });
112
+
113
+ const user = await getUserByIdService({ orgUnitId, userId, cookie });
114
+
115
+ const { data: recipients = [], total = 0 } =
116
+ await getAddressRecipientsService({
117
+ unitId: orgUnitId,
118
+ customerId: contractId,
119
+ addressId,
120
+ cookie,
121
+ search: searchRecipients,
122
+ page: pageRecipients,
123
+ });
124
+
125
+ const { data: locations, total: totalLocations } =
126
+ await getAddressLocationService({
127
+ contractId,
128
+ addressId,
129
+ cookie,
130
+ unitId: orgUnitId,
131
+ search: searchLocations,
132
+ page: pageLocation,
133
+ });
134
+
135
+ return {
136
+ data: {
137
+ addressDetails: await getAddressDetailsService(
138
+ addressId,
139
+ cookie,
140
+ contractId,
141
+ orgUnitId
142
+ ),
143
+ recipientsData: {
144
+ data: recipients,
145
+ total,
146
+ search: searchRecipients ?? "",
147
+ page: pageRecipients ?? 1,
148
+ },
149
+ locationsData: {
150
+ data: locations,
151
+ total: totalLocations,
152
+ search: searchLocations ?? "",
153
+ page: pageLocation ?? 1,
154
+ },
155
+ },
156
+ context: {
157
+ clientContext: { customerId, cookie, userId, ...clientContext },
158
+ currentOrgUnit: orgUnit,
159
+ currentContract: contract,
160
+ currentUser: user,
161
+ },
162
+ };
163
+ }
164
+ );
162
165
  };
163
166
 
164
167
  export const loader = withLoaderErrorBoundary(loaderFunction, {
@@ -168,11 +171,10 @@ export const loader = withLoaderErrorBoundary(loaderFunction, {
168
171
 
169
172
  const AddressDetailsPage = ({
170
173
  data,
171
- context,
172
174
  hasError,
173
175
  error,
174
176
  }: AddressDetailsPageData) => (
175
- <BuyerPortalProvider {...context}>
177
+ <>
176
178
  {hasError ? (
177
179
  <ErrorTabsLayout error={error} />
178
180
  ) : (
@@ -182,12 +184,14 @@ const AddressDetailsPage = ({
182
184
  locationsData={data.locationsData ?? DEFAULT_ADDRESS_PAGINATED_DATA}
183
185
  />
184
186
  )}
185
- </BuyerPortalProvider>
187
+ </>
186
188
  );
187
189
 
188
- export default withErrorBoundary(AddressDetailsPage, {
189
- tags: {
190
- component: "AddressDetailsPage",
191
- errorType: "address_details_error",
192
- },
193
- });
190
+ export default withProviders(
191
+ withErrorBoundary(AddressDetailsPage, {
192
+ tags: {
193
+ component: "AddressDetailsPage",
194
+ errorType: "address_details_error",
195
+ },
196
+ })
197
+ );