From 454178bade1d754efde262f95ad40a185799e3b4 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:46:39 -0500 Subject: [PATCH] fix(headless-ssr-commerce): fix client side recommendation refresh with a productId (#4793) https://coveord.atlassian.net/browse/KIT-3801 --- .../src/ssr-commerce/providers.tsx | 15 ++++++-- .../headless-recommendations.ssr.ts | 14 ++++++-- .../headless-recommendations.test.ts | 10 +++++- .../headless-recommendations.ts | 3 +- .../recommendations-actions.ts | 1 + .../recommendations-slice.test.ts | 9 +++++ .../recommendations/recommendations-slice.ts | 8 ++++- .../recommendations/recommendations-state.ts | 2 ++ .../app/products/[productId]/page.tsx | 35 +++++++++++++++++-- .../recommendations/viewed-together.tsx | 22 ++++++++++++ .../lib/commerce-engine-config.ts | 5 +++ .../lib/commerce-engine.ts | 1 + 12 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 packages/samples/headless-ssr-commerce/components/recommendations/viewed-together.tsx diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index 837deee04c3..bdaa232a320 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -15,7 +15,8 @@ import { Context, HydrateStaticStateOptions, ParameterManager, - Parameters, // Recommendations, + Parameters, + Recommendations, } from '@coveo/headless/ssr-commerce'; import {PropsWithChildren, useEffect, useState} from 'react'; import {ReactCommerceEngineDefinition} from './commerce-engine.js'; @@ -90,9 +91,17 @@ export function buildProviderWithDefinition< }; break; } - case Kind.Recommendations: - //KIT-3801: Done here + case Kind.Recommendations: { + const recommendations = getController( + controllers, + key + ); + + hydrateArguments[key] = { + productId: recommendations.state.productId, + }; break; + } } } diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index f4b194fc24c..7f7dcb9472e 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -2,6 +2,7 @@ import { recommendationInternalOptionKey, RecommendationOnlyControllerDefinitionWithProps, } from '../../../app/commerce-ssr-engine/types/common.js'; +import {Kind} from '../../../app/commerce-ssr-engine/types/kind.js'; import { RecommendationsOptions, RecommendationsState, @@ -43,15 +44,24 @@ export function defineRecommendations( [recommendationInternalOptionKey]: { ...props.options, }, - //@ts-expect-error fixed in KIT-3801 buildWithProps: ( engine, options: Omit ) => { const staticOptions = props.options; - return buildRecommendations(engine, { + const controller = buildRecommendations(engine, { options: {...staticOptions, ...options}, }); + const copy = Object.defineProperties( + {}, + Object.getOwnPropertyDescriptors(controller) + ); + + Object.defineProperty(copy, '_kind', { + value: Kind.Recommendations, + }); + + return copy as typeof controller & {_kind: Kind.Recommendations}; }, }; } diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts index b0a46156409..547f7815ca8 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.test.ts @@ -2,6 +2,7 @@ import {ChildProduct} from '../../../api/commerce/common/product.js'; import { fetchRecommendations, promoteChildToParent, + registerRecommendationsSlot, } from '../../../features/commerce/recommendations/recommendations-actions.js'; import {recommendationsReducer} from '../../../features/commerce/recommendations/recommendations-slice.js'; import {buildMockCommerceState} from '../../../test/mock-commerce-state.js'; @@ -23,7 +24,7 @@ describe('headless recommendations', () => { beforeEach(() => { engine = buildMockCommerceEngine(buildMockCommerceState()); recommendations = buildRecommendations(engine, { - options: {slotId: 'slot-id'}, + options: {slotId: 'slot-id', productId: 'product-id'}, }); }); @@ -48,4 +49,11 @@ describe('headless recommendations', () => { recommendations.refresh(); expect(fetchRecommendations).toHaveBeenCalled(); }); + + it('dispatches #registerRecommendationsSlot with the correct arguments', () => { + expect(registerRecommendationsSlot).toHaveBeenCalledWith({ + slotId: 'slot-id', + productId: 'product-id', + }); + }); }); diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts index 690d718478e..927d4aca2fe 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ts @@ -84,6 +84,7 @@ export interface RecommendationsState { error: CommerceAPIErrorStatusResponse | null; isLoading: boolean; responseId: string; + productId?: string; } export interface RecommendationsOptions { @@ -133,7 +134,7 @@ export function buildRecommendations( const {dispatch} = engine; const {slotId, productId} = props.options; - dispatch(registerRecommendationsSlot({slotId})); + dispatch(registerRecommendationsSlot({slotId, productId})); const recommendationStateSelector = createSelector( (state: CommerceEngineState) => state.recommendations[slotId]!, diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts b/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts index 37ca8d2c9df..c30a9a76049 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-actions.ts @@ -133,6 +133,7 @@ export const fetchMoreRecommendations = createAsyncThunk< export interface SlotIdPayload { slotId: string; + productId?: string; } export type RegisterRecommendationsSlotPayload = SlotIdPayload; diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts b/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts index 3c5e8aacd76..913a2bfe264 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-slice.test.ts @@ -53,6 +53,15 @@ describe('recommendation-slice', () => { ); expect(finalState[slotId]).toEqual(buildMockRecommendationsSlice()); }); + + it('when slot does not exists, sets the #productId to the one provided', () => { + const productId = 'some-product-id'; + const finalState = recommendationsReducer( + state, + registerRecommendationsSlot({slotId, productId}) + ); + expect(finalState[slotId]!.productId).toEqual(productId); + }); }); describe('on #fetchRecommendations.fulfilled', () => { diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts b/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts index b1e3a5885e5..2652440b36a 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-slice.ts @@ -27,12 +27,18 @@ export const recommendationsReducer = createReducer( builder .addCase(registerRecommendationsSlot, (state, action) => { const slotId = action.payload.slotId; + const productId = action.payload.productId; if (slotId in state) { return; } - state[slotId] = buildRecommendationsSlice(); + if (!productId) { + state[slotId] = buildRecommendationsSlice(); + return; + } + + state[slotId] = buildRecommendationsSlice({productId}); }) .addCase(fetchRecommendations.rejected, (state, action) => { handleError(state, action.meta.arg.slotId, action.payload); diff --git a/packages/headless/src/features/commerce/recommendations/recommendations-state.ts b/packages/headless/src/features/commerce/recommendations/recommendations-state.ts index ef4cfee5060..e7e3dc31800 100644 --- a/packages/headless/src/features/commerce/recommendations/recommendations-state.ts +++ b/packages/headless/src/features/commerce/recommendations/recommendations-state.ts @@ -7,6 +7,7 @@ export interface RecommendationsSlice { isLoading: boolean; responseId: string; products: Product[]; + productId?: string; } /** @@ -25,4 +26,5 @@ export const getRecommendationsSliceInitialState = isLoading: false, responseId: '', products: [], + productId: undefined, }); diff --git a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx index 5c5fac01fda..83118b26dbd 100644 --- a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx @@ -1,8 +1,15 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import ContextDropdown from '@/components/context-dropdown'; -import {StandaloneProvider} from '@/components/providers/providers'; +import { + RecommendationProvider, + StandaloneProvider, +} from '@/components/providers/providers'; +import ViewedTogether from '@/components/recommendations/viewed-together'; import StandaloneSearchBox from '@/components/standalone-search-box'; -import {standaloneEngineDefinition} from '@/lib/commerce-engine'; +import { + recommendationEngineDefinition, + standaloneEngineDefinition, +} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -38,6 +45,23 @@ export default async function ProductDescriptionPage({ }, }); + const recsStaticState = await recommendationEngineDefinition.fetchStaticState( + { + controllers: { + viewedTogether: {enabled: true, productId: params.productId}, + cart: {initialState: {items}}, + context: { + language: defaultContext.language, + country: defaultContext.country, + currency: defaultContext.currency, + view: { + url: `https://sports.barca.group/products/${params.productId}`, + }, + }, + }, + } + ); + const resolvedSearchParams = await searchParams; const price = Number(resolvedSearchParams.price) ?? NaN; const name = resolvedSearchParams.name ?? params.productId; @@ -54,6 +78,13 @@ export default async function ProductDescriptionPage({ {name} ({params.productId}) - ${price}


+ + + + ); } diff --git a/packages/samples/headless-ssr-commerce/components/recommendations/viewed-together.tsx b/packages/samples/headless-ssr-commerce/components/recommendations/viewed-together.tsx new file mode 100644 index 00000000000..87a655d14fc --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/recommendations/viewed-together.tsx @@ -0,0 +1,22 @@ +'use client'; + +import {useViewedTogether} from '@/lib/commerce-engine'; +import ProductButtonWithImage from '../product-button-with-image'; + +export default function ViewedTogether() { + const {state, methods} = useViewedTogether(); + + return ( + <> + + + ); +} diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts index b777e2a251c..95f70baf6d1 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts @@ -41,6 +41,11 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), + viewedTogether: defineRecommendations({ + options: { + slotId: 'ff5d8804-d398-4dd5-b68c-6a729c66454b', + }, + }), cart: defineCart(), searchBox: defineSearchBox(), context: defineContext(), diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index 175ecd99a32..67c02f0b17c 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -21,6 +21,7 @@ export const { usePagination, usePopularBought, usePopularViewed, + useViewedTogether, useProductView, useQueryTrigger, useRecentQueriesList,