@faststore/core 4.4.1-dev.0 → 4.5.0-dev.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/.turbo/turbo-generate.log +4 -4
- package/.turbo/turbo-test.log +137 -51
- package/@generated/cached-operations.json +4 -0
- package/@generated/gql.ts +30 -0
- package/@generated/graphql.ts +141 -0
- package/@generated/persisted-documents.json +5 -0
- package/@generated/schema.graphql +100 -0
- package/CHANGELOG.md +26 -0
- package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
- package/cms/faststore/schema.json +89 -0
- package/discovery.config.default.js +3 -0
- package/discovery.config.js +10 -1
- package/discovery.config.local.example.js +62 -0
- package/package.json +6 -6
- package/public/icons.svg +4 -0
- package/src/Layout.tsx +6 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +52 -23
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
- package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
- package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
- package/src/components/account/Layout/Layout.tsx +14 -5
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
- package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
- package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
- package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
- package/src/components/cms/global/Components.ts +10 -0
- package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
- package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
- package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
- package/src/components/sections/RecommendationShelf/index.ts +1 -0
- package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
- package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
- package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
- package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
- package/src/pages/[slug]/p.tsx +45 -0
- package/src/pages/pvt/account/quotes/index.tsx +264 -0
- package/src/sdk/account/changeContractToken.ts +119 -0
- package/src/sdk/account/clearPersistedSessionState.ts +26 -0
- package/src/sdk/account/getMyAccountRoutes.ts +7 -0
- package/src/sdk/account/refreshToken.ts +21 -8
- package/src/sdk/account/useAvailableContracts.ts +49 -0
- package/src/sdk/account/useSwitchContract.ts +60 -0
- package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
- package/src/sdk/analytics/utils.ts +9 -0
- package/src/utils/quoteStatus.ts +27 -0
- package/src/utils/retry.ts +69 -0
- package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
- package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
- package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
- package/test/components/RecommendationShelf/vrn.test.ts +56 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
- package/test/components/account/Layout.test.tsx +62 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
- package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
- package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
- package/test/components/account/doLogout.browser.test.ts +102 -0
- package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
- package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
- package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
- package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
- package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
- package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
- package/test/components/quotes/quoteFilters.test.ts +185 -0
- package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
- package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
- package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
- package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
- package/test/sdk/account/refreshToken.browser.test.ts +34 -0
- package/test/sdk/account/refreshToken.test.ts +87 -0
- package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
- package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
- package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
- package/test/sdk/analytics/utils.test.ts +23 -0
- package/test/server/index.test.ts +5 -0
- package/test/utils/retry.test.ts +59 -0
|
@@ -113,6 +113,9 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"$ref": "#/components/ShoppingAssistant"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"$ref": "#/components/RecommendationShelf"
|
|
116
119
|
}
|
|
117
120
|
]
|
|
118
121
|
}
|
|
@@ -4425,6 +4428,92 @@
|
|
|
4425
4428
|
"writeOnly": false,
|
|
4426
4429
|
"deprecated": false,
|
|
4427
4430
|
"$abstract": false
|
|
4431
|
+
},
|
|
4432
|
+
"RecommendationShelf": {
|
|
4433
|
+
"$extends": ["#/$defs/base-component"],
|
|
4434
|
+
"$componentKey": "RecommendationShelf",
|
|
4435
|
+
"$componentTitle": "Recommendation Shelf",
|
|
4436
|
+
"title": "Recommendation Shelf",
|
|
4437
|
+
"description": "Display a carousel of personalized product recommendations from a VTEX Recommendations campaign.",
|
|
4438
|
+
"type": "object",
|
|
4439
|
+
"required": ["campaignVrn"],
|
|
4440
|
+
"properties": {
|
|
4441
|
+
"title": {
|
|
4442
|
+
"title": "Title",
|
|
4443
|
+
"description": "Override the shelf title. If not set, the campaign title will be used.",
|
|
4444
|
+
"type": "string"
|
|
4445
|
+
},
|
|
4446
|
+
"campaignVrn": {
|
|
4447
|
+
"title": "Campaign VRN",
|
|
4448
|
+
"description": "Recommendation campaign VRN (e.g. vrn:recommendations:my-account:rec-persona-v2:abc123).",
|
|
4449
|
+
"type": "string",
|
|
4450
|
+
"pattern": "^vrn:recommendations:[^:]+:(rec-cross-v2|rec-similar-v2|rec-persona-v2|rec-last-v2|rec-top-items-v2|rec-search-v2|rec-next-v2|rec-visual-v2):[^:]+$"
|
|
4451
|
+
},
|
|
4452
|
+
"itemsContext": {
|
|
4453
|
+
"title": "Items context",
|
|
4454
|
+
"description": "Source of the products used as context for the recommendation request. 'Product page' uses the product currently being viewed; 'Cart' uses the items currently in the cart (useful for cross-sell shelves on the cart page). Only affects context-based campaigns such as cross-sell, similar items, visual similarity and next interaction.",
|
|
4455
|
+
"type": "string",
|
|
4456
|
+
"default": "PDP",
|
|
4457
|
+
"enum": ["PDP", "CART"],
|
|
4458
|
+
"enumNames": ["Product page", "Cart"]
|
|
4459
|
+
},
|
|
4460
|
+
"carouselConfiguration": {
|
|
4461
|
+
"title": "Carousel Configuration",
|
|
4462
|
+
"type": "object",
|
|
4463
|
+
"properties": {
|
|
4464
|
+
"itemsPerPageDesktop": {
|
|
4465
|
+
"title": "Items per page (desktop)",
|
|
4466
|
+
"description": "Number of products shown per page on desktop viewports.",
|
|
4467
|
+
"type": "number",
|
|
4468
|
+
"default": 4
|
|
4469
|
+
},
|
|
4470
|
+
"itemsPerPageMobile": {
|
|
4471
|
+
"title": "Items per page (mobile)",
|
|
4472
|
+
"description": "Number of products shown per page on mobile and tablet viewports.",
|
|
4473
|
+
"type": "number",
|
|
4474
|
+
"default": 2
|
|
4475
|
+
},
|
|
4476
|
+
"variant": {
|
|
4477
|
+
"title": "Carousel track variant",
|
|
4478
|
+
"description": "How the carousel navigates between items.",
|
|
4479
|
+
"type": "string",
|
|
4480
|
+
"default": "scroll",
|
|
4481
|
+
"enum": ["slide", "scroll"],
|
|
4482
|
+
"enumNames": ["Slide", "Scroll"]
|
|
4483
|
+
},
|
|
4484
|
+
"infiniteMode": {
|
|
4485
|
+
"title": "Infinite navigation?",
|
|
4486
|
+
"description": "Only applies to the 'slide' variant.",
|
|
4487
|
+
"type": "boolean",
|
|
4488
|
+
"default": false
|
|
4489
|
+
},
|
|
4490
|
+
"controls": {
|
|
4491
|
+
"title": "Navigation controls",
|
|
4492
|
+
"description": "Which navigation elements should be visible.",
|
|
4493
|
+
"type": "string",
|
|
4494
|
+
"default": "complete",
|
|
4495
|
+
"enum": ["complete", "navigationArrows", "paginationBullets"],
|
|
4496
|
+
"enumNames": ["Arrows and bullets", "Arrows only", "Bullets only"]
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
},
|
|
4500
|
+
"productCardConfiguration": {
|
|
4501
|
+
"title": "Product Card Configuration",
|
|
4502
|
+
"type": "object",
|
|
4503
|
+
"properties": {
|
|
4504
|
+
"showDiscountBadge": {
|
|
4505
|
+
"title": "Show discount badge?",
|
|
4506
|
+
"type": "boolean",
|
|
4507
|
+
"default": true
|
|
4508
|
+
},
|
|
4509
|
+
"bordered": {
|
|
4510
|
+
"title": "Cards should be bordered?",
|
|
4511
|
+
"type": "boolean",
|
|
4512
|
+
"default": true
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4428
4517
|
}
|
|
4429
4518
|
}
|
|
4430
4519
|
}
|
|
@@ -201,6 +201,9 @@ module.exports = {
|
|
|
201
201
|
},
|
|
202
202
|
refreshToken: false,
|
|
203
203
|
scrollRestoration: false,
|
|
204
|
+
// Opt-in flag for VTEX Recommendations. When disabled (default), the store
|
|
205
|
+
// never starts a personalization session nor calls the Recommendations API.
|
|
206
|
+
enableRecommendations: false,
|
|
204
207
|
/** Package names to transpile (e.g. ['@vtex/components']). Use a non-empty list to enable Next.js transpilation. */
|
|
205
208
|
transpilePackages: [],
|
|
206
209
|
},
|
package/discovery.config.js
CHANGED
|
@@ -6,6 +6,15 @@ const deepmerge = require('deepmerge')
|
|
|
6
6
|
/**
|
|
7
7
|
* @type {typeof defaultConfig & Record<string, any>}
|
|
8
8
|
* */
|
|
9
|
-
|
|
9
|
+
let finalConfig = deepmerge(defaultConfig, starterConfig)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
finalConfig = deepmerge(finalConfig, require('./discovery.config.local'))
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error?.code !== 'MODULE_NOT_FOUND') {
|
|
15
|
+
throw error
|
|
16
|
+
}
|
|
17
|
+
// Optional local overrides (packages/core/discovery.config.local.js).
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
module.exports = finalConfig
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example local overrides for B2B contract switcher development.
|
|
3
|
+
*
|
|
4
|
+
* Copy to discovery.config.local.js (gitignored) before running locally.
|
|
5
|
+
* Run from `packages/core`:
|
|
6
|
+
*
|
|
7
|
+
* cp discovery.config.local.example.js discovery.config.local.js
|
|
8
|
+
*
|
|
9
|
+
* Or from the repo root:
|
|
10
|
+
*
|
|
11
|
+
* cp packages/core/discovery.config.local.example.js packages/core/discovery.config.local.js
|
|
12
|
+
*/
|
|
13
|
+
module.exports = {
|
|
14
|
+
plugins: ['@vtex/faststore-plugin-buyer-portal'],
|
|
15
|
+
|
|
16
|
+
api: {
|
|
17
|
+
storeId: 'b2bfaststoredev',
|
|
18
|
+
hideUnavailableItems: true,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
storeUrl: 'https://b2bfaststore.vtexfaststore.com',
|
|
22
|
+
secureSubdomain: 'https://b2bfaststore.vtexfaststore.com',
|
|
23
|
+
checkoutUrl:
|
|
24
|
+
process.env.NODE_ENV === 'development'
|
|
25
|
+
? 'http://localhost:3000/checkout/cart'
|
|
26
|
+
: 'https://b2bfaststore.vtexfaststore.com/checkout',
|
|
27
|
+
loginUrl: 'https://b2bfaststore.vtexfaststore.com/api/io/login',
|
|
28
|
+
accountUrl: 'https://b2bfaststore.vtexfaststore.com/api/io/account',
|
|
29
|
+
|
|
30
|
+
experimental: {
|
|
31
|
+
enableFaststoreMyAccount: true,
|
|
32
|
+
refreshToken: true,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
vtexHeadlessCms: {
|
|
36
|
+
webhookUrls: [
|
|
37
|
+
'https://b2bfaststoredev.myvtex.com/cms-releases/webhook-releases',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Dev-only: proxy VTEX Identity routes (WebOps handles this in production).
|
|
43
|
+
const { storeId } = module.exports.api
|
|
44
|
+
|
|
45
|
+
module.exports.rewrites = async () => {
|
|
46
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
47
|
+
return { beforeFiles: [] }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
beforeFiles: [
|
|
52
|
+
{
|
|
53
|
+
source: '/api/authenticator/:path*',
|
|
54
|
+
destination: `https://${storeId}.myvtex.com/api/authenticator/:path*`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
source: '/api/vtexid/:path*',
|
|
58
|
+
destination: `https://${storeId}.myvtex.com/api/vtexid/:path*`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0-dev.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"style-loader": "^3.3.1",
|
|
72
72
|
"swr": "^2.2.5",
|
|
73
73
|
"use-sync-external-store": "^1.6.0",
|
|
74
|
-
"@faststore/api": "4.
|
|
75
|
-
"@faststore/
|
|
76
|
-
"@faststore/
|
|
77
|
-
"@faststore/ui": "4.
|
|
78
|
-
"@faststore/
|
|
74
|
+
"@faststore/api": "4.5.0-dev.1",
|
|
75
|
+
"@faststore/sdk": "4.5.0-dev.1",
|
|
76
|
+
"@faststore/diagnostics": "4.5.0-dev.1",
|
|
77
|
+
"@faststore/ui": "4.5.0-dev.1",
|
|
78
|
+
"@faststore/lighthouse": "4.5.0-dev.1"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@cypress/code-coverage": "^3.12.1",
|
package/public/icons.svg
CHANGED
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
|
|
74
74
|
<symbol id="FadersHorizontal" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><line x1="135.99902" y1="171.99304" x2="39.99902" y2="171.99317" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><line x1="215.99902" y1="171.99317" x2="167.99902" y2="171.99304" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><line x1="71.99902" y1="83.99304" x2="39.99902" y2="83.99246" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><line x1="215.99902" y1="83.99246" x2="103.99902" y2="83.99304" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><line x1="103.99902" y1="59.99304" x2="103.99902" y2="107.99304" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><line x1="167.99902" y1="195.99304" x2="167.99902" y2="147.99304" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line></symbol>
|
|
75
75
|
|
|
76
|
+
<symbol id="FolderOpen" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M32,96V72a8,8,0,0,1,8-8H93.33a8,8,0,0,1,4.8,1.6L128,88H216a8,8,0,0,1,8,8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M40,200H216a16,16,0,0,0,15.7-13L248,96H32L16,187a16,16,0,0,0,15.7,13Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
|
77
|
+
|
|
76
78
|
<symbol id="Gift" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><rect x="32" y="80" width="192" height="48" rx="7.99999" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" fill="none"></rect><path d="M208,128v72a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><line x1="128" y1="80" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line><path d="M173.25483,68.68629C161.94113,80,128,80,128,80s0-33.94113,11.31371-45.25483a24,24,0,0,1,33.94112,33.94112Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M82.74517,68.68629C94.05887,80,128,80,128,80s0-33.94113-11.31371-45.25483A24,24,0,0,0,82.74517,68.68629Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
|
77
79
|
|
|
78
80
|
<symbol id="Globe" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24Zm88 104a87.61 87.61 0 0 1-3.33 24h-38.51a157.44 157.44 0 0 0 0-48h38.51a87.61 87.61 0 0 1 3.33 24Zm-114 40h52a115.11 115.11 0 0 1-26 45 115.27 115.27 0 0 1-26-45Zm-3.9-16a140.84 140.84 0 0 1 0-48h59.88a140.84 140.84 0 0 1 0 48ZM40 128a87.61 87.61 0 0 1 3.33-24h38.51a157.44 157.44 0 0 0 0 48H43.33A87.61 87.61 0 0 1 40 128Zm114-40h-52a115.11 115.11 0 0 1 26-45 115.27 115.27 0 0 1 26 45Zm52.33 0h-35.62a135.28 135.28 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.28 135.28 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6ZM49.63 168h35.66a135.28 135.28 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168Zm98.78 45.6a135.28 135.28 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6Z"/></symbol>
|
|
@@ -109,6 +111,8 @@
|
|
|
109
111
|
|
|
110
112
|
<symbol id="SealWarning" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-11.55,39.29c-4.79,5-9.75,10.17-12.38,16.52-2.52,6.1-2.63,13.07-2.73,19.82-.1,7-.21,14.33-3.32,17.43s-10.39,3.22-17.43,3.32c-6.75.1-13.72.21-19.82,2.73-6.35,2.63-11.52,7.59-16.52,12.38S132,224,128,224s-9.15-4.92-14.11-9.69-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82-2.63-6.35-7.59-11.52-12.38-16.52S32,132,32,128s4.92-9.15,9.69-14.11,9.75-10.17,12.38-16.52c2.52-6.1,2.63-13.07,2.73-19.82.1-7,.21-14.33,3.32-17.43S70.51,56.9,77.55,56.8c6.75-.1,13.72-.21,19.82-2.73,6.35-2.63,11.52-7.59,16.52-12.38S124,32,128,32s9.15,4.92,14.11,9.69,10.17,9.75,16.52,12.38c6.1,2.52,13.07,2.63,19.82,2.73,7,.1,14.33.21,17.43,3.32s3.22,10.39,3.32,17.43c.1,6.75.21,13.72,2.73,19.82,2.63,6.35,7.59,11.52,12.38,16.52S224,124,224,128,219.08,137.15,214.31,142.11ZM120,136V80a8,8,0,0,1,16,0v56a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,172Z"></path></symbol>
|
|
111
113
|
|
|
114
|
+
<symbol id="SignOut" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M112,216H48a16,16,0,0,1-16-16V56A16,16,0,0,1,48,40h64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><polyline points="168 176 216 128 168 80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></polyline><line x1="104" y1="128" x2="216" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></line></symbol>
|
|
115
|
+
|
|
112
116
|
<symbol id="ShieldCheck" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M40,114.66667V56a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8v58.66667c0,84.01533-71.306,111.85016-85.5438,116.57058a7.54755,7.54755,0,0,1-4.9124,0C111.306,226.51683,40,198.682,40,114.66667Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><polyline points="172 104 113.333 160 84 132" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></polyline></symbol>
|
|
113
117
|
|
|
114
118
|
<symbol id="ShoppingCart" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M184,184H69.81818L41.92162,30.56892A8,8,0,0,0,34.05066,24H16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><circle cx="80" cy="204" r="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></circle><circle cx="184" cy="204" r="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></circle><path d="M62.54543,144H188.10132a16,16,0,0,0,15.74192-13.13783L216,64H48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
package/src/Layout.tsx
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { isValidElement, type PropsWithChildren } from 'react'
|
|
2
2
|
|
|
3
3
|
import { usePageViewEvent } from './sdk/analytics/hooks/usePageViewEvent'
|
|
4
|
+
import { useStartRecommendationSession } from './sdk/analytics/hooks/useStartRecommendationSession'
|
|
4
5
|
|
|
5
6
|
function Layout({ children }: PropsWithChildren) {
|
|
6
7
|
const props = isValidElement(children) ? children.props : undefined
|
|
7
8
|
usePageViewEvent(props)
|
|
9
|
+
// Implemented here because the personalization session must be initiated once
|
|
10
|
+
// per browser session on each page. Gated behind the
|
|
11
|
+
// `experimental.enableRecommendations` flag: stores that don't opt into
|
|
12
|
+
// Recommendations never start a session nor call the Recommendations API.
|
|
13
|
+
useStartRecommendationSession()
|
|
8
14
|
|
|
9
15
|
return <>{children}</>
|
|
10
16
|
}
|
package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { Button, Icon, IconButton, Loader } from '@faststore/ui'
|
|
2
|
+
import { useState, type ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useAvailableContracts } from 'src/sdk/account/useAvailableContracts'
|
|
5
|
+
import { useSwitchContract } from 'src/sdk/account/useSwitchContract'
|
|
6
|
+
import { useSession } from 'src/sdk/session'
|
|
7
|
+
|
|
8
|
+
export type ContractSwitcherProps = {
|
|
9
|
+
/** Returns to the drawer menu view (also used by Cancel). */
|
|
10
|
+
onBack: () => void
|
|
11
|
+
/** Closes the whole drawer. */
|
|
12
|
+
onClose: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Contract that can be displayed by the switcher. `isDefault` is optional so the
|
|
17
|
+
* component compiles against both the local mock and the real
|
|
18
|
+
* `useAvailableContracts` hook (which does not expose a default yet).
|
|
19
|
+
*/
|
|
20
|
+
type SwitcherContract = {
|
|
21
|
+
id: string
|
|
22
|
+
corporateName: string
|
|
23
|
+
isActive: boolean
|
|
24
|
+
isDefault?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialOf = (name: string) => name.trim().charAt(0).toUpperCase()
|
|
28
|
+
|
|
29
|
+
type ContractSwitcherContentProps = {
|
|
30
|
+
loading: boolean
|
|
31
|
+
error: Error | null
|
|
32
|
+
currentContract: SwitcherContract | null
|
|
33
|
+
alternatives: SwitcherContract[]
|
|
34
|
+
ordered: SwitcherContract[]
|
|
35
|
+
search: string
|
|
36
|
+
selectedId: string | null
|
|
37
|
+
switchError: Error | null
|
|
38
|
+
onSearchChange: (value: string) => void
|
|
39
|
+
onSearchClear: () => void
|
|
40
|
+
onSelectContract: (contractId: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ContractSwitcherLoading = () => (
|
|
44
|
+
<div data-fs-contract-switcher-loading aria-busy="true">
|
|
45
|
+
<Loader />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const ContractSwitcherLoadError = () => (
|
|
50
|
+
<div data-fs-contract-switcher-message role="alert">
|
|
51
|
+
<p>We couldn't load your contracts. Please try again.</p>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const ContractSwitcherCurrentSession = ({
|
|
56
|
+
currentContract,
|
|
57
|
+
}: {
|
|
58
|
+
currentContract: SwitcherContract | null
|
|
59
|
+
}) => (
|
|
60
|
+
<div data-fs-contract-switcher-current>
|
|
61
|
+
<span data-fs-contract-switcher-label>Current session</span>
|
|
62
|
+
{currentContract && (
|
|
63
|
+
<div data-fs-contract-switcher-current-card>
|
|
64
|
+
<span data-fs-contract-switcher-avatar aria-hidden="true">
|
|
65
|
+
{initialOf(currentContract.corporateName)}
|
|
66
|
+
</span>
|
|
67
|
+
<span data-fs-contract-switcher-option-name>
|
|
68
|
+
{currentContract.corporateName}
|
|
69
|
+
</span>
|
|
70
|
+
{currentContract.isDefault && (
|
|
71
|
+
<Icon
|
|
72
|
+
data-fs-contract-switcher-default
|
|
73
|
+
name="Star"
|
|
74
|
+
width={20}
|
|
75
|
+
height={20}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const ContractSwitcherEmptyAlternatives = () => (
|
|
84
|
+
<div data-fs-contract-switcher-message data-fs-contract-switcher-empty>
|
|
85
|
+
<p>No other contracts are available for your organization.</p>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
type ContractSwitcherAlternativesProps = {
|
|
90
|
+
alternatives: SwitcherContract[]
|
|
91
|
+
ordered: SwitcherContract[]
|
|
92
|
+
search: string
|
|
93
|
+
selectedId: string | null
|
|
94
|
+
onSearchChange: (value: string) => void
|
|
95
|
+
onSearchClear: () => void
|
|
96
|
+
onSelectContract: (contractId: string) => void
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ContractSwitcherSearch = ({
|
|
100
|
+
search,
|
|
101
|
+
onSearchChange,
|
|
102
|
+
onSearchClear,
|
|
103
|
+
}: Pick<
|
|
104
|
+
ContractSwitcherAlternativesProps,
|
|
105
|
+
'search' | 'onSearchChange' | 'onSearchClear'
|
|
106
|
+
>) => (
|
|
107
|
+
<div data-fs-contract-switcher-search>
|
|
108
|
+
<Icon name="MagnifyingGlass" width={20} height={20} />
|
|
109
|
+
<input
|
|
110
|
+
type="text"
|
|
111
|
+
value={search}
|
|
112
|
+
placeholder="Search"
|
|
113
|
+
aria-label="Search contracts"
|
|
114
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
115
|
+
/>
|
|
116
|
+
{search && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
data-fs-contract-switcher-search-clear
|
|
120
|
+
aria-label="Clear search"
|
|
121
|
+
onClick={onSearchClear}
|
|
122
|
+
>
|
|
123
|
+
<Icon name="X" width={18} height={18} />
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const ContractSwitcherOptionList = ({
|
|
130
|
+
ordered,
|
|
131
|
+
selectedId,
|
|
132
|
+
onSelectContract,
|
|
133
|
+
}: Pick<
|
|
134
|
+
ContractSwitcherAlternativesProps,
|
|
135
|
+
'ordered' | 'selectedId' | 'onSelectContract'
|
|
136
|
+
>) => (
|
|
137
|
+
<ul data-fs-contract-switcher-list>
|
|
138
|
+
{ordered.map((contract) => {
|
|
139
|
+
const isSelected = selectedId === contract.id
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<li key={contract.id} data-fs-contract-switcher-item>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
data-fs-contract-switcher-option
|
|
146
|
+
data-fs-contract-switcher-option-selected={isSelected}
|
|
147
|
+
aria-pressed={isSelected}
|
|
148
|
+
onClick={() => onSelectContract(contract.id)}
|
|
149
|
+
>
|
|
150
|
+
<span data-fs-contract-switcher-avatar aria-hidden="true">
|
|
151
|
+
{initialOf(contract.corporateName)}
|
|
152
|
+
</span>
|
|
153
|
+
<span data-fs-contract-switcher-option-name>
|
|
154
|
+
{contract.corporateName}
|
|
155
|
+
</span>
|
|
156
|
+
{contract.isDefault && (
|
|
157
|
+
<Icon
|
|
158
|
+
data-fs-contract-switcher-default
|
|
159
|
+
name="Star"
|
|
160
|
+
width={20}
|
|
161
|
+
height={20}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</button>
|
|
165
|
+
</li>
|
|
166
|
+
)
|
|
167
|
+
})}
|
|
168
|
+
</ul>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const ContractSwitcherAlternatives = ({
|
|
172
|
+
alternatives,
|
|
173
|
+
ordered,
|
|
174
|
+
search,
|
|
175
|
+
selectedId,
|
|
176
|
+
onSearchChange,
|
|
177
|
+
onSearchClear,
|
|
178
|
+
onSelectContract,
|
|
179
|
+
}: ContractSwitcherAlternativesProps) => {
|
|
180
|
+
if (alternatives.length === 0) {
|
|
181
|
+
return <ContractSwitcherEmptyAlternatives />
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let listContent: ReactNode
|
|
185
|
+
if (ordered.length === 0) {
|
|
186
|
+
listContent = (
|
|
187
|
+
<div data-fs-contract-switcher-message>
|
|
188
|
+
<p>No contracts match your search.</p>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
} else {
|
|
192
|
+
listContent = (
|
|
193
|
+
<ContractSwitcherOptionList
|
|
194
|
+
ordered={ordered}
|
|
195
|
+
selectedId={selectedId}
|
|
196
|
+
onSelectContract={onSelectContract}
|
|
197
|
+
/>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<p data-fs-contract-switcher-count>
|
|
204
|
+
Select one of <strong>{alternatives.length}</strong> available
|
|
205
|
+
contracts:
|
|
206
|
+
</p>
|
|
207
|
+
<ContractSwitcherSearch
|
|
208
|
+
search={search}
|
|
209
|
+
onSearchChange={onSearchChange}
|
|
210
|
+
onSearchClear={onSearchClear}
|
|
211
|
+
/>
|
|
212
|
+
{listContent}
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const ContractSwitcherSwitchError = () => (
|
|
218
|
+
<div data-fs-contract-switcher-message role="alert">
|
|
219
|
+
<p>
|
|
220
|
+
We couldn't switch your contract. The previous contract is still active.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const ContractSwitcherContent = ({
|
|
226
|
+
loading,
|
|
227
|
+
error,
|
|
228
|
+
currentContract,
|
|
229
|
+
alternatives,
|
|
230
|
+
ordered,
|
|
231
|
+
search,
|
|
232
|
+
selectedId,
|
|
233
|
+
switchError,
|
|
234
|
+
onSearchChange,
|
|
235
|
+
onSearchClear,
|
|
236
|
+
onSelectContract,
|
|
237
|
+
}: ContractSwitcherContentProps) => {
|
|
238
|
+
if (loading) {
|
|
239
|
+
return <ContractSwitcherLoading />
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (error) {
|
|
243
|
+
return <ContractSwitcherLoadError />
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
<ContractSwitcherCurrentSession currentContract={currentContract} />
|
|
249
|
+
<ContractSwitcherAlternatives
|
|
250
|
+
alternatives={alternatives}
|
|
251
|
+
ordered={ordered}
|
|
252
|
+
search={search}
|
|
253
|
+
selectedId={selectedId}
|
|
254
|
+
onSearchChange={onSearchChange}
|
|
255
|
+
onSearchClear={onSearchClear}
|
|
256
|
+
onSelectContract={onSelectContract}
|
|
257
|
+
/>
|
|
258
|
+
{switchError && <ContractSwitcherSwitchError />}
|
|
259
|
+
</>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Sub-view of the OrganizationDrawer that lets a B2B buyer change the active
|
|
265
|
+
* contract within their Organization Unit (REQ-03..06). Replaces the whole
|
|
266
|
+
* drawer body while open: own header (back / title / close), the current
|
|
267
|
+
* session, a searchable list of alternatives, and a Cancel / Confirm footer.
|
|
268
|
+
*/
|
|
269
|
+
export const ContractSwitcher = ({
|
|
270
|
+
onBack,
|
|
271
|
+
onClose,
|
|
272
|
+
}: ContractSwitcherProps) => {
|
|
273
|
+
const { contracts, loading, error } = useAvailableContracts(true)
|
|
274
|
+
const { b2b } = useSession()
|
|
275
|
+
const {
|
|
276
|
+
switchContract,
|
|
277
|
+
loading: switching,
|
|
278
|
+
error: switchError,
|
|
279
|
+
enabled: switchEnabled,
|
|
280
|
+
} = useSwitchContract()
|
|
281
|
+
|
|
282
|
+
const [search, setSearch] = useState('')
|
|
283
|
+
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
284
|
+
|
|
285
|
+
const allContracts: SwitcherContract[] = contracts
|
|
286
|
+
const activeContractId = b2b?.customerId?.trim() ?? ''
|
|
287
|
+
const currentContract =
|
|
288
|
+
(activeContractId
|
|
289
|
+
? allContracts.find((contract) => contract.id === activeContractId)
|
|
290
|
+
: null) ??
|
|
291
|
+
allContracts.find((contract) => contract.isActive) ??
|
|
292
|
+
allContracts[0] ??
|
|
293
|
+
null
|
|
294
|
+
|
|
295
|
+
const alternatives = allContracts.filter(
|
|
296
|
+
(contract) => contract.id !== currentContract?.id
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const query = search.toLowerCase().trim()
|
|
300
|
+
const filtered = query
|
|
301
|
+
? alternatives.filter((contract) =>
|
|
302
|
+
contract.corporateName.toLowerCase().includes(query)
|
|
303
|
+
)
|
|
304
|
+
: alternatives
|
|
305
|
+
|
|
306
|
+
const ordered = [...filtered].sort((a, b) => {
|
|
307
|
+
if (a.isDefault && !b.isDefault) return -1
|
|
308
|
+
if (b.isDefault && !a.isDefault) return 1
|
|
309
|
+
return a.corporateName.localeCompare(b.corporateName)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const canConfirm =
|
|
313
|
+
switchEnabled &&
|
|
314
|
+
selectedId !== null &&
|
|
315
|
+
selectedId !== currentContract?.id &&
|
|
316
|
+
ordered.some((contract) => contract.id === selectedId) &&
|
|
317
|
+
!switching
|
|
318
|
+
|
|
319
|
+
const handleConfirm = async () => {
|
|
320
|
+
if (!selectedId) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await switchContract(selectedId)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<section data-fs-contract-switcher>
|
|
329
|
+
<header data-fs-contract-switcher-header>
|
|
330
|
+
<IconButton
|
|
331
|
+
data-fs-contract-switcher-back
|
|
332
|
+
aria-label="Back to account menu"
|
|
333
|
+
icon={<Icon name="ArrowLeft" width={20} height={20} />}
|
|
334
|
+
onClick={onBack}
|
|
335
|
+
/>
|
|
336
|
+
<h2 data-fs-contract-switcher-title>Change contract</h2>
|
|
337
|
+
<IconButton
|
|
338
|
+
data-fs-contract-switcher-close
|
|
339
|
+
aria-label="Close"
|
|
340
|
+
icon={<Icon name="X" width={20} height={20} />}
|
|
341
|
+
onClick={onClose}
|
|
342
|
+
/>
|
|
343
|
+
</header>
|
|
344
|
+
|
|
345
|
+
<div data-fs-contract-switcher-content>
|
|
346
|
+
<ContractSwitcherContent
|
|
347
|
+
loading={loading}
|
|
348
|
+
error={error}
|
|
349
|
+
currentContract={currentContract}
|
|
350
|
+
alternatives={alternatives}
|
|
351
|
+
ordered={ordered}
|
|
352
|
+
search={search}
|
|
353
|
+
selectedId={selectedId}
|
|
354
|
+
switchError={switchError}
|
|
355
|
+
onSearchChange={setSearch}
|
|
356
|
+
onSearchClear={() => setSearch('')}
|
|
357
|
+
onSelectContract={setSelectedId}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<footer data-fs-contract-switcher-footer>
|
|
362
|
+
<Button variant="tertiary" onClick={onBack}>
|
|
363
|
+
Cancel
|
|
364
|
+
</Button>
|
|
365
|
+
<Button
|
|
366
|
+
variant="primary"
|
|
367
|
+
disabled={!canConfirm}
|
|
368
|
+
loading={switching}
|
|
369
|
+
onClick={handleConfirm}
|
|
370
|
+
>
|
|
371
|
+
Confirm
|
|
372
|
+
</Button>
|
|
373
|
+
</footer>
|
|
374
|
+
</section>
|
|
375
|
+
)
|
|
376
|
+
}
|