@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 +9 -55
- package/package.json +1 -1
- package/plugin.config.js +4 -0
- package/src/features/b2b-agent/layouts/B2BAgentLayout/B2BAgentLayout.tsx +100 -0
- package/src/features/b2b-agent/layouts/B2BAgentLayout/b2b-agent-layout.scss +15 -0
- package/src/features/b2b-agent/layouts/index.ts +1 -0
- package/src/features/shared/utils/buyerPortalRoutes.ts +2 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/pages/b2b-agent.tsx +94 -0
- package/src/themes/layouts.scss +4 -0
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.
|
|
10
|
+
## [1.3.55] - 2026-01-08
|
|
11
11
|
|
|
12
12
|
### Added
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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
package/plugin.config.js
CHANGED
|
@@ -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 @@
|
|
|
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 }) =>
|
|
@@ -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
|
+
});
|
package/src/themes/layouts.scss
CHANGED
|
@@ -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";
|