@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.
Files changed (89) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +137 -51
  3. package/@generated/cached-operations.json +4 -0
  4. package/@generated/gql.ts +30 -0
  5. package/@generated/graphql.ts +141 -0
  6. package/@generated/persisted-documents.json +5 -0
  7. package/@generated/schema.graphql +100 -0
  8. package/CHANGELOG.md +26 -0
  9. package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
  10. package/cms/faststore/schema.json +89 -0
  11. package/discovery.config.default.js +3 -0
  12. package/discovery.config.js +10 -1
  13. package/discovery.config.local.example.js +62 -0
  14. package/package.json +6 -6
  15. package/public/icons.svg +4 -0
  16. package/src/Layout.tsx +6 -0
  17. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
  18. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
  19. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +52 -23
  20. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
  21. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
  22. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
  23. package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
  24. package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
  25. package/src/components/account/Layout/Layout.tsx +14 -5
  26. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
  27. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
  28. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
  29. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
  30. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
  31. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
  32. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
  33. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
  34. package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
  35. package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
  36. package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
  37. package/src/components/cms/global/Components.ts +10 -0
  38. package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
  39. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
  40. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
  41. package/src/components/sections/RecommendationShelf/index.ts +1 -0
  42. package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
  43. package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
  44. package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
  45. package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
  46. package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
  47. package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
  48. package/src/pages/[slug]/p.tsx +45 -0
  49. package/src/pages/pvt/account/quotes/index.tsx +264 -0
  50. package/src/sdk/account/changeContractToken.ts +119 -0
  51. package/src/sdk/account/clearPersistedSessionState.ts +26 -0
  52. package/src/sdk/account/getMyAccountRoutes.ts +7 -0
  53. package/src/sdk/account/refreshToken.ts +21 -8
  54. package/src/sdk/account/useAvailableContracts.ts +49 -0
  55. package/src/sdk/account/useSwitchContract.ts +60 -0
  56. package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
  57. package/src/sdk/analytics/utils.ts +9 -0
  58. package/src/utils/quoteStatus.ts +27 -0
  59. package/src/utils/retry.ts +69 -0
  60. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
  61. package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
  62. package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
  63. package/test/components/RecommendationShelf/vrn.test.ts +56 -0
  64. package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
  65. package/test/components/account/Layout.test.tsx +62 -0
  66. package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
  67. package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
  68. package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
  69. package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
  70. package/test/components/account/doLogout.browser.test.ts +102 -0
  71. package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
  72. package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
  73. package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
  74. package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
  75. package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
  76. package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
  77. package/test/components/quotes/quoteFilters.test.ts +185 -0
  78. package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
  79. package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
  80. package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
  81. package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
  82. package/test/sdk/account/refreshToken.browser.test.ts +34 -0
  83. package/test/sdk/account/refreshToken.test.ts +87 -0
  84. package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
  85. package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
  86. package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
  87. package/test/sdk/analytics/utils.test.ts +23 -0
  88. package/test/server/index.test.ts +5 -0
  89. 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
  },
@@ -6,6 +6,15 @@ const deepmerge = require('deepmerge')
6
6
  /**
7
7
  * @type {typeof defaultConfig & Record<string, any>}
8
8
  * */
9
- const finalConfig = deepmerge(defaultConfig, starterConfig)
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.4.1-dev.0",
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.4.1-dev.0",
75
- "@faststore/lighthouse": "4.4.1-dev.0",
76
- "@faststore/sdk": "4.4.1-dev.0",
77
- "@faststore/ui": "4.4.1-dev.0",
78
- "@faststore/diagnostics": "4.4.1-dev.0"
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
  }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { ContractSwitcher } from './ContractSwitcher'
2
+ export type { ContractSwitcherProps } from './ContractSwitcher'