feat(email-settings): Add email settings
This commit is contained in:
@ -9,7 +9,6 @@ import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
|||||||
// const log = debug('fcc:models:userIdent');
|
// const log = debug('fcc:models:userIdent');
|
||||||
|
|
||||||
export default function(UserIdent) {
|
export default function(UserIdent) {
|
||||||
|
|
||||||
UserIdent.on('dataSourceAttached', () => {
|
UserIdent.on('dataSourceAttached', () => {
|
||||||
UserIdent.findOne$ = observeMethod(UserIdent, 'findOne');
|
UserIdent.findOne$ = observeMethod(UserIdent, 'findOne');
|
||||||
});
|
});
|
||||||
@ -33,7 +32,7 @@ export default function(UserIdent) {
|
|||||||
// get the social provider data and the external id from auth0
|
// get the social provider data and the external id from auth0
|
||||||
profile.id = profile.id || profile.openid;
|
profile.id = profile.id || profile.openid;
|
||||||
const auth0IdString = '' + profile.id;
|
const auth0IdString = '' + profile.id;
|
||||||
const [ provider, socialExtId ] = auth0IdString.split('|');
|
const [provider, socialExtId] = auth0IdString.split('|');
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
provider: provider,
|
provider: provider,
|
||||||
@ -42,8 +41,10 @@ export default function(UserIdent) {
|
|||||||
include: 'user'
|
include: 'user'
|
||||||
};
|
};
|
||||||
// get the email from the auth0 (its expected from social providers)
|
// get the email from the auth0 (its expected from social providers)
|
||||||
const email = (profile && profile.emails && profile.emails[0]) ?
|
const email =
|
||||||
profile.emails[0].value : '';
|
profile && profile.emails && profile.emails[0]
|
||||||
|
? profile.emails[0].value
|
||||||
|
: '';
|
||||||
if (!isEmail('' + email)) {
|
if (!isEmail('' + email)) {
|
||||||
throw wrapHandledError(
|
throw wrapHandledError(
|
||||||
new Error('invalid or empty email recieved from auth0'),
|
new Error('invalid or empty email recieved from auth0'),
|
||||||
@ -60,12 +61,11 @@ export default function(UserIdent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider === 'email') {
|
if (provider === 'email') {
|
||||||
|
|
||||||
return User.findOne$({ where: { email } })
|
return User.findOne$({ where: { email } })
|
||||||
.flatMap(user => {
|
.flatMap(user => {
|
||||||
return user ?
|
return user
|
||||||
Observable.of(user) :
|
? Observable.of(user)
|
||||||
User.create$({ email }).toPromise();
|
: User.create$({ email }).toPromise();
|
||||||
})
|
})
|
||||||
.flatMap(user => {
|
.flatMap(user => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -81,56 +81,51 @@ export default function(UserIdent) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const createToken = observeQuery(
|
const createToken = observeQuery(AccessToken, 'create', {
|
||||||
AccessToken,
|
|
||||||
'create',
|
|
||||||
{
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
ttl: user.constructor.settings.ttl
|
ttl: user.constructor.settings.ttl
|
||||||
}
|
});
|
||||||
);
|
const updateUserPromise = new Promise((resolve, reject) =>
|
||||||
const updateUser = user.update$({
|
user.updateAttributes(
|
||||||
|
{
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
emailAuthLinkTTL: null,
|
emailAuthLinkTTL: null,
|
||||||
emailVerifyTTL: null
|
emailVerifyTTL: null
|
||||||
});
|
},
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
Observable.of(user),
|
Observable.of(user),
|
||||||
createToken,
|
createToken,
|
||||||
updateUser,
|
Observable.fromPromise(updateUserPromise),
|
||||||
(user, token) => ({user, token})
|
(user, token) => ({ user, token })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(({ user, token }) => cb(null, user, null, token), cb);
|
||||||
({ user, token }) => cb(null, user, null, token),
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return UserIdent.findOne$(query)
|
return UserIdent.findOne$(query)
|
||||||
.flatMap(identity => {
|
.flatMap(identity => {
|
||||||
return identity ?
|
return identity
|
||||||
Observable.of(identity.user()) :
|
? Observable.of(identity.user())
|
||||||
User.findOne$({ where: { email } })
|
: User.findOne$({ where: { email } }).flatMap(user => {
|
||||||
.flatMap(user => {
|
return user
|
||||||
return user ?
|
? Observable.of(user)
|
||||||
Observable.of(user) :
|
: User.create$({ email }).toPromise();
|
||||||
User.create$({ email }).toPromise();
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.flatMap(user => {
|
.flatMap(user => {
|
||||||
|
const createToken = observeQuery(AccessToken, 'create', {
|
||||||
const createToken = observeQuery(
|
|
||||||
AccessToken,
|
|
||||||
'create',
|
|
||||||
{
|
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
ttl: user.constructor.settings.ttl
|
ttl: user.constructor.settings.ttl
|
||||||
}
|
});
|
||||||
);
|
|
||||||
const updateUser = user.update$({
|
const updateUser = user.update$({
|
||||||
email: email,
|
email: email,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
@ -144,11 +139,7 @@ export default function(UserIdent) {
|
|||||||
(user, token) => ({ user, token })
|
(user, token) => ({ user, token })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(({ user, token }) => cb(null, user, null, token), cb);
|
||||||
({ user, token }) => cb(null, user, null, token),
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -614,10 +614,8 @@ module.exports = function(User) {
|
|||||||
this.update$({ emailAuthLinkTTL })
|
this.update$({ emailAuthLinkTTL })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map(() =>
|
.map(() => 'Check your email and click the link we sent you to confirm' +
|
||||||
dedent`
|
' your new email address.'
|
||||||
Check your email and click the link we sent you to confirm you email.
|
|
||||||
`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -691,12 +689,18 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatMap(()=>{
|
.flatMap(()=>{
|
||||||
|
const updatePromise = new Promise((resolve, reject) =>
|
||||||
|
this.updateAttributes(updateConfig, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
}));
|
||||||
return Observable.forkJoin(
|
return Observable.forkJoin(
|
||||||
this.update$(updateConfig),
|
Observable.fromPromise(updatePromise),
|
||||||
this.requestAuthEmail(false, newEmail),
|
this.requestAuthEmail(false, newEmail),
|
||||||
(_, message) => message
|
(_, message) => message
|
||||||
)
|
);
|
||||||
.doOnNext(() => this.manualReload());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,14 +2,12 @@ import dedent from 'dedent';
|
|||||||
|
|
||||||
const ALLOWED_METHODS = ['GET'];
|
const ALLOWED_METHODS = ['GET'];
|
||||||
const EXCLUDED_PATHS = [
|
const EXCLUDED_PATHS = [
|
||||||
'/api/flyers/findOne',
|
|
||||||
'/signout',
|
'/signout',
|
||||||
'/accept-privacy-terms',
|
'/accept-privacy-terms',
|
||||||
'/update-email',
|
'/update-email',
|
||||||
'/confirm-email',
|
'/confirm-email',
|
||||||
'/passwordless-change',
|
'/passwordless-change'
|
||||||
'/external/services/user'
|
].reduce((list, item) => [...list, item, `/internal${item}`], []);
|
||||||
];
|
|
||||||
|
|
||||||
export default function emailNotVerifiedNotice() {
|
export default function emailNotVerifiedNotice() {
|
||||||
return function(req, res, next) {
|
return function(req, res, next) {
|
||||||
|
@ -12,20 +12,25 @@ import { submitNewAbout, updateUserFlag } from '../redux/settings';
|
|||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import Spacer from '../components/helpers/Spacer';
|
import Spacer from '../components/helpers/Spacer';
|
||||||
import Loader from '../components/helpers/Loader';
|
import Loader from '../components/helpers/Loader';
|
||||||
import { FullWidthRow } from '../components/helpers';
|
import FullWidthRow from '../components/helpers/FullWidthRow';
|
||||||
import About from '../components/settings/About';
|
import About from '../components/settings/About';
|
||||||
import Privacy from '../components/settings/Privacy';
|
import Privacy from '../components/settings/Privacy';
|
||||||
|
import Email from '../components/settings/Email';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
about: PropTypes.string,
|
about: PropTypes.string,
|
||||||
|
email: PropTypes.string,
|
||||||
|
isEmailVerified: PropTypes.bool,
|
||||||
location: PropTypes.string,
|
location: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
|
sendQuincyEmail: PropTypes.bool,
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
submitNewAbout: PropTypes.func.isRequired,
|
submitNewAbout: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
toggleNightMode: PropTypes.func.isRequired,
|
toggleNightMode: PropTypes.func.isRequired,
|
||||||
|
updateQuincyEmail: PropTypes.func.isRequired,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,8 +39,22 @@ const mapStateToProps = createSelector(
|
|||||||
userSelector,
|
userSelector,
|
||||||
(
|
(
|
||||||
showLoading,
|
showLoading,
|
||||||
{ username = '', about, picture, points, name, location, theme }
|
{
|
||||||
|
username = '',
|
||||||
|
about,
|
||||||
|
email,
|
||||||
|
sendQuincyEmail,
|
||||||
|
isEmailVerified,
|
||||||
|
picture,
|
||||||
|
points,
|
||||||
|
name,
|
||||||
|
location,
|
||||||
|
theme
|
||||||
|
}
|
||||||
) => ({
|
) => ({
|
||||||
|
email,
|
||||||
|
sendQuincyEmail,
|
||||||
|
isEmailVerified,
|
||||||
showLoading,
|
showLoading,
|
||||||
username,
|
username,
|
||||||
about,
|
about,
|
||||||
@ -49,12 +68,19 @@ const mapStateToProps = createSelector(
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{ submitNewAbout, toggleNightMode: theme => updateUserFlag({theme}) },
|
{
|
||||||
|
submitNewAbout,
|
||||||
|
toggleNightMode: theme => updateUserFlag({ theme }),
|
||||||
|
updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail })
|
||||||
|
},
|
||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
function ShowSettings(props) {
|
function ShowSettings(props) {
|
||||||
const {
|
const {
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
sendQuincyEmail,
|
||||||
showLoading,
|
showLoading,
|
||||||
username,
|
username,
|
||||||
about,
|
about,
|
||||||
@ -64,7 +90,8 @@ function ShowSettings(props) {
|
|||||||
location,
|
location,
|
||||||
name,
|
name,
|
||||||
submitNewAbout,
|
submitNewAbout,
|
||||||
toggleNightMode
|
toggleNightMode,
|
||||||
|
updateQuincyEmail
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
@ -120,9 +147,14 @@ function ShowSettings(props) {
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<Privacy />
|
<Privacy />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{/* <EmailSettings />
|
<Email
|
||||||
|
email={email}
|
||||||
|
isEmailVerified={isEmailVerified}
|
||||||
|
sendQuincyEmail={sendQuincyEmail}
|
||||||
|
updateQuincyEmail={updateQuincyEmail}
|
||||||
|
/>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<InternetSettings />
|
{/* <InternetSettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<PortfolioSettings />
|
<PortfolioSettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
250
client/src/components/settings/Email.js
Normal file
250
client/src/components/settings/Email.js
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'gatsby';
|
||||||
|
import {
|
||||||
|
HelpBlock,
|
||||||
|
Alert,
|
||||||
|
FormGroup,
|
||||||
|
ControlLabel,
|
||||||
|
FormControl,
|
||||||
|
Button
|
||||||
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
|
||||||
|
import { updateMyEmail } from '../../redux/settings';
|
||||||
|
import { maybeEmailRE } from '../../utils';
|
||||||
|
|
||||||
|
import FullWidthRow from '../helpers/FullWidthRow';
|
||||||
|
import Spacer from '../helpers/Spacer';
|
||||||
|
import SectionHeader from './SectionHeader';
|
||||||
|
import BlockSaveButton from '../helpers/form/BlockSaveButton';
|
||||||
|
import ToggleSetting from './ToggleSetting';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ updateMyEmail }, dispatch);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
isEmailVerified: PropTypes.bool,
|
||||||
|
sendQuincyEmail: PropTypes.bool,
|
||||||
|
updateMyEmail: PropTypes.func.isRequired,
|
||||||
|
updateQuincyEmail: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UpdateEmailButton() {
|
||||||
|
return (
|
||||||
|
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
||||||
|
<Button block={true} bsSize='lg' bsStyle='primary'>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailSettings extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
emailForm: {
|
||||||
|
currentEmail: props.email,
|
||||||
|
newEmail: '',
|
||||||
|
confirmNewEmail: '',
|
||||||
|
isPristine: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const {
|
||||||
|
emailForm: { newEmail }
|
||||||
|
} = this.state;
|
||||||
|
const { updateMyEmail } = this.props;
|
||||||
|
return updateMyEmail(newEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
createHandleEmailFormChange = key => e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const userInput = e.target.value.slice();
|
||||||
|
return this.setState(state => ({
|
||||||
|
emailForm: {
|
||||||
|
...state.emailForm,
|
||||||
|
[key]: userInput,
|
||||||
|
isPristine: userInput === state.emailForm.currentEmail
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
getValidationForNewEmail = () => {
|
||||||
|
const {
|
||||||
|
emailForm: { newEmail, currentEmail }
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!maybeEmailRE.test(newEmail)) {
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newEmail === currentEmail) {
|
||||||
|
return {
|
||||||
|
state: 'error',
|
||||||
|
message: 'This email is the same as your current email'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isEmail(newEmail)) {
|
||||||
|
return { state: 'success', message: '' };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
state: 'warning',
|
||||||
|
message:
|
||||||
|
'We could not validate your email correctly, ' +
|
||||||
|
'please ensure it is correct'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getValidationForConfirmEmail = () => {
|
||||||
|
const {
|
||||||
|
emailForm: { confirmNewEmail, newEmail }
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!maybeEmailRE.test(newEmail)) {
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const isMatch = newEmail === confirmNewEmail;
|
||||||
|
if (maybeEmailRE.test(confirmNewEmail)) {
|
||||||
|
return {
|
||||||
|
state: isMatch ? 'success' : 'error',
|
||||||
|
message: isMatch ? '' : 'Both new email addresses must be the same'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
state: null,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
emailForm: { newEmail, confirmNewEmail, currentEmail, isPristine }
|
||||||
|
} = this.state;
|
||||||
|
const { isEmailVerified, updateQuincyEmail, sendQuincyEmail } = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: newEmailValidation,
|
||||||
|
message: newEmailValidationMessage
|
||||||
|
} = this.getValidationForNewEmail();
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: confirmEmailValidation,
|
||||||
|
message: confirmEmailValidationMessage
|
||||||
|
} = this.getValidationForConfirmEmail();
|
||||||
|
if (!currentEmail) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FullWidthRow>
|
||||||
|
<p className='large-p text-center'>
|
||||||
|
You do not have an email associated with this account.
|
||||||
|
</p>
|
||||||
|
</FullWidthRow>
|
||||||
|
<FullWidthRow>
|
||||||
|
<UpdateEmailButton />
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='email-settings'>
|
||||||
|
<SectionHeader>Email Settings</SectionHeader>
|
||||||
|
{isEmailVerified ? null : (
|
||||||
|
<FullWidthRow>
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert bsStyle='info' className='text-center'>
|
||||||
|
Your email has not been verified.
|
||||||
|
<br />
|
||||||
|
Please check your email, or{' '}
|
||||||
|
<Link to='/update-email'>
|
||||||
|
request a new verification email here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock>
|
||||||
|
</FullWidthRow>
|
||||||
|
)}
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='form-update-email' onSubmit={this.handleSubmit}>
|
||||||
|
<FormGroup controlId='current-email'>
|
||||||
|
<ControlLabel>Current Email</ControlLabel>
|
||||||
|
<FormControl.Static>{currentEmail}</FormControl.Static>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
controlId='new-email'
|
||||||
|
validationState={newEmailValidation}
|
||||||
|
>
|
||||||
|
<ControlLabel>New Email</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={this.createHandleEmailFormChange('newEmail')}
|
||||||
|
type='email'
|
||||||
|
value={newEmail}
|
||||||
|
/>
|
||||||
|
{newEmailValidationMessage ? (
|
||||||
|
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
|
||||||
|
) : null}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
controlId='confirm-email'
|
||||||
|
validationState={confirmEmailValidation}
|
||||||
|
>
|
||||||
|
<ControlLabel>Confirm New Email</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={this.createHandleEmailFormChange('confirmNewEmail')}
|
||||||
|
type='email'
|
||||||
|
value={confirmNewEmail}
|
||||||
|
/>
|
||||||
|
{confirmEmailValidationMessage ? (
|
||||||
|
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
|
||||||
|
) : null}
|
||||||
|
</FormGroup>
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={
|
||||||
|
newEmailValidation !== 'success' ||
|
||||||
|
confirmEmailValidation !== 'success' ||
|
||||||
|
isPristine
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='form-quincy-email' onSubmit={this.handleSubmit}>
|
||||||
|
<ToggleSetting
|
||||||
|
action="Send me Quincy's weekly email"
|
||||||
|
flag={sendQuincyEmail}
|
||||||
|
flagName='sendQuincyEmail'
|
||||||
|
offLabel='No thanks'
|
||||||
|
onLabel='Yes please'
|
||||||
|
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailSettings.displayName = 'EmailSettings';
|
||||||
|
EmailSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(EmailSettings);
|
@ -15,13 +15,15 @@ import {
|
|||||||
Button
|
Button
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import { Spacer } from '../components/helpers';
|
import { Spacer } from '../components/helpers';
|
||||||
import './update-email.css';
|
import './update-email.css';
|
||||||
import { userSelector, updateMyEmail } from '../redux';
|
import { userSelector } from '../redux';
|
||||||
import { isString } from 'lodash';
|
import { updateMyEmail } from '../redux/settings';
|
||||||
import isEmail from 'validator/lib/isEmail';
|
import { maybeEmailRE } from '../utils';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isNewEmail: PropTypes.bool,
|
isNewEmail: PropTypes.bool,
|
||||||
@ -38,8 +40,6 @@ const mapStateToProps = createSelector(
|
|||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators({ updateMyEmail }, dispatch);
|
bindActionCreators({ updateMyEmail }, dispatch);
|
||||||
|
|
||||||
const maybeEmailRE = /[\w.+]*?@\w*?\.\w+?/;
|
|
||||||
|
|
||||||
class UpdateEmail extends Component {
|
class UpdateEmail extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -48,16 +48,15 @@ class UpdateEmail extends Component {
|
|||||||
emailValue: ''
|
emailValue: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// this.createSubmitHandler = this.createSubmitHandler.bind(this);
|
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
createSubmitHandler(fn) {
|
handleSubmit = e => {
|
||||||
return e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return fn(this.state.emailValue);
|
const { emailValue } = this.state;
|
||||||
|
const { updateMyEmail } = this.props;
|
||||||
|
return updateMyEmail(emailValue);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
const change = e.target.value;
|
const change = e.target.value;
|
||||||
@ -78,7 +77,7 @@ class UpdateEmail extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isNewEmail, updateMyEmail } = this.props;
|
const { isNewEmail } = this.props;
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -90,10 +89,7 @@ class UpdateEmail extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col sm={6} smOffset={3}>
|
<Col sm={6} smOffset={3}>
|
||||||
<Row>
|
<Row>
|
||||||
<Form
|
<Form horizontal={true} onSubmit={this.handleSubmit}>
|
||||||
horizontal={true}
|
|
||||||
onSubmit={this.createSubmitHandler(updateMyEmail)}
|
|
||||||
>
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
controlId='emailInput'
|
controlId='emailInput'
|
||||||
validationState={this.getEmailValidationState()}
|
validationState={this.getEmailValidationState()}
|
||||||
|
@ -6,7 +6,6 @@ import { createAcceptTermsSaga } from './accept-terms-saga';
|
|||||||
import { createAppMountSaga } from './app-mount-saga';
|
import { createAppMountSaga } from './app-mount-saga';
|
||||||
import { createReportUserSaga } from './report-user-saga';
|
import { createReportUserSaga } from './report-user-saga';
|
||||||
import { createShowCertSaga } from './show-cert-saga';
|
import { createShowCertSaga } from './show-cert-saga';
|
||||||
import { createUpdateMyEmailSaga } from './update-email-saga';
|
|
||||||
import { createNightModeSaga } from './night-mode-saga';
|
import { createNightModeSaga } from './night-mode-saga';
|
||||||
|
|
||||||
import { types as settingsTypes } from './settings';
|
import { types as settingsTypes } from './settings';
|
||||||
@ -38,7 +37,6 @@ const types = createTypes(
|
|||||||
...createAsyncTypes('fetchUser'),
|
...createAsyncTypes('fetchUser'),
|
||||||
...createAsyncTypes('acceptTerms'),
|
...createAsyncTypes('acceptTerms'),
|
||||||
...createAsyncTypes('showCert'),
|
...createAsyncTypes('showCert'),
|
||||||
...createAsyncTypes('updateMyEmail'),
|
|
||||||
...createAsyncTypes('reportUser')
|
...createAsyncTypes('reportUser')
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
@ -48,7 +46,6 @@ export const sagas = [
|
|||||||
...createAcceptTermsSaga(types),
|
...createAcceptTermsSaga(types),
|
||||||
...createAppMountSaga(types),
|
...createAppMountSaga(types),
|
||||||
...createFetchUserSaga(types),
|
...createFetchUserSaga(types),
|
||||||
...createUpdateMyEmailSaga(types),
|
|
||||||
...createShowCertSaga(types),
|
...createShowCertSaga(types),
|
||||||
...createReportUserSaga(types),
|
...createReportUserSaga(types),
|
||||||
...createNightModeSaga({ ...types, ...settingsTypes })
|
...createNightModeSaga({ ...types, ...settingsTypes })
|
||||||
@ -72,10 +69,6 @@ export const showCert = createAction(types.showCert);
|
|||||||
export const showCertComplete = createAction(types.showCertComplete);
|
export const showCertComplete = createAction(types.showCertComplete);
|
||||||
export const showCertError = createAction(types.showCertError);
|
export const showCertError = createAction(types.showCertError);
|
||||||
|
|
||||||
export const updateMyEmail = createAction(types.updateMyEmail);
|
|
||||||
export const updateMyEmailComplete = createAction(types.updateMyEmailComplete);
|
|
||||||
export const updateMyEmailError = createAction(types.updateMyEmailError);
|
|
||||||
|
|
||||||
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
|
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
|
||||||
|
|
||||||
export const signInLoadingSelector = state =>
|
export const signInLoadingSelector = state =>
|
||||||
@ -96,6 +89,19 @@ export const userSelector = state => {
|
|||||||
return state[ns].user[username] || {};
|
return state[ns].user[username] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function spreadThePayloadOnUser(state, payload) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[state.appUsername]: {
|
||||||
|
...state.user[state.appUsername],
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const reducer = handleActions(
|
export const reducer = handleActions(
|
||||||
{
|
{
|
||||||
[types.fetchUser]: state => ({
|
[types.fetchUser]: state => ({
|
||||||
@ -164,31 +170,11 @@ export const reducer = handleActions(
|
|||||||
}
|
}
|
||||||
: state,
|
: state,
|
||||||
[settingsTypes.submitNewAboutComplete]: (state, { payload }) =>
|
[settingsTypes.submitNewAboutComplete]: (state, { payload }) =>
|
||||||
payload
|
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||||
? {
|
[settingsTypes.updateMyEmailComplete]: (state, { payload }) =>
|
||||||
...state,
|
payload ? spreadThePayloadOnUser(state, payload) : state,
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
[state.appUsername]: {
|
|
||||||
...state.user[state.appUsername],
|
|
||||||
...payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: state,
|
|
||||||
[settingsTypes.updateUserFlagComplete]: (state, { payload }) =>
|
[settingsTypes.updateUserFlagComplete]: (state, { payload }) =>
|
||||||
payload
|
payload ? spreadThePayloadOnUser(state, payload) : state
|
||||||
? {
|
|
||||||
...state,
|
|
||||||
user: {
|
|
||||||
...state.user,
|
|
||||||
[state.appUsername]: {
|
|
||||||
...state.user[state.appUsername],
|
|
||||||
...payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: state
|
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import { createAction, handleActions } from 'redux-actions';
|
|||||||
|
|
||||||
import { createTypes, createAsyncTypes } from '../../utils/createTypes';
|
import { createTypes, createAsyncTypes } from '../../utils/createTypes';
|
||||||
import { createSettingsSagas } from './settings-sagas';
|
import { createSettingsSagas } from './settings-sagas';
|
||||||
|
import { createUpdateMyEmailSaga } from './update-email-saga';
|
||||||
|
|
||||||
const ns = 'settings';
|
const ns = 'settings';
|
||||||
|
|
||||||
@ -24,16 +25,21 @@ export const types = createTypes(
|
|||||||
...createAsyncTypes('validateUsername'),
|
...createAsyncTypes('validateUsername'),
|
||||||
...createAsyncTypes('submitNewAbout'),
|
...createAsyncTypes('submitNewAbout'),
|
||||||
...createAsyncTypes('submitNewUsername'),
|
...createAsyncTypes('submitNewUsername'),
|
||||||
|
...createAsyncTypes('updateMyEmail'),
|
||||||
...createAsyncTypes('updateUserFlag'),
|
...createAsyncTypes('updateUserFlag'),
|
||||||
...createAsyncTypes('submitProfileUI')
|
...createAsyncTypes('submitProfileUI')
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const sagas = [
|
||||||
|
...createSettingsSagas(types),
|
||||||
|
...createUpdateMyEmailSaga(types)
|
||||||
|
];
|
||||||
|
|
||||||
const checkForSuccessPayload = ({ type, payload }) =>
|
const checkForSuccessPayload = ({ type, payload }) =>
|
||||||
type === 'success' ? payload : null;
|
type === 'success' ? payload : null;
|
||||||
|
|
||||||
export const sagas = [...createSettingsSagas(types)];
|
|
||||||
|
|
||||||
export const submitNewAbout = createAction(types.submitNewAbout);
|
export const submitNewAbout = createAction(types.submitNewAbout);
|
||||||
export const submitNewAboutComplete = createAction(
|
export const submitNewAboutComplete = createAction(
|
||||||
types.submitNewAboutComplete,
|
types.submitNewAboutComplete,
|
||||||
@ -57,6 +63,10 @@ export const submitProfileUIComplete = createAction(
|
|||||||
);
|
);
|
||||||
export const submitProfileUIError = createAction(types.submitProfileUIError);
|
export const submitProfileUIError = createAction(types.submitProfileUIError);
|
||||||
|
|
||||||
|
export const updateMyEmail = createAction(types.updateMyEmail);
|
||||||
|
export const updateMyEmailComplete = createAction(types.updateMyEmailComplete);
|
||||||
|
export const updateMyEmailError = createAction(types.updateMyEmailError);
|
||||||
|
|
||||||
export const updateUserFlag = createAction(types.updateUserFlag);
|
export const updateUserFlag = createAction(types.updateUserFlag);
|
||||||
export const updateUserFlagComplete = createAction(
|
export const updateUserFlagComplete = createAction(
|
||||||
types.updateUserFlagComplete,
|
types.updateUserFlagComplete,
|
||||||
|
32
client/src/redux/settings/update-email-saga.js
Normal file
32
client/src/redux/settings/update-email-saga.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
|
||||||
|
import { updateMyEmailComplete, updateMyEmailError } from './';
|
||||||
|
import { createFlashMessage } from '../../components/Flash/redux';
|
||||||
|
|
||||||
|
import { putUserUpdateEmail } from '../../utils/ajax';
|
||||||
|
import reallyWeirdErrorMessage from '../../utils/reallyWeirdErrorMessage';
|
||||||
|
|
||||||
|
function* updateMyEmailSaga({ payload: email = '' }) {
|
||||||
|
console.log('saga', email);
|
||||||
|
if (!email || !isEmail(email)) {
|
||||||
|
yield put(createFlashMessage(reallyWeirdErrorMessage));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data: response } = yield call(putUserUpdateEmail, email);
|
||||||
|
yield put(
|
||||||
|
updateMyEmailComplete({
|
||||||
|
...response,
|
||||||
|
payload: { email, isEmailVerified: false }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
yield put(createFlashMessage(response));
|
||||||
|
} catch (e) {
|
||||||
|
yield put(updateMyEmailError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUpdateMyEmailSaga(types) {
|
||||||
|
return [takeEvery(types.updateMyEmail, updateMyEmailSaga)];
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import { updateMyEmailComplete, updateMyEmailError } from './';
|
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
|
||||||
|
|
||||||
import { putUserUpdateEmail } from '../utils/ajax';
|
|
||||||
|
|
||||||
function* updateMyEmailSaga({ payload: newEmail }) {
|
|
||||||
try {
|
|
||||||
const { data: response } = yield call(putUserUpdateEmail, newEmail);
|
|
||||||
|
|
||||||
yield put(updateMyEmailComplete());
|
|
||||||
yield put(createFlashMessage(response));
|
|
||||||
} catch (e) {
|
|
||||||
yield put(updateMyEmailError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createUpdateMyEmailSaga(types) {
|
|
||||||
return [takeEvery(types.updateMyEmail, updateMyEmailSaga)];
|
|
||||||
}
|
|
3
client/src/utils/index.js
Normal file
3
client/src/utils/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This regex is not for validation, it is purely to see
|
||||||
|
// if we are looking at something like an email before we try to validate
|
||||||
|
export const maybeEmailRE = /.*@.*\.\w\w/;
|
Reference in New Issue
Block a user