@faststore/core 4.5.0-dev.5 → 4.5.0-dev.7

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.
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.5.0-dev.4 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.5.0-dev.6 generate /home/runner/work/faststore/faststore/packages/core
3
3
  > pnpm run gen-types && pnpm run cache-graphql
4
4
 
5
5
 
6
- > @faststore/core@4.5.0-dev.4 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.5.0-dev.6 gen-types /home/runner/work/faststore/faststore/packages/core
7
7
  > node ../cli/bin/run generate-types .
8
8
 
9
9
  [STARTED] Parse Configuration
@@ -19,7 +19,7 @@
19
19
  [COMPLETED] Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
20
20
  [COMPLETED] Generate outputs
21
21
 
22
- > @faststore/core@4.5.0-dev.4 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.5.0-dev.6 cache-graphql /home/runner/work/faststore/faststore/packages/core
23
23
  > node ../cli/bin/run cache-graphql --config=./discovery.config.default.js --queries=./@generated/persisted-documents.json
24
24
 
25
25
  [Info] - Config file location: /home/runner/work/faststore/faststore/packages/core/discovery.config.default.js
@@ -1,11 +1,11 @@
1
1
 
2
- > @faststore/core@4.5.0-dev.4 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.5.0-dev.6 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.0.7 /home/runner/work/faststore/faststore/packages/core
7
7
 
8
- ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 38ms
8
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 46ms
9
9
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
10
10
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=8199d03debb0a6751ec53d8ee8987b78d0292acb
11
11
   at node:internal/deps/undici/undici:16416:13
@@ -32,13 +32,15 @@
32
32
  }
33
33
  }
34
34
 
35
+ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
36
+ npx update-browserslist-db@latest
37
+ Why you should do it regularly: https://github.com/browserslist/update-db#readme
35
38
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
36
39
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
37
40
 
38
- ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (8 tests) 276ms
39
- ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2269ms
40
- ✓ shows an error state when the list fails to load  428ms
41
- ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  420ms
41
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (8 tests) 237ms
42
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 1804ms
43
+ ✓ shows an error state when the list fails to load  571ms
42
44
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > posts the password and redirects to the requested return path
43
45
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
44
46
 
@@ -268,10 +270,12 @@ act(() => {
268
270
  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
269
271
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
270
272
 
271
- ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 43ms
272
- ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1568ms
273
- ✓ renders the password protection form  617ms
274
- ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 52ms
273
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1941ms
274
+ ✓ renders the password protection form  822ms
275
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 25ms
276
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 59ms
277
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1651ms
278
+ ✓ renders the menu view with contract details and manage footer  974ms
275
279
  stderr | test/sdk/localization/store-url.browser.test.ts
276
280
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
277
281
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:121:27)
@@ -279,12 +283,9 @@ This ensures that you're testing the behavior the user would see in the browser.
279
283
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
280
284
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
281
285
 
282
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 17ms
283
- ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 2247ms
284
- ✓ renders the menu view with contract details and manage footer  1249ms
285
- ✓ opens the contract switcher and returns to the menu on back  505ms
286
- ✓ resets to the menu when the drawer closes  303ms
287
- ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 85ms
286
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 36ms
287
+ ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 91ms
288
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 53ms
288
289
  stderr | test/components/account/doLogout.browser.test.ts
289
290
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
290
291
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:121:27)
@@ -292,9 +293,8 @@ This ensures that you're testing the behavior the user would see in the browser.
292
293
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
293
294
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
294
295
 
295
- ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 47ms
296
- ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 45ms
297
- ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 263ms
296
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 24ms
297
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 249ms
298
298
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
299
299
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
300
300
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:121:27)
@@ -302,12 +302,12 @@ This ensures that you're testing the behavior the user would see in the browser.
302
302
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
303
303
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
304
304
 
305
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 248ms
305
306
  ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 8ms
306
- ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 243ms
307
- ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 5ms
308
- ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 19ms
307
+ ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 4ms
308
+ ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 23ms
309
309
  ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 3ms
310
- ✓  node  test/server/password-protection-service.test.ts (22 tests) 31ms
310
+ ✓  node  test/server/password-protection-service.test.ts (22 tests) 30ms
311
311
  stderr | test/sdk/account/refreshToken.browser.test.ts
312
312
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
313
313
  at validateCart (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/index.ts:121:27)
@@ -315,11 +315,11 @@ This ensures that you're testing the behavior the user would see in the browser.
315
315
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
316
316
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
317
317
 
318
- ✓  node  test/utils/match-url.test.ts (10 tests) 9ms
319
- ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 5ms
320
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 14ms
321
- ✓  node  test/pages/api/unlock.test.ts (15 tests) 223ms
322
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 10ms
318
+ ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 6ms
319
+ ✓  node  test/utils/match-url.test.ts (10 tests) 10ms
320
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 10ms
321
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 201ms
322
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 6ms
323
323
  stderr | test/components/search/SearchInputComponent.test.tsx
324
324
  Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
325
325
 
@@ -383,22 +383,23 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
383
383
  at Suspense
384
384
  at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
385
385
 
386
- ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 82ms
387
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 14ms
386
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 73ms
387
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 13ms
388
388
  ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 51ms
389
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 22ms
390
- ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 356ms
389
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 9ms
390
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 27ms
391
391
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
392
392
  Set password request failed: Bad Request
393
393
 
394
- ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 46ms
395
- ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 25ms
396
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 5ms
397
- ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 5ms
398
- ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 840ms
399
- ✓ navigates with the search term on blur  362ms
400
- ✓ navigates without a label param when the search is cleared  363ms
401
- ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 46ms
394
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 48ms
395
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 354ms
396
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 32ms
397
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 4ms
398
+ ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 4ms
399
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 833ms
400
+ ✓ navigates with the search term on blur  363ms
401
+ ✓ navigates without a label param when the search is cleared  356ms
402
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 43ms
402
403
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
403
404
  Graphql execution returned with error: [
404
405
  {
@@ -446,45 +447,47 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
446
447
  ]
447
448
 
448
449
  ✓  node  test/pages/api/graphql.test.ts (10 tests) 24ms
449
- ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 53ms
450
- ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 63ms
451
- ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 151ms
452
- ✓  node  test/proxy.localization.test.ts (5 tests) 45ms
453
- ✓  node  test/server/index.test.ts (7 tests) 803ms
454
- ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 48ms
450
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 52ms
451
+ ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 58ms
452
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 145ms
453
+ ✓  node  test/proxy.localization.test.ts (5 tests) 73ms
454
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (10 tests) 5ms
455
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 37ms
456
+ ✓  node  test/server/index.test.ts (7 tests) 1241ms
457
+ ✓ should handle options and execute  743ms
455
458
  ✓  node  test/proxy.test.ts (4 tests) 25ms
456
- ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 39ms
457
- ✓  node  test/server/webops-api.test.ts (9 tests) 26ms
458
- ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 121ms
459
- ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 6ms
460
- ✓  node  test/server/cms/global.test.ts (3 tests) 6ms
461
- ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (8 tests) 7ms
462
- ✓  node  test/server/content/service.test.ts (5 tests) 6ms
463
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 5ms
459
+ ✓  node  test/server/webops-api.test.ts (9 tests) 22ms
460
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 72ms
461
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 7ms
462
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 37ms
463
+ ✓  node  test/server/cms/global.test.ts (3 tests) 3ms
464
+ ✓  node  test/server/content/service.test.ts (5 tests) 5ms
465
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 4ms
464
466
  ✓  node  test/sdk/analytics/hasEnabledRecommendationShelf.test.ts (6 tests) 4ms
465
467
  ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 6ms
466
- ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 37ms
467
- ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 168ms
468
- ✓  node  test/pages/api/preview.test.ts (2 tests) 7ms
469
- ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 5ms
468
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 29ms
469
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 135ms
470
+ ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 3ms
471
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 16ms
470
472
  ✓  node  test/components/search/SearchInput.test.ts (6 tests) 8ms
471
- ✓  node  test/components/RecommendationShelf/vrn.test.ts (5 tests) 6ms
472
- ✓  node  test/server/cms/index.test.ts (2 tests) 15ms
473
- ✓  node  test/utils/retry.test.ts (5 tests) 12ms
474
- ✓  node  test/components/account/Layout.test.tsx (4 tests) 41ms
473
+ ✓  node  test/components/RecommendationShelf/vrn.test.ts (5 tests) 5ms
474
+ ✓  node  test/server/cms/index.test.ts (2 tests) 8ms
475
+ ✓  node  test/utils/retry.test.ts (5 tests) 13ms
476
+ ✓  node  test/components/account/Layout.test.tsx (4 tests) 39ms
475
477
  ✓  node  test/components/RecommendationShelf/useRecommendationUserId.test.ts (4 tests) 190ms
476
- ✓  node  test/utils/unlockResponse.test.ts (11 tests) 6ms
478
+ ✓  node  test/utils/unlockResponse.test.ts (11 tests) 5ms
479
+ ✓  node  test/server/cms/myAccountDefaultSections.test.ts (5 tests) 4ms
477
480
  ✓  node  test/components/RecommendationShelf/useRecommendations.test.ts (2 tests) 5ms
478
- ✓  node  test/utils/isLocalHost.test.ts (7 tests) 5ms
481
+ ✓  node  test/utils/isLocalHost.test.ts (7 tests) 6ms
479
482
  ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 4ms
480
- ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 54ms
481
- ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 4ms
482
- ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 37ms
483
- ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 14ms
484
- ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 25ms
485
-
486
-  Test Files  71 passed (71)
487
-  Tests  558 passed (558)
488
-  Start at  17:14:38
489
-  Duration  36.96s (transform 9.22s, setup 0ms, collect 28.92s, tests 11.09s, environment 50.83s, prepare 1.28s)
483
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 40ms
484
+ ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 3ms
485
+ ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 26ms
486
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 13ms
487
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 23ms
488
+
489
+  Test Files  73 passed (73)
490
+  Tests  570 passed (570)
491
+  Start at  21:42:33
492
+  Duration  33.01s (transform 7.79s, setup 0ms, collect 26.92s, tests 10.65s, environment 41.89s, prepare 1.09s)
490
493
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.5.0-dev.7](https://github.com/vtex/faststore/compare/v4.5.0-dev.6...v4.5.0-dev.7) (2026-08-03)
7
+
8
+ ### Features
9
+
10
+ - CMS-driven custom My Account pages ([#3411](https://github.com/vtex/faststore/issues/3411)) ([e38b278](https://github.com/vtex/faststore/commit/e38b2780bb7386a8ef7b83a9e3de5956bf6de50e))
11
+
12
+ # [4.5.0-dev.6](https://github.com/vtex/faststore/compare/v4.5.0-dev.5...v4.5.0-dev.6) (2026-07-28)
13
+
14
+ ### Bug Fixes
15
+
16
+ - prevent My Account 500s from empty session profile and missing CMS content ([#3417](https://github.com/vtex/faststore/issues/3417)) ([2c2a18f](https://github.com/vtex/faststore/commit/2c2a18fe8f4127404d6634933cce778a64169c08))
17
+
6
18
  # [4.5.0-dev.5](https://github.com/vtex/faststore/compare/v4.5.0-dev.4...v4.5.0-dev.5) (2026-07-27)
7
19
 
8
20
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.5.0-dev.5",
3
+ "version": "4.5.0-dev.7",
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.5.0-dev.5",
75
- "@faststore/diagnostics": "4.5.0-dev.5",
76
- "@faststore/sdk": "4.5.0-dev.5",
77
- "@faststore/lighthouse": "4.5.0-dev.5",
78
- "@faststore/ui": "4.5.0-dev.5"
74
+ "@faststore/lighthouse": "4.5.0-dev.7",
75
+ "@faststore/diagnostics": "4.5.0-dev.7",
76
+ "@faststore/api": "4.5.0-dev.7",
77
+ "@faststore/ui": "4.5.0-dev.7",
78
+ "@faststore/sdk": "4.5.0-dev.7"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@cypress/code-coverage": "^3.12.1",
@@ -0,0 +1,146 @@
1
+ import { gql } from '@generated/gql'
2
+ import type {
3
+ ServerAccountPageQueryQuery,
4
+ ServerAccountPageQueryQueryVariables,
5
+ } from '@generated/graphql'
6
+ import type { Locator } from '@vtex/client-cms'
7
+ import type { GetServerSideProps } from 'next'
8
+
9
+ import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
10
+ import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
11
+ import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
12
+ import { validateUser } from 'src/sdk/account/validateUser'
13
+ import { execute } from 'src/server'
14
+ import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
15
+ import { injectGlobalSections } from 'src/server/cms/global'
16
+ import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
17
+ import { localizeRedirectDestination } from 'src/utils/localization/localizeRedirectDestination'
18
+ import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
19
+ import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
20
+ import storeConfig from '../../discovery.config'
21
+ import type { MyAccountProps } from './myAccountServerSideProps'
22
+
23
+ export type MyAccountCmsPageProps = MyAccountProps & {
24
+ pageSections: Array<{
25
+ name: string
26
+ data: Record<string, unknown>
27
+ $componentKey?: string
28
+ }>
29
+ navigationLabels: AccountNavigationLabels
30
+ }
31
+
32
+ const query = gql(`
33
+ query ServerAccountPageQuery {
34
+ accountProfile {
35
+ name
36
+ }
37
+ }
38
+ `)
39
+
40
+ /**
41
+ * Reusable SSR factory for CMS-driven custom My Account pages.
42
+ * Keeps the same auth/redirect guards as `myAccountServerSideProps`.
43
+ */
44
+ export function myAccountCmsServerSideProps(
45
+ contentType: string,
46
+ pagePath: string
47
+ ): GetServerSideProps<MyAccountCmsPageProps, Record<string, string>, Locator> {
48
+ const handler: GetServerSideProps<
49
+ MyAccountCmsPageProps,
50
+ Record<string, string>,
51
+ Locator
52
+ > = async (context) => {
53
+ const contentContext = {
54
+ previewData: context.previewData,
55
+ locale: context.locale,
56
+ }
57
+
58
+ const validationResult = await validateUser(context)
59
+
60
+ if (!validationResult.isValid && !validationResult.needsRefresh) {
61
+ return {
62
+ redirect: {
63
+ destination: localizeRedirectDestination('/login', context),
64
+ permanent: false,
65
+ },
66
+ }
67
+ }
68
+
69
+ if (!validationResult.isValid && validationResult.needsRefresh) {
70
+ const currentPath = context.req.url || '/pvt/account'
71
+ return {
72
+ redirect: {
73
+ destination: localizeRedirectDestination(
74
+ `/pvt/account/403?from=${encodeURIComponent(currentPath)}`,
75
+ context
76
+ ),
77
+ permanent: false,
78
+ },
79
+ }
80
+ }
81
+
82
+ const { isFaststoreMyAccountEnabled, redirect } = getMyAccountRedirect({
83
+ query: context.query,
84
+ })
85
+
86
+ if (!isFaststoreMyAccountEnabled) {
87
+ return { redirect }
88
+ }
89
+
90
+ const isRepresentative = getIsRepresentative({
91
+ headers: context.req.headers as Record<string, string>,
92
+ account: storeConfig.api.storeId,
93
+ })
94
+
95
+ const [
96
+ globalSectionsPromise,
97
+ globalSectionsHeaderPromise,
98
+ globalSectionsFooterPromise,
99
+ ] = getGlobalSectionsData(contentContext)
100
+
101
+ const [
102
+ pageContent,
103
+ account,
104
+ globalSections,
105
+ globalSectionsHeader,
106
+ globalSectionsFooter,
107
+ ] = await Promise.all([
108
+ fetchMyAccountPageContent(contentType, contentContext, pagePath),
109
+ execute<
110
+ ServerAccountPageQueryQueryVariables,
111
+ ServerAccountPageQueryQuery
112
+ >(
113
+ {
114
+ variables: {},
115
+ operation: query,
116
+ },
117
+ { headers: { ...context.req.headers } }
118
+ ),
119
+ globalSectionsPromise,
120
+ globalSectionsHeaderPromise,
121
+ globalSectionsFooterPromise,
122
+ ])
123
+
124
+ const { pageSections, navigationData } = extractAccountNavigationData(
125
+ pageContent.sections
126
+ )
127
+
128
+ const globalSectionsResult = injectGlobalSections({
129
+ globalSections,
130
+ globalSectionsHeader,
131
+ globalSectionsFooter,
132
+ })
133
+
134
+ return {
135
+ props: {
136
+ globalSections: globalSectionsResult,
137
+ accountName: account.data?.accountProfile?.name ?? '',
138
+ isRepresentative,
139
+ pageSections,
140
+ navigationLabels: navigationData as AccountNavigationLabels,
141
+ },
142
+ }
143
+ }
144
+
145
+ return withLocaleValidationSSR(handler)
146
+ }
@@ -114,7 +114,7 @@ const getServerSidePropsBase: GetServerSideProps<
114
114
  return {
115
115
  props: {
116
116
  globalSections: globalSectionsResult,
117
- accountName: account.data.accountProfile.name,
117
+ accountName: account.data?.accountProfile?.name ?? '',
118
118
  isRepresentative,
119
119
  },
120
120
  }
@@ -147,9 +147,9 @@ const getServerSidePropsBase: GetServerSideProps<
147
147
 
148
148
  return {
149
149
  props: {
150
- page,
150
+ page: page ?? ({ sections: [], settings: {} } as PageContentType),
151
151
  globalSections: globalSectionsResult,
152
- accountName: account.data.accountProfile.name,
152
+ accountName: account.data?.accountProfile?.name ?? '',
153
153
  },
154
154
  }
155
155
  }
@@ -3,6 +3,12 @@ export interface Route {
3
3
  title: string
4
4
  /* Accessible path within My Account */
5
5
  route: string
6
+ /**
7
+ * When set, the page body renders from this CMS content-type.
8
+ * Presence of this field is the single opt-in for a CMS-driven page.
9
+ * Pure CMS vs hybrid is derived by the CLI from whether a code body exists.
10
+ */
11
+ contentType?: string
6
12
  }
7
13
 
8
14
  export type AccountNavigationLabels = Partial<{
@@ -16,22 +16,23 @@ export async function fetchMyAccountPageContent(
16
16
  context: FetchMyAccountPageContentContext,
17
17
  pagePath: string
18
18
  ) {
19
- const pageContent = await contentService
20
- .getSingleContent<PageContentType>({
21
- contentType,
22
- previewData: context.previewData,
23
- locale: context.locale,
24
- })
25
- .catch((err) => {
26
- console.warn('[MyAccount CMS fallback]', {
19
+ const pageContent =
20
+ (await contentService
21
+ .getSingleContent<PageContentType>({
27
22
  contentType,
23
+ previewData: context.previewData,
28
24
  locale: context.locale,
29
- fallbackReason: err instanceof Error ? err.message : String(err),
30
- page: pagePath,
31
25
  })
26
+ .catch((err) => {
27
+ console.warn('[MyAccount CMS fallback]', {
28
+ contentType,
29
+ locale: context.locale,
30
+ fallbackReason: err instanceof Error ? err.message : String(err),
31
+ page: pagePath,
32
+ })
32
33
 
33
- return { sections: [], settings: {} } as PageContentType
34
- })
34
+ return { sections: [], settings: {} } as PageContentType
35
+ })) ?? ({ sections: [], settings: {} } as PageContentType)
35
36
 
36
37
  const sections = withDefaultMyAccountSections(
37
38
  contentType,
@@ -1,4 +1,4 @@
1
- export type MyAccountContentType =
1
+ export type NativeMyAccountContentType =
2
2
  | 'myAccountProfile'
3
3
  | 'myAccountOrders'
4
4
  | 'myAccountOrderDetails'
@@ -6,13 +6,16 @@ export type MyAccountContentType =
6
6
  | 'myAccountSecurity'
7
7
  | 'myAccountUnauthorized'
8
8
 
9
+ /** Store content-types are opaque strings; native literals keep autocompletion. */
10
+ export type MyAccountContentType = NativeMyAccountContentType | (string & {})
11
+
9
12
  export type DefaultMyAccountSection = {
10
13
  name: string
11
14
  $componentKey: string
12
15
  data: Record<string, unknown>
13
16
  }
14
17
 
15
- const DEFAULT_SECTION_KEYS: Record<MyAccountContentType, string[]> = {
18
+ const DEFAULT_SECTION_KEYS: Record<NativeMyAccountContentType, string[]> = {
16
19
  myAccountProfile: ['AccountNavigation', 'AccountProfile'],
17
20
  myAccountOrders: ['AccountNavigation', 'AccountOrdersList'],
18
21
  myAccountOrderDetails: [
@@ -34,7 +37,9 @@ const DEFAULT_SECTION_KEYS: Record<MyAccountContentType, string[]> = {
34
37
  export function getDefaultMyAccountSections(
35
38
  contentType: MyAccountContentType
36
39
  ): DefaultMyAccountSection[] {
37
- return (DEFAULT_SECTION_KEYS[contentType] ?? []).map((key) => ({
40
+ return (
41
+ DEFAULT_SECTION_KEYS[contentType as NativeMyAccountContentType] ?? []
42
+ ).map((key) => ({
38
43
  name: key,
39
44
  $componentKey: key,
40
45
  data: {},
@@ -1,6 +1,16 @@
1
1
  import storeConfig from 'discovery.config'
2
2
  import type { ParsedUrlQuery } from 'node:querystring'
3
3
 
4
+ type MyAccountRedirect = { destination: string; permanent: boolean }
5
+
6
+ /**
7
+ * Discriminated union so callers that guard on `isFaststoreMyAccountEnabled`
8
+ * get a non-nullable `redirect`.
9
+ */
10
+ export type MyAccountRedirectResult =
11
+ | { isFaststoreMyAccountEnabled: false; redirect: MyAccountRedirect }
12
+ | { isFaststoreMyAccountEnabled: true; redirect: null }
13
+
4
14
  /**
5
15
  * Check if the Faststore My Account feature flag is enabled.
6
16
  * If not, redirect to the legacy My Account URL with the query parameters.
@@ -8,10 +18,9 @@ import type { ParsedUrlQuery } from 'node:querystring'
8
18
  * @param {ParsedUrlQuery} query - The query parameters from the request.
9
19
  * @returns {Object} - An object containing the feature flag status and redirect information.
10
20
  */
11
- export function getMyAccountRedirect({ query }: { query: ParsedUrlQuery }): {
12
- isFaststoreMyAccountEnabled: boolean
13
- redirect: { destination: string; permanent: boolean } | null
14
- } {
21
+ export function getMyAccountRedirect({
22
+ query,
23
+ }: { query: ParsedUrlQuery }): MyAccountRedirectResult {
15
24
  const isFaststoreMyAccountEnabled =
16
25
  storeConfig.experimental?.enableFaststoreMyAccount
17
26
 
@@ -29,8 +38,8 @@ export function getMyAccountRedirect({ query }: { query: ParsedUrlQuery }): {
29
38
  permanent: false,
30
39
  }
31
40
 
32
- return { isFaststoreMyAccountEnabled, redirect }
41
+ return { isFaststoreMyAccountEnabled: false, redirect }
33
42
  }
34
43
 
35
- return { isFaststoreMyAccountEnabled, redirect: null }
44
+ return { isFaststoreMyAccountEnabled: true, redirect: null }
36
45
  }
@@ -0,0 +1,221 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const mockValidateUser = vi.fn()
4
+ const mockGetMyAccountRedirect = vi.fn()
5
+ const mockGetIsRepresentative = vi.fn()
6
+ const mockFetchMyAccountPageContent = vi.fn()
7
+ const mockGetGlobalSectionsData = vi.fn()
8
+ const mockExecute = vi.fn()
9
+ const mockInjectGlobalSections = vi.fn()
10
+ const mockLocalizeRedirectDestination = vi.fn(
11
+ (destination: string) => destination
12
+ )
13
+
14
+ vi.mock('src/sdk/account/validateUser', () => ({
15
+ validateUser: (...args: unknown[]) => mockValidateUser(...args),
16
+ }))
17
+
18
+ vi.mock('src/utils/myAccountRedirect', () => ({
19
+ getMyAccountRedirect: (...args: unknown[]) =>
20
+ mockGetMyAccountRedirect(...args),
21
+ }))
22
+
23
+ vi.mock('src/sdk/account/getIsRepresentative', () => ({
24
+ getIsRepresentative: (...args: unknown[]) => mockGetIsRepresentative(...args),
25
+ }))
26
+
27
+ vi.mock('src/server/cms/fetchMyAccountPageContent', () => ({
28
+ fetchMyAccountPageContent: (...args: unknown[]) =>
29
+ mockFetchMyAccountPageContent(...args),
30
+ }))
31
+
32
+ vi.mock('src/components/cms/GlobalSections', () => ({
33
+ getGlobalSectionsData: (...args: unknown[]) =>
34
+ mockGetGlobalSectionsData(...args),
35
+ }))
36
+
37
+ vi.mock('src/server', () => ({
38
+ execute: (...args: unknown[]) => mockExecute(...args),
39
+ }))
40
+
41
+ vi.mock('src/server/cms/global', () => ({
42
+ injectGlobalSections: (...args: unknown[]) =>
43
+ mockInjectGlobalSections(...args),
44
+ }))
45
+
46
+ vi.mock('src/utils/localization/localizeRedirectDestination', () => ({
47
+ localizeRedirectDestination: (...args: unknown[]) =>
48
+ mockLocalizeRedirectDestination(...(args as [string])),
49
+ }))
50
+
51
+ vi.mock('src/utils/localization/withLocaleValidation', () => ({
52
+ withLocaleValidationSSR: <T>(fn: T) => fn,
53
+ }))
54
+
55
+ vi.mock('../../../discovery.config', () => ({
56
+ default: { api: { storeId: 'teststore' } },
57
+ }))
58
+
59
+ vi.mock('@generated/gql', () => ({
60
+ gql: (source: string) => source,
61
+ }))
62
+
63
+ import { myAccountCmsServerSideProps } from '../../../src/experimental/myAccountCmsServerSideProps'
64
+
65
+ function buildContext(overrides: Record<string, unknown> = {}) {
66
+ return {
67
+ previewData: undefined,
68
+ locale: 'en-US',
69
+ query: {},
70
+ req: {
71
+ url: '/pvt/account/wishlist',
72
+ headers: {},
73
+ },
74
+ ...overrides,
75
+ } as never
76
+ }
77
+
78
+ describe('myAccountCmsServerSideProps', () => {
79
+ beforeEach(() => {
80
+ vi.clearAllMocks()
81
+ mockGetIsRepresentative.mockReturnValue(false)
82
+ mockGetGlobalSectionsData.mockReturnValue([
83
+ Promise.resolve({ sections: [], settings: {} }),
84
+ Promise.resolve({ sections: [] }),
85
+ Promise.resolve({ sections: [] }),
86
+ ])
87
+ mockInjectGlobalSections.mockReturnValue({
88
+ sections: [{ name: 'Children', data: {} }],
89
+ settings: {},
90
+ })
91
+ mockExecute.mockResolvedValue({
92
+ data: { accountProfile: { name: 'Ada' } },
93
+ errors: undefined,
94
+ })
95
+ })
96
+
97
+ it('redirects to login when the user is invalid and does not need refresh', async () => {
98
+ mockValidateUser.mockResolvedValue({
99
+ isValid: false,
100
+ needsRefresh: false,
101
+ })
102
+
103
+ const result = await myAccountCmsServerSideProps(
104
+ 'myAccountWishlist',
105
+ '/pvt/account/wishlist'
106
+ )(buildContext())
107
+
108
+ expect(result).toEqual({
109
+ redirect: { destination: '/login', permanent: false },
110
+ })
111
+ })
112
+
113
+ it('redirects to 403 when the user is invalid and needs refresh', async () => {
114
+ mockValidateUser.mockResolvedValue({
115
+ isValid: false,
116
+ needsRefresh: true,
117
+ })
118
+
119
+ const result = await myAccountCmsServerSideProps(
120
+ 'myAccountWishlist',
121
+ '/pvt/account/wishlist'
122
+ )(buildContext())
123
+
124
+ expect(result).toEqual({
125
+ redirect: {
126
+ destination: `/pvt/account/403?from=${encodeURIComponent('/pvt/account/wishlist')}`,
127
+ permanent: false,
128
+ },
129
+ })
130
+ })
131
+
132
+ it('returns the My Account disabled redirect', async () => {
133
+ mockValidateUser.mockResolvedValue({ isValid: true })
134
+ mockGetMyAccountRedirect.mockReturnValue({
135
+ isFaststoreMyAccountEnabled: false,
136
+ redirect: { destination: '/login', permanent: false },
137
+ })
138
+
139
+ const result = await myAccountCmsServerSideProps(
140
+ 'myAccountWishlist',
141
+ '/pvt/account/wishlist'
142
+ )(buildContext())
143
+
144
+ expect(result).toEqual({
145
+ redirect: { destination: '/login', permanent: false },
146
+ })
147
+ })
148
+
149
+ it('returns pageSections, navigationLabels and accountName on success', async () => {
150
+ mockValidateUser.mockResolvedValue({ isValid: true })
151
+ mockGetMyAccountRedirect.mockReturnValue({
152
+ isFaststoreMyAccountEnabled: true,
153
+ redirect: { destination: '/login', permanent: false },
154
+ })
155
+ mockFetchMyAccountPageContent.mockResolvedValue({
156
+ sections: [
157
+ {
158
+ name: 'AccountNavigation',
159
+ $componentKey: 'AccountNavigation',
160
+ data: { profileLabel: 'Profile' },
161
+ },
162
+ {
163
+ name: 'HelloAccount',
164
+ $componentKey: 'HelloAccount',
165
+ data: { label: 'Hi' },
166
+ },
167
+ ],
168
+ settings: {},
169
+ })
170
+
171
+ const result = await myAccountCmsServerSideProps(
172
+ 'myAccountWishlist',
173
+ '/pvt/account/wishlist'
174
+ )(buildContext())
175
+
176
+ expect(result).toMatchObject({
177
+ props: {
178
+ accountName: 'Ada',
179
+ isRepresentative: false,
180
+ navigationLabels: { profileLabel: 'Profile' },
181
+ pageSections: [
182
+ {
183
+ name: 'HelloAccount',
184
+ $componentKey: 'HelloAccount',
185
+ data: { label: 'Hi' },
186
+ },
187
+ ],
188
+ },
189
+ })
190
+ expect(mockFetchMyAccountPageContent).toHaveBeenCalledWith(
191
+ 'myAccountWishlist',
192
+ { previewData: undefined, locale: 'en-US' },
193
+ '/pvt/account/wishlist'
194
+ )
195
+ })
196
+
197
+ it('returns empty pageSections when the content-type is unpublished', async () => {
198
+ mockValidateUser.mockResolvedValue({ isValid: true })
199
+ mockGetMyAccountRedirect.mockReturnValue({
200
+ isFaststoreMyAccountEnabled: true,
201
+ redirect: { destination: '/login', permanent: false },
202
+ })
203
+ mockFetchMyAccountPageContent.mockResolvedValue({
204
+ sections: [],
205
+ settings: {},
206
+ })
207
+
208
+ const result = await myAccountCmsServerSideProps(
209
+ 'myAccountWishlist',
210
+ '/pvt/account/wishlist'
211
+ )(buildContext())
212
+
213
+ expect(result).toMatchObject({
214
+ props: {
215
+ pageSections: [],
216
+ navigationLabels: {},
217
+ accountName: 'Ada',
218
+ },
219
+ })
220
+ })
221
+ })
@@ -73,6 +73,45 @@ describe('getMyAccountRoutes', () => {
73
73
  expect(result.find((r) => r.route === QUOTES_ROUTE)?.title).toBe('Quotes')
74
74
  })
75
75
 
76
+ it('preserves contentType through getMyAccountRoutes', () => {
77
+ const result = getMyAccountRoutes({
78
+ routes: [
79
+ {
80
+ title: 'Wishlist',
81
+ route: '/pvt/account/wishlist',
82
+ contentType: 'myAccountWishlist',
83
+ },
84
+ ],
85
+ })
86
+
87
+ const wishlist = result.find((r) => r.route === '/pvt/account/wishlist')
88
+ expect(wishlist?.contentType).toBe('myAccountWishlist')
89
+ expect(wishlist?.title).toBe('Wishlist')
90
+ })
91
+
92
+ it('keeps custom-route titles when CMS labels override native routes (FR-011)', () => {
93
+ const result = getMyAccountRoutes({
94
+ routes: [
95
+ {
96
+ title: 'Wishlist',
97
+ route: '/pvt/account/wishlist',
98
+ contentType: 'myAccountWishlist',
99
+ },
100
+ ],
101
+ labels: { profileLabel: 'Meu Perfil', ordersLabel: 'Meus Pedidos' },
102
+ })
103
+
104
+ expect(result.find((r) => r.route === PROFILE_ROUTE)?.title).toBe(
105
+ 'Meu Perfil'
106
+ )
107
+ expect(result.find((r) => r.route === ORDERS_ROUTE)?.title).toBe(
108
+ 'Meus Pedidos'
109
+ )
110
+ expect(result.find((r) => r.route === '/pvt/account/wishlist')?.title).toBe(
111
+ 'Wishlist'
112
+ )
113
+ })
114
+
76
115
  it('marks Quotes as a B2B-only route', () => {
77
116
  expect(ROUTES_ONLY_FOR_B2B_MEMBERS).toContain(QUOTES_ROUTE)
78
117
  })
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ getDefaultMyAccountSections,
5
+ withDefaultMyAccountSections,
6
+ } from '../../../src/server/cms/myAccountDefaultSections'
7
+
8
+ describe('getDefaultMyAccountSections', () => {
9
+ it('returns the native section list for myAccountProfile', () => {
10
+ expect(getDefaultMyAccountSections('myAccountProfile')).toEqual([
11
+ {
12
+ name: 'AccountNavigation',
13
+ $componentKey: 'AccountNavigation',
14
+ data: {},
15
+ },
16
+ { name: 'AccountProfile', $componentKey: 'AccountProfile', data: {} },
17
+ ])
18
+ })
19
+
20
+ it('returns an empty list for store (unknown) content-types', () => {
21
+ expect(getDefaultMyAccountSections('myAccountWishlist')).toEqual([])
22
+ })
23
+ })
24
+
25
+ describe('withDefaultMyAccountSections', () => {
26
+ it('returns published sections when present', () => {
27
+ const sections = [
28
+ {
29
+ name: 'HelloAccount',
30
+ $componentKey: 'HelloAccount',
31
+ data: { label: 'Hi' },
32
+ },
33
+ ]
34
+
35
+ expect(withDefaultMyAccountSections('myAccountWishlist', sections)).toEqual(
36
+ sections
37
+ )
38
+ })
39
+
40
+ it('falls back to defaults for native types when sections are empty', () => {
41
+ expect(withDefaultMyAccountSections('myAccountSecurity', [])).toEqual(
42
+ getDefaultMyAccountSections('myAccountSecurity')
43
+ )
44
+ })
45
+
46
+ it('falls back to [] for store types when sections are empty', () => {
47
+ expect(withDefaultMyAccountSections('myAccountWishlist', null)).toEqual([])
48
+ })
49
+ })