Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
74f916e
Hide signUp card for signed in users in newsletter
Marianaguardian Nov 26, 2025
6c6ffc1
removed unused imports
Marianaguardian Nov 26, 2025
177412b
fixed lint issues
Marianaguardian Nov 26, 2025
b999d89
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Nov 26, 2025
da6e644
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Nov 26, 2025
a5b9c4b
remove console
Marianaguardian Nov 26, 2025
7ad7a22
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Nov 26, 2025
6f12d91
updated from main branch
Marianaguardian Nov 26, 2025
bb54b31
updated idApiUrl type
Marianaguardian Nov 26, 2025
4927841
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Nov 26, 2025
8582503
Minor changes for testing
Marianaguardian Nov 28, 2025
b56c69f
Merge branch 'main' into 14890-show-or-hide-the-sign-up-newsletter-co…
Marianaguardian Nov 28, 2025
618f558
reverted test changes and correct response structure for user newslet…
Marianaguardian Dec 1, 2025
08ba7cc
Merge branch '14890-show-or-hide-the-sign-up-newsletter-component' of…
Marianaguardian Dec 1, 2025
6e620a5
Merge branch 'main' into 14890-show-or-hide-the-sign-up-newsletter-co…
Marianaguardian Dec 1, 2025
8eed71c
render newsletter list id of current news letter for testing and upda…
Marianaguardian Dec 1, 2025
97e519d
added console logs for debuging
Marianaguardian Dec 1, 2025
f0de441
render auth status and api reponse
Marianaguardian Dec 1, 2025
d5dbdaa
fix lint error
Marianaguardian Dec 1, 2025
6161c89
Removed debugging logs and cleaned up code
Marianaguardian Dec 1, 2025
67ce48a
Wrapped email signup wrapper in Island for runtime render
Marianaguardian Dec 1, 2025
c190059
Addressed PR comments
Marianaguardian Dec 2, 2025
0a9eb75
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Dec 2, 2025
c2f8825
resolved lint issue
Marianaguardian Dec 2, 2025
060eca6
added placeholder to avoid layout shifting
Marianaguardian Dec 3, 2025
57f734c
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Dec 3, 2025
52c929d
test case updated
Marianaguardian Dec 3, 2025
66dc49d
removed lint issues
Marianaguardian Dec 3, 2025
da534de
Merge branch 'main' into 14890-show-or-hide-the-sign-up-newsletter-co…
Marianaguardian Dec 4, 2025
b1607e5
addressed PR comments
Marianaguardian Dec 5, 2025
1c9f788
Merge branch 'main' of https://github.com/guardian/dotcom-rendering i…
Marianaguardian Dec 5, 2025
c8da91b
Merge branch '14890-show-or-hide-the-sign-up-newsletter-component' of…
Marianaguardian Dec 5, 2025
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
7 changes: 7 additions & 0 deletions dotcom-rendering/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ import { Picture } from '../src/components/Picture';
import { mockFetch } from '../src/lib/mockRESTCalls';
import { setABTests } from '../src/lib/useAB';
import { ConfigContextDecorator } from './decorators/configContextDecorator';
import { sb } from 'storybook/test';
import { Preview } from '@storybook/react-webpack5';
import {
globalColourScheme,
globalColourSchemeDecorator,
} from './toolbar/globalColourScheme';
import { palette as sourcePalette } from '@guardian/source/foundations';

// Set up module mocking for auth and newsletter subscription hooks
sb.mock(import('../src/lib/useNewsletterSubscription.ts'), { spy: true });
sb.mock(import('../src/lib/useAuthStatus.ts'), { spy: true });
sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });

// Prevent components being lazy rendered when we're taking Chromatic snapshots
Lazy.disabled = isChromatic();
Picture.disableLazyLoading = isChromatic();
Expand Down Expand Up @@ -64,6 +70,7 @@ style.appendChild(document.createTextNode(css));
},
page: {
ajaxUrl: 'https://api.nextgen.guardianapps.co.uk',
idApiUrl: 'https://idapi.theguardian.com',
},
tests: {},
switches: {},
Expand Down
4 changes: 4 additions & 0 deletions dotcom-rendering/src/components/ArticleBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Props = {
isRightToLeftLang?: boolean;
shouldHideAds: boolean;
serverTime?: number;
idApiUrl?: string;
};

const globalOlStyles = () => css`
Expand Down Expand Up @@ -139,6 +140,7 @@ export const ArticleBody = ({
editionId,
shouldHideAds,
serverTime,
idApiUrl,
}: Props) => {
const isInteractiveContent =
format.design === ArticleDesign.Interactive ||
Expand Down Expand Up @@ -208,6 +210,7 @@ export const ArticleBody = ({
editionId={editionId}
shouldHideAds={shouldHideAds}
serverTime={serverTime}
idApiUrl={idApiUrl}
/>
</div>
);
Expand Down Expand Up @@ -256,6 +259,7 @@ export const ArticleBody = ({
editionId={editionId}
contributionsServiceUrl={contributionsServiceUrl}
shouldHideAds={shouldHideAds}
idApiUrl={idApiUrl}
/>
</div>
{hasObserverPublicationTag && <ObserverFooter />}
Expand Down
77 changes: 77 additions & 0 deletions dotcom-rendering/src/components/EmailSignUpWrapper.importable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Breakpoint } from '@guardian/source/foundations';
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
import type { EmailSignUpProps } from './EmailSignup';
import { EmailSignup } from './EmailSignup';
import { InlineSkipToWrapper } from './InlineSkipToWrapper';
import { Island } from './Island';
import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage';
import { Placeholder } from './Placeholder';
import { SecureSignup } from './SecureSignup.importable';

/**
* Approximate heights of the EmailSignup component at different breakpoints.
*/
const PLACEHOLDER_HEIGHTS = new Map<Breakpoint, number>([
['mobile', 220],
['tablet', 180],
['desktop', 180],
]);

interface EmailSignUpWrapperProps extends EmailSignUpProps {
index: number;
listId: number;
identityName: string;
successDescription: string;
idApiUrl: string;
/** You should only set this to true if the privacy message will be shown elsewhere on the page */
hidePrivacyMessage?: boolean;
}

/**
* EmailSignUpWrapper as an importable island component.
*
* This component needs to be hydrated client-side because it uses
* the useNewsletterSubscription hook which depends on auth status
* to determine if the user is already subscribed to the newsletter.
*
* If the user is signed in and already subscribed, this component
* will return null (hide the signup form).
*/
export const EmailSignUpWrapper = ({
index,
listId,
idApiUrl,
...emailSignUpProps
}: EmailSignUpWrapperProps) => {
const isSubscribed = useNewsletterSubscription(listId, idApiUrl);

// Show placeholder while subscription status is being determined
// This prevents layout shift in both subscribed and non-subscribed cases
if (isSubscribed === undefined) {
return <Placeholder heights={PLACEHOLDER_HEIGHTS} />;
}

// Don't render if user is signed in and already subscribed
if (isSubscribed) {
return null;
}

return (
<InlineSkipToWrapper
id={`EmailSignup-skip-link-${index}`}
blockDescription="newsletter promotion"
>
<EmailSignup {...emailSignUpProps}>
<Island priority="feature" defer={{ until: 'visible' }}>
<SecureSignup
newsletterId={emailSignUpProps.identityName}
successDescription={emailSignUpProps.description}
/>
</Island>
{!emailSignUpProps.hidePrivacyMessage && (
<NewsletterPrivacyMessage />
)}
</EmailSignup>
</InlineSkipToWrapper>
);
};
67 changes: 64 additions & 3 deletions dotcom-rendering/src/components/EmailSignUpWrapper.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,96 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { EmailSignUpWrapper } from './EmailSignUpWrapper';
import { mocked } from 'storybook/test';
import { lazyFetchEmailWithTimeout } from '../lib/fetchEmail';
import { useIsSignedIn } from '../lib/useAuthStatus';
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
import { EmailSignUpWrapper } from './EmailSignUpWrapper.importable';

const meta: Meta<typeof EmailSignUpWrapper> = {
title: 'Components/EmailSignUpWrapper',
component: EmailSignUpWrapper,
};

type Story = StoryObj<typeof EmailSignUpWrapper>;

const defaultArgs = {
index: 10,
listId: 4147,
identityName: 'the-recap',
description:
'The best of our sports journalism from the past seven days and a heads-up on the weekends action',
"The best of our sports journalism from the past seven days and a heads-up on the weekend's action",
name: 'The Recap',
frequency: 'Weekly',
successDescription: "We'll send you The Recap every week",
theme: 'sport',
idApiUrl: 'https://idapi.theguardian.com',
} satisfies Story['args'];
type Story = StoryObj<typeof EmailSignUpWrapper>;

// Loading state - shows placeholder while auth status is being determined
// This prevents layout shift when subscription status is resolved
export const Placeholder: Story = {
args: {
hidePrivacyMessage: false,
...defaultArgs,
},
async beforeEach() {
mocked(useNewsletterSubscription).mockReturnValue(undefined);
},
};

// Default story - signed out user sees the signup form with email input
export const DefaultStory: Story = {
args: {
hidePrivacyMessage: true,
...defaultArgs,
},
async beforeEach() {
mocked(useNewsletterSubscription).mockReturnValue(false);
mocked(useIsSignedIn).mockReturnValue(false);
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
Promise.resolve(null),
);
},
};

export const DefaultStoryWithPrivacy: Story = {
args: {
hidePrivacyMessage: false,
...defaultArgs,
},
async beforeEach() {
mocked(useNewsletterSubscription).mockReturnValue(false);
mocked(useIsSignedIn).mockReturnValue(false);
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
Promise.resolve(null),
);
},
};

// User is signed in but NOT subscribed - email field is hidden, only signup button shows
export const SignedInNotSubscribed: Story = {
args: {
hidePrivacyMessage: false,
...defaultArgs,
},
async beforeEach() {
mocked(useNewsletterSubscription).mockReturnValue(false);
mocked(useIsSignedIn).mockReturnValue(true);
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
Promise.resolve('[email protected]'),
);
},
};

// User is signed in and IS subscribed - component returns null (hidden)
// Note: This story will render nothing as the component returns null when subscribed
export const SignedInAlreadySubscribed: Story = {
args: {
hidePrivacyMessage: false,
...defaultArgs,
},
async beforeEach() {
mocked(useNewsletterSubscription).mockReturnValue(true);
},
};

export default meta;
38 changes: 0 additions & 38 deletions dotcom-rendering/src/components/EmailSignUpWrapper.tsx

This file was deleted.

12 changes: 12 additions & 0 deletions dotcom-rendering/src/components/LiveBlock.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const VideoAsSecond = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -129,6 +130,7 @@ export const Title = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -198,6 +200,7 @@ export const Video = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -242,6 +245,7 @@ export const RichLink = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -277,6 +281,7 @@ export const FirstImage = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -338,6 +343,7 @@ export const ImageRoles = () => {
isSensitive={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -388,6 +394,7 @@ export const Thumbnail = () => {
isSensitive={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -424,6 +431,7 @@ export const ImageAndTitle = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -456,6 +464,7 @@ export const Updated = () => {
isPinnedPost={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -492,6 +501,7 @@ export const Contributor = () => {
isSensitive={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -526,6 +536,7 @@ export const NoAvatar = () => {
isSensitive={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down Expand Up @@ -563,6 +574,7 @@ export const TitleAndContributor = () => {
isSensitive={false}
editionId={'UK'}
shouldHideAds={false}
idApiUrl="https://idapi.theguardian.com"
/>
</Wrapper>
);
Expand Down
3 changes: 3 additions & 0 deletions dotcom-rendering/src/components/LiveBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Props = {
editionId: EditionId;
shouldHideAds: boolean;
serverTime?: number;
idApiUrl?: string;
};

export const LiveBlock = ({
Expand All @@ -46,6 +47,7 @@ export const LiveBlock = ({
editionId,
shouldHideAds,
serverTime,
idApiUrl,
}: Props) => {
if (block.elements.length === 0) return null;

Expand Down Expand Up @@ -91,6 +93,7 @@ export const LiveBlock = ({
isPinnedPost={isPinnedPost}
editionId={editionId}
shouldHideAds={shouldHideAds}
idApiUrl={idApiUrl}
/>
))}
<footer
Expand Down
Loading
Loading