mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: add email sign up alert (#61218)
Co-authored-by: Niraj Nandish <nirajnandish@icloud.com> Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
@@ -147,7 +147,8 @@ model user {
|
|||||||
/// Valuable for selectively performing random logic.
|
/// Valuable for selectively performing random logic.
|
||||||
rand Float?
|
rand Float?
|
||||||
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
|
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
|
||||||
sendQuincyEmail Boolean
|
// Nullable tri-state: null (likely new user), true (subscribed), false (unsubscribed)
|
||||||
|
sendQuincyEmail Boolean?
|
||||||
theme String? // Undefined
|
theme String? // Undefined
|
||||||
timezone String? // Undefined
|
timezone String? // Undefined
|
||||||
twitter String? // Null | Undefined
|
twitter String? // Null | Undefined
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const newUser = (email: string) => ({
|
|||||||
progressTimestamps: [expect.any(Number)],
|
progressTimestamps: [expect.any(Number)],
|
||||||
rand: null, // TODO(Post-MVP): delete from schema (it's not used or required).
|
rand: null, // TODO(Post-MVP): delete from schema (it's not used or required).
|
||||||
savedChallenges: [],
|
savedChallenges: [],
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
timezone: null,
|
timezone: null,
|
||||||
twitter: null,
|
twitter: null,
|
||||||
|
|||||||
@@ -332,26 +332,6 @@ describe('auth0 plugin', () => {
|
|||||||
expect(res.headers.location).toMatch(HOME_LOCATION);
|
expect(res.headers.location).toMatch(HOME_LOCATION);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should redirect to email-sign-up if the user has not acceptedPrivacyTerms', async () => {
|
|
||||||
mockAuthSuccess();
|
|
||||||
// Using an italian path to make sure redirection works.
|
|
||||||
const italianReturnTo = 'https://www.freecodecamp.org/italian/settings';
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/auth/auth0/callback?state=valid',
|
|
||||||
cookies: {
|
|
||||||
'login-returnto': sign(italianReturnTo)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toEqual(
|
|
||||||
expect.stringContaining(
|
|
||||||
'https://www.freecodecamp.org/italian/email-sign-up?'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should populate the user with the correct data', async () => {
|
test('should populate the user with the correct data', async () => {
|
||||||
mockAuthSuccess();
|
mockAuthSuccess();
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ import {
|
|||||||
} from '../utils/env';
|
} from '../utils/env';
|
||||||
import { findOrCreateUser } from '../routes/helpers/auth-helpers';
|
import { findOrCreateUser } from '../routes/helpers/auth-helpers';
|
||||||
import { createAccessToken } from '../utils/tokens';
|
import { createAccessToken } from '../utils/tokens';
|
||||||
import {
|
import { getLoginRedirectParams } from '../utils/redirection';
|
||||||
getLoginRedirectParams,
|
|
||||||
getPrefixedLandingPath
|
|
||||||
} from '../utils/redirection';
|
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
@@ -108,8 +105,7 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { returnTo, pathPrefix, origin } = getLoginRedirectParams(req);
|
const { returnTo } = getLoginRedirectParams(req);
|
||||||
const redirectBase = getPrefixedLandingPath(origin, pathPrefix);
|
|
||||||
|
|
||||||
let token;
|
let token;
|
||||||
try {
|
try {
|
||||||
@@ -166,24 +162,14 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, acceptedPrivacyTerms } = await findOrCreateUser(
|
const { id } = await findOrCreateUser(fastify, email);
|
||||||
fastify,
|
|
||||||
email
|
|
||||||
);
|
|
||||||
|
|
||||||
reply.setAccessTokenCookie(createAccessToken(id));
|
reply.setAccessTokenCookie(createAccessToken(id));
|
||||||
|
|
||||||
if (acceptedPrivacyTerms) {
|
|
||||||
void reply.redirectWithMessage(returnTo, {
|
void reply.redirectWithMessage(returnTo, {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
content: 'flash.signin-success'
|
content: 'flash.signin-success'
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
void reply.redirectWithMessage(`${redirectBase}/email-sign-up`, {
|
|
||||||
type: 'success',
|
|
||||||
content: 'flash.signin-success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ const testUserData: Prisma.userCreateInput = {
|
|||||||
],
|
],
|
||||||
yearsTopContributor: ['2018'],
|
yearsTopContributor: ['2018'],
|
||||||
twitter: '@foobar',
|
twitter: '@foobar',
|
||||||
linkedin: 'linkedin.com/foobar'
|
linkedin: 'linkedin.com/foobar',
|
||||||
|
sendQuincyEmail: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const minimalUserData: Prisma.userCreateInput = {
|
const minimalUserData: Prisma.userCreateInput = {
|
||||||
@@ -301,6 +302,7 @@ const publicUserData = {
|
|||||||
profileUI: testUserData.profileUI,
|
profileUI: testUserData.profileUI,
|
||||||
savedChallenges: testUserData.savedChallenges,
|
savedChallenges: testUserData.savedChallenges,
|
||||||
twitter: 'https://twitter.com/foobar',
|
twitter: 'https://twitter.com/foobar',
|
||||||
|
sendQuincyEmail: testUserData.sendQuincyEmail,
|
||||||
username: testUserData.username,
|
username: testUserData.username,
|
||||||
usernameDisplay: testUserData.usernameDisplay,
|
usernameDisplay: testUserData.usernameDisplay,
|
||||||
website: testUserData.website,
|
website: testUserData.website,
|
||||||
|
|||||||
@@ -697,6 +697,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
|
|||||||
user: {
|
user: {
|
||||||
[username]: {
|
[username]: {
|
||||||
...removeNulls(publicUser),
|
...removeNulls(publicUser),
|
||||||
|
sendQuincyEmail: publicUser.sendQuincyEmail,
|
||||||
...normalizeFlags(flags),
|
...normalizeFlags(flags),
|
||||||
picture: publicUser.picture ?? '',
|
picture: publicUser.picture ?? '',
|
||||||
email: email ?? '',
|
email: email ?? '',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ vi.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
|||||||
// This is used to build a test user.
|
// This is used to build a test user.
|
||||||
const testUserData: Prisma.userCreateInput = {
|
const testUserData: Prisma.userCreateInput = {
|
||||||
...createUserInput(defaultUserEmail),
|
...createUserInput(defaultUserEmail),
|
||||||
|
sendQuincyEmail: true,
|
||||||
username: 'foobar',
|
username: 'foobar',
|
||||||
usernameDisplay: 'Foo Bar',
|
usernameDisplay: 'Foo Bar',
|
||||||
progressTimestamps: [1520002973119, 1520440323273],
|
progressTimestamps: [1520002973119, 1520440323273],
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const getSessionUser = {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
profileUI: Type.Optional(profileUI),
|
profileUI: Type.Optional(profileUI),
|
||||||
sendQuincyEmail: Type.Boolean(),
|
sendQuincyEmail: Type.Union([Type.Null(), Type.Boolean()]), // // Tri-state: null (likely new user), true (subscribed), false (unsubscribed)
|
||||||
theme: Type.String(),
|
theme: Type.String(),
|
||||||
twitter: Type.Optional(Type.String()),
|
twitter: Type.Optional(Type.String()),
|
||||||
website: Type.Optional(Type.String()),
|
website: Type.Optional(Type.String()),
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function createUserInput(email: string) {
|
|||||||
showPortfolio: false,
|
showPortfolio: false,
|
||||||
showTimeLine: false
|
showTimeLine: false
|
||||||
},
|
},
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
username,
|
username,
|
||||||
usernameDisplay: username,
|
usernameDisplay: username,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Container } from '@freecodecamp/ui';
|
||||||
|
import EmailOptions from '../email-options';
|
||||||
|
import { updateMyQuincyEmail } from '../../redux/settings/actions';
|
||||||
|
import { userSelector, isSignedInSelector } from '../../redux/selectors';
|
||||||
|
import { CompletedChallenge } from '../../redux/prop-types';
|
||||||
|
|
||||||
|
interface EmailSignUpAlertProps {
|
||||||
|
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||||
|
sendQuincyEmail: boolean | null;
|
||||||
|
isSignedIn: boolean;
|
||||||
|
completedChallengesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: unknown) => {
|
||||||
|
const user = userSelector(state) as {
|
||||||
|
sendQuincyEmail: boolean | null;
|
||||||
|
completedChallenges: CompletedChallenge[];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
sendQuincyEmail: user.sendQuincyEmail,
|
||||||
|
isSignedIn: isSignedInSelector(state),
|
||||||
|
completedChallengesCount: user.completedChallenges.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
updateQuincyEmail: (sendQuincyEmail: boolean) =>
|
||||||
|
updateMyQuincyEmail({ sendQuincyEmail })
|
||||||
|
};
|
||||||
|
|
||||||
|
function EmailSignUpAlert({
|
||||||
|
updateQuincyEmail,
|
||||||
|
sendQuincyEmail,
|
||||||
|
isSignedIn,
|
||||||
|
completedChallengesCount = 0
|
||||||
|
}: EmailSignUpAlertProps) {
|
||||||
|
const newAccount = isSignedIn && completedChallengesCount < 1;
|
||||||
|
const userHasMadeEmailSelection = sendQuincyEmail !== null;
|
||||||
|
|
||||||
|
if (userHasMadeEmailSelection || newAccount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container fluid={true} className='email-sign-up-alert'>
|
||||||
|
<EmailOptions
|
||||||
|
isSignedIn={isSignedIn}
|
||||||
|
updateQuincyEmail={updateQuincyEmail}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EmailSignUpAlert);
|
||||||
@@ -6,6 +6,7 @@ import Login from '../Header/components/login';
|
|||||||
import { Link, Loader } from '../helpers';
|
import { Link, Loader } from '../helpers';
|
||||||
|
|
||||||
import './intro.css';
|
import './intro.css';
|
||||||
|
import EmailSignUpAlert from './email-sign-up-alert';
|
||||||
import LearnAlert from './learn-alert';
|
import LearnAlert from './learn-alert';
|
||||||
|
|
||||||
interface IntroProps {
|
interface IntroProps {
|
||||||
@@ -64,6 +65,7 @@ const Intro = ({
|
|||||||
onLearnDonationAlertClick={onLearnDonationAlertClick}
|
onLearnDonationAlertClick={onLearnDonationAlertClick}
|
||||||
isDonating={isDonating}
|
isDonating={isDonating}
|
||||||
/>
|
/>
|
||||||
|
<EmailSignUpAlert />
|
||||||
{completedChallengeCount && slug && completedChallengeCount < 15 ? (
|
{completedChallengeCount && slug && completedChallengeCount < 15 ? (
|
||||||
<div className='intro-description'>
|
<div className='intro-description'>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
|
|||||||
@@ -42,3 +42,20 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email-sign-up-alert {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--quaternary-color);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-list-opt {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-author {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import Intro from '.';
|
|||||||
|
|
||||||
jest.mock('../../analytics');
|
jest.mock('../../analytics');
|
||||||
|
|
||||||
function renderWithRedux(ui: JSX.Element) {
|
function renderWithRedux(
|
||||||
return render(<Provider store={createStore()}>{ui}</Provider>);
|
ui: JSX.Element,
|
||||||
|
preloadedState: Record<string, unknown> = {}
|
||||||
|
) {
|
||||||
|
return render(<Provider store={createStore(preloadedState)}>{ui}</Provider>);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('<Intro />', () => {
|
describe('<Intro />', () => {
|
||||||
@@ -19,7 +22,19 @@ describe('<Intro />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has a blockquote when loggedIn', () => {
|
it('has a blockquote when loggedIn', () => {
|
||||||
renderWithRedux(<Intro {...loggedInProps} />);
|
// Provide a minimal preloaded state so connected components expecting a
|
||||||
|
// sessionUser (e.g. EmailSignUpAlert) do not receive null.
|
||||||
|
const preloadedState = {
|
||||||
|
app: {
|
||||||
|
user: {
|
||||||
|
sessionUser: {
|
||||||
|
completedChallenges: [{}],
|
||||||
|
sendQuincyEmail: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderWithRedux(<Intro {...loggedInProps} />, preloadedState);
|
||||||
expect(screen.getByTestId('quote-block')).toBeInTheDocument();
|
expect(screen.getByTestId('quote-block')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Col, Row, Button, Spacer } from '@freecodecamp/ui';
|
||||||
|
import { apiLocation } from '../../config/env.json';
|
||||||
|
|
||||||
|
interface EmailListOptInProps {
|
||||||
|
isSignedIn: boolean;
|
||||||
|
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmailListOptIn({
|
||||||
|
isSignedIn,
|
||||||
|
updateQuincyEmail
|
||||||
|
}: EmailListOptInProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (isSignedIn) {
|
||||||
|
return (
|
||||||
|
<Row className='email-list-opt'>
|
||||||
|
<Col md={4} mdOffset={2} sm={5} smOffset={1} xs={12}>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
variant='primary'
|
||||||
|
onClick={() => updateQuincyEmail(true)}
|
||||||
|
>
|
||||||
|
{t('buttons.yes-please')}
|
||||||
|
</Button>
|
||||||
|
<Spacer size='xs' />
|
||||||
|
</Col>
|
||||||
|
<Col md={4} sm={5} xs={12}>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
variant='primary'
|
||||||
|
onClick={() => updateQuincyEmail(false)}
|
||||||
|
>
|
||||||
|
{t('buttons.no-thanks')}
|
||||||
|
</Button>
|
||||||
|
<Spacer size='xs' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
<Spacer size='xs' />
|
||||||
|
<Button block={true} variant='primary' href={`${apiLocation}/signin`}>
|
||||||
|
{t('buttons.sign-up-email-list')}
|
||||||
|
</Button>
|
||||||
|
<Spacer size='xs' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmailOptionsProps {
|
||||||
|
isSignedIn: boolean;
|
||||||
|
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||||
|
isPage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmailOptions({
|
||||||
|
isSignedIn,
|
||||||
|
updateQuincyEmail,
|
||||||
|
isPage
|
||||||
|
}: EmailOptionsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col xs={12}>
|
||||||
|
{isPage ? (
|
||||||
|
<h1 className='text-center'>{t('misc.email-signup')}</h1>
|
||||||
|
) : (
|
||||||
|
<h2 className='text-center'>{t('misc.email-signup')}</h2>
|
||||||
|
)}
|
||||||
|
<Spacer size='xs' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
{...(isPage ? { md: 8, mdOffset: 2, sm: 10, smOffset: 1 } : {})}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<p>{t('misc.email-blast')}</p>
|
||||||
|
<span className='message-author'>{t('misc.quincy')}</span>
|
||||||
|
<Spacer size='m' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<EmailListOptIn
|
||||||
|
isSignedIn={isSignedIn}
|
||||||
|
updateQuincyEmail={updateQuincyEmail}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmailOptions;
|
||||||
@@ -11,7 +11,6 @@ window.___loader = { enqueue: () => {}, hovering: () => {} };
|
|||||||
|
|
||||||
const userProps = {
|
const userProps = {
|
||||||
user: {
|
user: {
|
||||||
acceptedPrivacyTerms: true,
|
|
||||||
currentChallengeId: 'string',
|
currentChallengeId: 'string',
|
||||||
email: 'string',
|
email: 'string',
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
|||||||
type EmailProps = {
|
type EmailProps = {
|
||||||
email: string;
|
email: string;
|
||||||
isEmailVerified: boolean;
|
isEmailVerified: boolean;
|
||||||
sendQuincyEmail: boolean;
|
sendQuincyEmail: boolean | null;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
updateMyEmail: (email: string) => void;
|
updateMyEmail: (email: string) => void;
|
||||||
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
|
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
|
||||||
@@ -250,7 +250,7 @@ function EmailSettings({
|
|||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<ToggleButtonSetting
|
<ToggleButtonSetting
|
||||||
action={t('settings.email.weekly')}
|
action={t('settings.email.weekly')}
|
||||||
flag={sendQuincyEmail}
|
flag={!!sendQuincyEmail}
|
||||||
flagName='sendQuincyEmail'
|
flagName='sendQuincyEmail'
|
||||||
offLabel={t('buttons.no-thanks')}
|
offLabel={t('buttons.no-thanks')}
|
||||||
onLabel={t('buttons.yes-please')}
|
onLabel={t('buttons.yes-please')}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.email-sign-up strong,
|
|
||||||
.email-sign-up p {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is a temporary fix until the component library is revisited.
|
|
||||||
See https://github.com/freeCodeCamp/freeCodeCamp/issues/52131#issuecomment-1788840851.
|
|
||||||
*/
|
|
||||||
.email-list-opt {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.email-sign-up strong,
|
|
||||||
.email-sign-up p {
|
|
||||||
font-size: 1.17rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import type { Dispatch } from 'redux';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Container, Col, Row, Button, Spacer } from '@freecodecamp/ui';
|
import { Container, Spacer } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import createRedirect from '../components/create-redirect';
|
import createRedirect from '../components/create-redirect';
|
||||||
import { Loader, Link } from '../components/helpers';
|
import { Loader } from '../components/helpers';
|
||||||
import { apiLocation } from '../../config/env.json';
|
import EmailOptions from '../components/email-options';
|
||||||
|
import { updateMyQuincyEmail } from '../redux/settings/actions';
|
||||||
import { acceptTerms } from '../redux/actions';
|
|
||||||
import {
|
import {
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
@@ -19,13 +16,11 @@ import {
|
|||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import type { User } from '../redux/prop-types';
|
import type { User } from '../redux/prop-types';
|
||||||
|
|
||||||
import './email-sign-up.css';
|
interface EmailSignUpProps {
|
||||||
interface AcceptPrivacyTermsProps {
|
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||||
acceptTerms: (accept: boolean | null) => void;
|
sendQuincyEmail: boolean | null | undefined;
|
||||||
acceptedPrivacyTerms: boolean;
|
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
completedChallengeCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@@ -33,90 +28,28 @@ const mapStateToProps = createSelector(
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
(user: User | null, isSignedIn: boolean, showLoading: boolean) => ({
|
(user: User | null, isSignedIn: boolean, showLoading: boolean) => ({
|
||||||
acceptedPrivacyTerms: !!user?.acceptedPrivacyTerms,
|
sendQuincyEmail: user?.sendQuincyEmail,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
showLoading,
|
showLoading
|
||||||
completedChallengeCount: user?.completedChallengeCount ?? 0
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
const mapDispatchToProps = {
|
||||||
bindActionCreators({ acceptTerms }, dispatch);
|
updateQuincyEmail: (sendQuincyEmail: boolean) =>
|
||||||
|
updateMyQuincyEmail({ sendQuincyEmail })
|
||||||
|
};
|
||||||
const RedirectToLearn = createRedirect('/learn');
|
const RedirectToLearn = createRedirect('/learn');
|
||||||
|
|
||||||
function EmailListOptIn({
|
function EmailSignUp({
|
||||||
|
updateQuincyEmail,
|
||||||
|
sendQuincyEmail,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
acceptTerms
|
showLoading
|
||||||
}: {
|
}: EmailSignUpProps) {
|
||||||
isSignedIn: boolean;
|
|
||||||
acceptTerms: (accepted: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (isSignedIn) {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Row className='email-list-opt'>
|
|
||||||
<Col md={4} mdOffset={2} sm={5} smOffset={1} xs={12}>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
size='large'
|
|
||||||
variant='primary'
|
|
||||||
onClick={() => acceptTerms(true)}
|
|
||||||
>
|
|
||||||
{t('buttons.yes-please')}
|
|
||||||
</Button>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
</Col>
|
|
||||||
<Col md={4} sm={5} xs={12}>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
size='large'
|
|
||||||
variant='primary'
|
|
||||||
onClick={() => acceptTerms(false)}
|
|
||||||
>
|
|
||||||
{t('buttons.no-thanks')}
|
|
||||||
</Button>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
size='large'
|
|
||||||
variant='primary'
|
|
||||||
href={`${apiLocation}/signin`}
|
|
||||||
>
|
|
||||||
{t('buttons.sign-up-email-list')}
|
|
||||||
</Button>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function AcceptPrivacyTerms({
|
const userHasMadeSelection = isSignedIn && sendQuincyEmail !== null;
|
||||||
acceptTerms,
|
|
||||||
acceptedPrivacyTerms,
|
|
||||||
isSignedIn,
|
|
||||||
showLoading,
|
|
||||||
completedChallengeCount
|
|
||||||
}: AcceptPrivacyTermsProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const acceptedPrivacyRef = useRef(acceptedPrivacyTerms);
|
|
||||||
const acceptTermsRef = useRef(acceptTerms);
|
|
||||||
const newAccount = isSignedIn && completedChallengeCount < 1;
|
|
||||||
|
|
||||||
useEffect(() => {
|
return userHasMadeSelection ? (
|
||||||
acceptedPrivacyRef.current = acceptedPrivacyTerms;
|
|
||||||
acceptTermsRef.current = acceptTerms;
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return acceptedPrivacyTerms ? (
|
|
||||||
<RedirectToLearn />
|
<RedirectToLearn />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -124,47 +57,20 @@ function AcceptPrivacyTerms({
|
|||||||
<title>{t('misc.email-signup')} | freeCodeCamp.org</title>
|
<title>{t('misc.email-signup')} | freeCodeCamp.org</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
|
||||||
<Spacer size='l' />
|
<Spacer size='l' />
|
||||||
<h1 className='text-center'>
|
|
||||||
{newAccount
|
|
||||||
? t('misc.brand-new-account')
|
|
||||||
: t('misc.email-signup')}
|
|
||||||
</h1>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{newAccount && (
|
|
||||||
<Row>
|
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
|
||||||
<p>
|
|
||||||
<Trans i18nKey='misc.duplicate-account-warning'>
|
|
||||||
<Link className='inline' to='/settings#danger-zone' />
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
<hr />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
<Row className='email-sign-up'>
|
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
<p>{t('misc.email-blast')}</p>
|
|
||||||
<Spacer size='xs' />
|
|
||||||
</Col>
|
|
||||||
{showLoading ? (
|
{showLoading ? (
|
||||||
<Loader fullScreen={true} />
|
<Loader fullScreen={true} />
|
||||||
) : (
|
) : (
|
||||||
<EmailListOptIn isSignedIn={isSignedIn} acceptTerms={acceptTerms} />
|
<EmailOptions
|
||||||
|
isSignedIn={isSignedIn}
|
||||||
|
updateQuincyEmail={updateQuincyEmail}
|
||||||
|
isPage={true}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Col xs={12}>
|
|
||||||
<Spacer size='m' />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Container>
|
</Container>
|
||||||
|
<Spacer size='l' />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AcceptPrivacyTerms);
|
export default connect(mapStateToProps, mapDispatchToProps)(EmailSignUp);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { navigate } from 'gatsby';
|
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
|
||||||
import { putUserAcceptsTerms } from '../utils/ajax';
|
|
||||||
import { acceptTermsComplete, acceptTermsError } from './actions';
|
|
||||||
|
|
||||||
function* acceptTermsSaga({ payload: quincyEmails }) {
|
|
||||||
try {
|
|
||||||
const { data } = yield call(putUserAcceptsTerms, quincyEmails);
|
|
||||||
|
|
||||||
yield put(acceptTermsComplete(quincyEmails));
|
|
||||||
yield put(createFlashMessage(data));
|
|
||||||
} catch (e) {
|
|
||||||
yield put(acceptTermsError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* acceptCompleteSaga() {
|
|
||||||
yield call(navigate, '/learn');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAcceptTermsSaga(types) {
|
|
||||||
return [
|
|
||||||
takeEvery(types.acceptTerms, acceptTermsSaga),
|
|
||||||
takeEvery(types.acceptTermsComplete, acceptCompleteSaga)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,6 @@ export const actionTypes = createTypes(
|
|||||||
...createAsyncTypes('fetchUser'),
|
...createAsyncTypes('fetchUser'),
|
||||||
...createAsyncTypes('postCharge'),
|
...createAsyncTypes('postCharge'),
|
||||||
...createAsyncTypes('fetchProfileForUser'),
|
...createAsyncTypes('fetchProfileForUser'),
|
||||||
...createAsyncTypes('acceptTerms'),
|
|
||||||
...createAsyncTypes('showCert'),
|
...createAsyncTypes('showCert'),
|
||||||
...createAsyncTypes('reportUser'),
|
...createAsyncTypes('reportUser'),
|
||||||
...createAsyncTypes('deleteUserToken'),
|
...createAsyncTypes('deleteUserToken'),
|
||||||
|
|||||||
@@ -42,12 +42,6 @@ export const saveChallengeComplete = createAction(
|
|||||||
actionTypes.saveChallengeComplete
|
actionTypes.saveChallengeComplete
|
||||||
);
|
);
|
||||||
|
|
||||||
export const acceptTerms = createAction(actionTypes.acceptTerms);
|
|
||||||
export const acceptTermsComplete = createAction(
|
|
||||||
actionTypes.acceptTermsComplete
|
|
||||||
);
|
|
||||||
export const acceptTermsError = createAction(actionTypes.acceptTermsError);
|
|
||||||
|
|
||||||
export const fetchUser = createAction(actionTypes.fetchUser);
|
export const fetchUser = createAction(actionTypes.fetchUser);
|
||||||
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
||||||
export const fetchUserTimeout = createAction(actionTypes.fetchUserTimeout);
|
export const fetchUserTimeout = createAction(actionTypes.fetchUserTimeout);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
CURRENT_CHALLENGE_KEY
|
CURRENT_CHALLENGE_KEY
|
||||||
} from '../templates/Challenges/redux/action-types';
|
} from '../templates/Challenges/redux/action-types';
|
||||||
import { getIsDailyCodingChallenge } from '../../../shared/config/challenge-types';
|
import { getIsDailyCodingChallenge } from '../../../shared/config/challenge-types';
|
||||||
import { createAcceptTermsSaga } from './accept-terms-saga';
|
|
||||||
import { actionTypes, ns as MainApp } from './action-types';
|
import { actionTypes, ns as MainApp } from './action-types';
|
||||||
import { createAppMountSaga } from './app-mount-saga';
|
import { createAppMountSaga } from './app-mount-saga';
|
||||||
import { createDonationSaga } from './donation-saga';
|
import { createDonationSaga } from './donation-saga';
|
||||||
@@ -88,7 +87,6 @@ const initialState = {
|
|||||||
export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
|
export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
|
||||||
|
|
||||||
export const sagas = [
|
export const sagas = [
|
||||||
...createAcceptTermsSaga(actionTypes),
|
|
||||||
...createThemeSaga(actionTypes),
|
...createThemeSaga(actionTypes),
|
||||||
...createAppMountSaga(actionTypes),
|
...createAppMountSaga(actionTypes),
|
||||||
...createDonationSaga(actionTypes),
|
...createDonationSaga(actionTypes),
|
||||||
@@ -117,26 +115,6 @@ function spreadThePayloadOnUser(state, payload) {
|
|||||||
|
|
||||||
export const reducer = handleActions(
|
export const reducer = handleActions(
|
||||||
{
|
{
|
||||||
[actionTypes.acceptTermsComplete]: (state, { payload }) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
sessionUser: {
|
|
||||||
...state.user.sessionUser,
|
|
||||||
// TODO: the user accepts the privacy terms in practice during auth
|
|
||||||
// however, it's currently being used to track if they've accepted
|
|
||||||
// or rejected the newsletter. Ideally this should be migrated,
|
|
||||||
// since they can't sign up without accepting the terms.
|
|
||||||
acceptedPrivacyTerms: true,
|
|
||||||
sendQuincyEmail:
|
|
||||||
payload === null
|
|
||||||
? state.user.sessionUser.sendQuincyEmail
|
|
||||||
: payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actionTypes.allowSectionDonationRequests]: (state, { payload }) => {
|
[actionTypes.allowSectionDonationRequests]: (state, { payload }) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export type User = {
|
|||||||
profileUI: ProfileUI;
|
profileUI: ProfileUI;
|
||||||
progressTimestamps: Array<unknown>;
|
progressTimestamps: Array<unknown>;
|
||||||
savedChallenges: SavedChallenges;
|
savedChallenges: SavedChallenges;
|
||||||
sendQuincyEmail: boolean;
|
sendQuincyEmail: boolean | null;
|
||||||
sound: boolean;
|
sound: boolean;
|
||||||
theme: UserThemes;
|
theme: UserThemes;
|
||||||
keyboardShortcuts: boolean;
|
keyboardShortcuts: boolean;
|
||||||
|
|||||||
@@ -361,9 +361,9 @@ export function putUpdateMyHonesty(
|
|||||||
return put('/update-my-honesty', update);
|
return put('/update-my-honesty', update);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function putUpdateMyQuincyEmail(
|
export function putUpdateMyQuincyEmail(update: {
|
||||||
update: Record<string, string>
|
sendQuincyEmail: boolean;
|
||||||
): Promise<ResponseWithData<void>> {
|
}): Promise<ResponseWithData<void>> {
|
||||||
return put('/update-my-quincy-email', update);
|
return put('/update-my-quincy-email', update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,12 +373,6 @@ export function putUpdateMyPortfolio(
|
|||||||
return put('/update-my-portfolio', update);
|
return put('/update-my-portfolio', update);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function putUserAcceptsTerms(
|
|
||||||
quincyEmails: boolean
|
|
||||||
): Promise<ResponseWithData<void>> {
|
|
||||||
return put('/update-privacy-terms', { quincyEmails });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function putUserUpdateEmail(
|
export function putUserUpdateEmail(
|
||||||
email: string
|
email: string
|
||||||
): Promise<ResponseWithData<void>> {
|
): Promise<ResponseWithData<void>> {
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import translations from '../client/i18n/locales/english/translations.json';
|
||||||
|
import { alertToBeVisible } from './utils/alerts';
|
||||||
|
|
||||||
|
test.describe('Email sign-up page when user is not signed in', () => {
|
||||||
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/learn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not display newsletter options', async ({ page }) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Email sign-up page when user is signed in', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// It's necessary to seed with a user that has not accepted the privacy
|
||||||
|
// terms, otherwise the user will be redirected away from the email sign-up
|
||||||
|
// page.
|
||||||
|
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||||
|
await page.goto('/learn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display the newsletter options correctly', async ({ page }) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-signup'])
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText(translations.misc['quincy'])).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable weekly newsletter if the user clicks No', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const noThanksButton = page.getByRole('button', {
|
||||||
|
name: translations.buttons['no-thanks']
|
||||||
|
});
|
||||||
|
await expect(noThanksButton).toBeVisible();
|
||||||
|
await noThanksButton.click();
|
||||||
|
await alertToBeVisible(
|
||||||
|
page,
|
||||||
|
translations.flash['subscribe-to-quincy-updated']
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).not.toBeVisible();
|
||||||
|
await page.goto('/settings');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should enable weekly newsletter if the user clicks Yes', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const yesPleaseButton = page.getByRole('button', {
|
||||||
|
name: translations.buttons['yes-please']
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(yesPleaseButton).toBeVisible();
|
||||||
|
await yesPleaseButton.click();
|
||||||
|
await alertToBeVisible(
|
||||||
|
page,
|
||||||
|
translations.flash['subscribe-to-quincy-updated']
|
||||||
|
);
|
||||||
|
await page.goto('/settings');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('group', { name: translations.settings.email.weekly })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Email sign-up page when the user is new', () => {
|
||||||
|
test.use({ storageState: 'playwright/.auth/development-user.json' });
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
execSync('node ./tools/scripts/seed/seed-demo-user');
|
||||||
|
|
||||||
|
await page.goto('/learn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not display newsletter options', async ({ page }) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Email sign-up page when the user has made a selection', () => {
|
||||||
|
test.use({ storageState: 'playwright/.auth/development-user.json' });
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
execSync(
|
||||||
|
'node ./tools/scripts/seed/seed-demo-user --certified-user --set-false sendQuincyEmail'
|
||||||
|
);
|
||||||
|
await page.goto('/learn');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not display newsletter options', async ({ page }) => {
|
||||||
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-blast'])
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ test.describe('Email sign-up page when user is not signed in', () => {
|
|||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||||
await page.goto('/email-sign-up');
|
await page.goto('/email-sign-up');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,21 +70,20 @@ test.describe('Email sign-up page when user is not signed in', () => {
|
|||||||
|
|
||||||
test.describe('Email sign-up page when user is signed in', () => {
|
test.describe('Email sign-up page when user is signed in', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// It's necessary to seed with a user that has not accepted the privacy
|
// It's necessary to seed with a user that has not selected an email newsletter option.
|
||||||
// terms, otherwise the user will be redirected away from the email sign-up
|
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||||
// page.
|
|
||||||
execSync(
|
|
||||||
'node ./tools/scripts/seed/seed-demo-user --certified-user --set-false acceptedPrivacyTerms'
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.goto('/email-sign-up');
|
await page.goto('/email-sign-up');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display the content correctly', async ({ page }) => {
|
test('should display the content correctly', async ({ page }) => {
|
||||||
await expect(page).toHaveTitle('Email Sign Up | freeCodeCamp.org');
|
await expect(
|
||||||
|
page.getByText(translations.misc['email-signup'])
|
||||||
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(translations.misc['email-blast'])
|
page.getByText(translations.misc['email-blast'])
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(page.getByText(translations.misc['quincy'])).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -148,9 +148,7 @@ test.describe('Email sign-up page when user is signed in', () => {
|
|||||||
page.getByRole('heading', { name: 'Welcome back, Full Stack User' })
|
page.getByRole('heading', { name: 'Welcome back, Full Stack User' })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// When the user clicks Yes, the /update-privacy-terms API is called
|
// `sendQuincyEmail` is not set in the DB since the endpoint is mocked,
|
||||||
// to update both `acceptedPrivacyTerms` and `sendQuincyEmail`.
|
|
||||||
// But `sendQuincyEmail` is not set in the DB since the endpoint is mocked,
|
|
||||||
// so we are overriding the user data once again to mimic the real behavior.
|
// so we are overriding the user data once again to mimic the real behavior.
|
||||||
await page.route('*/**/user/get-session-user', async route => {
|
await page.route('*/**/user/get-session-user', async route => {
|
||||||
const response = await route.fetch();
|
const response = await route.fetch();
|
||||||
@@ -172,40 +170,3 @@ test.describe('Email sign-up page when user is signed in', () => {
|
|||||||
).toHaveAttribute('aria-pressed', 'false');
|
).toHaveAttribute('aria-pressed', 'false');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Email sign-up page when the user is new', () => {
|
|
||||||
test.use({ storageState: 'playwright/.auth/development-user.json' });
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
// It's necessary to seed with a user that has not accepted the privacy
|
|
||||||
// terms, otherwise the user will be redirected away from the email sign-up
|
|
||||||
// page.
|
|
||||||
execSync(
|
|
||||||
'node ./tools/scripts/seed/seed-demo-user --set-false acceptedPrivacyTerms'
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.goto('/email-sign-up');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterAll(() => {
|
|
||||||
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display the content correctly', async ({ page }) => {
|
|
||||||
await expect(
|
|
||||||
page.getByRole('heading', {
|
|
||||||
level: 1,
|
|
||||||
name: translations.misc['brand-new-account']
|
|
||||||
})
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByText(translations.misc['email-blast'])
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: translations.buttons['yes-please'] })
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: translations.buttons['no-thanks'] })
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ module.exports.blankUser = {
|
|||||||
location: '',
|
location: '',
|
||||||
picture: '',
|
picture: '',
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
isHonest: false,
|
isHonest: false,
|
||||||
isFrontEndCert: false,
|
isFrontEndCert: false,
|
||||||
@@ -91,7 +91,7 @@ module.exports.publicUser = {
|
|||||||
location: '',
|
location: '',
|
||||||
picture: '',
|
picture: '',
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
isHonest: false,
|
isHonest: false,
|
||||||
isFrontEndCert: false,
|
isFrontEndCert: false,
|
||||||
@@ -153,7 +153,7 @@ module.exports.demoUser = {
|
|||||||
location: '',
|
location: '',
|
||||||
picture: '',
|
picture: '',
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
isHonest: false,
|
isHonest: false,
|
||||||
isFrontEndCert: false,
|
isFrontEndCert: false,
|
||||||
@@ -217,7 +217,7 @@ module.exports.fullyCertifiedUser = {
|
|||||||
location: '',
|
location: '',
|
||||||
picture: '',
|
picture: '',
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: false,
|
sendQuincyEmail: null,
|
||||||
currentChallengeId: '',
|
currentChallengeId: '',
|
||||||
isHonest: true,
|
isHonest: true,
|
||||||
isFrontEndCert: true,
|
isFrontEndCert: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user