@faststore/core 4.6.0-dev.2 → 4.6.0-dev.3

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 (22) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +80 -77
  3. package/@generated/cached-operations.json +1 -1
  4. package/@generated/gql.ts +6 -6
  5. package/@generated/graphql.ts +10 -10
  6. package/@generated/persisted-documents.json +1 -1
  7. package/CHANGELOG.md +4 -0
  8. package/package.json +6 -6
  9. package/src/components/sections/RecommendationShelf/README.md +4 -1
  10. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +11 -92
  11. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +3 -18
  12. package/src/sdk/recommendations/index.ts +17 -0
  13. package/src/sdk/recommendations/types.ts +17 -0
  14. package/src/sdk/recommendations/useRecommendationShelf.ts +141 -0
  15. package/src/{components/sections/RecommendationShelf → sdk/recommendations}/vrn.ts +1 -1
  16. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +6 -12
  17. package/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx +199 -0
  18. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.test.ts +1 -1
  19. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendations.test.ts +1 -1
  20. package/test/{components/RecommendationShelf → sdk/recommendations}/vrn.test.ts +1 -4
  21. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.ts +0 -0
  22. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendations.ts +0 -0
@@ -1,9 +1,9 @@
1
1
 
2
- > @faststore/core@4.6.0-dev.1 generate /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.6.0-dev.2 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.6.0-dev.1 gen-types /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@4.6.0-dev.2 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,11 +19,11 @@
19
19
  [COMPLETED] Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
20
20
  [COMPLETED] Generate outputs
21
21
 
22
- > @faststore/core@4.6.0-dev.1 cache-graphql /home/runner/work/faststore/faststore/packages/core
22
+ > @faststore/core@4.6.0-dev.2 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
26
26
  [Info] - Persisted documents at: /home/runner/work/faststore/faststore/packages/core/@generated/persisted-documents.json
27
27
  [Info] - Cached operations output: /home/runner/work/faststore/faststore/packages/core/@generated/cached-operations.json
28
28
  [Success] - GraphQL queries cached with success: 🎉
29
- Queries: ClientRecommendationsQuery, ServerAccountPageQuery, ServerCollectionPageQuery, ServerProductQuery, ServerOrderDetailsQuery, ServerListOrdersQuery, ServerProfileQuery, ServerListQuotesQuery, ServerQuotesOrganizationMemberQuery, ServerSecurity, ServerUserDetailsQuery, AvailableContractsQuery, ValidateUser, ClientPickupPointsQuery, OrderEntryOperationQuery, OrderFormItemsQuery, ClientProductCountQuery, ClientAllVariantProductsQuery, ClientManyProductsQueryWithSearchId, ClientProductGalleryQuery, ClientProductQuery, ClientManyProductsQuery, ClientManyProductsSelectedQuery, ClientProfileQuery, ClientSearchSuggestionsQuery, ClientTopSearchSuggestionsQuery, ClientShippingSimulationQuery, ServerManyProductsQuery
29
+ Queries: ServerAccountPageQuery, ServerCollectionPageQuery, ServerProductQuery, ServerOrderDetailsQuery, ServerListOrdersQuery, ServerProfileQuery, ServerListQuotesQuery, ServerQuotesOrganizationMemberQuery, ServerSecurity, ServerUserDetailsQuery, AvailableContractsQuery, ValidateUser, ClientPickupPointsQuery, OrderEntryOperationQuery, OrderFormItemsQuery, ClientProductCountQuery, ClientAllVariantProductsQuery, ClientManyProductsQueryWithSearchId, ClientProductGalleryQuery, ClientProductQuery, ClientManyProductsQuery, ClientManyProductsSelectedQuery, ClientProfileQuery, ClientRecommendationsQuery, ClientSearchSuggestionsQuery, ClientTopSearchSuggestionsQuery, ClientShippingSimulationQuery, ServerManyProductsQuery
@@ -1,13 +1,18 @@
1
1
 
2
- > @faststore/core@4.6.0-dev.1 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@4.6.0-dev.2 test /home/runner/work/faststore/faststore/packages/core
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.0.18 /home/runner/work/faststore/faststore/packages/core
7
7
 
8
+ ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 310ms
8
9
  Not implemented: navigation to another Document
9
10
  Not implemented: navigation to another Document
10
- ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 763ms
11
+ ✓  browser  test/sdk/ui/useScrollRestoration.browser.test.ts (43 tests) 881ms
12
+ ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 2268ms
13
+ ✓ shows an error state when the list fails to load  726ms
14
+ ✓ requires selecting a contract before Confirm is enabled, then switches (REQ-03)  347ms
15
+ ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 27ms
11
16
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx
12
17
  Error in optimistic validation: TypeError: Failed to parse URL from /api/graphql?operationName=ValidateCartMutation&operationHash=3c71b0d3fc2bade14df68abfa271858beb361b12
13
18
   at node:internal/deps/undici/undici:16416:13
@@ -37,14 +42,11 @@ Not implemented: navigation to another Document
37
42
  Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
38
43
  npx update-browserslist-db@latest
39
44
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
40
- ✓  browser  test/components/account/ContractSwitcher.browser.test.tsx (13 tests) 1389ms
41
45
  stderr | test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx > RecommendationShelf > renders nothing when there is an error
42
46
  Error fetching recommendations undefined boom for campaign vrn:recommendations:acc:rec-top-items-v2:campaign-1
43
47
 
44
- ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (8 tests) 169ms
45
- ✓  browser  test/components/ui/ProductGallery/GalleryPageHeightLock.browser.test.tsx (8 tests) 112ms
46
- ✓  browser  test/sdk/session/reconcileSessionLocale.browser.test.ts (5 tests) 14ms
47
- ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 19ms
48
+ ✓  browser  test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx (8 tests) 244ms
49
+ ✓  browser  test/sdk/recommendations/useRecommendationShelf.browser.test.tsx (10 tests) 104ms
48
50
  stderr | test/pages/password-protection.browser.test.tsx > PasswordProtectionLogin > posts the password and redirects to the requested return path
49
51
  Warning: An update to PasswordProtectionLogin inside a test was not wrapped in act(...).
50
52
 
@@ -274,11 +276,12 @@ act(() => {
274
276
  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
275
277
  at PasswordProtectionLogin (/home/runner/work/faststore/faststore/packages/core/src/pages/password-protection/index.tsx:19:69)
276
278
 
277
- ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 1159ms
278
- ✓ renders the password protection form  515ms
279
- ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 31ms
280
- ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 1172ms
281
- ✓ renders the menu view with contract details and manage footer  744ms
279
+ ✓  browser  test/pages/password-protection.browser.test.tsx (6 tests) 844ms
280
+ ✓ renders the password protection form  351ms
281
+ ✓  browser  test/sdk/session/getInitialSession.browser.test.ts (6 tests) 13ms
282
+ ✓  browser  test/sdk/account/changeContractToken.browser.test.ts (9 tests) 26ms
283
+ ✓  browser  test/components/account/OrganizationDrawer.browser.test.tsx (5 tests) 919ms
284
+ ✓ renders the menu view with contract details and manage footer  455ms
282
285
  stderr | test/sdk/localization/store-url.browser.test.ts
283
286
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
284
287
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -287,10 +290,10 @@ This ensures that you're testing the behavior the user would see in the browser.
287
290
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
288
291
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
289
292
 
290
- ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 7ms
291
- ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 31ms
292
- ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 21ms
293
- ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 36ms
293
+ ✓  browser  test/sdk/localization/store-url.browser.test.ts (3 tests) 11ms
294
+ ✓  browser  test/sdk/account/useAvailableContracts.browser.test.ts (6 tests) 32ms
295
+ ✓  browser  test/components/templates/SearchPage/SearchWrapper.browser.test.tsx (3 tests) 27ms
296
+ ✓  browser  test/sdk/account/useSwitchContract.browser.test.ts (4 tests) 34ms
294
297
  stderr | test/components/account/doLogout.browser.test.ts
295
298
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
296
299
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -299,8 +302,8 @@ This ensures that you're testing the behavior the user would see in the browser.
299
302
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
300
303
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
301
304
 
302
- ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 14ms
303
- ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 186ms
305
+ ✓  browser  test/components/account/doLogout.browser.test.ts (1 test) 21ms
306
+ ✓  browser  test/components/account/OrganizationDrawerHeader.browser.test.tsx (3 tests) 191ms
304
307
  stderr | test/sdk/account/clearPersistedSessionState.browser.test.ts
305
308
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
306
309
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -309,13 +312,13 @@ This ensures that you're testing the behavior the user would see in the browser.
309
312
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
310
313
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
311
314
 
312
- ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 14ms
313
- ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 239ms
315
+ ✓  browser  test/sdk/account/clearPersistedSessionState.browser.test.ts (1 test) 4ms
316
+ ✓  browser  test/components/account/OrganizationDrawerFooter.browser.test.tsx (3 tests) 253ms
314
317
  ✓  browser  test/utils/isLocalHost.browser.test.ts (3 tests) 4ms
315
- ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 2ms
318
+ ✓  browser  test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts (1 test) 3ms
319
+ ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 31ms
316
320
  ✓  node  test/utils/localization/bindingPaths.test.ts (71 tests) 21ms
317
- ✓  browser  test/components/skeletons/ProductGridSkeleton.browser.test.tsx (2 tests) 29ms
318
- ✓  node  test/server/password-protection-service.test.ts (22 tests) 25ms
321
+ ✓  node  test/server/password-protection-service.test.ts (22 tests) 26ms
319
322
  stderr | test/sdk/account/refreshToken.browser.test.ts
320
323
  Error in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
321
324
  at waitForSessionValidated (/home/runner/work/faststore/faststore/packages/core/src/sdk/cart/waitForSessionValidated.ts:16:32)
@@ -324,11 +327,11 @@ This ensures that you're testing the behavior the user would see in the browser.
324
327
  at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:354:23)
325
328
   at processTicksAndRejections (node:internal/process/task_queues:103:5)
326
329
 
327
- ✓  node  test/utils/match-url.test.ts (10 tests) 5ms
330
+ ✓  node  test/utils/match-url.test.ts (10 tests) 8ms
328
331
  ✓  browser  test/sdk/account/refreshToken.browser.test.ts (1 test) 4ms
329
- ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 11ms
330
- ✓  node  test/pages/api/unlock.test.ts (15 tests) 118ms
331
- ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 8ms
332
+ ✓  node  test/sdk/localization/useBindingSelector.test.tsx (22 tests) 12ms
333
+ ✓  node  test/pages/api/unlock.test.ts (15 tests) 125ms
334
+ ✓  node  test/sdk/localization/bindingSelector.test.ts (18 tests) 7ms
332
335
  stderr | test/components/search/SearchInputComponent.test.tsx
333
336
  Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
334
337
 
@@ -392,11 +395,11 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
392
395
  at Suspense
393
396
  at Wrapper (/home/runner/work/faststore/faststore/packages/core/test/components/search/SearchInputComponent.test.tsx:106:20)
394
397
 
395
- ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 58ms
396
- ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 10ms
397
- ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 44ms
398
- ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 8ms
399
- ✓  node  test/utils/clearCookies.test.ts (20 tests) 19ms
398
+ ✓  node  test/components/search/SearchInputComponent.test.tsx (6 tests) 68ms
399
+ ✓  node  test/utils/cookieCacheBusting.test.ts (10 tests) 8ms
400
+ ✓  node  test/sdk/search/useSearchHistory.test.ts (13 tests) 51ms
401
+ ✓  node  test/experimental/myAccountCmsServerSideProps.test.ts (5 tests) 7ms
402
+ ✓  node  test/utils/clearCookies.test.ts (20 tests) 17ms
400
403
  stderr | test/pages/api/graphql.test.ts > /api/graphql error status propagation > propagates the upstream 400 from a wrapped BadRequestError instead of 500
401
404
  Graphql execution returned with error: [
402
405
  {
@@ -489,64 +492,64 @@ Warning: React does not recognize the `providerProps` prop on a DOM element. If
489
492
  at file:///home/runner/work/faststore/faststore/node_modules/.pnpm/@vitest+runner@4.0.18/node_modules/@vitest/runner/dist/index.js:1653:37
490
493
  at Traces.$ (file:///home/runner/work/faststore/faststore/node_modules/.pnpm/vitest@4.0.18_@opentelemetry+api@1.9.0_@types+node@20.19.13_jiti@2.6.1_jsdom@27.4.0_sas_13be941c57d55f08f06eeaf1c180ddfe/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27)
491
494
 
492
- ✓  node  test/pages/api/graphql.test.ts (17 tests) 24ms
493
- ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 323ms
495
+ ✓  node  test/pages/api/graphql.test.ts (17 tests) 30ms
496
+ ✓  node  test/components/quotes/MyAccountListQuotesTable.test.tsx (12 tests) 302ms
497
+ ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 28ms
494
498
  stderr | test/sdk/account/useSetPassword.test.ts > useSetPassword > maps wrongcredentials error responses to the existing message
495
499
  Set password request failed: Bad Request
496
500
 
497
- ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 20ms
498
- ✓  node  test/sdk/orderEntry/useOrderEntryOperation.test.ts (8 tests) 24ms
499
- ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 795ms
500
- ✓ navigates with the search term on blur  360ms
501
- ✓ navigates without a label param when the search is cleared  358ms
502
- ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 26ms
503
- ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 6ms
504
- ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 4ms
505
- ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 37ms
506
- ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 103ms
507
- ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 38ms
508
- ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 30ms
509
- ✓  node  test/proxy.localization.test.ts (5 tests) 28ms
510
- ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (10 tests) 5ms
501
+ ✓  node  test/sdk/account/useSetPassword.test.ts (6 tests) 31ms
502
+ ✓  node  test/components/quotes/MyAccountListQuotes.test.tsx (9 tests) 830ms
503
+ ✓ navigates with the search term on blur  361ms
504
+ ✓ navigates without a label param when the search is cleared  361ms
505
+ ✓  node  test/utils/multipleTemplates.test.ts (8 tests) 5ms
506
+ ✓  node  test/components/quotes/quoteFilters.test.ts (23 tests) 29ms
507
+ ✓  node  test/utils/localization/localizeRedirectDestination.test.ts (11 tests) 6ms
508
+ ✓  node  test/sdk/orderEntry/useOrderEntryUpload.test.ts (8 tests) 42ms
509
+ ✓  node  test/components/quotes/MyAccountQuotesFilterSlider.test.tsx (5 tests) 121ms
510
+ ✓  node  test/sdk/analytics/useStartRecommendationSession.test.ts (6 tests) 47ms
511
+ ✓  node  test/sdk/orderEntry/useOrderEntry.test.ts (8 tests) 28ms
512
+ ✓  node  test/proxy.localization.test.ts (5 tests) 23ms
513
+ ✓  node  test/sdk/account/getMyAccountRoutes.test.ts (10 tests) 6ms
511
514
  ✓  node  test/sdk/session/syncSalesChannelFromOrderForm.test.ts (6 tests) 5ms
512
- ✓  node  test/server/index.test.ts (7 tests) 946ms
513
- ✓ should handle options and execute  513ms
514
- ✓  node  test/proxy.test.ts (4 tests) 13ms
515
- ✓  node  test/server/webops-api.test.ts (9 tests) 13ms
516
- ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 89ms
517
- ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 29ms
515
+ ✓  node  test/server/index.test.ts (7 tests) 874ms
516
+ ✓ should handle options and execute  439ms
517
+ ✓  node  test/proxy.test.ts (4 tests) 18ms
518
+ ✓  node  test/server/webops-api.test.ts (9 tests) 11ms
519
+ ✓  node  test/components/quotes/MyAccountQuotesSelectedTags.test.tsx (8 tests) 81ms
520
+ ✓  node  test/sdk/orderEntry/useOrderFormItems.test.ts (7 tests) 36ms
518
521
  ✓  node  test/server/content/service.test.ts (5 tests) 4ms
519
- ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 9ms
520
- ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 6ms
522
+ ✓  node  test/sdk/cart/waitForSessionValidated.test.ts (5 tests) 10ms
523
+ ✓  node  test/sdk/account/refreshToken.test.ts (5 tests) 7ms
521
524
  ✓  node  test/server/cms/global.test.ts (3 tests) 4ms
522
- ✓  node  test/sdk/localization/store-url.test.ts (1 test) 2ms
523
- ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 24ms
525
+ ✓  node  test/sdk/localization/store-url.test.ts (1 test) 3ms
526
+ ✓  node  test/sdk/auth/useAuth.test.ts (5 tests) 26ms
527
+ ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 110ms
524
528
  ✓  node  test/sdk/analytics/hasEnabledRecommendationShelf.test.ts (6 tests) 4ms
525
- ✓  node  test/components/quotes/MyAccountQuotesStatusSelector.test.tsx (6 tests) 92ms
526
- ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 3ms
527
- ✓  node  test/pages/api/preview.test.ts (2 tests) 5ms
529
+ ✓  node  test/utils/getRequestHostname.test.ts (12 tests) 5ms
530
+ ✓  node  test/pages/api/preview.test.ts (2 tests) 6ms
528
531
  ✓  node  test/utils/validateSessionRefreshToken.test.ts (6 tests) 4ms
529
- ✓  node  test/server/rootSpan.test.ts (4 tests) 38ms
530
- ✓  node  test/components/search/SearchInput.test.ts (6 tests) 6ms
531
- ✓  node  test/components/RecommendationShelf/vrn.test.ts (5 tests) 4ms
532
- ✓  node  test/server/cms/index.test.ts (2 tests) 4ms
533
- ✓  node  test/utils/retry.test.ts (5 tests) 12ms
534
- ✓  node  test/components/account/Layout.test.tsx (4 tests) 29ms
535
- ✓  node  test/components/RecommendationShelf/useRecommendationUserId.test.ts (4 tests) 185ms
532
+ ✓  node  test/components/search/SearchInput.test.ts (6 tests) 5ms
533
+ ✓  node  test/server/rootSpan.test.ts (4 tests) 30ms
534
+ ✓  node  test/sdk/recommendations/vrn.test.ts (5 tests) 4ms
535
+ ✓  node  test/server/cms/index.test.ts (2 tests) 5ms
536
+ ✓  node  test/utils/retry.test.ts (5 tests) 10ms
537
+ ✓  node  test/components/account/Layout.test.tsx (4 tests) 34ms
538
+ ✓  node  test/sdk/recommendations/useRecommendationUserId.test.ts (4 tests) 184ms
536
539
  ✓  node  test/utils/unlockResponse.test.ts (11 tests) 3ms
537
540
  ✓  node  test/server/cms/myAccountDefaultSections.test.ts (5 tests) 5ms
538
- ✓  node  test/components/RecommendationShelf/useRecommendations.test.ts (2 tests) 3ms
541
+ ✓  node  test/sdk/recommendations/useRecommendations.test.ts (2 tests) 3ms
539
542
  ✓  node  test/utils/isLocalHost.test.ts (7 tests) 4ms
543
+ ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 41ms
540
544
  ✓  node  test/utils/userOrderStatus.test.ts (3 tests) 3ms
541
- ✓  node  test/components/account/OrganizationDrawerBody.test.tsx (3 tests) 33ms
542
545
  ✓  node  test/sdk/analytics/utils.test.ts (2 tests) 3ms
543
- ✓  node  test/instrumentation.test.ts (3 tests) 6ms
546
+ ✓  node  test/instrumentation.test.ts (3 tests) 4ms
544
547
  ✓  node  test/components/auth/ProfileChallenge.test.tsx (3 tests) 32ms
545
- ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 13ms
546
- ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 18ms
548
+ ✓  node  test/sdk/account/clearPersistedSessionState.test.ts (1 test) 14ms
549
+ ✓  node  test/components/quotes/MyAccountQuoteStatusBadge.test.tsx (2 tests) 23ms
547
550
 
548
-  Test Files  81 passed (81)
549
-  Tests  651 passed (651)
550
-  Start at  02:49:36
551
-  Duration  29.47s (transform 7.03s, setup 0ms, import 25.03s, tests 8.86s, environment 38.42s)
551
+  Test Files  82 passed (82)
552
+  Tests  661 passed (661)
553
+  Start at  15:05:26
554
+  Duration  29.73s (transform 6.68s, setup 0ms, import 23.96s, tests 9.74s, environment 38.48s)
552
555
 
@@ -1,5 +1,4 @@
1
1
  [
2
- "ClientRecommendationsQuery",
3
2
  "ServerAccountPageQuery",
4
3
  "ServerCollectionPageQuery",
5
4
  "ServerProductQuery",
@@ -23,6 +22,7 @@
23
22
  "ClientManyProductsQuery",
24
23
  "ClientManyProductsSelectedQuery",
25
24
  "ClientProfileQuery",
25
+ "ClientRecommendationsQuery",
26
26
  "ClientSearchSuggestionsQuery",
27
27
  "ClientTopSearchSuggestionsQuery",
28
28
  "ClientShippingSimulationQuery",
package/@generated/gql.ts CHANGED
@@ -18,7 +18,6 @@ type Documents = {
18
18
  "\n fragment ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\t\tunitMultiplier\n\n isVariantOf {\n productGroupID\n name\n\t\t\tskuVariants {\n\t\t\t\tallVariantsByName\n\t\t\t\tactiveVariations\n\t\t\t\tslugsMap\n\t\t\t\tavailableVariations\n\t\t\t}\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n listPrice\n listPriceWithTaxes\n priceWithTaxes\n quantity\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n hasSpecifications\n\n unitMultiplier\n\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n deliveryPromiseBadges {\n typeName\n }\n }\n": typeof types.ProductSummary_ProductFragmentDoc,
19
19
  "\n fragment Filter_facets on StoreFacet {\n ... on StoreFacetRange {\n key\n label\n\n min {\n selected\n absolute\n }\n\n max {\n selected\n absolute\n }\n\n __typename\n }\n ... on StoreFacetBoolean {\n key\n label\n values {\n label\n value\n selected\n quantity\n }\n\n __typename\n }\n }\n": typeof types.Filter_FacetsFragmentDoc,
20
20
  "\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n quantity\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n": typeof types.ProductDetailsFragment_ProductFragmentDoc,
21
- "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n": typeof types.ClientRecommendationsQueryDocument,
22
21
  "\n fragment ProductComparisonFragment_product on StoreProduct {\n id: productID\n sku\n slug\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n quantity\n listPriceWithTaxes\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n hasSpecifications\n\n skuSpecifications {\n field {\n id\n name\n originalName\n }\n values {\n name\n id\n fieldId\n originalName\n }\n }\n\n specificationGroups {\n name\n originalName\n specifications {\n name\n originalName\n values\n }\n }\n }\n": typeof types.ProductComparisonFragment_ProductFragmentDoc,
23
22
  "\n fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {\n id: productID\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n\t\t\t\t\tsku\n name\n image {\n url\n alternateName\n }\n offers {\n highPrice\n lowPrice\n lowPriceWithTaxes\n offerCount\n priceCurrency\n offers {\n listPrice\n listPriceWithTaxes\n sellingPrice\n priceCurrency\n price\n priceWithTaxes\n priceValidUntil\n itemCondition\n availability\n quantity\n priceToken\n }\n }\n additionalProperty {\n propertyID\n value\n name\n valueReference\n }\n }\n }\n }\n }\n": typeof types.ProductSkuMatrixSidebarFragment_ProductFragmentDoc,
24
23
  "\n fragment ClientManyProducts on Query {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n }\n }\n": typeof types.ClientManyProductsFragmentDoc,
@@ -60,6 +59,7 @@ type Documents = {
60
59
  "\n query ClientManyProductsQuery(\n $first: Int!\n $after: String\n $sort: StoreSort!\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]!\n $sponsoredCount: Int\n ) {\n ...ClientManyProducts\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n ) {\n products {\n pageInfo {\n totalCount\n }\n edges {\n node {\n ...ProductSummary_product\n }\n }\n }\n }\n }\n": typeof types.ClientManyProductsQueryDocument,
61
60
  "\n query ClientManyProductsSelectedQuery(\n $productIds: [String!]!\n ) {\n products(productIds: $productIds) {\n ...ProductComparisonFragment_product\n }\n }\n": typeof types.ClientManyProductsSelectedQueryDocument,
62
61
  "\n query ClientProfileQuery($id: String!) {\n profile(id: $id) {\n addresses {\n country\n postalCode\n geoCoordinate\n city\n }\n }\n }\n": typeof types.ClientProfileQueryDocument,
62
+ "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n": typeof types.ClientRecommendationsQueryDocument,
63
63
  "\n query ClientSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n ...ProductSummary_product\n }\n }\n products {\n pageInfo {\n totalCount\n }\n }\n metadata {\n ...SearchEvent_metadata\n }\n searchId\n }\n }\n": typeof types.ClientSearchSuggestionsQueryDocument,
64
64
  "\n query ClientTopSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientTopSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n }\n }\n }\n": typeof types.ClientTopSearchSuggestionsQueryDocument,
65
65
  "\n mutation ValidateSession($session: IStoreSession!, $search: String!) {\n validateSession(session: $session, search: $search) {\n locale\n channel\n country\n addressType\n postalCode\n city\n deliveryMode {\n deliveryChannel\n deliveryMethod\n deliveryWindow {\n startDate\n endDate\n }\n }\n geoCoordinates {\n latitude\n longitude\n }\n currency {\n code\n symbol\n }\n person {\n id\n email\n givenName\n familyName\n }\n b2b {\n customerId\n isRepresentative\n unitName\n unitId\n firstName\n lastName\n userName\n userEmail\n savedPostalCode\n contractName\n organizationManager\n }\n marketingData {\n utmCampaign\n utmMedium\n utmSource\n utmiCampaign\n utmiPage\n utmiPart\n }\n refreshAfter\n }\n }\n": typeof types.ValidateSessionDocument,
@@ -70,7 +70,6 @@ const documents: Documents = {
70
70
  "\n fragment ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\t\tunitMultiplier\n\n isVariantOf {\n productGroupID\n name\n\t\t\tskuVariants {\n\t\t\t\tallVariantsByName\n\t\t\t\tactiveVariations\n\t\t\t\tslugsMap\n\t\t\t\tavailableVariations\n\t\t\t}\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n listPrice\n listPriceWithTaxes\n priceWithTaxes\n quantity\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n hasSpecifications\n\n unitMultiplier\n\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n deliveryPromiseBadges {\n typeName\n }\n }\n": types.ProductSummary_ProductFragmentDoc,
71
71
  "\n fragment Filter_facets on StoreFacet {\n ... on StoreFacetRange {\n key\n label\n\n min {\n selected\n absolute\n }\n\n max {\n selected\n absolute\n }\n\n __typename\n }\n ... on StoreFacetBoolean {\n key\n label\n values {\n label\n value\n selected\n quantity\n }\n\n __typename\n }\n }\n": types.Filter_FacetsFragmentDoc,
72
72
  "\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n quantity\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n": types.ProductDetailsFragment_ProductFragmentDoc,
73
- "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n": types.ClientRecommendationsQueryDocument,
74
73
  "\n fragment ProductComparisonFragment_product on StoreProduct {\n id: productID\n sku\n slug\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n quantity\n listPriceWithTaxes\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n hasSpecifications\n\n skuSpecifications {\n field {\n id\n name\n originalName\n }\n values {\n name\n id\n fieldId\n originalName\n }\n }\n\n specificationGroups {\n name\n originalName\n specifications {\n name\n originalName\n values\n }\n }\n }\n": types.ProductComparisonFragment_ProductFragmentDoc,
75
74
  "\n fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {\n id: productID\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n\t\t\t\t\tsku\n name\n image {\n url\n alternateName\n }\n offers {\n highPrice\n lowPrice\n lowPriceWithTaxes\n offerCount\n priceCurrency\n offers {\n listPrice\n listPriceWithTaxes\n sellingPrice\n priceCurrency\n price\n priceWithTaxes\n priceValidUntil\n itemCondition\n availability\n quantity\n priceToken\n }\n }\n additionalProperty {\n propertyID\n value\n name\n valueReference\n }\n }\n }\n }\n }\n": types.ProductSkuMatrixSidebarFragment_ProductFragmentDoc,
76
75
  "\n fragment ClientManyProducts on Query {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n }\n }\n": types.ClientManyProductsFragmentDoc,
@@ -112,6 +111,7 @@ const documents: Documents = {
112
111
  "\n query ClientManyProductsQuery(\n $first: Int!\n $after: String\n $sort: StoreSort!\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]!\n $sponsoredCount: Int\n ) {\n ...ClientManyProducts\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n ) {\n products {\n pageInfo {\n totalCount\n }\n edges {\n node {\n ...ProductSummary_product\n }\n }\n }\n }\n }\n": types.ClientManyProductsQueryDocument,
113
112
  "\n query ClientManyProductsSelectedQuery(\n $productIds: [String!]!\n ) {\n products(productIds: $productIds) {\n ...ProductComparisonFragment_product\n }\n }\n": types.ClientManyProductsSelectedQueryDocument,
114
113
  "\n query ClientProfileQuery($id: String!) {\n profile(id: $id) {\n addresses {\n country\n postalCode\n geoCoordinate\n city\n }\n }\n }\n": types.ClientProfileQueryDocument,
114
+ "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n": types.ClientRecommendationsQueryDocument,
115
115
  "\n query ClientSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n ...ProductSummary_product\n }\n }\n products {\n pageInfo {\n totalCount\n }\n }\n metadata {\n ...SearchEvent_metadata\n }\n searchId\n }\n }\n": types.ClientSearchSuggestionsQueryDocument,
116
116
  "\n query ClientTopSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientTopSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n }\n }\n }\n": types.ClientTopSearchSuggestionsQueryDocument,
117
117
  "\n mutation ValidateSession($session: IStoreSession!, $search: String!) {\n validateSession(session: $session, search: $search) {\n locale\n channel\n country\n addressType\n postalCode\n city\n deliveryMode {\n deliveryChannel\n deliveryMethod\n deliveryWindow {\n startDate\n endDate\n }\n }\n geoCoordinates {\n latitude\n longitude\n }\n currency {\n code\n symbol\n }\n person {\n id\n email\n givenName\n familyName\n }\n b2b {\n customerId\n isRepresentative\n unitName\n unitId\n firstName\n lastName\n userName\n userEmail\n savedPostalCode\n contractName\n organizationManager\n }\n marketingData {\n utmCampaign\n utmMedium\n utmSource\n utmiCampaign\n utmiPage\n utmiPart\n }\n refreshAfter\n }\n }\n": types.ValidateSessionDocument,
@@ -131,10 +131,6 @@ export function gql(source: "\n fragment Filter_facets on StoreFacet {\n ...
131
131
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
132
132
  */
133
133
  export function gql(source: "\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n name\n productID\n }\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n quantity\n priceToken\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n"): typeof import('./graphql').ProductDetailsFragment_ProductFragmentDoc;
134
- /**
135
- * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
136
- */
137
- export function gql(source: "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n"): typeof import('./graphql').ClientRecommendationsQueryDocument;
138
134
  /**
139
135
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
140
136
  */
@@ -299,6 +295,10 @@ export function gql(source: "\n query ClientManyProductsSelectedQuery(\n $pr
299
295
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
300
296
  */
301
297
  export function gql(source: "\n query ClientProfileQuery($id: String!) {\n profile(id: $id) {\n addresses {\n country\n postalCode\n geoCoordinate\n city\n }\n }\n }\n"): typeof import('./graphql').ClientProfileQueryDocument;
298
+ /**
299
+ * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
300
+ */
301
+ export function gql(source: "query ClientRecommendationsQuery(\n $campaignVrn: String!\n $userId: String\n $products: [String!]\n) {\n recommendations(\n userId: $userId\n campaignVrn: $campaignVrn\n products: $products\n ) {\n products {\n ...ProductSummary_product\n }\n correlationId\n campaign {\n id\n title\n type\n }\n }\n}\n"): typeof import('./graphql').ClientRecommendationsQueryDocument;
302
302
  /**
303
303
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
304
304
  */
@@ -2727,15 +2727,6 @@ export type Filter_FacetsFragment =
2727
2727
 
2728
2728
  export type ProductDetailsFragment_ProductFragment = { sku: string, name: string, gtin: string, description: string, unitMultiplier: number | null, id: string, isVariantOf: { name: string, productGroupID: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, offers: { lowPrice: number, lowPriceWithTaxes: number, offers: Array<{ availability: string, price: number, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, quantity: number, priceToken: string | null, seller: { identifier: string } }> }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> };
2729
2729
 
2730
- export type ClientRecommendationsQueryQueryVariables = Exact<{
2731
- campaignVrn: Scalars['String']['input'];
2732
- userId: InputMaybe<Scalars['String']['input']>;
2733
- products: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
2734
- }>;
2735
-
2736
-
2737
- export type ClientRecommendationsQueryQuery = { recommendations: { correlationId: string, products: Array<{ slug: string, sku: string, name: string, gtin: string, unitMultiplier: number | null, hasSpecifications: boolean | null, id: string, brand: { name: string, brandName: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { allVariantsByName: any | null, activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, image: Array<{ url: string, alternateName: string }>, offers: { lowPrice: number, lowPriceWithTaxes: number, offers: Array<{ availability: string, price: number, listPrice: number, listPriceWithTaxes: number, priceWithTaxes: number, quantity: number, priceToken: string | null, seller: { identifier: string } }> }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }>, advertisement: { adId: string, adResponseId: string } | null, deliveryPromiseBadges: Array<{ typeName: string | null } | null> | null }>, campaign: { id: string, title: string | null, type: string } } };
2738
-
2739
2730
  export type ProductComparisonFragment_ProductFragment = { sku: string, slug: string, name: string, gtin: string, description: string, unitMultiplier: number | null, hasSpecifications: boolean | null, id: string, isVariantOf: { name: string, productGroupID: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, offers: { lowPrice: number, lowPriceWithTaxes: number, offers: Array<{ availability: string, price: number, priceWithTaxes: number, listPrice: number, quantity: number, listPriceWithTaxes: number, priceToken: string | null, seller: { identifier: string } }> }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }>, advertisement: { adId: string, adResponseId: string } | null, skuSpecifications: Array<{ field: { id: string | null, name: string, originalName: string | null }, values: Array<{ name: string, id: string | null, fieldId: string | null, originalName: string | null }> }>, specificationGroups: Array<{ name: string, originalName: string, specifications: Array<{ name: string, originalName: string, values: Array<string> }> }> };
2740
2731
 
2741
2732
  export type ProductSkuMatrixSidebarFragment_ProductFragment = { id: string, isVariantOf: { name: string, productGroupID: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ sku: string, name: string, image: Array<{ url: string, alternateName: string }>, offers: { highPrice: number, lowPrice: number, lowPriceWithTaxes: number, offerCount: number, priceCurrency: string, offers: Array<{ listPrice: number, listPriceWithTaxes: number, sellingPrice: number, priceCurrency: string, price: number, priceWithTaxes: number, priceValidUntil: string, itemCondition: string, availability: string, quantity: number, priceToken: string | null }> }, additionalProperty: Array<{ propertyID: string, value: any, name: string, valueReference: any }> }> | null } | null } };
@@ -2994,6 +2985,15 @@ export type ClientProfileQueryQueryVariables = Exact<{
2994
2985
 
2995
2986
  export type ClientProfileQueryQuery = { profile: { addresses: Array<{ country: string | null, postalCode: string | null, geoCoordinate: Array<number | null> | null, city: string | null } | null> | null } | null };
2996
2987
 
2988
+ export type ClientRecommendationsQueryQueryVariables = Exact<{
2989
+ campaignVrn: Scalars['String']['input'];
2990
+ userId: InputMaybe<Scalars['String']['input']>;
2991
+ products: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
2992
+ }>;
2993
+
2994
+
2995
+ export type ClientRecommendationsQueryQuery = { recommendations: { correlationId: string, products: Array<{ slug: string, sku: string, name: string, gtin: string, unitMultiplier: number | null, hasSpecifications: boolean | null, id: string, brand: { name: string, brandName: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { allVariantsByName: any | null, activeVariations: any | null, slugsMap: any | null, availableVariations: any | null, allVariantProducts: Array<{ name: string, productID: string }> | null } | null }, image: Array<{ url: string, alternateName: string }>, offers: { lowPrice: number, lowPriceWithTaxes: number, offers: Array<{ availability: string, price: number, listPrice: number, listPriceWithTaxes: number, priceWithTaxes: number, quantity: number, priceToken: string | null, seller: { identifier: string } }> }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }>, advertisement: { adId: string, adResponseId: string } | null, deliveryPromiseBadges: Array<{ typeName: string | null } | null> | null }>, campaign: { id: string, title: string | null, type: string } } };
2996
+
2997
2997
  export type ClientSearchSuggestionsQueryQueryVariables = Exact<{
2998
2998
  term: Scalars['String']['input'];
2999
2999
  selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>;
@@ -3555,7 +3555,6 @@ export const SearchEvent_MetadataFragmentDoc = new TypedDocumentString(`
3555
3555
  fuzzy
3556
3556
  }
3557
3557
  `, {"fragmentName":"SearchEvent_metadata"}) as unknown as TypedDocumentString<SearchEvent_MetadataFragment, unknown>;
3558
- export const ClientRecommendationsQueryDocument = {"__meta__":{"operationName":"ClientRecommendationsQuery","operationHash":"b227071b0388469d7b90d04cff4cbc11f8f134d4"}} as unknown as TypedDocumentString<ClientRecommendationsQueryQuery, ClientRecommendationsQueryQueryVariables>;
3559
3558
  export const ServerAccountPageQueryDocument = {"__meta__":{"operationName":"ServerAccountPageQuery","operationHash":"9baae331b75848a310fecb457e8c971ae27897ff"}} as unknown as TypedDocumentString<ServerAccountPageQueryQuery, ServerAccountPageQueryQueryVariables>;
3560
3559
  export const ServerCollectionPageQueryDocument = {"__meta__":{"operationName":"ServerCollectionPageQuery","operationHash":"d46841b30ae1f6350021b5cf02f253d56c848664"}} as unknown as TypedDocumentString<ServerCollectionPageQueryQuery, ServerCollectionPageQueryQueryVariables>;
3561
3560
  export const ServerProductQueryDocument = {"__meta__":{"operationName":"ServerProductQuery","operationHash":"b89e93519be01aebc01c402489a0ae640d38675a"}} as unknown as TypedDocumentString<ServerProductQueryQuery, ServerProductQueryQueryVariables>;
@@ -3586,6 +3585,7 @@ export const ClientProductQueryDocument = {"__meta__":{"operationName":"ClientPr
3586
3585
  export const ClientManyProductsQueryDocument = {"__meta__":{"operationName":"ClientManyProductsQuery","operationHash":"ee14fd92b4a04fc3751efcbfcb60b3886d912253"}} as unknown as TypedDocumentString<ClientManyProductsQueryQuery, ClientManyProductsQueryQueryVariables>;
3587
3586
  export const ClientManyProductsSelectedQueryDocument = {"__meta__":{"operationName":"ClientManyProductsSelectedQuery","operationHash":"7e1b06c167e411905ceac9db11c1a6a7892cfdd6"}} as unknown as TypedDocumentString<ClientManyProductsSelectedQueryQuery, ClientManyProductsSelectedQueryQueryVariables>;
3588
3587
  export const ClientProfileQueryDocument = {"__meta__":{"operationName":"ClientProfileQuery","operationHash":"34ea14c0d4a57ddf9bc11e4be0cd2b5a6506d3d4"}} as unknown as TypedDocumentString<ClientProfileQueryQuery, ClientProfileQueryQueryVariables>;
3588
+ export const ClientRecommendationsQueryDocument = {"__meta__":{"operationName":"ClientRecommendationsQuery","operationHash":"b227071b0388469d7b90d04cff4cbc11f8f134d4"}} as unknown as TypedDocumentString<ClientRecommendationsQueryQuery, ClientRecommendationsQueryQueryVariables>;
3589
3589
  export const ClientSearchSuggestionsQueryDocument = {"__meta__":{"operationName":"ClientSearchSuggestionsQuery","operationHash":"ee57cd392fdfde90620787c9954e2ecae47881f9"}} as unknown as TypedDocumentString<ClientSearchSuggestionsQueryQuery, ClientSearchSuggestionsQueryQueryVariables>;
3590
3590
  export const ClientTopSearchSuggestionsQueryDocument = {"__meta__":{"operationName":"ClientTopSearchSuggestionsQuery","operationHash":"e2385b0f11726d0068f96548f57a8dd441c064e3"}} as unknown as TypedDocumentString<ClientTopSearchSuggestionsQueryQuery, ClientTopSearchSuggestionsQueryQueryVariables>;
3591
3591
  export const ValidateSessionDocument = {"__meta__":{"operationName":"ValidateSession","operationHash":"8c3a5999496e227f167e9dc79697e6c478d48a9e"}} as unknown as TypedDocumentString<ValidateSessionMutation, ValidateSessionMutationVariables>;
@@ -1,5 +1,4 @@
1
1
  {
2
- "b227071b0388469d7b90d04cff4cbc11f8f134d4": "fragment ProductSummary_product on StoreProduct { additionalProperty { name propertyID value valueReference } advertisement { adId adResponseId } brand { brandName: name } brand { name } deliveryPromiseBadges { typeName } gtin hasSpecifications image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantsByName availableVariations slugsMap } } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku slug unitMultiplier unitMultiplier } query ClientRecommendationsQuery($campaignVrn: String!, $products: [String!], $userId: String) { recommendations(userId: $userId, campaignVrn: $campaignVrn, products: $products) { campaign { id title type } correlationId products { ...ProductSummary_product } } }",
3
2
  "9baae331b75848a310fecb457e8c971ae27897ff": "query ServerAccountPageQuery { accountProfile { name } }",
4
3
  "d46841b30ae1f6350021b5cf02f253d56c848664": "fragment ServerCollectionPage on Query { collection(slug: $slug) { id } } query ServerCollectionPageQuery($slug: String!) { collection(slug: $slug) { breadcrumbList { itemListElement { item name position } } meta { selectedFacets { key value } } otherLocales { locale slug } seo { description title } } ...ServerCollectionPage }",
5
4
  "b89e93519be01aebc01c402489a0ae640d38675a": "fragment CartProductItem on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations availableVariations slugsMap } } name sku unitMultiplier } fragment ProductDetailsFragment_product on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } description gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku unitMultiplier ...CartProductItem } fragment ServerProduct on Query { product(locator: $locator) { id: productID } } query ServerProductQuery($locator: [IStoreSelectedFacet!]!) { product(locator: $locator) { brand { name } breadcrumbList { itemListElement { item name position } } description gtin image { alternateName url } isVariantOf { productGroupID } mpn name offers { highPrice lowPrice lowPriceWithTaxes offers { availability itemCondition price priceCurrency priceToken priceValidUntil seller { identifier } } priceCurrency } otherLocales { locale slug } id: productID releaseDate seo { canonical description title } sku ...ProductDetailsFragment_product } ...ServerProduct }",
@@ -30,6 +29,7 @@
30
29
  "ee14fd92b4a04fc3751efcbfcb60b3886d912253": "fragment ClientManyProducts on Query { search( first: $first after: $after sort: $sort term: $term selectedFacets: $selectedFacets sponsoredCount: $sponsoredCount ) { products { pageInfo { totalCount } } } } fragment ProductSummary_product on StoreProduct { additionalProperty { name propertyID value valueReference } advertisement { adId adResponseId } brand { brandName: name } brand { name } deliveryPromiseBadges { typeName } gtin hasSpecifications image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantsByName availableVariations slugsMap } } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku slug unitMultiplier unitMultiplier } query ClientManyProductsQuery($after: String, $first: Int!, $selectedFacets: [IStoreSelectedFacet!]!, $sort: StoreSort!, $sponsoredCount: Int, $term: String!) { search( first: $first after: $after sort: $sort term: $term selectedFacets: $selectedFacets sponsoredCount: $sponsoredCount ) { products { edges { node { ...ProductSummary_product } } pageInfo { totalCount } } } ...ClientManyProducts }",
31
30
  "7e1b06c167e411905ceac9db11c1a6a7892cfdd6": "fragment ProductComparisonFragment_product on StoreProduct { additionalProperty { name propertyID value valueReference } advertisement { adId adResponseId } brand { name } description gtin hasSpecifications image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku skuSpecifications { field { id name originalName } values { fieldId id name originalName } } slug specificationGroups { name originalName specifications { name originalName values } } unitMultiplier } query ClientManyProductsSelectedQuery($productIds: [String!]!) { products(productIds: $productIds) { ...ProductComparisonFragment_product } }",
32
31
  "34ea14c0d4a57ddf9bc11e4be0cd2b5a6506d3d4": "query ClientProfileQuery($id: String!) { profile(id: $id) { addresses { city country geoCoordinate postalCode } } }",
32
+ "b227071b0388469d7b90d04cff4cbc11f8f134d4": "fragment ProductSummary_product on StoreProduct { additionalProperty { name propertyID value valueReference } advertisement { adId adResponseId } brand { brandName: name } brand { name } deliveryPromiseBadges { typeName } gtin hasSpecifications image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantsByName availableVariations slugsMap } } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku slug unitMultiplier unitMultiplier } query ClientRecommendationsQuery($campaignVrn: String!, $products: [String!], $userId: String) { recommendations(userId: $userId, campaignVrn: $campaignVrn, products: $products) { campaign { id title type } correlationId products { ...ProductSummary_product } } }",
33
33
  "ee57cd392fdfde90620787c9954e2ecae47881f9": "fragment ClientSearchSuggestions on Query { search(first: 5, term: $term, selectedFacets: $selectedFacets) { suggestions { terms { value } } } } fragment ProductSummary_product on StoreProduct { additionalProperty { name propertyID value valueReference } advertisement { adId adResponseId } brand { brandName: name } brand { name } deliveryPromiseBadges { typeName } gtin hasSpecifications image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations allVariantsByName availableVariations slugsMap } } isVariantOf { name productGroupID skuVariants { activeVariations allVariantProducts { name productID } availableVariations slugsMap } } name offers { lowPrice lowPriceWithTaxes offers { availability listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } } id: productID sku slug unitMultiplier unitMultiplier } fragment SearchEvent_metadata on SearchMetadata { fuzzy isTermMisspelled logicalOperator } query ClientSearchSuggestionsQuery($selectedFacets: [IStoreSelectedFacet!], $term: String!) { search(first: 5, term: $term, selectedFacets: $selectedFacets) { metadata { ...SearchEvent_metadata } products { pageInfo { totalCount } } searchId suggestions { products { ...ProductSummary_product } terms { value } } } ...ClientSearchSuggestions }",
34
34
  "e2385b0f11726d0068f96548f57a8dd441c064e3": "fragment ClientTopSearchSuggestions on Query { search(first: 5, term: $term, selectedFacets: $selectedFacets) { suggestions { terms { value } } } } query ClientTopSearchSuggestionsQuery($selectedFacets: [IStoreSelectedFacet!], $term: String!) { search(first: 5, term: $term, selectedFacets: $selectedFacets) { suggestions { terms { value } } } ...ClientTopSearchSuggestions }",
35
35
  "8c3a5999496e227f167e9dc79697e6c478d48a9e": "mutation ValidateSession($search: String!, $session: IStoreSession!) { validateSession(session: $session, search: $search) { addressType b2b { contractName customerId firstName isRepresentative lastName organizationManager savedPostalCode unitId unitName userEmail userName } channel city country currency { code symbol } deliveryMode { deliveryChannel deliveryMethod deliveryWindow { endDate startDate } } geoCoordinates { latitude longitude } locale marketingData { utmCampaign utmMedium utmSource utmiCampaign utmiPage utmiPart } person { email familyName givenName id } postalCode refreshAfter } }",
package/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
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.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12)
7
+
8
+ **Note:** Version bump only for package @faststore/core
9
+
6
10
  # [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12)
7
11
 
8
12
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.6.0-dev.2",
3
+ "version": "4.6.0-dev.3",
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/diagnostics": "4.6.0-dev.2",
75
- "@faststore/api": "4.6.0-dev.2",
76
- "@faststore/lighthouse": "4.6.0-dev.2",
77
- "@faststore/sdk": "4.6.0-dev.2",
78
- "@faststore/ui": "4.6.0-dev.2"
74
+ "@faststore/diagnostics": "4.6.0-dev.3",
75
+ "@faststore/lighthouse": "4.6.0-dev.3",
76
+ "@faststore/sdk": "4.6.0-dev.3",
77
+ "@faststore/ui": "4.6.0-dev.3",
78
+ "@faststore/api": "4.6.0-dev.3"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@cypress/code-coverage": "^3.12.1",
@@ -99,8 +99,11 @@ All recommendation work runs client-side after hydration:
99
99
  5. **Render** — shows `ProductShelfSkeleton` while loading; returns `null` on error or empty results; otherwise renders the carousel and optional heading.
100
100
  6. **Tracking** — when correlation and campaign identifiers are present, the shelf emits `data-af-*` attributes for Activity Flow. PDP product views use `product:*` meta tags from `pages/[slug]/p.tsx`.
101
101
 
102
+ Steps 1 to 4 carry no presentation: they live in `src/sdk/recommendations` and are orchestrated by `useRecommendationShelf()`, which the shelf consumes.
103
+
102
104
  Related files:
103
105
 
106
+ - Shared data layer: `src/sdk/recommendations/`
104
107
  - Shelf: `src/components/sections/RecommendationShelf/`
105
108
  - Hook: `src/sdk/analytics/hooks/useStartRecommendationSession.ts`
106
109
  - Layout: `src/Layout.tsx`
@@ -129,7 +132,7 @@ import { RecommendationShelf } from 'src/components/sections/RecommendationShelf
129
132
  - **`ProductCard`**: custom card component (defaults to the core `ProductCard`).
130
133
  - **`mapProductToProductCard`**: maps each recommended product (normalized `StoreProduct`) into card props. When provided, it fully owns the card props and the default `productCardConfiguration` merge no longer applies.
131
134
 
132
- Campaign types and VRN validation are defined in `vrn.ts` (`VRN_TYPE_TO_RECOMMENDATION`). Updates should be made there and mirrored in the CMS schema `pattern`.
135
+ Campaign types and VRN validation are defined in `src/sdk/recommendations/vrn.ts` (`VRN_TYPE_TO_RECOMMENDATION`). Updates should be made there and mirrored in the CMS schema `pattern`.
133
136
 
134
137
  ## Placement guidelines
135
138
 
@@ -1,14 +1,13 @@
1
1
  import { type ComponentType, useId, useMemo } from 'react'
2
2
 
3
- import { usePDP } from '@faststore/core'
4
- import { ProductShelf, Carousel } from '@faststore/ui'
3
+ import { Carousel, ProductShelf } from '@faststore/ui'
5
4
  import type { ProductSummary_ProductFragment } from '@generated/graphql'
6
5
 
7
6
  import DefaultProductCard, {
8
7
  type ProductCardProps,
9
8
  } from 'src/components/product/ProductCard'
10
9
  import ProductShelfSkeleton from 'src/components/skeletons/ProductShelfSkeleton'
11
- import { useCart } from 'src/sdk/cart'
10
+ import { useRecommendationShelf } from 'src/sdk/recommendations'
12
11
  import useScreenResize from 'src/sdk/ui/useScreenResize'
13
12
 
14
13
  import type {
@@ -16,48 +15,6 @@ import type {
16
15
  RecommendationShelfProps,
17
16
  } from './RecommendationShelf.types'
18
17
  import styles from './section.module.scss'
19
- import {
20
- useRecommendations,
21
- type RecommendationInput,
22
- } from './useRecommendations'
23
- import { useRecommendationUserId } from './useRecommendationUserId'
24
- import { getTypeFromVrn } from './vrn'
25
-
26
- function getRecommendationArguments(
27
- campaignVrn: string,
28
- context: { userId?: string | null; contextProducts: string[] }
29
- ): RecommendationInput | null {
30
- const { userId, contextProducts } = context
31
- const type = getTypeFromVrn(campaignVrn)
32
-
33
- // `type` is null for malformed/unknown VRNs; bail out so we never fetch (and
34
- // never throw) on an invalid campaign coming from the CMS.
35
- if (!type || !userId) return null
36
-
37
- switch (type) {
38
- case 'NEXT_INTERACTION':
39
- case 'VISUAL_SIMILARITY':
40
- case 'CROSS_SELL':
41
- case 'SIMILAR_ITEMS':
42
- // These campaigns need product context. Without any (e.g. `CART` context
43
- // on an empty cart, or `PDP` context outside a product page), skip the
44
- // fetch instead of requesting recommendations we can't anchor.
45
- if (contextProducts.length === 0) {
46
- return null
47
- }
48
- return {
49
- userId,
50
- campaignVrn,
51
- products: contextProducts,
52
- }
53
- default:
54
- return {
55
- userId,
56
- campaignVrn,
57
- products: [],
58
- }
59
- }
60
- }
61
18
 
62
19
  export function RecommendationShelf<
63
20
  TCardProps extends object = ProductCardProps,
@@ -96,43 +53,14 @@ export function RecommendationShelf<
96
53
  const itemsPerPage =
97
54
  isMobile || isTablet ? itemsPerPageMobile : itemsPerPageDesktop
98
55
 
99
- const userId = useRecommendationUserId(campaignVrn)
100
-
101
- const { data: productDetailPage } = usePDP()
102
- const { items: cartItems } = useCart()
103
-
104
- // Resolve the products used as context for the request from the configured
105
- // source: the current PDP product, or the (deduplicated) cart items.
106
- const contextProducts = useMemo(() => {
107
- if (itemsContext === 'CART') {
108
- return Array.from(
109
- new Set(
110
- cartItems
111
- .map((item) => item.itemOffered.isVariantOf.productGroupID)
112
- .filter(Boolean)
113
- )
114
- )
115
- }
116
-
117
- const pdpProduct = productDetailPage?.product?.isVariantOf?.productGroupID
118
-
119
- return pdpProduct ? [pdpProduct] : []
120
- }, [itemsContext, cartItems, productDetailPage])
121
-
122
- // Gate the whole feature behind the CMS opt-in: when Recommendations is
123
- // disabled the shelf never requests recommendations (nor renders).
124
- const recommendationArgs = enableRecommendations
125
- ? getRecommendationArguments(campaignVrn, {
126
- userId,
127
- contextProducts,
128
- })
129
- : null
130
-
131
- const { data, isLoading, error } = useRecommendations(recommendationArgs)
56
+ const { items, isLoading, error, campaign, correlationId } =
57
+ useRecommendationShelf({
58
+ campaignVrn,
59
+ enableRecommendations,
60
+ itemsContext,
61
+ })
132
62
 
133
- const items = data?.products || []
134
- const correlationId = data?.correlationId
135
- const campaignId = data?.campaign.id
63
+ const campaignId = campaign?.id
136
64
 
137
65
  const productIds = useMemo(
138
66
  () => items.map((p) => p.isVariantOf.productGroupID).join(', '),
@@ -147,15 +75,6 @@ export function RecommendationShelf<
147
75
  )
148
76
 
149
77
  if (error) {
150
- // Don't log `recommendationArgs`: it carries the userId. Log only
151
- // non-identifying context.
152
- console.error(
153
- 'Error fetching recommendations',
154
- error.cause,
155
- error.message,
156
- 'for campaign',
157
- campaignVrn
158
- )
159
78
  return null
160
79
  }
161
80
 
@@ -178,9 +97,9 @@ export function RecommendationShelf<
178
97
  : {})}
179
98
  >
180
99
  <ProductShelfSkeleton loading={isLoading} itemsPerPage={itemsPerPage}>
181
- {(title || data?.campaign.title) && (
100
+ {(title || campaign?.title) && (
182
101
  <h2 className="text__title-section layout__content">
183
- {title || data?.campaign.title}
102
+ {title || campaign?.title}
184
103
  </h2>
185
104
  )}
186
105
 
@@ -4,6 +4,9 @@ import type { CarouselProps } from '@faststore/ui'
4
4
  import type { ProductSummary_ProductFragment } from '@generated/graphql'
5
5
 
6
6
  import type { ProductCardProps } from 'src/components/product/ProductCard'
7
+ import type { ItemContext } from 'src/sdk/recommendations'
8
+
9
+ export type { ItemContext, RecommendationType } from 'src/sdk/recommendations'
7
10
 
8
11
  /**
9
12
  * Maps a recommendation product (a normalized `StoreProduct`, identical to the
@@ -17,14 +20,6 @@ export type RecommendationProductCardMapper<TCardProps = ProductCardProps> = (
17
20
  index: number
18
21
  ) => TCardProps
19
22
 
20
- /**
21
- * Source of the products used as context for the recommendation request:
22
- * - `'PDP'`: the current product detail page product.
23
- * - `'CART'`: the products currently in the cart (useful for cross-sell on the
24
- * cart page).
25
- */
26
- export type ItemContext = 'PDP' | 'CART'
27
-
28
23
  export type RecommendationShelfProps<
29
24
  TCardProps extends object = ProductCardProps,
30
25
  > = {
@@ -106,13 +101,3 @@ export type RecommendationShelfProps<
106
101
  readonly bordered?: boolean
107
102
  }
108
103
  }
109
-
110
- export type RecommendationType =
111
- | 'CROSS_SELL'
112
- | 'SIMILAR_ITEMS'
113
- | 'PERSONALIZED'
114
- | 'TOP_ITEMS'
115
- | 'LAST_SEEN'
116
- | 'SEARCH_BASED'
117
- | 'VISUAL_SIMILARITY'
118
- | 'NEXT_INTERACTION'
@@ -0,0 +1,17 @@
1
+ export type { ItemContext, RecommendationType } from './types'
2
+ export type {
3
+ RecommendationInput,
4
+ RecommendationProduct,
5
+ RecommendationResponse,
6
+ } from './useRecommendations'
7
+ export { useRecommendations } from './useRecommendations'
8
+ export { useRecommendationUserId } from './useRecommendationUserId'
9
+ export type {
10
+ UseRecommendationShelfArgs,
11
+ UseRecommendationShelfResult,
12
+ } from './useRecommendationShelf'
13
+ export {
14
+ getRecommendationArguments,
15
+ useRecommendationShelf,
16
+ } from './useRecommendationShelf'
17
+ export { getTypeFromVrn, isValidVrn } from './vrn'
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Source of the products used as context for the recommendation request:
3
+ * - `'PDP'`: the current product detail page product.
4
+ * - `'CART'`: the products currently in the cart (useful for cross-sell on the
5
+ * cart page and inside the cart drawer).
6
+ */
7
+ export type ItemContext = 'PDP' | 'CART'
8
+
9
+ export type RecommendationType =
10
+ | 'CROSS_SELL'
11
+ | 'SIMILAR_ITEMS'
12
+ | 'PERSONALIZED'
13
+ | 'TOP_ITEMS'
14
+ | 'LAST_SEEN'
15
+ | 'SEARCH_BASED'
16
+ | 'VISUAL_SIMILARITY'
17
+ | 'NEXT_INTERACTION'
@@ -0,0 +1,141 @@
1
+ import { usePDP } from '@faststore/core'
2
+ import { useMemo } from 'react'
3
+
4
+ import { useCart } from 'src/sdk/cart'
5
+
6
+ import type { ItemContext } from './types'
7
+ import {
8
+ type RecommendationInput,
9
+ type RecommendationResponse,
10
+ useRecommendations,
11
+ } from './useRecommendations'
12
+ import { useRecommendationUserId } from './useRecommendationUserId'
13
+ import { getTypeFromVrn } from './vrn'
14
+
15
+ export function getRecommendationArguments(
16
+ campaignVrn: string,
17
+ context: { userId?: string | null; contextProducts: string[] }
18
+ ): RecommendationInput | null {
19
+ const { userId, contextProducts } = context
20
+ const type = getTypeFromVrn(campaignVrn)
21
+
22
+ // `type` is null for malformed/unknown VRNs; bail out so we never fetch (and
23
+ // never throw) on an invalid campaign coming from the CMS.
24
+ if (!type || !userId) return null
25
+
26
+ switch (type) {
27
+ case 'NEXT_INTERACTION':
28
+ case 'VISUAL_SIMILARITY':
29
+ case 'CROSS_SELL':
30
+ case 'SIMILAR_ITEMS':
31
+ // These campaigns need product context. Without any (e.g. `CART` context
32
+ // on an empty cart, or `PDP` context outside a product page), skip the
33
+ // fetch instead of requesting recommendations we can't anchor.
34
+ if (contextProducts.length === 0) {
35
+ return null
36
+ }
37
+ return {
38
+ userId,
39
+ campaignVrn,
40
+ products: contextProducts,
41
+ }
42
+ default:
43
+ return {
44
+ userId,
45
+ campaignVrn,
46
+ products: [],
47
+ }
48
+ }
49
+ }
50
+
51
+ export type UseRecommendationShelfArgs = {
52
+ readonly campaignVrn: string
53
+ /**
54
+ * Opt-in for VTEX Recommendations. When `false` (default) the campaign is
55
+ * never requested. The anonymous user id is still resolved, but it goes
56
+ * unused and never leaves the browser.
57
+ */
58
+ readonly enableRecommendations?: boolean
59
+ /**
60
+ * Where to read the products used as context for the recommendation request.
61
+ * @default 'PDP'
62
+ */
63
+ readonly itemsContext?: ItemContext
64
+ }
65
+
66
+ export type UseRecommendationShelfResult = {
67
+ readonly items: RecommendationResponse['products']
68
+ readonly isLoading: boolean
69
+ readonly error: unknown
70
+ readonly campaign?: RecommendationResponse['campaign']
71
+ readonly correlationId?: string
72
+ }
73
+
74
+ const EMPTY_ITEMS: RecommendationResponse['products'] = []
75
+
76
+ /**
77
+ * Headless data layer behind every recommendation shelf: resolves the campaign
78
+ * type, the anonymous user id and the product context, then fetches the
79
+ * campaign. It renders nothing — presentation lives in the shelf components
80
+ * that consume it, so a new surface can reuse the campaign rules instead of
81
+ * reimplementing them.
82
+ */
83
+ export function useRecommendationShelf({
84
+ campaignVrn,
85
+ enableRecommendations = false,
86
+ itemsContext = 'PDP',
87
+ }: UseRecommendationShelfArgs): UseRecommendationShelfResult {
88
+ const userId = useRecommendationUserId(campaignVrn)
89
+
90
+ const { data: productDetailPage } = usePDP()
91
+ const { items: cartItems } = useCart()
92
+
93
+ // Resolve the products used as context for the request from the configured
94
+ // source: the current PDP product, or the (deduplicated) cart items.
95
+ const contextProducts = useMemo(() => {
96
+ if (itemsContext === 'CART') {
97
+ return Array.from(
98
+ new Set(
99
+ cartItems
100
+ .map((item) => item.itemOffered.isVariantOf.productGroupID)
101
+ .filter(Boolean)
102
+ )
103
+ )
104
+ }
105
+
106
+ const pdpProduct = productDetailPage?.product?.isVariantOf?.productGroupID
107
+
108
+ return pdpProduct ? [pdpProduct] : []
109
+ }, [itemsContext, cartItems, productDetailPage])
110
+
111
+ // Gate the whole feature behind the CMS opt-in: when Recommendations is
112
+ // disabled the shelf never requests recommendations (nor renders).
113
+ const recommendationArgs = enableRecommendations
114
+ ? getRecommendationArguments(campaignVrn, {
115
+ userId,
116
+ contextProducts,
117
+ })
118
+ : null
119
+
120
+ const { data, isLoading, error } = useRecommendations(recommendationArgs)
121
+
122
+ if (error) {
123
+ // Don't log `recommendationArgs`: it carries the userId. Log only
124
+ // non-identifying context.
125
+ console.error(
126
+ 'Error fetching recommendations',
127
+ (error as { cause?: unknown })?.cause,
128
+ (error as { message?: string })?.message,
129
+ 'for campaign',
130
+ campaignVrn
131
+ )
132
+ }
133
+
134
+ return {
135
+ items: error ? EMPTY_ITEMS : (data?.products ?? EMPTY_ITEMS),
136
+ isLoading,
137
+ error,
138
+ campaign: data?.campaign,
139
+ correlationId: data?.correlationId,
140
+ }
141
+ }
@@ -1,4 +1,4 @@
1
- import type { RecommendationType } from './RecommendationShelf.types'
1
+ import type { RecommendationType } from './types'
2
2
 
3
3
  // Single source of truth for the campaign taxonomy: maps each Intelligent Search
4
4
  // campaign VRN type to its `RecommendationType`. The supported VRN types, the
@@ -29,23 +29,17 @@ vi.mock('src/components/skeletons/ProductShelfSkeleton', () => ({
29
29
  }))
30
30
 
31
31
  const useRecommendations = vi.hoisted(() => vi.fn())
32
- vi.mock(
33
- 'src/components/sections/RecommendationShelf/useRecommendations',
34
- () => ({
35
- useRecommendations,
36
- })
37
- )
32
+ vi.mock('src/sdk/recommendations/useRecommendations', () => ({
33
+ useRecommendations,
34
+ }))
38
35
 
39
36
  const useScreenResize = vi.hoisted(() => vi.fn())
40
37
  vi.mock('src/sdk/ui/useScreenResize', () => ({ default: useScreenResize }))
41
38
 
42
39
  const useRecommendationUserId = vi.hoisted(() => vi.fn())
43
- vi.mock(
44
- 'src/components/sections/RecommendationShelf/useRecommendationUserId',
45
- () => ({
46
- useRecommendationUserId,
47
- })
48
- )
40
+ vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({
41
+ useRecommendationUserId,
42
+ }))
49
43
 
50
44
  import { RecommendationShelf } from 'src/components/sections/RecommendationShelf/RecommendationShelf'
51
45
 
@@ -0,0 +1,199 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ const usePDP = vi.hoisted(() => vi.fn())
5
+ vi.mock('@faststore/core', () => ({ usePDP }))
6
+
7
+ const useCart = vi.hoisted(() => vi.fn())
8
+ vi.mock('src/sdk/cart', () => ({ useCart }))
9
+
10
+ const useRecommendations = vi.hoisted(() => vi.fn())
11
+ vi.mock('src/sdk/recommendations/useRecommendations', () => ({
12
+ useRecommendations,
13
+ }))
14
+
15
+ const useRecommendationUserId = vi.hoisted(() => vi.fn())
16
+ vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({
17
+ useRecommendationUserId,
18
+ }))
19
+
20
+ import { useRecommendationShelf } from 'src/sdk/recommendations/useRecommendationShelf'
21
+
22
+ const TOP_ITEMS_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
23
+ const CROSS_SELL_VRN = 'vrn:recommendations:acc:rec-cross-v2:campaign-1'
24
+
25
+ const cartItem = (productGroupID: string) => ({
26
+ itemOffered: { isVariantOf: { productGroupID } },
27
+ })
28
+
29
+ const campaignData = {
30
+ products: [
31
+ { id: 'p-1', isVariantOf: { productGroupID: 'p-1' } },
32
+ { id: 'p-2', isVariantOf: { productGroupID: 'p-2' } },
33
+ ],
34
+ correlationId: 'corr-1',
35
+ campaign: { id: 'camp-1', title: 'Recommended for you', type: 'TOP_ITEMS' },
36
+ }
37
+
38
+ const lastArgs = () => useRecommendations.mock.calls.at(-1)?.[0]
39
+
40
+ beforeEach(() => {
41
+ usePDP.mockReturnValue({ data: undefined })
42
+ useCart.mockReturnValue({ items: [] })
43
+ useRecommendationUserId.mockReturnValue('user-1')
44
+ useRecommendations.mockReturnValue({
45
+ data: campaignData,
46
+ isLoading: false,
47
+ error: null,
48
+ })
49
+ })
50
+
51
+ afterEach(() => {
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ describe('useRecommendationShelf', () => {
56
+ it('exposes the campaign payload when enabled', () => {
57
+ const { result } = renderHook(() =>
58
+ useRecommendationShelf({
59
+ campaignVrn: TOP_ITEMS_VRN,
60
+ enableRecommendations: true,
61
+ })
62
+ )
63
+
64
+ expect(result.current.items).toHaveLength(2)
65
+ expect(result.current.campaign?.title).toBe('Recommended for you')
66
+ expect(result.current.correlationId).toBe('corr-1')
67
+ })
68
+
69
+ it('never fetches while recommendations are disabled', () => {
70
+ renderHook(() => useRecommendationShelf({ campaignVrn: TOP_ITEMS_VRN }))
71
+
72
+ expect(lastArgs()).toBeNull()
73
+ })
74
+
75
+ it('never fetches before a userId resolves', () => {
76
+ useRecommendationUserId.mockReturnValue(null)
77
+
78
+ renderHook(() =>
79
+ useRecommendationShelf({
80
+ campaignVrn: TOP_ITEMS_VRN,
81
+ enableRecommendations: true,
82
+ })
83
+ )
84
+
85
+ expect(lastArgs()).toBeNull()
86
+ })
87
+
88
+ it('never fetches on a malformed campaign vrn', () => {
89
+ renderHook(() =>
90
+ useRecommendationShelf({
91
+ campaignVrn: 'not-a-vrn',
92
+ enableRecommendations: true,
93
+ })
94
+ )
95
+
96
+ expect(lastArgs()).toBeNull()
97
+ })
98
+
99
+ it('anchors cart-context campaigns on the deduplicated cart products', () => {
100
+ useCart.mockReturnValue({
101
+ items: [cartItem('pg-1'), cartItem('pg-2'), cartItem('pg-1')],
102
+ })
103
+
104
+ renderHook(() =>
105
+ useRecommendationShelf({
106
+ campaignVrn: CROSS_SELL_VRN,
107
+ enableRecommendations: true,
108
+ itemsContext: 'CART',
109
+ })
110
+ )
111
+
112
+ expect(lastArgs()).toEqual({
113
+ userId: 'user-1',
114
+ campaignVrn: CROSS_SELL_VRN,
115
+ products: ['pg-1', 'pg-2'],
116
+ })
117
+ })
118
+
119
+ it('skips cart-context campaigns while the cart is empty', () => {
120
+ renderHook(() =>
121
+ useRecommendationShelf({
122
+ campaignVrn: CROSS_SELL_VRN,
123
+ enableRecommendations: true,
124
+ itemsContext: 'CART',
125
+ })
126
+ )
127
+
128
+ expect(lastArgs()).toBeNull()
129
+ })
130
+
131
+ it('still fetches context-agnostic campaigns on an empty cart', () => {
132
+ renderHook(() =>
133
+ useRecommendationShelf({
134
+ campaignVrn: TOP_ITEMS_VRN,
135
+ enableRecommendations: true,
136
+ itemsContext: 'CART',
137
+ })
138
+ )
139
+
140
+ expect(lastArgs()).toEqual({
141
+ userId: 'user-1',
142
+ campaignVrn: TOP_ITEMS_VRN,
143
+ products: [],
144
+ })
145
+ })
146
+
147
+ it('follows the live cart context when not frozen', () => {
148
+ useCart.mockReturnValue({ items: [cartItem('pg-1')] })
149
+
150
+ const { rerender } = renderHook(() =>
151
+ useRecommendationShelf({
152
+ campaignVrn: CROSS_SELL_VRN,
153
+ enableRecommendations: true,
154
+ itemsContext: 'CART',
155
+ })
156
+ )
157
+
158
+ useCart.mockReturnValue({ items: [cartItem('pg-1'), cartItem('pg-2')] })
159
+ rerender()
160
+
161
+ expect(lastArgs()?.products).toEqual(['pg-1', 'pg-2'])
162
+ })
163
+
164
+ it('anchors PDP-context campaigns on the current product', () => {
165
+ usePDP.mockReturnValue({
166
+ data: { product: { isVariantOf: { productGroupID: 'pg-pdp' } } },
167
+ })
168
+
169
+ renderHook(() =>
170
+ useRecommendationShelf({
171
+ campaignVrn: CROSS_SELL_VRN,
172
+ enableRecommendations: true,
173
+ })
174
+ )
175
+
176
+ expect(lastArgs()?.products).toEqual(['pg-pdp'])
177
+ })
178
+
179
+ it('returns no items and logs without the userId on error', () => {
180
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
181
+ useRecommendations.mockReturnValue({
182
+ data: undefined,
183
+ isLoading: false,
184
+ error: new Error('boom'),
185
+ })
186
+
187
+ const { result } = renderHook(() =>
188
+ useRecommendationShelf({
189
+ campaignVrn: TOP_ITEMS_VRN,
190
+ enableRecommendations: true,
191
+ })
192
+ )
193
+
194
+ expect(result.current.items).toEqual([])
195
+ expect(result.current.error).toBeInstanceOf(Error)
196
+ expect(consoleError).toHaveBeenCalled()
197
+ expect(JSON.stringify(consoleError.mock.calls)).not.toContain('user-1')
198
+ })
199
+ })
@@ -14,7 +14,7 @@ vi.mock('src/utils/retry', () => ({
14
14
  retry: (fn: () => unknown) => Promise.resolve().then(fn),
15
15
  }))
16
16
 
17
- import { useRecommendationUserId } from 'src/components/sections/RecommendationShelf/useRecommendationUserId'
17
+ import { useRecommendationUserId } from 'src/sdk/recommendations/useRecommendationUserId'
18
18
 
19
19
  const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
20
20
 
@@ -7,7 +7,7 @@ vi.mock('src/sdk/graphql/useQuery', () => ({
7
7
  useQuery: useQueryMock,
8
8
  }))
9
9
 
10
- import { useRecommendations } from 'src/components/sections/RecommendationShelf/useRecommendations'
10
+ import { useRecommendations } from 'src/sdk/recommendations/useRecommendations'
11
11
 
12
12
  afterEach(() => {
13
13
  vi.clearAllMocks()
@@ -1,9 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
- import {
4
- getTypeFromVrn,
5
- isValidVrn,
6
- } from 'src/components/sections/RecommendationShelf/vrn'
3
+ import { getTypeFromVrn, isValidVrn } from 'src/sdk/recommendations/vrn'
7
4
 
8
5
  const vrn = (type: string) => `vrn:recommendations:account:${type}:campaign-1`
9
6