From 2495c312d2be8a345542f56dba4f1ee54520125c Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 15 Jun 2026 17:31:17 -0300 Subject: [PATCH 1/3] fix: disableOffers now suppresses product:price:amount in SSR Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 3 +++ react/ProductOpenGraph.tsx | 5 ++--- react/hooks/useAppSettings.ts | 5 ++++- react/queries/getSettings.graphql | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f2487..0968025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed +- `disableOffers` setting now correctly suppresses `product:price:amount` during SSR. + ## [1.4.1] - 2025-08-13 ### Fixed diff --git a/react/ProductOpenGraph.tsx b/react/ProductOpenGraph.tsx index b407f76..f6d8d55 100644 --- a/react/ProductOpenGraph.tsx +++ b/react/ProductOpenGraph.tsx @@ -21,12 +21,11 @@ interface MetaTag { } function ProductOpenGraph() { - const { disableOffers } = useAppSettings() + const { disableOffers, loading: settingsLoading } = useAppSettings() const productContext = useContext(ProductContext) as ProductContext const runtime = useRuntime() as RenderContext - const hasValue = productContext?.product - if (!hasValue) { + if (settingsLoading || !productContext?.product) { return null } diff --git a/react/hooks/useAppSettings.ts b/react/hooks/useAppSettings.ts index 787fbd9..ce48da2 100644 --- a/react/hooks/useAppSettings.ts +++ b/react/hooks/useAppSettings.ts @@ -6,21 +6,24 @@ const DEFAULT_DISABLE_OFFERS = false interface Settings { disableOffers: boolean + loading: boolean } const useAppSettings = (): Settings => { - const { data } = useQuery(GET_SETTINGS, { ssr: false }) + const { data, loading } = useQuery(GET_SETTINGS, { ssr: true }) if (data?.publicSettingsForApp?.message) { const { disableOffers } = JSON.parse(data.publicSettingsForApp.message) return { disableOffers: disableOffers || DEFAULT_DISABLE_OFFERS, + loading: false, } } return { disableOffers: DEFAULT_DISABLE_OFFERS, + loading, } } diff --git a/react/queries/getSettings.graphql b/react/queries/getSettings.graphql index 3552687..2f5f68a 100644 --- a/react/queries/getSettings.graphql +++ b/react/queries/getSettings.graphql @@ -3,4 +3,4 @@ query getSettings { @context(provider: "vtex.apps-graphql") { message } -} \ No newline at end of file +} From 5d894cf834d6faa9155a96d02ded49931c718988 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 22 Jun 2026 15:06:59 -0300 Subject: [PATCH 2/3] fix: address Copilot review comments on disableOffers SSR fix - Gate settingsLoading only during SSR (!canUseDOM) to avoid blocking client renders and breaking tests that lack an Apollo settings mock - Export canUseDOM=true from the render-runtime mock so the guard is correctly bypassed in the test environment - Wrap JSON.parse in try/catch in useAppSettings to prevent a malformed settings payload from crashing SSR with a 500 - Add regression tests: disableOffers=true omits product:price:amount, disableOffers=false keeps it Co-Authored-By: Claude Sonnet 4.6 --- react/ProductOpenGraph.tsx | 2 +- react/__mocks__/vtex.render-runtime.tsx | 2 ++ react/__tests__/ProductOpenGraph.tsx | 38 +++++++++++++++++++++++++ react/hooks/useAppSettings.ts | 14 +++++---- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/react/ProductOpenGraph.tsx b/react/ProductOpenGraph.tsx index f6d8d55..0b16286 100644 --- a/react/ProductOpenGraph.tsx +++ b/react/ProductOpenGraph.tsx @@ -25,7 +25,7 @@ function ProductOpenGraph() { const productContext = useContext(ProductContext) as ProductContext const runtime = useRuntime() as RenderContext - if (settingsLoading || !productContext?.product) { + if ((!canUseDOM && settingsLoading) || !productContext?.product) { return null } diff --git a/react/__mocks__/vtex.render-runtime.tsx b/react/__mocks__/vtex.render-runtime.tsx index c198ef8..71065ef 100644 --- a/react/__mocks__/vtex.render-runtime.tsx +++ b/react/__mocks__/vtex.render-runtime.tsx @@ -21,6 +21,8 @@ interface MetaTag { content: string } +export const canUseDOM = true + export const useRuntime = () => ({ culture: { locale: 'en-US', diff --git a/react/__tests__/ProductOpenGraph.tsx b/react/__tests__/ProductOpenGraph.tsx index c00b3e7..2af7afe 100644 --- a/react/__tests__/ProductOpenGraph.tsx +++ b/react/__tests__/ProductOpenGraph.tsx @@ -3,6 +3,16 @@ import { render } from '@vtex/test-tools/react' import { ProductContext } from 'vtex.product-context' import ProductOpenGraph from '../ProductOpenGraph' +import useAppSettings from '../hooks/useAppSettings' + +jest.mock('../hooks/useAppSettings', () => ({ + __esModule: true, + default: jest.fn(() => ({ disableOffers: false, loading: false })), +})) + +const mockUseAppSettings = useAppSettings as jest.MockedFunction< + typeof useAppSettings +> const renderComponent = (productContext: object) => { const helpers = render( @@ -14,6 +24,34 @@ const renderComponent = (productContext: object) => { return helpers } +const priceProductContext = { + product: {}, + selectedItem: { + images: [], + sellers: [ + { + commertialOffer: { + spotPrice: 10, + AvailableQuantity: 1, + }, + }, + ], + }, +} + +test('should not render product:price:amount when disableOffers is true', () => { + mockUseAppSettings.mockReturnValueOnce({ disableOffers: true, loading: false }) + const { queryByTestId } = renderComponent(priceProductContext) + + expect(queryByTestId('product:price:amount')).toBeNull() +}) + +test('should render product:price:amount when disableOffers is false', () => { + const { getByTestId } = renderComponent(priceProductContext) + + expect(getByTestId('product:price:amount').innerHTML).toBe('10') +}) + test('should have a brand', () => { const brand = 'Nike' const productContext = { diff --git a/react/hooks/useAppSettings.ts b/react/hooks/useAppSettings.ts index ce48da2..ae8aa31 100644 --- a/react/hooks/useAppSettings.ts +++ b/react/hooks/useAppSettings.ts @@ -13,11 +13,15 @@ const useAppSettings = (): Settings => { const { data, loading } = useQuery(GET_SETTINGS, { ssr: true }) if (data?.publicSettingsForApp?.message) { - const { disableOffers } = JSON.parse(data.publicSettingsForApp.message) - - return { - disableOffers: disableOffers || DEFAULT_DISABLE_OFFERS, - loading: false, + try { + const { disableOffers } = JSON.parse(data.publicSettingsForApp.message) + + return { + disableOffers: disableOffers ?? DEFAULT_DISABLE_OFFERS, + loading: false, + } + } catch { + return { disableOffers: DEFAULT_DISABLE_OFFERS, loading: false } } } From eedcea64539b029c4493a9ca212d3d53b345df80 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 22 Jun 2026 15:13:08 -0300 Subject: [PATCH 3/3] style: expand inline object in mockReturnValueOnce to multi-line Fixes prettier lint error in the disableOffers regression test. Co-Authored-By: Claude Sonnet 4.6 --- react/__tests__/ProductOpenGraph.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/react/__tests__/ProductOpenGraph.tsx b/react/__tests__/ProductOpenGraph.tsx index 2af7afe..598f6a0 100644 --- a/react/__tests__/ProductOpenGraph.tsx +++ b/react/__tests__/ProductOpenGraph.tsx @@ -40,7 +40,10 @@ const priceProductContext = { } test('should not render product:price:amount when disableOffers is true', () => { - mockUseAppSettings.mockReturnValueOnce({ disableOffers: true, loading: false }) + mockUseAppSettings.mockReturnValueOnce({ + disableOffers: true, + loading: false, + }) const { queryByTestId } = renderComponent(priceProductContext) expect(queryByTestId('product:price:amount')).toBeNull()