diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma
index 31aafe1f9ff..f193ac6bf2a 100644
--- a/api/prisma/schema.prisma
+++ b/api/prisma/schema.prisma
@@ -147,7 +147,8 @@ model user {
/// Valuable for selectively performing random logic.
rand Float?
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
- sendQuincyEmail Boolean
+ // Nullable tri-state: null (likely new user), true (subscribed), false (unsubscribed)
+ sendQuincyEmail Boolean?
theme String? // Undefined
timezone String? // Undefined
twitter String? // Null | Undefined
diff --git a/api/src/plugins/__fixtures__/user.ts b/api/src/plugins/__fixtures__/user.ts
index f61e11a3072..0d79851410c 100644
--- a/api/src/plugins/__fixtures__/user.ts
+++ b/api/src/plugins/__fixtures__/user.ts
@@ -79,7 +79,7 @@ export const newUser = (email: string) => ({
progressTimestamps: [expect.any(Number)],
rand: null, // TODO(Post-MVP): delete from schema (it's not used or required).
savedChallenges: [],
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
theme: 'default',
timezone: null,
twitter: null,
diff --git a/api/src/plugins/auth0.test.ts b/api/src/plugins/auth0.test.ts
index 078468a2bdf..78946581c1c 100644
--- a/api/src/plugins/auth0.test.ts
+++ b/api/src/plugins/auth0.test.ts
@@ -332,26 +332,6 @@ describe('auth0 plugin', () => {
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 () => {
mockAuthSuccess();
diff --git a/api/src/plugins/auth0.ts b/api/src/plugins/auth0.ts
index 6051aa018d1..85250fb2955 100644
--- a/api/src/plugins/auth0.ts
+++ b/api/src/plugins/auth0.ts
@@ -15,10 +15,7 @@ import {
} from '../utils/env';
import { findOrCreateUser } from '../routes/helpers/auth-helpers';
import { createAccessToken } from '../utils/tokens';
-import {
- getLoginRedirectParams,
- getPrefixedLandingPath
-} from '../utils/redirection';
+import { getLoginRedirectParams } from '../utils/redirection';
declare module 'fastify' {
interface FastifyInstance {
@@ -108,8 +105,7 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
}
}
- const { returnTo, pathPrefix, origin } = getLoginRedirectParams(req);
- const redirectBase = getPrefixedLandingPath(origin, pathPrefix);
+ const { returnTo } = getLoginRedirectParams(req);
let token;
try {
@@ -166,24 +162,14 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
});
}
- const { id, acceptedPrivacyTerms } = await findOrCreateUser(
- fastify,
- email
- );
+ const { id } = await findOrCreateUser(fastify, email);
reply.setAccessTokenCookie(createAccessToken(id));
- if (acceptedPrivacyTerms) {
- void reply.redirectWithMessage(returnTo, {
- type: 'success',
- content: 'flash.signin-success'
- });
- } else {
- void reply.redirectWithMessage(`${redirectBase}/email-sign-up`, {
- type: 'success',
- content: 'flash.signin-success'
- });
- }
+ void reply.redirectWithMessage(returnTo, {
+ type: 'success',
+ content: 'flash.signin-success'
+ });
});
done();
diff --git a/api/src/routes/protected/user.test.ts b/api/src/routes/protected/user.test.ts
index ff112fffe01..606896162e1 100644
--- a/api/src/routes/protected/user.test.ts
+++ b/api/src/routes/protected/user.test.ts
@@ -151,7 +151,8 @@ const testUserData: Prisma.userCreateInput = {
],
yearsTopContributor: ['2018'],
twitter: '@foobar',
- linkedin: 'linkedin.com/foobar'
+ linkedin: 'linkedin.com/foobar',
+ sendQuincyEmail: false
};
const minimalUserData: Prisma.userCreateInput = {
@@ -301,6 +302,7 @@ const publicUserData = {
profileUI: testUserData.profileUI,
savedChallenges: testUserData.savedChallenges,
twitter: 'https://twitter.com/foobar',
+ sendQuincyEmail: testUserData.sendQuincyEmail,
username: testUserData.username,
usernameDisplay: testUserData.usernameDisplay,
website: testUserData.website,
diff --git a/api/src/routes/protected/user.ts b/api/src/routes/protected/user.ts
index a1b80ab50e7..c2daa636846 100644
--- a/api/src/routes/protected/user.ts
+++ b/api/src/routes/protected/user.ts
@@ -697,6 +697,7 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = (
user: {
[username]: {
...removeNulls(publicUser),
+ sendQuincyEmail: publicUser.sendQuincyEmail,
...normalizeFlags(flags),
picture: publicUser.picture ?? '',
email: email ?? '',
diff --git a/api/src/routes/public/user.test.ts b/api/src/routes/public/user.test.ts
index c26ed7da066..96f7eeb803d 100644
--- a/api/src/routes/public/user.test.ts
+++ b/api/src/routes/public/user.test.ts
@@ -26,6 +26,7 @@ vi.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
// This is used to build a test user.
const testUserData: Prisma.userCreateInput = {
...createUserInput(defaultUserEmail),
+ sendQuincyEmail: true,
username: 'foobar',
usernameDisplay: 'Foo Bar',
progressTimestamps: [1520002973119, 1520440323273],
diff --git a/api/src/schemas/user/get-session-user.ts b/api/src/schemas/user/get-session-user.ts
index b09d4dcd282..6e4623d2907 100644
--- a/api/src/schemas/user/get-session-user.ts
+++ b/api/src/schemas/user/get-session-user.ts
@@ -108,7 +108,7 @@ export const getSessionUser = {
})
),
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(),
twitter: Type.Optional(Type.String()),
website: Type.Optional(Type.String()),
diff --git a/api/src/utils/create-user.ts b/api/src/utils/create-user.ts
index 7d25c378a10..52c08e3d58e 100644
--- a/api/src/utils/create-user.ts
+++ b/api/src/utils/create-user.ts
@@ -82,7 +82,7 @@ export function createUserInput(email: string) {
showPortfolio: false,
showTimeLine: false
},
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
theme: 'default',
username,
usernameDisplay: username,
diff --git a/client/src/components/Intro/email-sign-up-alert.tsx b/client/src/components/Intro/email-sign-up-alert.tsx
new file mode 100644
index 00000000000..09ae41d9db1
--- /dev/null
+++ b/client/src/components/Intro/email-sign-up-alert.tsx
@@ -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 (
+
+
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EmailSignUpAlert);
diff --git a/client/src/components/Intro/index.tsx b/client/src/components/Intro/index.tsx
index 6b850240a7e..6d0ed1e80be 100644
--- a/client/src/components/Intro/index.tsx
+++ b/client/src/components/Intro/index.tsx
@@ -6,6 +6,7 @@ import Login from '../Header/components/login';
import { Link, Loader } from '../helpers';
import './intro.css';
+import EmailSignUpAlert from './email-sign-up-alert';
import LearnAlert from './learn-alert';
interface IntroProps {
@@ -64,6 +65,7 @@ const Intro = ({
onLearnDonationAlertClick={onLearnDonationAlertClick}
isDonating={isDonating}
/>
+
{completedChallengeCount && slug && completedChallengeCount < 15 ? (
diff --git a/client/src/components/Intro/intro.css b/client/src/components/Intro/intro.css
index ea8b5c064b6..36901de3634 100644
--- a/client/src/components/Intro/intro.css
+++ b/client/src/components/Intro/intro.css
@@ -42,3 +42,20 @@
font-style: normal;
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;
+}
diff --git a/client/src/components/Intro/intro.test.tsx b/client/src/components/Intro/intro.test.tsx
index 702a5c0e50f..da26fc0f546 100644
--- a/client/src/components/Intro/intro.test.tsx
+++ b/client/src/components/Intro/intro.test.tsx
@@ -7,8 +7,11 @@ import Intro from '.';
jest.mock('../../analytics');
-function renderWithRedux(ui: JSX.Element) {
- return render(
{ui});
+function renderWithRedux(
+ ui: JSX.Element,
+ preloadedState: Record
= {}
+) {
+ return render({ui});
}
describe('', () => {
@@ -19,7 +22,19 @@ describe('', () => {
});
it('has a blockquote when loggedIn', () => {
- renderWithRedux();
+ // 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(, preloadedState);
expect(screen.getByTestId('quote-block')).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
diff --git a/client/src/components/email-options.tsx b/client/src/components/email-options.tsx
new file mode 100644
index 00000000000..9669fc290cd
--- /dev/null
+++ b/client/src/components/email-options.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+interface EmailOptionsProps {
+ isSignedIn: boolean;
+ updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
+ isPage?: boolean;
+}
+
+function EmailOptions({
+ isSignedIn,
+ updateQuincyEmail,
+ isPage
+}: EmailOptionsProps) {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+
+ {isPage ? (
+ {t('misc.email-signup')}
+ ) : (
+ {t('misc.email-signup')}
+ )}
+
+
+
+
+
+
+ {t('misc.email-blast')}
+ {t('misc.quincy')}
+
+
+
+
+ >
+ );
+}
+
+export default EmailOptions;
diff --git a/client/src/components/profile/profile.test.tsx b/client/src/components/profile/profile.test.tsx
index 55167362125..e178bfff5fa 100644
--- a/client/src/components/profile/profile.test.tsx
+++ b/client/src/components/profile/profile.test.tsx
@@ -11,7 +11,6 @@ window.___loader = { enqueue: () => {}, hovering: () => {} };
const userProps = {
user: {
- acceptedPrivacyTerms: true,
currentChallengeId: 'string',
email: 'string',
emailVerified: true,
diff --git a/client/src/components/settings/email.tsx b/client/src/components/settings/email.tsx
index 854781ceeb7..485d75cae47 100644
--- a/client/src/components/settings/email.tsx
+++ b/client/src/components/settings/email.tsx
@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
type EmailProps = {
email: string;
isEmailVerified: boolean;
- sendQuincyEmail: boolean;
+ sendQuincyEmail: boolean | null;
t: TFunction;
updateMyEmail: (email: string) => void;
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
@@ -250,7 +250,7 @@ function EmailSettings({
void;
- acceptedPrivacyTerms: boolean;
+interface EmailSignUpProps {
+ updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
+ sendQuincyEmail: boolean | null | undefined;
isSignedIn: boolean;
showLoading: boolean;
- completedChallengeCount: number;
}
const mapStateToProps = createSelector(
@@ -33,90 +28,28 @@ const mapStateToProps = createSelector(
isSignedInSelector,
signInLoadingSelector,
(user: User | null, isSignedIn: boolean, showLoading: boolean) => ({
- acceptedPrivacyTerms: !!user?.acceptedPrivacyTerms,
+ sendQuincyEmail: user?.sendQuincyEmail,
isSignedIn,
- showLoading,
- completedChallengeCount: user?.completedChallengeCount ?? 0
+ showLoading
})
);
-const mapDispatchToProps = (dispatch: Dispatch) =>
- bindActionCreators({ acceptTerms }, dispatch);
+const mapDispatchToProps = {
+ updateQuincyEmail: (sendQuincyEmail: boolean) =>
+ updateMyQuincyEmail({ sendQuincyEmail })
+};
const RedirectToLearn = createRedirect('/learn');
-function EmailListOptIn({
+function EmailSignUp({
+ updateQuincyEmail,
+ sendQuincyEmail,
isSignedIn,
- acceptTerms
-}: {
- isSignedIn: boolean;
- acceptTerms: (accepted: boolean) => void;
-}) {
+ showLoading
+}: EmailSignUpProps) {
const { t } = useTranslation();
- if (isSignedIn) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
- } else {
- return (
-
-
-
-
-
- );
- }
-}
-function AcceptPrivacyTerms({
- acceptTerms,
- acceptedPrivacyTerms,
- isSignedIn,
- showLoading,
- completedChallengeCount
-}: AcceptPrivacyTermsProps) {
- const { t } = useTranslation();
- const acceptedPrivacyRef = useRef(acceptedPrivacyTerms);
- const acceptTermsRef = useRef(acceptTerms);
- const newAccount = isSignedIn && completedChallengeCount < 1;
+ const userHasMadeSelection = isSignedIn && sendQuincyEmail !== null;
- useEffect(() => {
- acceptedPrivacyRef.current = acceptedPrivacyTerms;
- acceptTermsRef.current = acceptTerms;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return acceptedPrivacyTerms ? (
+ return userHasMadeSelection ? (
) : (
<>
@@ -124,47 +57,20 @@ function AcceptPrivacyTerms({
{t('misc.email-signup')} | freeCodeCamp.org
-
-
-
-
- {newAccount
- ? t('misc.brand-new-account')
- : t('misc.email-signup')}
-
-
-
-
- {newAccount && (
-
-
-
-
-
-
-
-
-
-
+
+ {showLoading ? (
+
+ ) : (
+
)}
-
-
-
- {t('misc.email-blast')}
-
-
- {showLoading ? (
-
- ) : (
-
- )}
-
-
-
-
+
>
);
}
-export default connect(mapStateToProps, mapDispatchToProps)(AcceptPrivacyTerms);
+export default connect(mapStateToProps, mapDispatchToProps)(EmailSignUp);
diff --git a/client/src/redux/accept-terms-saga.js b/client/src/redux/accept-terms-saga.js
deleted file mode 100644
index 8397de24416..00000000000
--- a/client/src/redux/accept-terms-saga.js
+++ /dev/null
@@ -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)
- ];
-}
diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js
index d242e461e26..dd85c8d1860 100644
--- a/client/src/redux/action-types.js
+++ b/client/src/redux/action-types.js
@@ -42,7 +42,6 @@ export const actionTypes = createTypes(
...createAsyncTypes('fetchUser'),
...createAsyncTypes('postCharge'),
...createAsyncTypes('fetchProfileForUser'),
- ...createAsyncTypes('acceptTerms'),
...createAsyncTypes('showCert'),
...createAsyncTypes('reportUser'),
...createAsyncTypes('deleteUserToken'),
diff --git a/client/src/redux/actions.ts b/client/src/redux/actions.ts
index a6d2ed2f9ef..5f363fe7fb4 100644
--- a/client/src/redux/actions.ts
+++ b/client/src/redux/actions.ts
@@ -42,12 +42,6 @@ export const saveChallengeComplete = createAction(
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 fetchUserComplete = createAction(actionTypes.fetchUserComplete);
export const fetchUserTimeout = createAction(actionTypes.fetchUserTimeout);
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index 2335cd15dfe..fa63d9226eb 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -7,7 +7,6 @@ import {
CURRENT_CHALLENGE_KEY
} from '../templates/Challenges/redux/action-types';
import { getIsDailyCodingChallenge } from '../../../shared/config/challenge-types';
-import { createAcceptTermsSaga } from './accept-terms-saga';
import { actionTypes, ns as MainApp } from './action-types';
import { createAppMountSaga } from './app-mount-saga';
import { createDonationSaga } from './donation-saga';
@@ -88,7 +87,6 @@ const initialState = {
export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
export const sagas = [
- ...createAcceptTermsSaga(actionTypes),
...createThemeSaga(actionTypes),
...createAppMountSaga(actionTypes),
...createDonationSaga(actionTypes),
@@ -117,26 +115,6 @@ function spreadThePayloadOnUser(state, payload) {
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 }) => {
return {
...state,
diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts
index 52406aefdd9..4b03493c677 100644
--- a/client/src/redux/prop-types.ts
+++ b/client/src/redux/prop-types.ts
@@ -393,7 +393,7 @@ export type User = {
profileUI: ProfileUI;
progressTimestamps: Array;
savedChallenges: SavedChallenges;
- sendQuincyEmail: boolean;
+ sendQuincyEmail: boolean | null;
sound: boolean;
theme: UserThemes;
keyboardShortcuts: boolean;
diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts
index b3d19785cb4..f2ae23c74a2 100644
--- a/client/src/utils/ajax.ts
+++ b/client/src/utils/ajax.ts
@@ -361,9 +361,9 @@ export function putUpdateMyHonesty(
return put('/update-my-honesty', update);
}
-export function putUpdateMyQuincyEmail(
- update: Record
-): Promise> {
+export function putUpdateMyQuincyEmail(update: {
+ sendQuincyEmail: boolean;
+}): Promise> {
return put('/update-my-quincy-email', update);
}
@@ -373,12 +373,6 @@ export function putUpdateMyPortfolio(
return put('/update-my-portfolio', update);
}
-export function putUserAcceptsTerms(
- quincyEmails: boolean
-): Promise> {
- return put('/update-privacy-terms', { quincyEmails });
-}
-
export function putUserUpdateEmail(
email: string
): Promise> {
diff --git a/e2e/email-sign-up-alert.spec.ts b/e2e/email-sign-up-alert.spec.ts
new file mode 100644
index 00000000000..15a7fe61376
--- /dev/null
+++ b/e2e/email-sign-up-alert.spec.ts
@@ -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();
+ });
+});
diff --git a/e2e/quincy-email-sign-up.spec.ts b/e2e/quincy-email-sign-up.spec.ts
index 20e09fa3b68..add0b7514e5 100644
--- a/e2e/quincy-email-sign-up.spec.ts
+++ b/e2e/quincy-email-sign-up.spec.ts
@@ -11,6 +11,7 @@ test.describe('Email sign-up page when user is not signed in', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test.beforeEach(async ({ page }) => {
+ execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
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.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 --set-false acceptedPrivacyTerms'
- );
+ // It's necessary to seed with a user that has not selected an email newsletter option.
+ execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
await page.goto('/email-sign-up');
});
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(
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();
@@ -148,9 +148,7 @@ test.describe('Email sign-up page when user is signed in', () => {
page.getByRole('heading', { name: 'Welcome back, Full Stack User' })
).toBeVisible();
- // When the user clicks Yes, the /update-privacy-terms API is called
- // to update both `acceptedPrivacyTerms` and `sendQuincyEmail`.
- // But `sendQuincyEmail` is not set in the DB since the endpoint is mocked,
+ // `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.
await page.route('*/**/user/get-session-user', async route => {
const response = await route.fetch();
@@ -172,40 +170,3 @@ test.describe('Email sign-up page when user is signed in', () => {
).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();
- });
-});
diff --git a/tools/scripts/seed/user-data.js b/tools/scripts/seed/user-data.js
index 3ea49b2cecb..7e9490b7fb8 100644
--- a/tools/scripts/seed/user-data.js
+++ b/tools/scripts/seed/user-data.js
@@ -29,7 +29,7 @@ module.exports.blankUser = {
location: '',
picture: '',
acceptedPrivacyTerms: true,
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
currentChallengeId: '',
isHonest: false,
isFrontEndCert: false,
@@ -91,7 +91,7 @@ module.exports.publicUser = {
location: '',
picture: '',
acceptedPrivacyTerms: true,
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
currentChallengeId: '',
isHonest: false,
isFrontEndCert: false,
@@ -153,7 +153,7 @@ module.exports.demoUser = {
location: '',
picture: '',
acceptedPrivacyTerms: true,
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
currentChallengeId: '',
isHonest: false,
isFrontEndCert: false,
@@ -217,7 +217,7 @@ module.exports.fullyCertifiedUser = {
location: '',
picture: '',
acceptedPrivacyTerms: true,
- sendQuincyEmail: false,
+ sendQuincyEmail: null,
currentChallengeId: '',
isHonest: true,
isFrontEndCert: true,