feat(accept-pp-tos): Add Privacy and ToS accept page

This commit is contained in:
Bouncey
2018-08-25 00:24:19 +01:00
committed by mrugesh mohapatra
parent 4f2241d39f
commit 7fd0b5b84b
9 changed files with 237 additions and 22 deletions

View File

@ -74,20 +74,6 @@ module.exports = function enableAuthentication(app) {
});
});
router.get(
'/accept-privacy-terms',
ifNoUserRedirectHome,
(req, res) => {
const { user } = req;
if (user && !user.acceptedPrivacyTerms) {
return res.render('account/accept-privacy-terms', {
title: 'Privacy Policy and Terms of Service'
});
}
return res.redirect('/settings');
}
);
const defaultErrorMsg = dedent`
Oops, something is not right,
please request a fresh link to sign in / sign up.

View File

@ -50,7 +50,8 @@ export const userPropsForSession = [
'completedChallengeCount',
'completedProjectCount',
'completedCertCount',
'completedLegacyCertCount'
'completedLegacyCertCount',
'acceptedPrivacyTerms'
];
export function normaliseUserFields(user) {

View File

@ -0,0 +1,172 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {
Grid,
Row,
Col,
Button,
FormGroup,
ControlLabel,
Checkbox
} from 'react-bootstrap';
import Helmet from 'react-helmet';
import Layout from '../components/layout';
import { ButtonSpacer, Spacer } from '../components/helpers';
import { acceptTerms, isSignedInSelector, userSelector } from '../redux';
import { createSelector } from 'reselect';
import { navigateTo } from 'gatsby';
const propTypes = {
acceptTerms: PropTypes.func.isRequired,
acceptedPrivacyTerms: PropTypes.bool,
isSignedIn: PropTypes.bool
};
const mapStateToProps = createSelector(
isSignedInSelector,
userSelector,
(isSignedIn, { acceptedPrivacyTerms }) => ({
isSignedIn,
acceptedPrivacyTerms
})
);
const mapDispatchToProps = dispatch =>
bindActionCreators({ acceptTerms }, dispatch);
class AcceptPrivacyTerms extends Component {
constructor(props) {
super(props);
this.state = {
privacyPolicy: false,
termsOfService: false,
quincyEmail: true
};
this.createHandleChange = this.createHandleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
createHandleChange(prop) {
return () =>
this.setState(prevState => ({
[prop]: !prevState[prop]
}));
}
handleSubmit(e) {
e.preventDefault();
const { privacyPolicy, termsOfService, quincyEmail } = this.state;
if (!privacyPolicy || !termsOfService) {
return null;
}
return this.props.acceptTerms(quincyEmail);
}
render() {
const { isSignedIn, acceptedPrivacyTerms } = this.props;
if (!isSignedIn || acceptedPrivacyTerms) {
navigateTo(isSignedIn ? '/welcome' : '/');
return null;
}
const { privacyPolicy, termsOfService, quincyEmail } = this.state;
return (
<Layout>
<Helmet>
<title>Privacy Policy and Terms of Service</title>
</Helmet>
<Grid>
<Row>
<Col xs={12}>
<div className='text-center'>
<Spacer />
<Spacer />
<h3>
Please review our updated privacy policy and the terms of
service.
</h3>
<Spacer />
</div>
</Col>
</Row>
<Row>
<Col sm={6} smOffset={3}>
<form onSubmit={this.handleSubmit}>
<FormGroup>
<ControlLabel htmlFor='terms-of-service'>
Terms of Service
</ControlLabel>
<Spacer />
<Checkbox
checked={termsOfService}
id='terms-of-service'
inline={true}
onChange={this.createHandleChange('termsOfService')}
>
I accept the{' '}
<a href='https://www.freecodecamp/terms' target='_blank'>
terms of service
</a>{' '}
(required)
</Checkbox>
</FormGroup>
<FormGroup>
<ControlLabel htmlFor='privacy-policy'>
Privacy Policy
</ControlLabel>
<Spacer />
<Checkbox
checked={privacyPolicy}
id='privacy-policy'
inline={true}
onChange={this.createHandleChange('privacyPolicy')}
>
I accept the{' '}
<a href='https://www.freecodecamp/privacy' target='_blank'>
privacy policy
</a>{' '}
(required)
</Checkbox>
</FormGroup>
<FormGroup>
<ControlLabel htmlFor='quincy-email'>
Quincy's Emails
</ControlLabel>
<Spacer />
<Checkbox
checked={quincyEmail}
id='quincy-email'
inline={true}
onChange={this.createHandleChange('quincyEmail')}
>
I want weekly emails from Quincy, freeCodeCamp.org's
founder.
</Checkbox>
</FormGroup>
<ButtonSpacer />
<Button
block={true}
bsStyle='primary'
className='big-cta-btn'
disabled={!privacyPolicy || !termsOfService}
type='submit'
>
Continue to freeCodeCamp
</Button>
</form>
</Col>
</Row>
</Grid>
</Layout>
);
}
}
AcceptPrivacyTerms.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(AcceptPrivacyTerms);

0
src/pages/signin.js Normal file
View File

View File

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { navigateTo } from 'gatsby';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -19,6 +20,7 @@ const propTypes = {
errored: PropTypes.bool
}),
user: PropTypes.shape({
acceptedPrivacyTerms: PropTypes.bool,
username: PropTypes.string,
completedChallengeCount: PropTypes.number,
completedProjectCount: PropTypes.number,
@ -37,11 +39,12 @@ const mapDispatchToProps = dispatch => bindActionCreators({}, dispatch);
function Welcome({
fetchState: { pending, complete },
user: {
name,
completedChallengeCount,
completedProjectCount,
completedCertCount,
completedLegacyCertCount
acceptedPrivacyTerms,
name = '',
completedChallengeCount = 0,
completedProjectCount = 0,
completedCertCount = 0,
completedLegacyCertCount = 0
}
}) {
if (pending && !complete) {
@ -52,6 +55,11 @@ function Welcome({
);
}
if (!acceptedPrivacyTerms) {
navigateTo('/accept-privacy-terms');
return null;
}
const { quote, author } = randomQuote();
return (
<Layout>

View File

@ -0,0 +1,24 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { acceptTermsComplete, acceptTermsError } from './';
import { putUserAcceptsTerms } from '../utils/ajax';
import { createFlashMessage } from '../components/Flash/redux';
function* acceptTermsSaga({ payload: quincyEmails }) {
console.log('hello?');
try {
const {
data: response
} = yield call(putUserAcceptsTerms, quincyEmails);
yield put(acceptTermsComplete());
yield put(createFlashMessage(response));
} catch (e) {
yield put(acceptTermsError(e));
}
}
export function createAcceptTermsSaga(types) {
return [takeEvery(types.acceptTerms, acceptTermsSaga)];
}

View File

@ -2,6 +2,7 @@ import { createAction, handleActions } from 'redux-actions';
import { createTypes, createAsyncTypes } from '../utils/createTypes';
import { createFetchUserSaga } from './fetch-user-saga';
import { createAcceptTermsSaga } from './accept-terms-saga';
const ns = 'app';
@ -16,14 +17,24 @@ const initialState = {
user: {}
};
const types = createTypes([...createAsyncTypes('fetchUser')], ns);
const types = createTypes(
[...createAsyncTypes('fetchUser'), ...createAsyncTypes('acceptTerms')],
ns
);
export const sagas = [...createFetchUserSaga(types)];
export const sagas = [
...createFetchUserSaga(types),
...createAcceptTermsSaga(types)
];
export const fetchUser = createAction(types.fetchUser);
export const fetchUserComplete = createAction(types.fetchUserComplete);
export const fetchUserError = createAction(types.fetchUserError);
export const acceptTerms = createAction(types.acceptTerms);
export const acceptTermsComplete = createAction(types.acceptTermsComplete);
export const acceptTermsError = createAction(types.acceptTermsError);
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
export const userFetchStateSelector = state => state[ns].fetchState;
export const usernameSelector = state => state[ns].appUsername;

View File

@ -20,3 +20,7 @@ function sniff(things) {
export function getSessionUser() {
return get('/user/get-session-user').then(sniff);
}
export function putUserAcceptsTerms(quincyEmails) {
return put('/update-privacy-terms', {quincyEmails})
}

9
static/_redirects Normal file
View File

@ -0,0 +1,9 @@
# signin redirects
/signup /signin 301
/email-signin /signin 301
/login /signin 301
/deprecated-signin /signin 301
# signout redirects
/logout /signout 301