feat(client): add duplicate account warning (#50555)

* feat(client): add duplicate account warning

* feat: check completed challenge count

* feat: stop redirecting /learn to /email-sign-up

* test: update to account for the lack of redirects

Also, in an extremely WET way, test both options.

* Update client/src/pages/email-sign-up.tsx

---------

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Mrugesh Mohapatra
2023-06-02 00:56:19 +05:30
committed by GitHub
parent 41b9d94a4e
commit 16cfbd5829
8 changed files with 100 additions and 60 deletions

View File

@@ -529,6 +529,8 @@
"unsubscribed": "You have successfully been unsubscribed",
"keep-coding": "Whatever you go on to, keep coding!",
"email-signup": "Email Sign Up",
"brand-new-account": "Welcome to your brand new freeCodeCamp account. Let's get started.",
"duplicate-account-warning": "If you meant to sign into an existing account instead of creating this account, <0>click here to delete this account</0> and try another email address.",
"quincy": "- Quincy Larson, the teacher who founded freeCodeCamp.org",
"email-blast": "By the way, each Friday I send an email with 5 links about programming and computer science. I send these to about 4 million people. Would you like me to send this to you, too?",
"update-email-1": "Update your email address",

View File

@@ -11,7 +11,10 @@ function IntroDescription(): JSX.Element {
return (
<div className='intro-description'>
<strong>{t('learn.read-this.heading')}</strong>
<Spacer size='medium' />
<p className='text-center'>
<strong>{t('learn.read-this.heading')}</strong>
</p>
<Spacer size='medium' />
<p>{t('learn.read-this.p1')}</p>
<p>{t('learn.read-this.p2')}</p>
@@ -33,6 +36,7 @@ function IntroDescription(): JSX.Element {
</Trans>
</p>
<p>{t('learn.read-this.p12')}</p>
<strong>{t('misc.quincy')}</strong>
</div>
);
}

View File

@@ -82,7 +82,7 @@ const Intro = ({
return (
<>
<Spacer size='medium' />
<h1>{t('learn.heading')}</h1>
<h1 className='text-center'>{t('learn.heading')}</h1>
<Spacer size='medium' />
<IntroDescription />
<Spacer size='medium' />

View File

@@ -4,13 +4,8 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Loader } from '../../components/helpers';
import { tryToShowDonationModal } from '../../redux/actions';
import {
userFetchStateSelector,
isSignedInSelector,
userSelector
} from '../../redux/selectors';
import { userFetchStateSelector } from '../../redux/selectors';
import DonateModal from '../Donation/donation-modal';
import createRedirect from '../create-redirect';
import './prism.css';
import './prism-night.css';
@@ -29,12 +24,8 @@ type User = {
const mapStateToProps = createSelector(
userFetchStateSelector,
isSignedInSelector,
userSelector,
(fetchState: FetchState, isSignedIn, user: User) => ({
fetchState,
isSignedIn,
user
(fetchState: FetchState) => ({
fetchState
})
);
@@ -42,8 +33,6 @@ const mapDispatchToProps = {
tryToShowDonationModal
};
const RedirectEmailSignUp = createRedirect('/email-sign-up');
type LearnLayoutProps = {
isSignedIn?: boolean;
fetchState: FetchState;
@@ -54,9 +43,7 @@ type LearnLayoutProps = {
};
function LearnLayout({
isSignedIn,
fetchState,
user,
tryToShowDonationModal,
children,
hasEditableBoundaries
@@ -79,10 +66,6 @@ function LearnLayout({
return <Loader fullScreen={true} />;
}
if (isSignedIn && !user.acceptedPrivacyTerms) {
return <RedirectEmailSignUp />;
}
return (
<>
<Helmet>

View File

@@ -44,7 +44,7 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
return (
<FullWidthRow className='danger-zone text-center'>
<Panel bsStyle='danger'>
<Panel bsStyle='danger' id='danger-zone'>
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
<Spacer size='medium' />
<p>{t('settings.danger.be-careful')}</p>

View File

@@ -6,6 +6,7 @@ exports[`<EmailSignUp /> Non-Authenticated user "not accepted terms and conditio
<div
className="container"
>
<div
className="row"
>
@@ -16,16 +17,28 @@ exports[`<EmailSignUp /> Non-Authenticated user "not accepted terms and conditio
className="spacer"
style={
{
"padding": "15px 0",
"padding": "5px 0",
}
}
/>
<div
className="intro-description"
>
<strong>
learn.read-this.heading
</strong>
<div
className="spacer"
style={
{
"padding": "15px 0",
}
}
/>
<p
className="text-center"
>
<strong>
learn.read-this.heading
</strong>
</p>
<div
className="spacer"
style={
@@ -80,6 +93,9 @@ exports[`<EmailSignUp /> Non-Authenticated user "not accepted terms and conditio
<p>
learn.read-this.p12
</p>
<strong>
misc.quincy
</strong>
</div>
<hr />
</div>
@@ -91,14 +107,11 @@ exports[`<EmailSignUp /> Non-Authenticated user "not accepted terms and conditio
<div
className="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1 col-xs-12"
>
<strong>
misc.quincy
</strong>
<div
className="spacer"
style={
{
"padding": "15px 0",
"padding": "5px 0",
}
}
/>
@@ -109,7 +122,7 @@ exports[`<EmailSignUp /> Non-Authenticated user "not accepted terms and conditio
className="spacer"
style={
{
"padding": "15px 0",
"padding": "5px 0",
}
}
/>

View File

@@ -2,14 +2,14 @@ import { Row, Col, Button, Grid } from '@freecodecamp/react-bootstrap';
import React, { useEffect, useRef } from 'react';
import Helmet from 'react-helmet';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { withTranslation, Trans } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import IntroDescription from '../components/Intro/components/intro-description';
import createRedirect from '../components/create-redirect';
import { Spacer, Loader } from '../components/helpers';
import { Spacer, Loader, Link } from '../components/helpers';
import { apiLocation } from '../../../config/env.json';
import { acceptTerms } from '../redux/actions';
@@ -26,6 +26,7 @@ interface AcceptPrivacyTermsProps {
isSignedIn: boolean;
t: TFunction;
showLoading: boolean;
completedChallengeCount?: number;
}
const mapStateToProps = createSelector(
@@ -33,13 +34,17 @@ const mapStateToProps = createSelector(
isSignedInSelector,
signInLoadingSelector,
(
{ acceptedPrivacyTerms }: { acceptedPrivacyTerms: boolean },
{
acceptedPrivacyTerms,
completedChallengeCount
}: { acceptedPrivacyTerms: boolean; completedChallengeCount: number },
isSignedIn: boolean,
showLoading: boolean
) => ({
acceptedPrivacyTerms,
isSignedIn,
showLoading
showLoading,
completedChallengeCount
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
@@ -51,7 +56,8 @@ function AcceptPrivacyTerms({
acceptedPrivacyTerms,
isSignedIn,
t,
showLoading
showLoading,
completedChallengeCount = 0
}: AcceptPrivacyTermsProps) {
const acceptedPrivacyRef = useRef(acceptedPrivacyTerms);
const acceptTermsRef = useRef(acceptTerms);
@@ -61,24 +67,11 @@ function AcceptPrivacyTerms({
acceptTermsRef.current = acceptTerms;
});
useEffect(() => {
return () => {
// if a user navigates away from here we should set acceptedPrivacyTerms
// to true (so they do not get pulled back) without changing their email
// preferences (hence the null payload)
// This makes sure that the user has to opt in to Quincy's emails and that
// they are only asked twice
if (!acceptedPrivacyRef.current) {
acceptTermsRef.current(null);
}
};
}, []);
function onClick(isWeeklyEmailAccepted: boolean) {
acceptTerms(isWeeklyEmailAccepted);
}
function renderEmailListOptin(isSignedIn: boolean, showLoading: boolean) {
function renderEmailListOptIn(isSignedIn: boolean, showLoading: boolean) {
if (showLoading) {
return <Loader fullScreen={true} />;
}
@@ -138,21 +131,36 @@ function AcceptPrivacyTerms({
<title>{t('misc.email-signup')} | freeCodeCamp.org</title>
</Helmet>
<Grid>
{isSignedIn && completedChallengeCount < 1 ? (
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='large' />
<h1 className='text-center'>{t('misc.brand-new-account')}</h1>
<Spacer size='small' />
<p>
<Trans i18nKey='misc.duplicate-account-warning'>
<Link className='inline' to='/settings#danger-zone' />
</Trans>
</p>
</Col>
</Row>
) : (
''
)}
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='medium' />
<Spacer size='small' />
<IntroDescription />
<hr />
</Col>
</Row>
<Row className='email-sign-up' data-cy='email-sign-up'>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<strong>{t('misc.quincy')}</strong>
<Spacer size='medium' />
<Spacer size='small' />
<p>{t('misc.email-blast')}</p>
<Spacer size='medium' />
<Spacer size='small' />
</Col>
{renderEmailListOptin(isSignedIn, showLoading)}
{renderEmailListOptIn(isSignedIn, showLoading)}
<Col xs={12}>
<Spacer size='medium' />
</Col>

View File

@@ -1,5 +1,6 @@
// TODO: DRY out the parts before clicking "Yes please" and "No thanks"
describe('Privacy terms', () => {
it('should not redirect away from email sign up page on login', () => {
it('should accept update privacy terms if requests emails from Quincy', () => {
// Flag used to identify if the `/update-privacy-terms` have been called
let privacyTermsUpdated = false;
cy.intercept('PUT', '/update-privacy-terms', () => {
@@ -13,13 +14,13 @@ describe('Privacy terms', () => {
// 2. The /update-privacy-terms has not been requested
cy.visit('/');
cy.get('[data-test-label="landing-small-cta"]').click();
cy.location('pathname').should('contain', '/email-sign-up');
// Since we're using the dev login, we do
cy.wrap(privacyTermsUpdated).should('eq', false);
cy.visit('/email-sign-up');
// Assert email sign up elements and make sure we don't get redirected somewhere else
cy.title().should('contain', 'Email Sign Up');
cy.get('[data-cy="email-sign-up"]').should('exist');
// Navigate away from this page via quincy emails which should unmount the component
// and request /update-privacy-terms
// Accept
cy.get('button:contains("Yes please")').click();
cy.wait('@updatePrivacyTerms').then(() => {
expect(privacyTermsUpdated).to.eq(true);
@@ -27,4 +28,33 @@ describe('Privacy terms', () => {
cy.location('pathname').should('contain', '/learn');
});
});
it('should accept update privacy terms if the user rejects emails from Quincy', () => {
// Flag used to identify if the `/update-privacy-terms` have been called
let privacyTermsUpdated = false;
cy.intercept('PUT', '/update-privacy-terms', () => {
privacyTermsUpdated = true;
}).as('updatePrivacyTerms');
// Seed dev user with `acceptedPrivacyTerms` unset
cy.exec('pnpm run seed -- --unset-privacy-terms');
// Go to the homepage and log in manually so we can assert the following:
// 1. Redirection to /email-sign-up works properly
// 2. The /update-privacy-terms has not been requested
cy.visit('/');
cy.get('[data-test-label="landing-small-cta"]').click();
// Since we're using the dev login, we do
cy.wrap(privacyTermsUpdated).should('eq', false);
cy.visit('/email-sign-up');
// Assert email sign up elements and make sure we don't get redirected somewhere else
cy.title().should('contain', 'Email Sign Up');
cy.get('[data-cy="email-sign-up"]').should('exist');
// Accept
cy.get('button:contains("No thanks")').click();
cy.wait('@updatePrivacyTerms').then(() => {
expect(privacyTermsUpdated).to.eq(true);
cy.contains('Welcome back');
cy.location('pathname').should('contain', '/learn');
});
});
});