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 (
+ <>
+
+ {state.headline}
+ {state.products.map((product) => (
+ -
+
+
+ ))}
+
+
+ >
+ );
+}
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,