mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-27 11:01:38 -04:00
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:
committed by
GitHub
parent
41b9d94a4e
commit
16cfbd5829
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user