From 4581b63e1b7255c4c341102aa61ae86b17f75c49 Mon Sep 17 00:00:00 2001 From: Juarez Mota Date: Mon, 13 Jul 2026 15:56:16 +0100 Subject: [PATCH 1/3] feat: refactor SDC integration and add preview-banner parameter support --- .../LiveblogGutterAskWrapper.island.tsx | 124 ++++++++---------- .../components/StickyBottomBanner.island.tsx | 4 +- .../ReaderRevenueBanner.tsx | 4 +- .../src/lib/readerRevenueDevUtils.ts | 14 ++ dotcom-rendering/src/lib/sdcRequests.test.ts | 18 +++ 5 files changed, 96 insertions(+), 68 deletions(-) diff --git a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx index ddbf8b1454d..14d6ba7f3f7 100644 --- a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx +++ b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx @@ -3,16 +3,13 @@ import { getCookie, isUndefined } from '@guardian/libs'; import type { ComponentEvent } from '@guardian/ophan-tracker-js'; import { palette, space } from '@guardian/source/foundations'; import { getGutterLiveblog } from '@guardian/support-dotcom-components'; -import type { - ModuleData, - ModuleDataResponse, -} from '@guardian/support-dotcom-components/dist/dotcom/types'; +import type { ModuleDataResponse } from '@guardian/support-dotcom-components/dist/dotcom/types'; import type { GutterPayload, GutterProps, } from '@guardian/support-dotcom-components/dist/shared/types'; import type { Tracking } from '@guardian/support-dotcom-components/dist/shared/types/props/shared'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { submitComponentEvent } from '../client/ophan/ophan'; import { shouldHideSupportMessaging } from '../lib/contributions'; import { useAB } from '../lib/useAB'; @@ -41,95 +38,90 @@ const LiveblogGutterAskBuilder = ({ pageUrl, pageId, }: LiveblogGutterAskBuilderProps) => { - const [gutterVariantResponse, setGutterVariantResponse] = - useState | null>(null); const [GutterWrapperComponent, setGutterWrapperComponent] = useState>(); + const [gutterVariantResponse, setGutterVariantResponse] = + useState>(); const { renderingTarget } = useConfig(); const isSignedIn = useIsSignedIn(); const abTests = useAB(); - // get gutter props - useEffect((): void => { - if (isUndefined(countryCode) || isSignedIn === 'Pending') { - return; - } - const tagIds = tags.map((tag) => tag.id); + const tagIds = tags.map((tag) => tag.id); - const hideSupportMessagingForUser = - shouldHideSupportMessaging(isSignedIn); - if (hideSupportMessagingForUser === 'Pending') { - // We don't yet know the user's supporter status - return; - } + const hideSupportMessagingForUser = shouldHideSupportMessaging( + isSignedIn as boolean, + ); - const inHoldbackGroup = - abTests?.isUserInTestGroup('growth-holdback-group', 'control') ?? - false; + const inHoldbackGroup = + abTests?.isUserInTestGroup('growth-holdback-group', 'control') ?? false; - // CALL the API - const payload: GutterPayload = { + // Build payload for SDC request + const payload: GutterPayload = useMemo( + () => ({ targeting: { showSupportMessaging: !hideSupportMessagingForUser, countryCode, mvtId: Number( getCookie({ name: 'GU_mvt_id', shouldMemoize: true }), ), - isSignedIn, + isSignedIn: isSignedIn as boolean, tagIds, sectionId, pageId, inHoldbackGroup, }, - }; + }), + [ + hideSupportMessagingForUser, + countryCode, + isSignedIn, + tagIds, + sectionId, + pageId, + inHoldbackGroup, + ], + ); + // Fetch gutter data from SDC + useEffect((): void => { getGutterLiveblog(contributionsServiceUrl, payload) - .then((response: ModuleDataResponse) => { - if (response.data) { - const { module } = response.data; - setGutterVariantResponse(module); - - import(`./marketing/gutters/GutterAskWrapper`) - .then((gutterModule) => { - setGutterWrapperComponent( - () => gutterModule.GutterAskWrapper, - ); - }) - .catch((err) => { - const msg = `Error importing GutterLiveBlog: ${String( - err, - )}`; - window.guardian.modules.sentry.reportError( - new Error(msg), - 'rr-gutter-liveblog-dynamic-import', - ); - }); - } + .then((response) => { + setGutterVariantResponse(response); }) - .catch((error) => { - const msg = `Error fetching gutter-ask tests: ${String(error)}`; - - console.log(msg); + .catch((err) => { + const msg = `Error fetching gutter data: ${String(err)}`; window.guardian.modules.sentry.reportError( new Error(msg), - 'rr-gutter-liveblog-ask-fetch', + 'rr-gutter-liveblog-fetch', ); }); - }, [ - countryCode, - isSignedIn, - contributionsServiceUrl, - pageViewId, - pageUrl, - sectionId, - tags, - pageId, - abTests, - ]); - - if (gutterVariantResponse && !isUndefined(GutterWrapperComponent)) { - const { props } = gutterVariantResponse; + }, [contributionsServiceUrl, payload]); + + // Dynamically import the wrapper component when we have data + useEffect((): void => { + if (gutterVariantResponse?.data) { + import(`./marketing/gutters/GutterAskWrapper`) + .then((gutterModule) => { + setGutterWrapperComponent( + () => gutterModule.GutterAskWrapper, + ); + }) + .catch((err) => { + const msg = `Error importing GutterLiveBlog: ${String( + err, + )}`; + window.guardian.modules.sentry.reportError( + new Error(msg), + 'rr-gutter-liveblog-dynamic-import', + ); + }); + } + }, [gutterVariantResponse]); + + if (gutterVariantResponse?.data && !isUndefined(GutterWrapperComponent)) { + const { module } = gutterVariantResponse.data; + const { props } = module; const tracking: Tracking = { ...props.tracking, diff --git a/dotcom-rendering/src/components/StickyBottomBanner.island.tsx b/dotcom-rendering/src/components/StickyBottomBanner.island.tsx index ffdacc541d7..a252e8bce97 100644 --- a/dotcom-rendering/src/components/StickyBottomBanner.island.tsx +++ b/dotcom-rendering/src/components/StickyBottomBanner.island.tsx @@ -371,13 +371,15 @@ export const StickyBottomBanner = ({ const hasForceBannerParam = window.location.search.includes('force-banner'); + const hasPreviewBannerParam = + window.location.search.includes('preview-banner'); const hasForceBrazeMessageParam = window.location.hash.includes( 'force-braze-message', ); let candidates: SlotConfig['candidates']; - if (hasForceBannerParam) { + if (hasForceBannerParam || hasPreviewBannerParam) { candidates = [CMP, readerRevenue]; } else if (hasForceBrazeMessageParam) { candidates = [CMP, brazeBannersSystem, brazeBanner]; diff --git a/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx index 3a7cf32b508..caaf2996745 100644 --- a/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx +++ b/dotcom-rendering/src/components/StickyBottomBanner/ReaderRevenueBanner.tsx @@ -232,8 +232,10 @@ export const canShowRRBanner: CanShowFunctionType< signInBannerLastClosedAt === undefined; const hasForceBannerParam = window.location.search.includes('force-banner'); + const hasPreviewBannerParam = + window.location.search.includes('preview-banner'); - if (!showSignInPrompt && !hasForceBannerParam) { + if (!showSignInPrompt && !hasForceBannerParam && !hasPreviewBannerParam) { // Don't show a banner if one was closed recently. This is to improve user experience by not showing banners on consecutive pageviews if ( recentlyClosedBanner(engagementBannerLastClosedAt) || diff --git a/dotcom-rendering/src/lib/readerRevenueDevUtils.ts b/dotcom-rendering/src/lib/readerRevenueDevUtils.ts index aab225ee3de..f867ad1bc07 100644 --- a/dotcom-rendering/src/lib/readerRevenueDevUtils.ts +++ b/dotcom-rendering/src/lib/readerRevenueDevUtils.ts @@ -176,6 +176,19 @@ const getForcedVariant = (type: 'epic' | 'banner'): string | null => { return null; }; +const getPreviewVariant = (type: 'epic' | 'banner'): string | null => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unnecessary-condition -- Safety in global, + if (URLSearchParams) { + const params = new URLSearchParams(window.location.search); + const value = params.get(`preview-${type}`); + if (value) { + return value; + } + } + + return null; +}; + export { changeGeolocation, showMeTheEpic, @@ -183,4 +196,5 @@ export { showNextVariant, showPreviousVariant, getForcedVariant, + getPreviewVariant, }; diff --git a/dotcom-rendering/src/lib/sdcRequests.test.ts b/dotcom-rendering/src/lib/sdcRequests.test.ts index c7b8caa1ee8..84e4b92b58e 100644 --- a/dotcom-rendering/src/lib/sdcRequests.test.ts +++ b/dotcom-rendering/src/lib/sdcRequests.test.ts @@ -143,6 +143,24 @@ describe('sdcRequests', () => { }, ); }); + + it('does not append force when neither is present', async () => { + const { getEpic } = await import('./sdcRequests'); + await getEpic('https://contributions.guardianapis.com', { + targeting: {}, + } as never); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://contributions.guardianapis.com/epic', + { + method: 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: '{"targeting":{}}', + }, + ); + }); }); describe('getBanner', () => { From f3fd6c185c1e940d13db8f7403499997a59dad93 Mon Sep 17 00:00:00 2001 From: Juarez Mota Date: Tue, 14 Jul 2026 17:28:14 +0100 Subject: [PATCH 2/3] fix: bump @guardian/support-dotcom-components from 10.0.1 to 10.1.0 and memoize tagIds in LiveblogGutterAskWrapper --- dotcom-rendering/package.json | 2 +- .../src/components/LiveblogGutterAskWrapper.island.tsx | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 307b1a0227b..746ffe07177 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -44,7 +44,7 @@ "@guardian/shimport": "1.0.2", "@guardian/source": "12.2.1", "@guardian/source-development-kitchen": "28.1.0", - "@guardian/support-dotcom-components": "10.0.1", + "@guardian/support-dotcom-components": "10.1.0", "@guardian/tsconfig": "catalog:", "@playwright/test": "1.60.0", "@sentry/browser": "10.52.0", diff --git a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx index 14d6ba7f3f7..bb0b5c283bf 100644 --- a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx +++ b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx @@ -47,7 +47,7 @@ const LiveblogGutterAskBuilder = ({ const isSignedIn = useIsSignedIn(); const abTests = useAB(); - const tagIds = tags.map((tag) => tag.id); + const tagIds = useMemo(() => tags.map((tag) => tag.id), [tags]); const hideSupportMessagingForUser = shouldHideSupportMessaging( isSignedIn as boolean, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc8d1c4a824..849cd554a74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -363,8 +363,8 @@ importers: specifier: 28.1.0 version: 28.1.0(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@guardian/libs@32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3))(@guardian/source@12.2.1(@emotion/react@11.14.0(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@6.0.3))(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@6.0.3) '@guardian/support-dotcom-components': - specifier: 10.0.1 - version: 10.0.1(@guardian/libs@32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3))(@guardian/ophan-tracker-js@4.0.2)(zod@4.1.12) + specifier: 10.1.0 + version: 10.1.0(@guardian/libs@32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3))(@guardian/ophan-tracker-js@4.0.2)(zod@4.1.12) '@guardian/tsconfig': specifier: 'catalog:' version: 1.0.1 @@ -2661,8 +2661,8 @@ packages: typescript: optional: true - '@guardian/support-dotcom-components@10.0.1': - resolution: {integrity: sha512-lxV3Zf5crtOBN8qZd1CO4ELn7FT2MYYUsE/R/m4f0kyfilR8pvemMRT2F0SxVaGLUsTM2Q7Pz5ebRbSOjpQPog==} + '@guardian/support-dotcom-components@10.1.0': + resolution: {integrity: sha512-jhykTjlqoDg1Ihz6TIWrteUp/rn4TQbU+nGX8IYH2zhzaTvp8t3fMXpJf6TBPlqPkoETcHY21B3Z2vc4HpZEkQ==} peerDependencies: '@guardian/libs': ^22.0.0 '@guardian/ophan-tracker-js': 2.8.0 @@ -12032,7 +12032,7 @@ snapshots: react: 18.3.1 typescript: 6.0.3 - '@guardian/support-dotcom-components@10.0.1(@guardian/libs@32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3))(@guardian/ophan-tracker-js@4.0.2)(zod@4.1.12)': + '@guardian/support-dotcom-components@10.1.0(@guardian/libs@32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3))(@guardian/ophan-tracker-js@4.0.2)(zod@4.1.12)': dependencies: '@guardian/libs': 32.0.0(@guardian/ophan-tracker-js@4.0.2)(tslib@2.6.2)(typescript@6.0.3) '@guardian/ophan-tracker-js': 4.0.2 From 55e36d419a4e1ad3635dac0c4b8ded8a2e0658f8 Mon Sep 17 00:00:00 2001 From: Juarez Mota Date: Wed, 15 Jul 2026 13:33:57 +0100 Subject: [PATCH 3/3] refactor: handle pending sign-in state in LiveblogGutterAskWrapper to prevent premature gutter data fetching --- .../LiveblogGutterAskWrapper.island.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx index bb0b5c283bf..1afba4e15af 100644 --- a/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx +++ b/dotcom-rendering/src/components/LiveblogGutterAskWrapper.island.tsx @@ -49,9 +49,10 @@ const LiveblogGutterAskBuilder = ({ const tagIds = useMemo(() => tags.map((tag) => tag.id), [tags]); - const hideSupportMessagingForUser = shouldHideSupportMessaging( - isSignedIn as boolean, - ); + const hideSupportMessagingForUser = + isSignedIn === 'Pending' + ? 'Pending' + : shouldHideSupportMessaging(isSignedIn); const inHoldbackGroup = abTests?.isUserInTestGroup('growth-holdback-group', 'control') ?? false; @@ -60,12 +61,15 @@ const LiveblogGutterAskBuilder = ({ const payload: GutterPayload = useMemo( () => ({ targeting: { - showSupportMessaging: !hideSupportMessagingForUser, + showSupportMessaging: + hideSupportMessagingForUser === 'Pending' + ? false + : !hideSupportMessagingForUser, countryCode, mvtId: Number( getCookie({ name: 'GU_mvt_id', shouldMemoize: true }), ), - isSignedIn: isSignedIn as boolean, + isSignedIn: isSignedIn === 'Pending' ? false : isSignedIn, tagIds, sectionId, pageId, @@ -85,6 +89,11 @@ const LiveblogGutterAskBuilder = ({ // Fetch gutter data from SDC useEffect((): void => { + if (hideSupportMessagingForUser === 'Pending') { + // We don't yet know the user's supporter status + return; + } + getGutterLiveblog(contributionsServiceUrl, payload) .then((response) => { setGutterVariantResponse(response); @@ -96,7 +105,7 @@ const LiveblogGutterAskBuilder = ({ 'rr-gutter-liveblog-fetch', ); }); - }, [contributionsServiceUrl, payload]); + }, [contributionsServiceUrl, payload, hideSupportMessagingForUser]); // Dynamically import the wrapper component when we have data useEffect((): void => {