Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions dotcom-rendering/src/components/FeastContextualNudge.island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
} from '@guardian/source/foundations';
import { LinkButton } from '@guardian/source/react-components';
import { useEffect, useState } from 'react';
import {
BrazeBannersSystemDisplay,
BrazeBannersSystemPlacementId,
isPlacementStale,
} from '../lib/braze/BrazeBannersSystem';
import { useAB } from '../lib/useAB';
import { useBraze } from '../lib/useBraze';
import type { StageType } from '../types/config';
import type { RecipeBlockElement } from '../types/content';
import { useConfig } from './ConfigContext';
Expand All @@ -16,10 +22,8 @@ import { useConfig } from './ConfigContext';

const FEAST_BG = '#F3F3E9';
const FEAST_BG_DARK = '#2B2B26';
const FEAST_TEXT = sourcePalette.neutral[10];
const FEAST_TEXT_DARK = sourcePalette.neutral[100];
const FEAST_SUBTEXT = sourcePalette.neutral[20];
const FEAST_SUBTEXT_DARK = sourcePalette.neutral[93];
const FEAST_TEXT = sourcePalette.neutral[20];
const FEAST_TEXT_DARK = sourcePalette.neutral[93];
const FEAST_GREEN = '#68773C';
const FEAST_GREEN_HOVER = '#4d5c2b';
const FEAST_BORDER = FEAST_GREEN;
Expand All @@ -29,15 +33,13 @@ const FEAST_BORDER_DARK = FEAST_GREEN;

const lightVars = css`
--feast-nudge-bg: ${FEAST_BG};
--feast-nudge-heading: ${FEAST_TEXT};
--feast-nudge-subtext: ${FEAST_SUBTEXT};
--feast-nudge-text: ${FEAST_TEXT};
--feast-nudge-border: ${FEAST_BORDER};
`;

const darkVars = css`
--feast-nudge-bg: ${FEAST_BG_DARK};
--feast-nudge-heading: ${FEAST_TEXT_DARK};
--feast-nudge-subtext: ${FEAST_SUBTEXT_DARK};
--feast-nudge-text: ${FEAST_TEXT_DARK};
--feast-nudge-border: ${FEAST_BORDER_DARK};
`;

Expand All @@ -46,9 +48,12 @@ const darkVars = css`
const FEAST_ADJUST_TOKEN_PROD = '20wmhy68';
const FEAST_ADJUST_TOKEN_CODE = '20o7ykck';

const getAdjustToken = (stage: StageType): string => {
return stage === 'PROD' ? FEAST_ADJUST_TOKEN_PROD : FEAST_ADJUST_TOKEN_CODE;
};

const buildFeastLink = (recipeId: string, stage: StageType): string => {
const token =
stage === 'PROD' ? FEAST_ADJUST_TOKEN_PROD : FEAST_ADJUST_TOKEN_CODE;
const token = getAdjustToken(stage);
return `https://guardian-feast.go.link/recipe/${encodeURIComponent(
recipeId,
)}?adj_t=${encodeURIComponent(token)}`;
Expand Down Expand Up @@ -109,7 +114,7 @@ const buttonWrapperStyles = css`

const descriptionStyles = css`
${article15};
color: var(--feast-nudge-subtext);
color: var(--feast-nudge-text);
b {
font-weight: bold;
}
Expand All @@ -122,6 +127,8 @@ type FeastContextualNudgeProps = {
recipeArticleTitle: string;
pageId: string;
isDev: boolean;
nudgeIndex: number;
idApiUrl: string | undefined;
};

/**
Expand All @@ -142,13 +149,17 @@ export const FeastContextualNudge = ({
recipeArticleTitle,
pageId,
isDev,
nudgeIndex,
idApiUrl,
}: FeastContextualNudgeProps) => {
const abTests = useAB();
const isVariant =
abTests?.isUserInTestGroup('feast-recipe-nudge-v2', 'variant-1') ??
false;

const { darkModeAvailable } = useConfig();
const { darkModeAvailable, renderingTarget } = useConfig();

const { braze } = useBraze(idApiUrl ?? '', renderingTarget);

const [isStorybook, setIsStorybook] = useState(false);
useEffect(() => {
Expand All @@ -174,6 +185,58 @@ export const FeastContextualNudge = ({

if (!isVariant) return null;

// If idApiUrl is defined and Braze has a banner for this placement slot,
// render the Braze banner instead of the native nudge.
if (idApiUrl !== undefined) {
const placementId =
BrazeBannersSystemPlacementId[
`FeastContextualNudge${nudgeIndex}` as keyof typeof BrazeBannersSystemPlacementId
];

// Guard against stale placements: if the last requestBannersRefresh
// was rate-limited AND this placement has suppressOnStale: true in
// PLACEMENT_SUPPRESS_ON_STALE, skip getBanner() and fall through to
// the native nudge below.
//
// Each FeastContextualNudge placement ID has its own entry in
// PLACEMENT_SUPPRESS_ON_STALE — change any individual one to `true`
// to suppress that specific nudge on a failed refresh.
const banner = !isPlacementStale(placementId)
? (braze?.getBanner(placementId) ?? null)
: null;

if (banner && braze) {
return (
<div
aria-description={`Open the recipe ${title} in the Feast app`}
data-component="feast-contextual-nudge"
css={css`
margin: ${space[2]}px 0;
`}
>
<BrazeBannersSystemDisplay
meta={{
id: `feast-contextual-nudge-${nudgeIndex}`,
braze,
banner,
}}
idApiUrl={idApiUrl}
stage={stage}
context={{
recipe,
recipeArticleTitle,
pageId,
isDev,
nudgeIndex,
darkMode: darkModeAvailable,
adjustToken: getAdjustToken(stage),
}}
/>
</div>
);
}
}

return (
<div
aria-description={`Open the recipe ${title} in the Feast app`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const meta = {
recipeArticleTitle: "Meera Sodha's spring onion pancakes",
recipe: mockRecipe,
isDev: true,
nudgeIndex: 1,
idApiUrl: undefined,
},
parameters: {
chromatic: { viewports: [375, 740, 980] },
Expand Down
18 changes: 17 additions & 1 deletion dotcom-rendering/src/lib/ArticleRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,25 @@ export const ArticleRenderer = ({

const result: (JSX.Element | null | undefined)[] = [...preSection];

/**
* Distribute up to 5 Braze placement slots evenly across all sections.
* interval = ceil(sections.length / 5)
* A section at 0-based index i gets nudgeIndex = (i+1)/interval
* only when (i+1) is an exact multiple of interval (and ≤ 5).
* All other sections get nudgeIndex = null (no nudge rendered).
*/
const MAX_NUDGES = 5;
const interval = Math.ceil(sections.length / MAX_NUDGES);

for (const section of sections) {
const position = section.index + 1; // 1-based
const nudgeIndex =
position % interval === 0 ? position / interval : null;

result.push(
<Fragment key={`recipe-section-${section.index}`}>
{section.subheadingEl}
{section.recipe && (
{section.recipe && nudgeIndex !== null && (
<Island
priority="feature"
defer={{ until: 'visible' }}
Expand All @@ -176,6 +190,8 @@ export const ArticleRenderer = ({
pageId={pageId}
recipe={section.recipe}
recipeArticleTitle={section.recipeArticleTitle}
nudgeIndex={nudgeIndex}
idApiUrl={idApiUrl}
/>
</Island>
)}
Expand Down
Loading
Loading