@faststore/core 4.6.0-dev.1 → 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 (29) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +104 -98
  3. package/@generated/cached-operations.json +1 -1
  4. package/@generated/gql.ts +9 -9
  5. package/@generated/graphql.ts +18 -12
  6. package/@generated/persisted-documents.json +2 -2
  7. package/@generated/schema.graphql +6 -0
  8. package/CHANGELOG.md +13 -0
  9. package/package.json +6 -6
  10. package/src/components/sections/RecommendationShelf/README.md +4 -1
  11. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +11 -92
  12. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +3 -18
  13. package/src/sdk/cart/index.ts +15 -1
  14. package/src/sdk/cart/waitForSessionValidated.ts +43 -0
  15. package/src/sdk/recommendations/index.ts +17 -0
  16. package/src/sdk/recommendations/types.ts +17 -0
  17. package/src/sdk/recommendations/useRecommendationShelf.ts +141 -0
  18. package/src/{components/sections/RecommendationShelf → sdk/recommendations}/vrn.ts +1 -1
  19. package/src/sdk/session/index.ts +14 -0
  20. package/src/sdk/session/syncSalesChannelFromOrderForm.ts +68 -0
  21. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +6 -12
  22. package/test/sdk/cart/waitForSessionValidated.test.ts +96 -0
  23. package/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx +199 -0
  24. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.test.ts +1 -1
  25. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendations.test.ts +1 -1
  26. package/test/{components/RecommendationShelf → sdk/recommendations}/vrn.test.ts +1 -4
  27. package/test/sdk/session/syncSalesChannelFromOrderForm.test.ts +125 -0
  28. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.ts +0 -0
  29. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendations.ts +0 -0
@@ -1538,6 +1538,12 @@ export type StoreOrder = {
1538
1538
  acceptedOffer: Array<StoreOffer>;
1539
1539
  /** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */
1540
1540
  orderNumber: Scalars['String']['output'];
1541
+ /**
1542
+ * Sales channel of the underlying orderForm when FastStore adopted it because
1543
+ * the browser session was stale (e.g. after Quick Order). Clients should align
1544
+ * `session.channel` to this value. Null when no SC adoption happened.
1545
+ */
1546
+ salesChannel: Maybe<Scalars['String']['output']>;
1541
1547
  /** Indicates whether or not items with attachments should be split. */
1542
1548
  shouldSplitItem: Maybe<Scalars['Boolean']['output']>;
1543
1549
  };
@@ -2721,15 +2727,6 @@ export type Filter_FacetsFragment =
2721
2727
 
2722
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 }> };
2723
2729
 
2724
- export type ClientRecommendationsQueryQueryVariables = Exact<{
2725
- campaignVrn: Scalars['String']['input'];
2726
- userId: InputMaybe<Scalars['String']['input']>;
2727
- products: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
2728
- }>;
2729
-
2730
-
2731
- 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 } } };
2732
-
2733
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> }> }> };
2734
2731
 
2735
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 } };
@@ -2863,7 +2860,7 @@ export type ValidateCartMutationMutationVariables = Exact<{
2863
2860
  }>;
2864
2861
 
2865
2862
 
2866
- export type ValidateCartMutationMutation = { validateCart: { order: { orderNumber: string, shouldSplitItem: boolean | null, acceptedOffer: Array<{ quantity: number, price: number, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, isGift: boolean | null, priceToken: string | null, seller: { identifier: string }, itemOffered: { sku: string, name: string, unitMultiplier: number | null, gtin: string, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null } | null }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } }> }, messages: Array<{ text: string, status: StoreStatus }> } | null };
2863
+ export type ValidateCartMutationMutation = { validateCart: { order: { orderNumber: string, salesChannel: string | null, shouldSplitItem: boolean | null, acceptedOffer: Array<{ quantity: number, price: number, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, isGift: boolean | null, priceToken: string | null, seller: { identifier: string }, itemOffered: { sku: string, name: string, unitMultiplier: number | null, gtin: string, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null } | null }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } }> }, messages: Array<{ text: string, status: StoreStatus }> } | null };
2867
2864
 
2868
2865
  export type CartMessageFragment = { text: string, status: StoreStatus };
2869
2866
 
@@ -2988,6 +2985,15 @@ export type ClientProfileQueryQueryVariables = Exact<{
2988
2985
 
2989
2986
  export type ClientProfileQueryQuery = { profile: { addresses: Array<{ country: string | null, postalCode: string | null, geoCoordinate: Array<number | null> | null, city: string | null } | null> | null } | null };
2990
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
+
2991
2997
  export type ClientSearchSuggestionsQueryQueryVariables = Exact<{
2992
2998
  term: Scalars['String']['input'];
2993
2999
  selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>;
@@ -3549,7 +3555,6 @@ export const SearchEvent_MetadataFragmentDoc = new TypedDocumentString(`
3549
3555
  fuzzy
3550
3556
  }
3551
3557
  `, {"fragmentName":"SearchEvent_metadata"}) as unknown as TypedDocumentString<SearchEvent_MetadataFragment, unknown>;
3552
- export const ClientRecommendationsQueryDocument = {"__meta__":{"operationName":"ClientRecommendationsQuery","operationHash":"b227071b0388469d7b90d04cff4cbc11f8f134d4"}} as unknown as TypedDocumentString<ClientRecommendationsQueryQuery, ClientRecommendationsQueryQueryVariables>;
3553
3558
  export const ServerAccountPageQueryDocument = {"__meta__":{"operationName":"ServerAccountPageQuery","operationHash":"9baae331b75848a310fecb457e8c971ae27897ff"}} as unknown as TypedDocumentString<ServerAccountPageQueryQuery, ServerAccountPageQueryQueryVariables>;
3554
3559
  export const ServerCollectionPageQueryDocument = {"__meta__":{"operationName":"ServerCollectionPageQuery","operationHash":"d46841b30ae1f6350021b5cf02f253d56c848664"}} as unknown as TypedDocumentString<ServerCollectionPageQueryQuery, ServerCollectionPageQueryQueryVariables>;
3555
3560
  export const ServerProductQueryDocument = {"__meta__":{"operationName":"ServerProductQuery","operationHash":"b89e93519be01aebc01c402489a0ae640d38675a"}} as unknown as TypedDocumentString<ServerProductQueryQuery, ServerProductQueryQueryVariables>;
@@ -3565,7 +3570,7 @@ export const CancelOrderMutationDocument = {"__meta__":{"operationName":"CancelO
3565
3570
  export const ProcessOrderAuthorizationMutationDocument = {"__meta__":{"operationName":"ProcessOrderAuthorizationMutation","operationHash":"8c25d37c8d6e7c20ab21bb8a4f4e6a2fe320ea8d"}} as unknown as TypedDocumentString<ProcessOrderAuthorizationMutationMutation, ProcessOrderAuthorizationMutationMutationVariables>;
3566
3571
  export const ValidateUserDocument = {"__meta__":{"operationName":"ValidateUser","operationHash":"32f99c73c3de958b64d6bece1afe800469f54548"}} as unknown as TypedDocumentString<ValidateUserQuery, ValidateUserQueryVariables>;
3567
3572
  export const StartRecommendationSessionDocument = {"__meta__":{"operationName":"StartRecommendationSession","operationHash":"1def6438c0cd87b85002411ac7326c221f192583"}} as unknown as TypedDocumentString<StartRecommendationSessionMutation, StartRecommendationSessionMutationVariables>;
3568
- export const ValidateCartMutationDocument = {"__meta__":{"operationName":"ValidateCartMutation","operationHash":"8199d03debb0a6751ec53d8ee8987b78d0292acb"}} as unknown as TypedDocumentString<ValidateCartMutationMutation, ValidateCartMutationMutationVariables>;
3573
+ export const ValidateCartMutationDocument = {"__meta__":{"operationName":"ValidateCartMutation","operationHash":"3c71b0d3fc2bade14df68abfa271858beb361b12"}} as unknown as TypedDocumentString<ValidateCartMutationMutation, ValidateCartMutationMutationVariables>;
3569
3574
  export const ClientPickupPointsQueryDocument = {"__meta__":{"operationName":"ClientPickupPointsQuery","operationHash":"3fa04e88c811fcb5ece7206fd5aa745bdbc143a8"}} as unknown as TypedDocumentString<ClientPickupPointsQueryQuery, ClientPickupPointsQueryQueryVariables>;
3570
3575
  export const SubscribeToNewsletterDocument = {"__meta__":{"operationName":"SubscribeToNewsletter","operationHash":"feb7005103a859e2bc8cf2360d568806fd88deba"}} as unknown as TypedDocumentString<SubscribeToNewsletterMutation, SubscribeToNewsletterMutationVariables>;
3571
3576
  export const StartOrderEntryOperationMutationDocument = {"__meta__":{"operationName":"StartOrderEntryOperationMutation","operationHash":"78c50fbf9b85d03dbeac9b05b06405217f2ec440"}} as unknown as TypedDocumentString<StartOrderEntryOperationMutationMutation, StartOrderEntryOperationMutationMutationVariables>;
@@ -3580,6 +3585,7 @@ export const ClientProductQueryDocument = {"__meta__":{"operationName":"ClientPr
3580
3585
  export const ClientManyProductsQueryDocument = {"__meta__":{"operationName":"ClientManyProductsQuery","operationHash":"ee14fd92b4a04fc3751efcbfcb60b3886d912253"}} as unknown as TypedDocumentString<ClientManyProductsQueryQuery, ClientManyProductsQueryQueryVariables>;
3581
3586
  export const ClientManyProductsSelectedQueryDocument = {"__meta__":{"operationName":"ClientManyProductsSelectedQuery","operationHash":"7e1b06c167e411905ceac9db11c1a6a7892cfdd6"}} as unknown as TypedDocumentString<ClientManyProductsSelectedQueryQuery, ClientManyProductsSelectedQueryQueryVariables>;
3582
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>;
3583
3589
  export const ClientSearchSuggestionsQueryDocument = {"__meta__":{"operationName":"ClientSearchSuggestionsQuery","operationHash":"ee57cd392fdfde90620787c9954e2ecae47881f9"}} as unknown as TypedDocumentString<ClientSearchSuggestionsQueryQuery, ClientSearchSuggestionsQueryQueryVariables>;
3584
3590
  export const ClientTopSearchSuggestionsQueryDocument = {"__meta__":{"operationName":"ClientTopSearchSuggestionsQuery","operationHash":"e2385b0f11726d0068f96548f57a8dd441c064e3"}} as unknown as TypedDocumentString<ClientTopSearchSuggestionsQueryQuery, ClientTopSearchSuggestionsQueryQueryVariables>;
3585
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 }",
@@ -15,7 +14,7 @@
15
14
  "8c25d37c8d6e7c20ab21bb8a4f4e6a2fe320ea8d": "mutation ProcessOrderAuthorizationMutation($data: IProcessOrderAuthorization!) { processOrderAuthorization(data: $data) { isPendingForOtherAuthorizer ruleForAuthorization { dimensionId orderAuthorizationId rule { authorizationData { authorizers { authorizationDate email id type } requireAllApprovals } authorizedEmails doId id isUserAuthorized isUserNextAuthorizer name notification priority scoreInterval { accept deny } status timeout trigger { condition { conditionType description expression greatherThan lessThan } effect { description effectType funcPath } } } } } }",
16
15
  "32f99c73c3de958b64d6bece1afe800469f54548": "query ValidateUser { validateUser { isValid } }",
17
16
  "1def6438c0cd87b85002411ac7326c221f192583": "mutation StartRecommendationSession { startRecommendationSession }",
18
- "8199d03debb0a6751ec53d8ee8987b78d0292acb": "fragment CartItem on StoreOffer { isGift itemOffered { ...CartProductItem } listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } fragment CartMessage on StoreCartMessage { status text } 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 } mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) { validateCart(cart: $cart, session: $session) { messages { ...CartMessage } order { acceptedOffer { ...CartItem } orderNumber shouldSplitItem } } }",
17
+ "3c71b0d3fc2bade14df68abfa271858beb361b12": "fragment CartItem on StoreOffer { isGift itemOffered { ...CartProductItem } listPrice listPriceWithTaxes price priceToken priceWithTaxes quantity seller { identifier } } fragment CartMessage on StoreCartMessage { status text } 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 } mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) { validateCart(cart: $cart, session: $session) { messages { ...CartMessage } order { acceptedOffer { ...CartItem } orderNumber salesChannel shouldSplitItem } } }",
19
18
  "3fa04e88c811fcb5ece7206fd5aa745bdbc143a8": "query ClientPickupPointsQuery($geoCoordinates: IStoreGeoCoordinates) { pickupPoints(geoCoordinates: $geoCoordinates) { pickupPointDistances { address { city number postalCode state street } distance isActive pickupId pickupName } } }",
20
19
  "feb7005103a859e2bc8cf2360d568806fd88deba": "mutation SubscribeToNewsletter($data: IPersonNewsletter!) { subscribeToNewsletter(data: $data) { id } }",
21
20
  "78c50fbf9b85d03dbeac9b05b06405217f2ec440": "mutation StartOrderEntryOperationMutation($data: IOrderEntryOperation!) { startOrderEntryOperation(data: $data) { operationId } }",
@@ -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 } }",
@@ -1538,6 +1538,12 @@ type StoreOrder {
1538
1538
  acceptedOffer: [StoreOffer!]!
1539
1539
  """Indicates whether or not items with attachments should be split."""
1540
1540
  shouldSplitItem: Boolean
1541
+ """
1542
+ Sales channel of the underlying orderForm when FastStore adopted it because
1543
+ the browser session was stale (e.g. after Quick Order). Clients should align
1544
+ `session.channel` to this value. Null when no SC adoption happened.
1545
+ """
1546
+ salesChannel: String
1541
1547
  }
1542
1548
 
1543
1549
  type StoreOrderEntryMissingItem {
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
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
+
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)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **api:** address Sonar and harden session sales-channel sync ([5f6e8e9](https://github.com/vtex/faststore/commit/5f6e8e909def75f60c24cdaffae9303ed2c0a052))
15
+ - **api:** keep orderForm sales channel after stale validateCart sync ([09b051b](https://github.com/vtex/faststore/commit/09b051b80aaff7183adf7f5e86aece438bcd38f0))
16
+ - **api:** preserve orderForm sales channel in validateCart ([2336cda](https://github.com/vtex/faststore/commit/2336cda2c95970850dd0d29c1c18043ca8c9078a))
17
+ - **api:** sync adopted orderForm sales channel into session ([8943da7](https://github.com/vtex/faststore/commit/8943da7821f8abec08a39b7907a258bf156b05ee))
18
+
6
19
  # [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11)
7
20
 
8
21
  ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "4.6.0-dev.1",
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/api": "4.6.0-dev.1",
75
- "@faststore/ui": "4.6.0-dev.1",
76
- "@faststore/lighthouse": "4.6.0-dev.1",
77
- "@faststore/diagnostics": "4.6.0-dev.1",
78
- "@faststore/sdk": "4.6.0-dev.1"
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'
@@ -13,8 +13,10 @@ import type {
13
13
 
14
14
  import storeConfig from '../../../discovery.config'
15
15
  import { request } from '../graphql/request'
16
- import { sessionStore } from '../session'
16
+ import { hasValidatedSessionStore, sessionStore } from '../session'
17
+ import { syncSalesChannelFromOrderForm } from '../session/syncSalesChannelFromOrderForm'
17
18
  import { createValidationStore, useStore } from '../useStore'
19
+ import { waitForSessionValidated } from './waitForSessionValidated'
18
20
 
19
21
  export interface CartItem
20
22
  extends SDKCartItem,
@@ -33,6 +35,7 @@ export const ValidateCartMutation = gql(`
33
35
  validateCart(cart: $cart, session: $session) {
34
36
  order {
35
37
  orderNumber
38
+ salesChannel
36
39
  acceptedOffer {
37
40
  ...CartItem
38
41
  }
@@ -114,6 +117,8 @@ const getItemId = (item: Pick<CartItem, 'itemOffered' | 'seller' | 'price'>) =>
114
117
  .join('::')
115
118
 
116
119
  const validateCart = async (cart: Cart): Promise<Cart | null> => {
120
+ await waitForSessionValidated(hasValidatedSessionStore)
121
+
117
122
  const { validateCart: validated = null } = await request<
118
123
  ValidateCartMutationMutation,
119
124
  ValidateCartMutationMutationVariables
@@ -151,6 +156,15 @@ const validateCart = async (cart: Cart): Promise<Cart | null> => {
151
156
  },
152
157
  })
153
158
 
159
+ const adoptedSalesChannel = validated?.order?.salesChannel
160
+ if (adoptedSalesChannel) {
161
+ syncSalesChannelFromOrderForm(
162
+ adoptedSalesChannel,
163
+ () => sessionStore.read(),
164
+ (session) => sessionStore.setSilent(session)
165
+ )
166
+ }
167
+
154
168
  return (
155
169
  validated && {
156
170
  id: validated.order.orderNumber,
@@ -0,0 +1,43 @@
1
+ import type { Store } from '@faststore/sdk'
2
+
3
+ /** Max time to wait for the first validateSession before validating the cart. */
4
+ export const SESSION_VALIDATION_WAIT_MS = 3_000
5
+
6
+ /**
7
+ * Defers cart validation until session has been validated at least once.
8
+ * This reduces first-paint races between Session and Cart; sales-channel
9
+ * correctness after external flows (e.g. Quick Order) is enforced in the API
10
+ * by preserving/adopting the orderForm SC.
11
+ */
12
+ export const waitForSessionValidated = async (
13
+ hasValidatedSessionStore: Store<boolean>,
14
+ waitMs = SESSION_VALIDATION_WAIT_MS
15
+ ): Promise<void> => {
16
+ if (hasValidatedSessionStore.read()) {
17
+ return
18
+ }
19
+
20
+ await new Promise<void>((resolve) => {
21
+ const timeoutId = setTimeout(() => {
22
+ unsubscribe()
23
+ resolve()
24
+ }, waitMs)
25
+
26
+ const unsubscribe = hasValidatedSessionStore.subscribe((validated) => {
27
+ if (!validated) {
28
+ return
29
+ }
30
+
31
+ clearTimeout(timeoutId)
32
+ unsubscribe()
33
+ resolve()
34
+ })
35
+
36
+ // Race: validation may have finished between read() and subscribe()
37
+ if (hasValidatedSessionStore.read()) {
38
+ clearTimeout(timeoutId)
39
+ unsubscribe()
40
+ resolve()
41
+ }
42
+ })
43
+ }
@@ -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'