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 ddbf8b1454d..1afba4e15af 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,99 @@ 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 = useMemo(() => tags.map((tag) => tag.id), [tags]); - const hideSupportMessagingForUser = - shouldHideSupportMessaging(isSignedIn); - if (hideSupportMessagingForUser === 'Pending') { - // We don't yet know the user's supporter status - return; - } + const hideSupportMessagingForUser = + isSignedIn === 'Pending' + ? 'Pending' + : shouldHideSupportMessaging(isSignedIn); - 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, + showSupportMessaging: + hideSupportMessagingForUser === 'Pending' + ? false + : !hideSupportMessagingForUser, countryCode, mvtId: Number( getCookie({ name: 'GU_mvt_id', shouldMemoize: true }), ), - isSignedIn, + isSignedIn: isSignedIn === 'Pending' ? false : isSignedIn, tagIds, sectionId, pageId, inHoldbackGroup, }, - }; + }), + [ + hideSupportMessagingForUser, + countryCode, + isSignedIn, + tagIds, + sectionId, + pageId, + inHoldbackGroup, + ], + ); + + // 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: 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, hideSupportMessagingForUser]); + + // 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', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fa73587ddb..b793f11b3bf 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 @@ -12031,7 +12031,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