Skip to content

Commit bfadeae

Browse files
Merge pull request #14929 from guardian/14890-show-or-hide-the-sign-up-newsletter-component
14890 show or hide the sign up newsletter component
2 parents f1c2b54 + 665cc96 commit bfadeae

21 files changed

+501
-43
lines changed

dotcom-rendering/.storybook/preview.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ import { Picture } from '../src/components/Picture';
1212
import { mockFetch } from '../src/lib/mockRESTCalls';
1313
import { setABTests } from '../src/lib/useAB';
1414
import { ConfigContextDecorator } from './decorators/configContextDecorator';
15+
import { sb } from 'storybook/test';
1516
import { Preview } from '@storybook/react-webpack5';
1617
import {
1718
globalColourScheme,
1819
globalColourSchemeDecorator,
1920
} from './toolbar/globalColourScheme';
2021
import { palette as sourcePalette } from '@guardian/source/foundations';
2122

23+
// Set up module mocking for auth and newsletter subscription hooks
24+
sb.mock(import('../src/lib/useNewsletterSubscription.ts'), { spy: true });
25+
sb.mock(import('../src/lib/useAuthStatus.ts'), { spy: true });
26+
sb.mock(import('../src/lib/fetchEmail.ts'), { spy: true });
27+
2228
// Prevent components being lazy rendered when we're taking Chromatic snapshots
2329
Lazy.disabled = isChromatic();
2430
Picture.disableLazyLoading = isChromatic();
@@ -64,6 +70,7 @@ style.appendChild(document.createTextNode(css));
6470
},
6571
page: {
6672
ajaxUrl: 'https://api.nextgen.guardianapps.co.uk',
73+
idApiUrl: 'https://idapi.theguardian.com',
6774
},
6875
tests: {},
6976
switches: {},

dotcom-rendering/src/components/ArticleBody.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Props = {
5555
isRightToLeftLang?: boolean;
5656
shouldHideAds: boolean;
5757
serverTime?: number;
58+
idApiUrl?: string;
5859
};
5960

6061
const globalOlStyles = () => css`
@@ -139,6 +140,7 @@ export const ArticleBody = ({
139140
editionId,
140141
shouldHideAds,
141142
serverTime,
143+
idApiUrl,
142144
}: Props) => {
143145
const isInteractiveContent =
144146
format.design === ArticleDesign.Interactive ||
@@ -208,6 +210,7 @@ export const ArticleBody = ({
208210
editionId={editionId}
209211
shouldHideAds={shouldHideAds}
210212
serverTime={serverTime}
213+
idApiUrl={idApiUrl}
211214
/>
212215
</div>
213216
);
@@ -256,6 +259,7 @@ export const ArticleBody = ({
256259
editionId={editionId}
257260
contributionsServiceUrl={contributionsServiceUrl}
258261
shouldHideAds={shouldHideAds}
262+
idApiUrl={idApiUrl}
259263
/>
260264
</div>
261265
{hasObserverPublicationTag && <ObserverFooter />}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { Breakpoint } from '@guardian/source/foundations';
2+
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
3+
import type { EmailSignUpProps } from './EmailSignup';
4+
import { EmailSignup } from './EmailSignup';
5+
import { InlineSkipToWrapper } from './InlineSkipToWrapper';
6+
import { Island } from './Island';
7+
import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage';
8+
import { Placeholder } from './Placeholder';
9+
import { SecureSignup } from './SecureSignup.importable';
10+
11+
/**
12+
* Approximate heights of the EmailSignup component at different breakpoints.
13+
*/
14+
const PLACEHOLDER_HEIGHTS = new Map<Breakpoint, number>([
15+
['mobile', 220],
16+
['tablet', 180],
17+
['desktop', 180],
18+
]);
19+
20+
interface EmailSignUpWrapperProps extends EmailSignUpProps {
21+
index: number;
22+
listId: number;
23+
identityName: string;
24+
successDescription: string;
25+
idApiUrl: string;
26+
/** You should only set this to true if the privacy message will be shown elsewhere on the page */
27+
hidePrivacyMessage?: boolean;
28+
}
29+
30+
/**
31+
* EmailSignUpWrapper as an importable island component.
32+
*
33+
* This component needs to be hydrated client-side because it uses
34+
* the useNewsletterSubscription hook which depends on auth status
35+
* to determine if the user is already subscribed to the newsletter.
36+
*
37+
* If the user is signed in and already subscribed, this component
38+
* will return null (hide the signup form).
39+
*/
40+
export const EmailSignUpWrapper = ({
41+
index,
42+
listId,
43+
idApiUrl,
44+
...emailSignUpProps
45+
}: EmailSignUpWrapperProps) => {
46+
const isSubscribed = useNewsletterSubscription(listId, idApiUrl);
47+
48+
// Show placeholder while subscription status is being determined
49+
// This prevents layout shift in both subscribed and non-subscribed cases
50+
if (isSubscribed === undefined) {
51+
return <Placeholder heights={PLACEHOLDER_HEIGHTS} />;
52+
}
53+
54+
// Don't render if user is signed in and already subscribed
55+
if (isSubscribed) {
56+
return null;
57+
}
58+
59+
return (
60+
<InlineSkipToWrapper
61+
id={`EmailSignup-skip-link-${index}`}
62+
blockDescription="newsletter promotion"
63+
>
64+
<EmailSignup {...emailSignUpProps}>
65+
<Island priority="feature" defer={{ until: 'visible' }}>
66+
<SecureSignup
67+
newsletterId={emailSignUpProps.identityName}
68+
successDescription={emailSignUpProps.description}
69+
/>
70+
</Island>
71+
{!emailSignUpProps.hidePrivacyMessage && (
72+
<NewsletterPrivacyMessage />
73+
)}
74+
</EmailSignup>
75+
</InlineSkipToWrapper>
76+
);
77+
};
Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,96 @@
11
import type { Meta, StoryObj } from '@storybook/react-webpack5';
2-
import { EmailSignUpWrapper } from './EmailSignUpWrapper';
2+
import { mocked } from 'storybook/test';
3+
import { lazyFetchEmailWithTimeout } from '../lib/fetchEmail';
4+
import { useIsSignedIn } from '../lib/useAuthStatus';
5+
import { useNewsletterSubscription } from '../lib/useNewsletterSubscription';
6+
import { EmailSignUpWrapper } from './EmailSignUpWrapper.importable';
37

48
const meta: Meta<typeof EmailSignUpWrapper> = {
59
title: 'Components/EmailSignUpWrapper',
610
component: EmailSignUpWrapper,
711
};
812

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

28+
// Loading state - shows placeholder while auth status is being determined
29+
// This prevents layout shift when subscription status is resolved
30+
export const Placeholder: Story = {
31+
args: {
32+
hidePrivacyMessage: false,
33+
...defaultArgs,
34+
},
35+
async beforeEach() {
36+
mocked(useNewsletterSubscription).mockReturnValue(undefined);
37+
},
38+
};
39+
40+
// Default story - signed out user sees the signup form with email input
2141
export const DefaultStory: Story = {
2242
args: {
2343
hidePrivacyMessage: true,
2444
...defaultArgs,
2545
},
46+
async beforeEach() {
47+
mocked(useNewsletterSubscription).mockReturnValue(false);
48+
mocked(useIsSignedIn).mockReturnValue(false);
49+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
50+
Promise.resolve(null),
51+
);
52+
},
2653
};
2754

2855
export const DefaultStoryWithPrivacy: Story = {
2956
args: {
3057
hidePrivacyMessage: false,
3158
...defaultArgs,
3259
},
60+
async beforeEach() {
61+
mocked(useNewsletterSubscription).mockReturnValue(false);
62+
mocked(useIsSignedIn).mockReturnValue(false);
63+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
64+
Promise.resolve(null),
65+
);
66+
},
67+
};
68+
69+
// User is signed in but NOT subscribed - email field is hidden, only signup button shows
70+
export const SignedInNotSubscribed: Story = {
71+
args: {
72+
hidePrivacyMessage: false,
73+
...defaultArgs,
74+
},
75+
async beforeEach() {
76+
mocked(useNewsletterSubscription).mockReturnValue(false);
77+
mocked(useIsSignedIn).mockReturnValue(true);
78+
mocked(lazyFetchEmailWithTimeout).mockReturnValue(() =>
79+
Promise.resolve('[email protected]'),
80+
);
81+
},
82+
};
83+
84+
// User is signed in and IS subscribed - component returns null (hidden)
85+
// Note: This story will render nothing as the component returns null when subscribed
86+
export const SignedInAlreadySubscribed: Story = {
87+
args: {
88+
hidePrivacyMessage: false,
89+
...defaultArgs,
90+
},
91+
async beforeEach() {
92+
mocked(useNewsletterSubscription).mockReturnValue(true);
93+
},
3394
};
3495

3596
export default meta;

dotcom-rendering/src/components/EmailSignUpWrapper.tsx

Lines changed: 0 additions & 38 deletions
This file was deleted.

dotcom-rendering/src/components/LiveBlock.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const VideoAsSecond = () => {
8181
isPinnedPost={false}
8282
editionId={'UK'}
8383
shouldHideAds={false}
84+
idApiUrl="https://idapi.theguardian.com"
8485
/>
8586
</Wrapper>
8687
);
@@ -129,6 +130,7 @@ export const Title = () => {
129130
isPinnedPost={false}
130131
editionId={'UK'}
131132
shouldHideAds={false}
133+
idApiUrl="https://idapi.theguardian.com"
132134
/>
133135
</Wrapper>
134136
);
@@ -198,6 +200,7 @@ export const Video = () => {
198200
isPinnedPost={false}
199201
editionId={'UK'}
200202
shouldHideAds={false}
203+
idApiUrl="https://idapi.theguardian.com"
201204
/>
202205
</Wrapper>
203206
);
@@ -242,6 +245,7 @@ export const RichLink = () => {
242245
isPinnedPost={false}
243246
editionId={'UK'}
244247
shouldHideAds={false}
248+
idApiUrl="https://idapi.theguardian.com"
245249
/>
246250
</Wrapper>
247251
);
@@ -277,6 +281,7 @@ export const FirstImage = () => {
277281
isPinnedPost={false}
278282
editionId={'UK'}
279283
shouldHideAds={false}
284+
idApiUrl="https://idapi.theguardian.com"
280285
/>
281286
</Wrapper>
282287
);
@@ -338,6 +343,7 @@ export const ImageRoles = () => {
338343
isSensitive={false}
339344
editionId={'UK'}
340345
shouldHideAds={false}
346+
idApiUrl="https://idapi.theguardian.com"
341347
/>
342348
</Wrapper>
343349
);
@@ -388,6 +394,7 @@ export const Thumbnail = () => {
388394
isSensitive={false}
389395
editionId={'UK'}
390396
shouldHideAds={false}
397+
idApiUrl="https://idapi.theguardian.com"
391398
/>
392399
</Wrapper>
393400
);
@@ -424,6 +431,7 @@ export const ImageAndTitle = () => {
424431
isPinnedPost={false}
425432
editionId={'UK'}
426433
shouldHideAds={false}
434+
idApiUrl="https://idapi.theguardian.com"
427435
/>
428436
</Wrapper>
429437
);
@@ -456,6 +464,7 @@ export const Updated = () => {
456464
isPinnedPost={false}
457465
editionId={'UK'}
458466
shouldHideAds={false}
467+
idApiUrl="https://idapi.theguardian.com"
459468
/>
460469
</Wrapper>
461470
);
@@ -492,6 +501,7 @@ export const Contributor = () => {
492501
isSensitive={false}
493502
editionId={'UK'}
494503
shouldHideAds={false}
504+
idApiUrl="https://idapi.theguardian.com"
495505
/>
496506
</Wrapper>
497507
);
@@ -526,6 +536,7 @@ export const NoAvatar = () => {
526536
isSensitive={false}
527537
editionId={'UK'}
528538
shouldHideAds={false}
539+
idApiUrl="https://idapi.theguardian.com"
529540
/>
530541
</Wrapper>
531542
);
@@ -563,6 +574,7 @@ export const TitleAndContributor = () => {
563574
isSensitive={false}
564575
editionId={'UK'}
565576
shouldHideAds={false}
577+
idApiUrl="https://idapi.theguardian.com"
566578
/>
567579
</Wrapper>
568580
);

dotcom-rendering/src/components/LiveBlock.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Props = {
2727
editionId: EditionId;
2828
shouldHideAds: boolean;
2929
serverTime?: number;
30+
idApiUrl?: string;
3031
};
3132

3233
export const LiveBlock = ({
@@ -46,6 +47,7 @@ export const LiveBlock = ({
4647
editionId,
4748
shouldHideAds,
4849
serverTime,
50+
idApiUrl,
4951
}: Props) => {
5052
if (block.elements.length === 0) return null;
5153

@@ -91,6 +93,7 @@ export const LiveBlock = ({
9193
isPinnedPost={isPinnedPost}
9294
editionId={editionId}
9395
shouldHideAds={shouldHideAds}
96+
idApiUrl={idApiUrl}
9497
/>
9598
))}
9699
<footer

0 commit comments

Comments
 (0)