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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,63 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.3.54] - 2026-01-07
10
+ ## [1.3.55] - 2026-01-08
11
11
 
12
12
  ### Added
13
13
 
14
- - Alternative Login Keys:
15
- - Edit User to support username
16
-
17
- ## [1.3.53] - 2026-01-06
18
-
19
- ### Fixed
20
-
21
- - Solved typo on import of add menu dropdown
22
-
23
- ## [1.3.52] - 2026-01-06
24
-
25
- ### Fixed
26
-
27
- - Removed settings object from LoadingTabs
28
-
29
- ## [1.3.51] - 2026-01-05
30
-
31
- ### Added
32
-
33
- - Add Settings Drawers for Credit Cards, Payment Methods and Collections
34
- - Implement `CreditCardSettingsDrawer` component for credit cards scope configuration
35
- - Implement `PaymentMethodSettingsDrawer` component for payment methods scope configuration
36
- - Implement `CollectionsSettingsDrawer` component for product assortment scope configuration
37
- - Integrate settings drawers with respective layout pages
38
-
39
- ## [1.3.50] - 2025-12-19
40
-
41
- ### Changed
42
-
43
- - Introduces several improvements and refactorings to the budget notification drawer, focusing on user experience.
44
-
45
- ## [1.3.49] - 2025-12-19
46
-
47
- ### Added
48
-
49
- - Add component of Criteria Selection of Custom Fields on Buying Policies
50
-
51
- ## [1.3.48] - 2025-12-19
52
-
53
- - Adjustment from merge to Collections to Products Assortment
54
- - Change Products Assortment to client side
55
-
56
- ## [1.3.47] - 2025-12-19
57
-
58
- - Alternative Login Keys:
59
- - Add auth setup drawer
60
- - Update AddUserDrawer to support username
61
-
62
- ## [1.3.46] - 2025-12-19
63
-
64
- ### Fixed
65
-
66
- - Organizational unit deletion now redirects to parent org unit instead of staying on deleted entity's page
14
+ - Enable Bulk Ops agent in the storefront via /b2b-agent route
15
+ - Add src/pages/b2b-agent.tsx protected by withAuthLoader and withLoaderErrorBoundary
16
+ - Render external agent app inside an iframe (B2BAgentLayout)
17
+ - Implement secure postMessage handshake (B2B_AGENT_READY / AUTH_TOKEN_UPDATE) sending VTEX auth token and customer/user context
18
+ - Add origin/source validation, targetOrigin enforcement, and fallback resend logic for resilience
19
+ - Add full-height layout styles for the agent iframe
67
20
 
68
21
  ## [1.3.45] - 2025-12-17
69
22
 
@@ -458,7 +411,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
458
411
  - Add CHANGELOG file
459
412
  - Add README file
460
413
 
461
- [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...HEAD
414
+ [unreleased]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.55...HEAD
415
+ [1.3.55]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.54...v1.3.55
462
416
  [1.3.54]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.53...v1.3.54
463
417
  [1.3.53]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.52...v1.3.53
464
418
  [1.3.52]: https://github.com/vtex/faststore-plugin-buyer-portal/compare/v1.3.51...v1.3.52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.3.54",
3
+ "version": "1.3.55",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/plugin.config.js CHANGED
@@ -88,6 +88,10 @@ module.exports = {
88
88
  path: "/pvt/organization-account/org-unit/[orgUnitId]",
89
89
  appLayout: false,
90
90
  },
91
+ "b2b-agent": {
92
+ path: "/pvt/organization-account/b2b-agent",
93
+ appLayout: false,
94
+ },
91
95
  },
92
96
 
93
97
  apis: {
@@ -0,0 +1,100 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+
3
+ import { isDevelopment } from "../../../shared/utils/environment";
4
+
5
+ const B2B_AGENT_URL = isDevelopment()
6
+ ? "http://localhost:3001/app/buyer-bulk-ops-agent"
7
+ : "https://buyer-bulk-ops-agent.vercel.app/app/buyer-bulk-ops-agent";
8
+
9
+ interface B2BAgentLayoutProps {
10
+ vtexIdclientAutCookie: string;
11
+ account: string;
12
+ locale?: string;
13
+ customerId?: string;
14
+ userId?: string;
15
+ }
16
+
17
+ export const B2BAgentLayout = ({
18
+ vtexIdclientAutCookie,
19
+ account,
20
+ locale,
21
+ customerId,
22
+ userId,
23
+ }: B2BAgentLayoutProps) => {
24
+ const iframeRef = useRef<HTMLIFrameElement>(null);
25
+
26
+ const agentOrigin = useMemo(() => new URL(B2B_AGENT_URL).origin, []);
27
+
28
+ useEffect(() => {
29
+ function postAuthToIframe() {
30
+ const targetWindow = iframeRef.current?.contentWindow;
31
+ if (!targetWindow) return;
32
+
33
+ // We send only the token (not the full Cookie header) to reduce exposure.
34
+ targetWindow.postMessage(
35
+ {
36
+ type: "AUTH_TOKEN_UPDATE",
37
+ vtexIdclientAutCookie: vtexIdclientAutCookie,
38
+ account,
39
+ locale,
40
+ customerId: customerId,
41
+ userId: userId,
42
+ },
43
+ agentOrigin
44
+ );
45
+ }
46
+
47
+ function onMessage(event: MessageEvent) {
48
+ // Only accept messages coming from the iframe's origin.
49
+ if (event.origin !== agentOrigin) return;
50
+
51
+ // Ensure the message is from the iframe window we embedded.
52
+ if (event.source !== iframeRef.current?.contentWindow) return;
53
+
54
+ // Handshake: child tells it is ready; parent responds by sending auth.
55
+ if (event.data?.type === "B2B_AGENT_READY") {
56
+ postAuthToIframe();
57
+ }
58
+ }
59
+
60
+ window.addEventListener("message", onMessage);
61
+
62
+ // Fallback: try once after iframe load in case READY was missed.
63
+ const iframe = iframeRef.current;
64
+ const onLoad = () => {
65
+ // Small delay to allow the iframe app to attach its message listener.
66
+ setTimeout(() => {
67
+ postAuthToIframe();
68
+ }, 150);
69
+ };
70
+
71
+ if (iframe) iframe.addEventListener("load", onLoad);
72
+
73
+ // If token changes while iframe is already up, proactively resend.
74
+ // This is safe because we still constrain by agentOrigin.
75
+ if (iframeRef.current?.contentWindow && vtexIdclientAutCookie) {
76
+ // Delay to avoid racing the initial mount.
77
+ setTimeout(() => {
78
+ postAuthToIframe();
79
+ }, 1);
80
+ }
81
+
82
+ return () => {
83
+ window.removeEventListener("message", onMessage);
84
+ if (iframe) iframe.removeEventListener("load", onLoad);
85
+ };
86
+ }, [vtexIdclientAutCookie, customerId, userId, agentOrigin]);
87
+
88
+ return (
89
+ <section data-fs-bp-b2b-agent>
90
+ <iframe
91
+ ref={iframeRef}
92
+ data-fs-bp-b2b-agent-iframe
93
+ src={B2B_AGENT_URL}
94
+ title="B2B Agent"
95
+ allow="clipboard-read; clipboard-write"
96
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
97
+ />
98
+ </section>
99
+ );
100
+ };
@@ -0,0 +1,15 @@
1
+ [data-fs-bp-b2b-agent] {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ height: 100vh;
6
+ overflow: hidden;
7
+
8
+ [data-fs-bp-b2b-agent-iframe] {
9
+ flex: 1;
10
+ width: 100%;
11
+ height: 100%;
12
+ border: none;
13
+ }
14
+ }
15
+
@@ -0,0 +1 @@
1
+ export { B2BAgentLayout } from "./B2BAgentLayout/B2BAgentLayout";
@@ -15,6 +15,8 @@ export const buyerPortalRoutes = {
15
15
  profileDetails: (params: { orgUnitId: string; contractId: string }) =>
16
16
  replaceParams(`${base}/profile/[orgUnitId]/[contractId]`, params),
17
17
 
18
+ b2bAgent: `${base}/b2b-agent`,
19
+
18
20
  addresses: (params: { orgUnitId: string; contractId: string }) =>
19
21
  replaceParams(`${base}/addresses/[orgUnitId]/[contractId]`, params),
20
22
  budgets: (params: { orgUnitId: string; contractId: string }) =>
@@ -22,4 +22,4 @@ export const SCOPE_KEYS = {
22
22
  CREDIT_CARDS: "creditCards",
23
23
  } as const;
24
24
 
25
- export const CURRENT_VERSION = "1.3.54";
25
+ export const CURRENT_VERSION = "1.3.55";
@@ -0,0 +1,94 @@
1
+ import storeConfig from "discovery.config";
2
+
3
+ import { B2BAgentLayout } from "../features/b2b-agent/layouts";
4
+ import { withErrorBoundary } from "../features/shared/components";
5
+ import {
6
+ withAuthLoader,
7
+ withLoaderErrorBoundary,
8
+ } from "../features/shared/utils";
9
+
10
+ import type { AuthRouteProps, LoaderData } from "../features/shared/types";
11
+
12
+ /**
13
+ * Extracts the VTEX auth token value from a Cookie header string.
14
+ * Returns only the cookie VALUE (JWT), never "CookieName=value".
15
+ */
16
+ const getAuthCookieInfo = (
17
+ cookieHeader: string
18
+ ): { token: string; accountFromCookie?: string } => {
19
+ if (!cookieHeader) return { token: "" };
20
+
21
+ // VtexIdclientAutCookie_<account>=<value>
22
+ const m = cookieHeader.match(/VtexIdclientAutCookie_([a-zA-Z0-9-]+)=([^;]+)/);
23
+ if (m?.[2]) return { accountFromCookie: m[1], token: m[2] };
24
+
25
+ // fallback: VtexIdclientAutCookie=<value>
26
+ const g = cookieHeader.match(/VtexIdclientAutCookie=([^;]+)/);
27
+ if (g?.[1]) return { token: g[1] };
28
+
29
+ return { token: "" };
30
+ };
31
+
32
+ type B2BAgentPageQuery = Record<string, never>;
33
+
34
+ const loaderFunction = async (
35
+ data: LoaderData<B2BAgentPageQuery>
36
+ ): Promise<AuthRouteProps<undefined>> => {
37
+ return withAuthLoader(data, async () => {
38
+ // No business data is returned.
39
+ // The authenticated client context is enough for this page.
40
+ return undefined;
41
+ });
42
+ };
43
+
44
+ export const loader = withLoaderErrorBoundary(loaderFunction, {
45
+ componentName: "B2BAgentPage",
46
+ redirectToError: true,
47
+ });
48
+
49
+ const B2BAgentPage = (props: AuthRouteProps<undefined>) => {
50
+ if (!props.authorized) return null;
51
+
52
+ const cookieHeader = props.clientContext?.cookie ?? "";
53
+ const tokenFromContext = props.clientContext?.vtexIdclientAutCookie ?? "";
54
+
55
+ const { token: tokenFromCookie, accountFromCookie } =
56
+ getAuthCookieInfo(cookieHeader);
57
+
58
+ // Prefer a token-like value if available; otherwise extract from cookie header.
59
+ const vtexIdclientAutCookie =
60
+ tokenFromContext && !tokenFromContext.includes("=")
61
+ ? tokenFromContext
62
+ : tokenFromCookie;
63
+
64
+ if (!vtexIdclientAutCookie) {
65
+ throw new Error("Missing VTEX auth token for B2B Agent iframe");
66
+ }
67
+
68
+ // account/locale via discovery.config (recomendado)
69
+ const account = storeConfig?.api?.storeId ?? accountFromCookie ?? "";
70
+ const locale = storeConfig?.session?.locale ?? "en-US";
71
+
72
+ const customerId = props.clientContext?.customerId;
73
+ const userId = props.clientContext?.userId;
74
+
75
+ return (
76
+ <B2BAgentLayout
77
+ vtexIdclientAutCookie={vtexIdclientAutCookie}
78
+ account={account}
79
+ locale={locale}
80
+ customerId={customerId}
81
+ userId={userId}
82
+ />
83
+ );
84
+ };
85
+
86
+ export default withErrorBoundary(B2BAgentPage, {
87
+ onError: (error) => {
88
+ console.error("onError", error);
89
+ },
90
+ tags: {
91
+ component: "B2BAgentPage",
92
+ errorType: "b2b_agent_error",
93
+ },
94
+ });
@@ -42,6 +42,10 @@
42
42
 
43
43
  // Payment Methods
44
44
  @import "../features/payment-methods/layouts/PaymentMethodsLayout/payment-methods-layout.scss";
45
+
45
46
  // Roles
46
47
  @import "../features/roles/layout/RolesLayout/roles-layout.scss";
47
48
  @import "../features/roles/layout/RoleDetailsLayout/role-details-layout.scss";
49
+
50
+ // B2B Agent
51
+ @import "../features/b2b-agent/layouts/B2BAgentLayout/b2b-agent-layout.scss";