Skip to content

Commit d5aa144

Browse files
Merge pull request #14443 from guardian/jr/soi-recaptcha-newsleter
Jr/soi recaptcha newsleter
2 parents 6ae8db5 + db315c5 commit d5aa144

14 files changed

+224
-61
lines changed

dotcom-rendering/src/components/ManyNewsletterSignUp.importable.tsx

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { useCallback, useEffect, useRef, useState } from 'react';
1212
// Note - the package also exports a component as a named export "ReCAPTCHA",
1313
// that version will compile and render but is non-functional.
1414
// Use the default export instead.
15-
import ReactGoogleRecaptcha from 'react-google-recaptcha';
15+
import type ReactGoogleRecaptcha from 'react-google-recaptcha';
1616
import {
1717
reportTrackingEvent,
1818
requestMultipleSignUps,
1919
} from '../lib/newsletter-sign-up-requests';
20+
import { useIsSignedIn } from '../lib/useAuthStatus';
2021
import { useConfig } from './ConfigContext';
2122
import { Flex } from './Flex';
2223
import { ManyNewslettersForm } from './ManyNewslettersForm';
@@ -49,7 +50,7 @@ const sectionWrapperStyle = (hide: boolean) => css`
4950
const desktopClearButtonWrapperStyle = css`
5051
display: none;
5152
padding-left: ${space[1]}px;
52-
margin-right: -10px;
53+
padding-right: ${space[1]}px;
5354
${from.leftCol} {
5455
display: block;
5556
}
@@ -121,12 +122,16 @@ const attributeToNumber = (
121122
type Props = {
122123
useReCaptcha: boolean;
123124
captchaSiteKey?: string;
125+
visibleRecaptcha?: boolean;
124126
};
125127

126128
export const ManyNewsletterSignUp = ({
127129
useReCaptcha,
128130
captchaSiteKey,
131+
visibleRecaptcha = false,
129132
}: Props) => {
133+
const isSignedIn = useIsSignedIn();
134+
130135
const [newslettersToSignUpFor, setNewslettersToSignUpFor] = useState<
131136
{
132137
/** unique identifier for the newsletter in kebab-case format */
@@ -137,6 +142,9 @@ export const ManyNewsletterSignUp = ({
137142
>([]);
138143
const [status, setStatus] = useState<FormStatus>('NotSent');
139144
const [email, setEmail] = useState('');
145+
const [marketingOptIn, setMarketingOptIn] = useState<boolean | undefined>(
146+
undefined,
147+
);
140148
const reCaptchaRef = useRef<ReactGoogleRecaptcha>(null);
141149

142150
const userCanInteract = status !== 'Success' && status !== 'Loading';
@@ -207,6 +215,12 @@ export const ManyNewsletterSignUp = ({
207215
setStatus('NotSent');
208216
}, [status]);
209217

218+
useEffect(() => {
219+
if (isSignedIn !== 'Pending' && !isSignedIn) {
220+
setMarketingOptIn(true);
221+
}
222+
}, [isSignedIn]);
223+
210224
useEffect(() => {
211225
const signUpButtons = [
212226
...document.querySelectorAll(`[data-role=${BUTTON_ROLE}]`),
@@ -242,6 +256,7 @@ export const ManyNewsletterSignUp = ({
242256
email,
243257
identityNames,
244258
reCaptchaToken,
259+
marketingOptIn,
245260
).catch(() => {
246261
return undefined;
247262
});
@@ -311,9 +326,11 @@ export const ManyNewsletterSignUp = ({
311326
'captcha-execute',
312327
renderingTarget,
313328
);
314-
const result = await reCaptchaRef.current.executeAsync();
329+
const result = visibleRecaptcha
330+
? reCaptchaRef.current.getValue()
331+
: await reCaptchaRef.current.executeAsync();
315332

316-
if (typeof result !== 'string') {
333+
if (!result) {
317334
void reportTrackingEvent(
318335
'ManyNewsletterSignUp',
319336
'captcha-failure',
@@ -384,36 +401,17 @@ export const ManyNewsletterSignUp = ({
384401
status,
385402
}}
386403
newsletterCount={newslettersToSignUpFor.length}
404+
marketingOptIn={marketingOptIn}
405+
setMarketingOptIn={setMarketingOptIn}
406+
useReCaptcha={useReCaptcha}
407+
captchaSiteKey={captchaSiteKey}
408+
visibleRecaptcha={visibleRecaptcha}
409+
reCaptchaRef={reCaptchaRef}
410+
handleCaptchaError={handleCaptchaError}
387411
/>
388412
<div css={desktopClearButtonWrapperStyle}>
389413
<ClearButton removeAll={removeAll} />
390414
</div>
391-
392-
{useReCaptcha && !!captchaSiteKey && (
393-
<div
394-
// The Google documentation specifies that if the 'recaptcha-badge' is hidden,
395-
// their T+C's must be displayed instead. While this component hides the
396-
// badge, the T+C's are inluded in the ManyNewslettersForm component.
397-
// https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed
398-
css={css`
399-
.grecaptcha-badge {
400-
visibility: hidden;
401-
}
402-
`}
403-
>
404-
<ReactGoogleRecaptcha
405-
sitekey={captchaSiteKey}
406-
ref={reCaptchaRef}
407-
onError={handleCaptchaError}
408-
size="invisible"
409-
// Note - the component supports an onExpired callback
410-
// (for when the user completed a challenge, but did
411-
// not submit the form before the token expired.
412-
// We don't need that here as setting the token
413-
// triggers the submission (onChange callback)
414-
/>
415-
</div>
416-
)}
417415
</Flex>
418416
</div>
419417
</Section>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const Default = () => {
4343
/>
4444
<ManyNewsletterSignUp
4545
useReCaptcha={false}
46+
visibleRecaptcha={false}
4647
captchaSiteKey="TEST_RECAPTCHA_SITE_KEY"
4748
/>
4849
</>

dotcom-rendering/src/components/ManyNewslettersForm.tsx

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,31 @@ import {
55
palette,
66
space,
77
} from '@guardian/source/foundations';
8-
import { Button, TextInput } from '@guardian/source/react-components';
9-
import { type ChangeEventHandler, useState } from 'react';
8+
import { Button } from '@guardian/source/react-components';
9+
import {
10+
type ChangeEventHandler,
11+
type ReactEventHandler,
12+
useState,
13+
} from 'react';
14+
import type ReactGoogleRecaptcha from 'react-google-recaptcha';
1015
import { InlineSkipToWrapper } from './InlineSkipToWrapper';
16+
import { ManyNewslettersFormFields } from './ManyNewslettersFormFields';
1117
import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage';
1218

13-
interface FormProps {
19+
export interface FormProps {
1420
status: 'NotSent' | 'Loading' | 'Success' | 'Failed' | 'InvalidEmail';
1521
email: string;
1622
handleTextInput: ChangeEventHandler<HTMLInputElement>;
1723
handleSubmitButton: { (): Promise<void> | void };
1824
newsletterCount: number;
25+
marketingOptIn?: boolean;
26+
setMarketingOptIn: (value: boolean) => void;
27+
// Captcha props
28+
useReCaptcha?: boolean;
29+
captchaSiteKey?: string;
30+
visibleRecaptcha?: boolean;
31+
reCaptchaRef?: React.RefObject<ReactGoogleRecaptcha>;
32+
handleCaptchaError?: ReactEventHandler<HTMLDivElement>;
1933
}
2034

2135
// The design brief requires the layout of the form to align with the
@@ -49,16 +63,7 @@ const formFieldsStyle = css`
4963
flex-basis: ${2 * CARD_CONTAINER_WIDTH - CARD_CONTAINER_PADDING * 2}px;
5064
flex-direction: row;
5165
flex-shrink: 0;
52-
align-items: flex-end;
53-
}
54-
`;
55-
56-
const inputWrapperStyle = css`
57-
margin-bottom: ${space[2]}px;
58-
${from.desktop} {
59-
margin-bottom: 0;
60-
margin-right: ${space[2]}px;
61-
flex-basis: 296px;
66+
align-items: last baseline;
6267
}
6368
`;
6469

@@ -114,6 +119,13 @@ export const ManyNewslettersForm = ({
114119
handleTextInput,
115120
handleSubmitButton,
116121
newsletterCount,
122+
marketingOptIn,
123+
setMarketingOptIn,
124+
useReCaptcha = false,
125+
captchaSiteKey,
126+
visibleRecaptcha = false,
127+
reCaptchaRef,
128+
handleCaptchaError,
117129
}: FormProps) => {
118130
const [formHasFocus, setFormHasFocus] = useState(false);
119131
const hidePrivacyOnMobile = !formHasFocus && email.length === 0;
@@ -123,13 +135,6 @@ export const ManyNewslettersForm = ({
123135
? 'Sign up for the newsletter you selected'
124136
: `Sign up for the ${newsletterCount} newsletters you selected`;
125137

126-
const errorMessage =
127-
status === 'Failed'
128-
? 'Sign up failed. Please try again'
129-
: status === 'InvalidEmail'
130-
? 'Please enter a valid email address'
131-
: undefined;
132-
133138
return (
134139
<form
135140
aria-label="sign-up confirmation form"
@@ -157,15 +162,18 @@ export const ManyNewslettersForm = ({
157162
<div css={formFieldsStyle}>
158163
{status !== 'Success' ? (
159164
<>
160-
<span css={inputWrapperStyle}>
161-
<TextInput
162-
label="Enter your email"
163-
value={email}
164-
onChange={handleTextInput}
165-
error={errorMessage}
166-
disabled={status === 'Loading'}
167-
/>
168-
</span>
165+
<ManyNewslettersFormFields
166+
status={status}
167+
email={email}
168+
handleTextInput={handleTextInput}
169+
marketingOptIn={marketingOptIn}
170+
setMarketingOptIn={setMarketingOptIn}
171+
useReCaptcha={useReCaptcha}
172+
captchaSiteKey={captchaSiteKey}
173+
visibleRecaptcha={visibleRecaptcha}
174+
reCaptchaRef={reCaptchaRef}
175+
handleCaptchaError={handleCaptchaError}
176+
/>
169177
<Button
170178
aria-label={ariaLabel}
171179
isLoading={status === 'Loading'}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { css } from '@emotion/react';
2+
import { from, palette, space, textSans14 } from '@guardian/source/foundations';
3+
import {
4+
Checkbox,
5+
CheckboxGroup,
6+
TextInput,
7+
} from '@guardian/source/react-components';
8+
import type { FC } from 'react';
9+
import { useState } from 'react';
10+
import ReactGoogleRecaptcha from 'react-google-recaptcha';
11+
import type { FormProps } from './ManyNewslettersForm';
12+
13+
const inputWrapperStyle = css`
14+
margin-bottom: ${space[2]}px;
15+
${from.desktop} {
16+
margin-bottom: 0;
17+
margin-right: ${space[2]}px;
18+
flex-basis: 296px;
19+
}
20+
`;
21+
22+
const inputAndOptInWrapperStyle = css`
23+
${from.desktop} {
24+
flex-basis: 296px;
25+
margin-right: ${space[2]}px;
26+
}
27+
`;
28+
29+
const optInCheckboxTextSmall = css`
30+
label > div {
31+
${textSans14};
32+
line-height: 16px;
33+
}
34+
`;
35+
36+
export interface ManyNewslettersFormFieldsProps
37+
extends Omit<FormProps, 'handleSubmitButton' | 'newsletterCount'> {}
38+
39+
export const ManyNewslettersFormFields: FC<ManyNewslettersFormFieldsProps> = ({
40+
status,
41+
email,
42+
handleTextInput,
43+
marketingOptIn,
44+
setMarketingOptIn,
45+
useReCaptcha,
46+
captchaSiteKey,
47+
visibleRecaptcha,
48+
reCaptchaRef,
49+
handleCaptchaError,
50+
}) => {
51+
const [firstInteractionOccurred, setFirstInteractionOccurred] =
52+
useState(false);
53+
54+
const isMarketingOptInVisible = marketingOptIn !== undefined;
55+
56+
const errorMessage =
57+
status === 'Failed'
58+
? 'Sign up failed. Please try again'
59+
: status === 'InvalidEmail'
60+
? 'Please enter a valid email address'
61+
: undefined;
62+
63+
return (
64+
<div css={inputAndOptInWrapperStyle}>
65+
<span css={inputWrapperStyle}>
66+
<TextInput
67+
label="Enter your email"
68+
value={email}
69+
onChange={handleTextInput}
70+
error={errorMessage}
71+
disabled={status === 'Loading'}
72+
onFocus={() => setFirstInteractionOccurred(true)}
73+
/>
74+
</span>
75+
{isMarketingOptInVisible && (
76+
<div>
77+
<CheckboxGroup
78+
name="marketing-preferences"
79+
label="Marketing preferences"
80+
hideLabel={true}
81+
cssOverrides={optInCheckboxTextSmall}
82+
>
83+
<Checkbox
84+
label="Get updates about our journalism and ways to support and enjoy our work."
85+
value="marketing-opt-in"
86+
checked={marketingOptIn}
87+
onChange={(e) =>
88+
setMarketingOptIn(e.target.checked)
89+
}
90+
theme={{
91+
fillUnselected: palette.neutral[100],
92+
}}
93+
/>
94+
</CheckboxGroup>
95+
</div>
96+
)}
97+
{useReCaptcha && !!captchaSiteKey && (
98+
<div
99+
css={css`
100+
.grecaptcha-badge {
101+
visibility: hidden;
102+
}
103+
`}
104+
>
105+
{(!visibleRecaptcha || firstInteractionOccurred) && (
106+
<ReactGoogleRecaptcha
107+
sitekey={captchaSiteKey}
108+
ref={reCaptchaRef}
109+
onError={handleCaptchaError}
110+
size={visibleRecaptcha ? 'normal' : 'invisible'}
111+
/>
112+
)}
113+
</div>
114+
)}
115+
</div>
116+
);
117+
};

dotcom-rendering/src/components/NewsletterPrivacyMessage.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ interface Props {
1010
textColor?: 'supporting' | 'regular';
1111
}
1212

13+
const GUARDIAN_HOMEPAGE = 'https://www.theguardian.com';
1314
const GUARDIAN_PRIVACY_POLICY =
1415
'https://www.theguardian.com/help/privacy-policy';
1516
const GOOGLE_PRIVACY_POLICY = 'https://policies.google.com/privacy';
1617
const GOOGLE_TERMS_OF_SERVICE = 'https://policies.google.com/terms';
1718

1819
type PolicyUrl =
20+
| typeof GUARDIAN_HOMEPAGE
1921
| typeof GUARDIAN_PRIVACY_POLICY
2022
| typeof GOOGLE_PRIVACY_POLICY
2123
| typeof GOOGLE_TERMS_OF_SERVICE;
@@ -80,8 +82,12 @@ export const NewsletterPrivacyMessage = ({
8082
}: Props) => (
8183
<span css={[termsStyle, textStyles(textColor)]}>
8284
<strong>Privacy Notice: </strong>
83-
Newsletters may contain info about charities, online ads, and content
84-
funded by outside parties. For more information see our{' '}
85+
Newsletters may contain information about charities, online ads, and
86+
content funded by outside parties. If you do not have an account, we
87+
will create a guest account for you on{' '}
88+
<LegalLink href={GUARDIAN_HOMEPAGE}>theguardian.com</LegalLink> to send
89+
you this newsletter. You can complete full registration at any time. For
90+
more information about how we use your data see our{' '}
8591
<LegalLink href={GUARDIAN_PRIVACY_POLICY}>Privacy Policy</LegalLink>. We
8692
use Google reCaptcha to protect our website and the Google{' '}
8793
<LegalLink href={GOOGLE_PRIVACY_POLICY}>Privacy Policy</LegalLink> and{' '}

0 commit comments

Comments
 (0)