Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,11 @@ const buildRRBannerConfigWith = ({
inNoShowMobileAboveNavVariant,
}),
show: ({ name, props }: ModuleData<BannerProps>) => (
<BannerComponent name={name} props={props} />
<BannerComponent
name={name}
props={props}
idApiUrl={idApiUrl}
/>
),
},
timeoutMillis: DEFAULT_BANNER_TIMEOUT_MILLIS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ jest.mock('../../lib/fetchEmail', () => ({
lazyFetchEmailWithTimeout: jest.fn(),
}));

jest.mock('../../lib/identity', () => ({
getAuthStatus: jest.fn(),
}));

jest.mock('../../lib/newsletterSubscriptionCache', () => ({
fetchNewsletterSubscriptions: jest.fn(),
getCachedSubscriptions: jest.fn(),
shouldInvalidateCache: jest.fn(),
}));

const mockGetCookie = jest.mocked(getCookie);
mockGetCookie.mockReturnValue(null);

const buildBannerPayload = async (
inAuxiaVariant: boolean,
Expand Down Expand Up @@ -47,6 +58,7 @@ const buildBannerPayload = async (
userConsent,
hideSupportMessagingForUser: false,
inAuxiaVariant,
userSubscriptions: [],
});
};

Expand All @@ -71,6 +83,54 @@ describe('getAuxiaRolloutMvtId', () => {
});

describe('banner request targeting payload', () => {
it.each([[[6042, 1234]], [[]]])(
'includes user subscriptions in the SDC payload: %j',
async (userSubscriptions: number[]) => {
const payload = await buildPayload({
isSignedIn: false,
contentType: 'Article',
shouldHideReaderRevenue: false,
isMinuteArticle: false,
isPaidContent: false,
isSensitive: false,
tags: [],
contributionsServiceUrl: 'https://contributions.example.com',
countryCode: 'GB',
optedOutOfArticleCount: false,
asyncArticleCounts: Promise.resolve(undefined),
userConsent: true,
hideSupportMessagingForUser: false,
userSubscriptions,
});

expect(payload.targeting).toHaveProperty(
'userSubscriptions',
userSubscriptions,
);
},
);

it('sends an empty subscription list when there are no subscriptions', async () => {
const payload = await buildPayload({
isSignedIn: true,
contentType: 'Article',
shouldHideReaderRevenue: false,
isMinuteArticle: false,
isPaidContent: false,
isSensitive: false,
tags: [],
contributionsServiceUrl: 'https://contributions.example.com',
countryCode: 'GB',
optedOutOfArticleCount: false,
asyncArticleCounts: Promise.resolve(undefined),
userConsent: true,
hideSupportMessagingForUser: false,
userSubscriptions: [],
});

expect(payload.targeting).toHaveProperty('userSubscriptions', []);
});

it.each([
['valid MVT ID', '123456', 123456],
['missing MVT ID', null, undefined],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ import {
import { getToday } from '../../lib/dailyArticleCount';
import { lazyFetchEmailWithTimeout } from '../../lib/fetchEmail';
import { getZIndex } from '../../lib/getZIndex';
import { getAuthStatus } from '../../lib/identity';
import type { CanShowResult } from '../../lib/messagePicker';
import {
fetchNewsletterSubscriptions,
getCachedSubscriptions,
shouldInvalidateCache,
} from '../../lib/newsletterSubscriptionCache';
import { getBanner } from '../../lib/sdcRequests';
import type { RenderingTarget } from '../../types/renderingTarget';
import type { TagType } from '../../types/tag';
Expand Down Expand Up @@ -61,6 +67,7 @@ type BuildPayloadProps = BaseProps & {
asyncArticleCounts: Promise<ArticleCounts | undefined>;
userConsent: boolean;
hideSupportMessagingForUser: boolean;
userSubscriptions: number[];
};

const MAX_AUXIA_ROLLOUT_MVT_ID = 1_000_000;
Expand Down Expand Up @@ -172,6 +179,7 @@ export const buildPayload = async ({
inHoldbackGroup,
inAuxiaVariant,
isSensitive,
userSubscriptions,
}: BuildPayloadProps): Promise<BannerPayload> => {
const getBrowserId = (): string | undefined => {
if (inAuxiaVariant !== true) {
Expand All @@ -197,6 +205,7 @@ export const buildPayload = async ({

const targeting: BannerPayload['targeting'] & {
auxiaRolloutMvtId?: number;
userSubscriptions: number[];
} = {
shouldHideReaderRevenue,
isPaidContent,
Expand All @@ -223,6 +232,7 @@ export const buildPayload = async ({
inHoldbackGroup,
browserId: getBrowserId(),
isSensitive,
userSubscriptions,
...(auxiaRolloutMvtId === undefined ? {} : { auxiaRolloutMvtId }),
};

Expand All @@ -248,6 +258,7 @@ export const canShowRRBanner: CanShowFunctionType<
signInBannerLastClosedAt,
abandonedBasketBannerLastClosedAt,
isPreview,
idApiUrl,
renderingTarget,
asyncArticleCounts,
ophanPageViewId,
Expand Down Expand Up @@ -310,6 +321,23 @@ export const canShowRRBanner: CanShowFunctionType<
const userConsent = await hasRequiredConsents();

const optedOutOfArticleCount = await hasOptedOutOfArticleCount();
let userSubscriptions: number[] = [];
if (isSignedIn) {
const authStatus = await getAuthStatus();
if (authStatus.kind === 'SignedIn') {
const userId = authStatus.idToken.claims.sub;
const cachedSubscriptions = getCachedSubscriptions();
userSubscriptions =
cachedSubscriptions &&
!shouldInvalidateCache(cachedSubscriptions, userId)
? cachedSubscriptions.listIds
: ((await fetchNewsletterSubscriptions(
idApiUrl,
userId,
authStatus,
)) ?? []);
}
}
const bannerPayload = await buildPayload({
isSignedIn,
countryCode,
Expand All @@ -329,6 +357,7 @@ export const canShowRRBanner: CanShowFunctionType<
asyncArticleCounts,
userConsent,
hideSupportMessagingForUser,
userSubscriptions,
pageId,
inHoldbackGroup,
inAuxiaVariant,
Expand Down Expand Up @@ -384,15 +413,22 @@ export const canShowRRBanner: CanShowFunctionType<
export const ReaderRevenueBanner = ({
name,
props,
}: ModuleData<BannerProps>) => {
idApiUrl,
}: ModuleData<BannerProps> & {
idApiUrl: string;
}) => {
const [Banner, setBanner] = useState<React.ElementType | null>(null);

useEffect(() => {
(name === 'SignInPromptBanner'
? /* webpackChunkName: "sign-in-prompt-banner" */
import(`../marketing/banners/signInPrompt/SignInPromptBanner`)
: /* webpackChunkName: "designable-banner" */
import(`../marketing/banners/designableBanner/Banner`)
: name === 'NewsletterBanner'
? /* webpackChunkName: "newsletter-banner" */
import(
`../marketing/banners/newsletterBanner/NewsletterBanner`
)
: /* webpackChunkName: "designable-banner" */
import(`../marketing/banners/designableBanner/Banner`)
)
.then((bannerModule: Record<string, React.ElementType>) => {
setBanner(() => bannerModule[name] ?? null);
Expand All @@ -416,7 +452,7 @@ export const ReaderRevenueBanner = ({
`}
>
{}
<Banner {...props} />
<Banner {...props} idApiUrl={idApiUrl} />
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
BannerId,
BannerRenderedContent,
BannerRenderProps,
PromotedNewsletter,
} from './types';

function getComponentIds(bannerId: BannerId) {
Expand Down Expand Up @@ -128,6 +129,11 @@ const withBannerData =
isCollapsible,
contributionsServiceUrl,
} = bannerProps;
const { idApiUrl, promotedNewsletter } =
bannerProps as CloseableBannerProps & {
idApiUrl?: string;
promotedNewsletter?: PromotedNewsletter;
};

const [hasBeenSeen, setNode] = useIsInView({
debounce: true,
Expand Down Expand Up @@ -410,6 +416,8 @@ const withBannerData =
design,
promoCodes,
isCollapsible,
idApiUrl,
promotedNewsletter,
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ import type {
} from '@guardian/support-dotcom-components/dist/shared/types';
import type { ChoiceCardsSettings } from '@guardian/support-dotcom-components/dist/shared/types/props/choiceCards';
import type { Tracking } from '@guardian/support-dotcom-components/dist/shared/types/props/shared';
import type { Newsletter } from '../../../../types/content';

export type BannerId = 'designable-banner' | 'sign-in-prompt-banner';
export type BannerId =
| 'designable-banner'
| 'sign-in-prompt-banner'
| 'newsletter-banner';

export type PromotedNewsletter = Pick<
Newsletter,
'listId' | 'identityName' | 'name' | 'frequency'
>;

export interface BannerEnrichedCta {
ctaUrl: string;
Expand Down Expand Up @@ -81,4 +90,6 @@ export interface BannerRenderProps {
design?: ConfigurableDesign;
promoCodes?: string[];
isCollapsible?: boolean;
idApiUrl?: string;
promotedNewsletter?: PromotedNewsletter;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { css } from '@emotion/react';
import {
palette,
space,
textSans17,
until,
} from '@guardian/source/foundations';
import {
Button,
Link,
SvgAlertRound,
SvgReload,
} from '@guardian/source/react-components';

const responseBoxStyles = css`
padding: 7px 12px 16px 7.5px;
border: 1px solid ${palette.neutral[86]};
border-radius: 4px;
margin-top: ${space[2]}px;
${until.tablet} {
margin: ${space[2]}px 10px 0 10px;
}
`;

const errorContainerStyles = css`
display: grid;
grid-template-columns: 32px minmax(0, 1fr);
column-gap: 7.5px;
color: ${palette.error[400]};

> svg {
width: 32px;
height: 32px;
fill: currentColor;
}
`;

const errorContentStyles = css`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: ${space[2]}px;
padding-top: 5px;
`;

const errorTextStyles = css`
${textSans17};
`;

const errorLinkStyles = css`
&,
&:visited,
&:hover,
&:active,
&:focus {
color: inherit;
font: inherit;
}
`;

const retryButtonStyles = css`
min-width: 240px;
min-height: 44px;
svg {
width: 24px;
height: 24px;
}
`;

export const FailureMessage = ({
onRetry,
text,
}: {
onRetry: React.MouseEventHandler<HTMLButtonElement>;
text: string;
}) => (
<div css={[responseBoxStyles, errorContainerStyles]}>
<SvgAlertRound />
<div css={errorContentStyles}>
<span css={errorTextStyles} role="alert">
{text} Please try again or contact{' '}
<Link
href="mailto:[email protected]"
target="_blank"
rel="noreferrer"
cssOverrides={errorLinkStyles}
>
[email protected]
</Link>
</span>
<Button
size="small"
priority="primary"
icon={<SvgReload />}
iconSide="right"
onClick={onRetry}
cssOverrides={retryButtonStyles}
>
Try again
</Button>
</div>
</div>
);
Loading
Loading