Feature(settings): complete settings page logic
This commit is contained in:
		@@ -9,7 +9,7 @@ export const updateTitle = createAction(types.updateTitle);
 | 
			
		||||
// used in combination with fetch-user-saga
 | 
			
		||||
export const fetchUser = createAction(types.fetchUser);
 | 
			
		||||
 | 
			
		||||
// setUser(
 | 
			
		||||
// addUser(
 | 
			
		||||
//   entities: { [userId]: User }
 | 
			
		||||
// ) => Action
 | 
			
		||||
export const addUser = createAction(
 | 
			
		||||
@@ -25,11 +25,21 @@ export const updateUserPoints = createAction(
 | 
			
		||||
  types.updateUserPoints,
 | 
			
		||||
  (username, points) => ({ username, points })
 | 
			
		||||
);
 | 
			
		||||
// updateUserPoints(username: String, flag: String) => Action
 | 
			
		||||
// updateUserFlag(username: String, flag: String) => Action
 | 
			
		||||
export const updateUserFlag = createAction(
 | 
			
		||||
  types.updateUserFlag,
 | 
			
		||||
  (username, flag) => ({ username, flag })
 | 
			
		||||
);
 | 
			
		||||
// updateUserEmail(username: String, email: String) => Action
 | 
			
		||||
export const updateUserEmail = createAction(
 | 
			
		||||
  types.updateUserFlag,
 | 
			
		||||
  (username, email) => ({ username, email })
 | 
			
		||||
);
 | 
			
		||||
// updateUserLang(username: String, lang: String) => Action
 | 
			
		||||
export const updateUserLang = createAction(
 | 
			
		||||
  types.updateUserLang,
 | 
			
		||||
  (username, lang) => ({ username, lang })
 | 
			
		||||
);
 | 
			
		||||
// updateCompletedChallenges(username: String) => Action
 | 
			
		||||
export const updateCompletedChallenges = createAction(
 | 
			
		||||
  types.updateCompletedChallenges
 | 
			
		||||
@@ -55,6 +65,16 @@ export const createErrorObservable = error => Observable.just({
 | 
			
		||||
  type: types.handleError,
 | 
			
		||||
  error
 | 
			
		||||
});
 | 
			
		||||
// doActionOnError(
 | 
			
		||||
//   actionCreator: (() => Action|Null)
 | 
			
		||||
// ) => (error: Error) => Observable[Action]
 | 
			
		||||
export const doActionOnError = actionCreator => error => Observable.of(
 | 
			
		||||
  {
 | 
			
		||||
    type: types.handleError,
 | 
			
		||||
    error
 | 
			
		||||
  },
 | 
			
		||||
  actionCreator()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// drawers
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,13 @@ const initialState = {
 | 
			
		||||
  user: {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// future refactor(berks): Several of the actions here are just updating props
 | 
			
		||||
// on the main user. These can be refactors into one response for all actions
 | 
			
		||||
export default function entities(state = initialState, action) {
 | 
			
		||||
  const { type, payload: { username, points, flag } = {} } = action;
 | 
			
		||||
  const {
 | 
			
		||||
    type,
 | 
			
		||||
    payload: { email, username, points, flag, languageTag } = {}
 | 
			
		||||
  } = action;
 | 
			
		||||
  if (type === updateCompletedChallenges) {
 | 
			
		||||
    const username = action.payload;
 | 
			
		||||
    const completedChallengeMap = state.user[username].challengeMap || {};
 | 
			
		||||
@@ -26,6 +31,12 @@ export default function entities(state = initialState, action) {
 | 
			
		||||
        }, {})
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (action.meta && action.meta.entities) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      ...action.meta.entities
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (type === updateUserPoints) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
@@ -38,12 +49,6 @@ export default function entities(state = initialState, action) {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (action.meta && action.meta.entities) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      ...action.meta.entities
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (action.type === types.updateUserFlag) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
@@ -56,5 +61,29 @@ export default function entities(state = initialState, action) {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (action.type === types.updateUserEmail) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      user: {
 | 
			
		||||
        ...state.user,
 | 
			
		||||
        [username]: {
 | 
			
		||||
          ...state.user[username],
 | 
			
		||||
          email
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  if (action.type === types.updateUserLang) {
 | 
			
		||||
    return {
 | 
			
		||||
      ...state,
 | 
			
		||||
      user: {
 | 
			
		||||
        ...state.user,
 | 
			
		||||
        [username]: {
 | 
			
		||||
          ...state.user[username],
 | 
			
		||||
          languageTag
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  return state;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ export default createTypes([
 | 
			
		||||
  'updateThisUser',
 | 
			
		||||
  'updateUserPoints',
 | 
			
		||||
  'updateUserFlag',
 | 
			
		||||
  'updateUserEmail',
 | 
			
		||||
  'updateUserLang',
 | 
			
		||||
  'updateCompletedChallenges',
 | 
			
		||||
  'showSignIn',
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,22 @@
 | 
			
		||||
import React, { PropTypes } from 'react';
 | 
			
		||||
import { Link } from 'react-router';
 | 
			
		||||
import { Button, Row, Col } from 'react-bootstrap';
 | 
			
		||||
import FA from 'react-fontawesome';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export function UpdateEmailButton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      block={ true }
 | 
			
		||||
      bsSize='lg'
 | 
			
		||||
      bsStyle='primary'
 | 
			
		||||
      className='btn-link-social'
 | 
			
		||||
      href='/update-email'
 | 
			
		||||
      >
 | 
			
		||||
      <FA name='envelope' />
 | 
			
		||||
      Update my Email
 | 
			
		||||
    </Button>
 | 
			
		||||
    <Link to='/settings/update-email'>
 | 
			
		||||
      <Button
 | 
			
		||||
        block={ true }
 | 
			
		||||
        bsSize='lg'
 | 
			
		||||
        bsStyle='primary'
 | 
			
		||||
        className='btn-link-social'
 | 
			
		||||
        >
 | 
			
		||||
        <FA name='envelope' />
 | 
			
		||||
        Update my Email
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,27 @@
 | 
			
		||||
import React, { PropTypes } from 'react';
 | 
			
		||||
import { createSelector } from 'reselect';
 | 
			
		||||
import { reduxForm } from 'redux-form';
 | 
			
		||||
import { FormControl } from 'react-bootstrap';
 | 
			
		||||
 | 
			
		||||
import { updateMyLang } from '../redux/actions';
 | 
			
		||||
import { userSelector } from '../../../redux/selectors';
 | 
			
		||||
import langs from '../../../../utils/supported-languages';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = createSelector(
 | 
			
		||||
 userSelector,
 | 
			
		||||
 ({ user: { languageTag } }) => ({
 | 
			
		||||
   // send null to prevent redux-form from initialize empty
 | 
			
		||||
   initialValues: languageTag ? { lang: languageTag } : null
 | 
			
		||||
 })
 | 
			
		||||
);
 | 
			
		||||
const actions = { updateMyLang };
 | 
			
		||||
const fields = [ 'lang' ];
 | 
			
		||||
const validator = values => {
 | 
			
		||||
  if (!langs[values.lang]) {
 | 
			
		||||
    return { lang: `${values.lang} is unsupported` };
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
};
 | 
			
		||||
const options = [(
 | 
			
		||||
    <option
 | 
			
		||||
      disabled={ true }
 | 
			
		||||
@@ -30,19 +50,58 @@ const options = [(
 | 
			
		||||
  )
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default function LangaugeSettings({ userLang }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormControl
 | 
			
		||||
      className='btn btn-block btn-primary btn-link-social'
 | 
			
		||||
      componentClass='select'
 | 
			
		||||
      defaultValue='not-the-momma'
 | 
			
		||||
      value={ userLang }
 | 
			
		||||
      >
 | 
			
		||||
      { options }
 | 
			
		||||
    </FormControl>
 | 
			
		||||
  );
 | 
			
		||||
export class LangaugeSettings extends React.Component {
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    fields: PropTypes.object,
 | 
			
		||||
    handleSubmit: PropTypes.func.isRequired,
 | 
			
		||||
    updateMyLang: PropTypes.func.isRequired
 | 
			
		||||
  };
 | 
			
		||||
  constructor(...props) {
 | 
			
		||||
    super(...props);
 | 
			
		||||
    this.handleChange = this.handleChange.bind(this);
 | 
			
		||||
  }
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    // make sure to clear timeout if it exist
 | 
			
		||||
    if (this.timeout) {
 | 
			
		||||
      clearTimeout(this.timeout);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleChange(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.props.fields.lang.onChange(e);
 | 
			
		||||
    // give redux-form HOC state time to catch up before
 | 
			
		||||
    // attempting to submit
 | 
			
		||||
    this.timeout = setTimeout(
 | 
			
		||||
      () => this.props.handleSubmit(this.props.updateMyLang)(),
 | 
			
		||||
      0
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      fields: { lang }
 | 
			
		||||
    } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <FormControl
 | 
			
		||||
        className='btn btn-block btn-primary btn-link-social'
 | 
			
		||||
        componentClass='select'
 | 
			
		||||
        { ...lang }
 | 
			
		||||
        onChange={ this.handleChange }
 | 
			
		||||
        >
 | 
			
		||||
        { options }
 | 
			
		||||
      </FormControl>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LangaugeSettings.propTypes = {
 | 
			
		||||
  userLang: PropTypes.string
 | 
			
		||||
};
 | 
			
		||||
export default reduxForm(
 | 
			
		||||
  {
 | 
			
		||||
    form: 'lang',
 | 
			
		||||
    fields,
 | 
			
		||||
    validator,
 | 
			
		||||
    overwriteOnInitialValuesChange: false
 | 
			
		||||
  },
 | 
			
		||||
  mapStateToProps,
 | 
			
		||||
  actions
 | 
			
		||||
)(LangaugeSettings);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,13 @@ import {
 | 
			
		||||
  openDeleteModal,
 | 
			
		||||
  hideDeleteModal
 | 
			
		||||
} from '../redux/actions';
 | 
			
		||||
import { toggleNightMode } from '../../../redux/actions';
 | 
			
		||||
import {
 | 
			
		||||
  toggleNightMode,
 | 
			
		||||
  updateTitle
 | 
			
		||||
} from '../../../redux/actions';
 | 
			
		||||
 | 
			
		||||
const actions = {
 | 
			
		||||
  updateTitle,
 | 
			
		||||
  toggleNightMode,
 | 
			
		||||
  openDeleteModal,
 | 
			
		||||
  hideDeleteModal,
 | 
			
		||||
@@ -57,8 +61,13 @@ const mapStateToProps = state => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Settings extends React.Component {
 | 
			
		||||
  constructor(...props) {
 | 
			
		||||
    super(...props);
 | 
			
		||||
    this.updateMyLang = this.updateMyLang.bind(this);
 | 
			
		||||
  }
 | 
			
		||||
  static displayName = 'Settings';
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    children: PropTypes.element,
 | 
			
		||||
    username: PropTypes.string,
 | 
			
		||||
    isDeleteOpen: PropTypes.bool,
 | 
			
		||||
    isLocked: PropTypes.bool,
 | 
			
		||||
@@ -69,17 +78,32 @@ export class Settings extends React.Component {
 | 
			
		||||
    sendMonthlyEmail: PropTypes.bool,
 | 
			
		||||
    sendNotificationEmail: PropTypes.bool,
 | 
			
		||||
    sendQuincyEmail: PropTypes.bool,
 | 
			
		||||
    toggleNightMode: PropTypes.func,
 | 
			
		||||
    toggleIsLocked: PropTypes.func,
 | 
			
		||||
    toggleQuincyEmail: PropTypes.func,
 | 
			
		||||
    toggleMonthlyEmail: PropTypes.func,
 | 
			
		||||
    toggleNotificationEmail: PropTypes.func,
 | 
			
		||||
    openDeleteModal: PropTypes.func,
 | 
			
		||||
    hideDeleteModal: PropTypes.func
 | 
			
		||||
    updateTitle: PropTypes.func.isRequired,
 | 
			
		||||
    toggleNightMode: PropTypes.func.isRequired,
 | 
			
		||||
    toggleIsLocked: PropTypes.func.isRequired,
 | 
			
		||||
    toggleQuincyEmail: PropTypes.func.isRequired,
 | 
			
		||||
    toggleMonthlyEmail: PropTypes.func.isRequired,
 | 
			
		||||
    toggleNotificationEmail: PropTypes.func.isRequired,
 | 
			
		||||
    openDeleteModal: PropTypes.func.isRequired,
 | 
			
		||||
    hideDeleteModal: PropTypes.func.isRequired,
 | 
			
		||||
    lang: PropTypes.string,
 | 
			
		||||
    initialLang: PropTypes.string,
 | 
			
		||||
    updateMyLang: PropTypes.func
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  updateMyLang(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const lang = e.target.value;
 | 
			
		||||
    this.props.updateMyLang(lang);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillMount() {
 | 
			
		||||
    this.props.updateTitle('Settings');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      children,
 | 
			
		||||
      username,
 | 
			
		||||
      isDeleteOpen,
 | 
			
		||||
      isLocked,
 | 
			
		||||
@@ -98,6 +122,18 @@ export class Settings extends React.Component {
 | 
			
		||||
      openDeleteModal,
 | 
			
		||||
      hideDeleteModal
 | 
			
		||||
    } = this.props;
 | 
			
		||||
    if (children) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Row>
 | 
			
		||||
          <Col
 | 
			
		||||
            sm={ 4 }
 | 
			
		||||
            smOffset={ 4 }
 | 
			
		||||
            >
 | 
			
		||||
            { children }
 | 
			
		||||
          </Col>
 | 
			
		||||
        </Row>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <Row>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import Settings from './components/Settings.jsx';
 | 
			
		||||
import updateEmail from './routes/update-email';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  path: 'settings',
 | 
			
		||||
  component: Settings
 | 
			
		||||
  component: Settings,
 | 
			
		||||
  childRoutes: [
 | 
			
		||||
    updateEmail
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,19 @@ const initialState = {
 | 
			
		||||
export const types = createTypes([
 | 
			
		||||
  'toggleUserFlag',
 | 
			
		||||
  'openDeleteModal',
 | 
			
		||||
  'hideDeleteModal'
 | 
			
		||||
  'hideDeleteModal',
 | 
			
		||||
  'updateMyEmail',
 | 
			
		||||
  'updateMyLang'
 | 
			
		||||
], 'settings');
 | 
			
		||||
 | 
			
		||||
export const toggleUserFlag = createAction(types.toggleUserFlag);
 | 
			
		||||
export const openDeleteModal = createAction(types.openDeleteModal);
 | 
			
		||||
export const hideDeleteModal = createAction(types.hideDeleteModal);
 | 
			
		||||
export const updateMyEmail = createAction(types.updateMyEmail);
 | 
			
		||||
export const updateMyLang = createAction(
 | 
			
		||||
  types.updateMyLang,
 | 
			
		||||
  (values) => values.lang
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default handleActions(
 | 
			
		||||
  {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								common/app/routes/settings/redux/selectors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common/app/routes/settings/redux/selectors.js
									
									
									
									
									
										Normal file
									
								
							@@ -1,7 +1,16 @@
 | 
			
		||||
import { Observable } from 'rx';
 | 
			
		||||
 | 
			
		||||
import { types } from './actions';
 | 
			
		||||
import combineSagas from '../../../utils/combine-sagas';
 | 
			
		||||
import { makeToast } from '../../../toasts/redux/actions';
 | 
			
		||||
import {
 | 
			
		||||
  updateUserFlag,
 | 
			
		||||
  updateUserEmail,
 | 
			
		||||
  updateUserLang,
 | 
			
		||||
  doActionOnError
 | 
			
		||||
} from '../../../redux/actions';
 | 
			
		||||
import { postJSON$ } from '../../../../utils/ajax-stream';
 | 
			
		||||
import { updateUserFlag, createErrorObservable } from '../../../redux/actions';
 | 
			
		||||
import langs from '../../../../utils/supported-languages';
 | 
			
		||||
 | 
			
		||||
const urlMap = {
 | 
			
		||||
  isLocked: 'lockdown',
 | 
			
		||||
@@ -9,7 +18,61 @@ const urlMap = {
 | 
			
		||||
  sendNotificationEmail: 'notification-email',
 | 
			
		||||
  sendMonthlyEmail: 'announcement-email'
 | 
			
		||||
};
 | 
			
		||||
export default function userUpdateSaga(actions$, getState) {
 | 
			
		||||
 | 
			
		||||
export function updateUserEmailSaga(actions$, getState) {
 | 
			
		||||
  return actions$
 | 
			
		||||
    .filter(({ type }) => type === types.updateMyEmail)
 | 
			
		||||
    .flatMap(({ payload: email }) => {
 | 
			
		||||
      const {
 | 
			
		||||
        app: { user: username, csrfToken: _csrf },
 | 
			
		||||
        entities: { user: userMap }
 | 
			
		||||
      } = getState();
 | 
			
		||||
      const { email: oldEmail } = userMap[username] || {};
 | 
			
		||||
      const body = { _csrf, email };
 | 
			
		||||
      const optimisticUpdate$ = Observable.just(
 | 
			
		||||
        updateUserEmail(username, email)
 | 
			
		||||
      );
 | 
			
		||||
      const ajaxUpdate$ = postJSON$('/update-my-email', body)
 | 
			
		||||
        .map(({ message }) => makeToast({ message }))
 | 
			
		||||
        .catch(doActionOnError(() => oldEmail ?
 | 
			
		||||
          updateUserFlag(username, oldEmail) :
 | 
			
		||||
          null
 | 
			
		||||
        ));
 | 
			
		||||
      return Observable.merge(optimisticUpdate$, ajaxUpdate$);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateUserLangSaga(actions$, getState) {
 | 
			
		||||
  const updateLang$ = actions$
 | 
			
		||||
    .filter(({ type, payload }) => (
 | 
			
		||||
      type === types.updateMyLang && !!langs[payload]
 | 
			
		||||
    ))
 | 
			
		||||
    .map(({ payload }) => payload);
 | 
			
		||||
  const ajaxUpdate$ = updateLang$
 | 
			
		||||
    .flatMap(lang => {
 | 
			
		||||
      const {
 | 
			
		||||
        app: { user: username, csrfToken: _csrf },
 | 
			
		||||
        entities: { user: userMap }
 | 
			
		||||
      } = getState();
 | 
			
		||||
      const { languageTag: oldLang } = userMap[username] || {};
 | 
			
		||||
      const body = {
 | 
			
		||||
        _csrf,
 | 
			
		||||
        lang
 | 
			
		||||
      };
 | 
			
		||||
      return postJSON$('/update-my-lang', body)
 | 
			
		||||
        .map(({ message }) => makeToast({ message }))
 | 
			
		||||
        .catch(doActionOnError(() => {
 | 
			
		||||
          return updateUserLang(username, oldLang);
 | 
			
		||||
        }));
 | 
			
		||||
    });
 | 
			
		||||
  const optimistic$ = updateLang$
 | 
			
		||||
    .map(lang => {
 | 
			
		||||
      const { app: { user: username } } = getState();
 | 
			
		||||
      return updateUserLang(username, lang);
 | 
			
		||||
    });
 | 
			
		||||
  return Observable.merge(ajaxUpdate$, optimistic$);
 | 
			
		||||
}
 | 
			
		||||
export function updateUserFlagSaga(actions$, getState) {
 | 
			
		||||
  const toggleFlag$ = actions$
 | 
			
		||||
    .filter(({ type, payload }) => type === types.toggleUserFlag && payload)
 | 
			
		||||
    .map(({ payload }) => payload);
 | 
			
		||||
@@ -34,7 +97,15 @@ export default function userUpdateSaga(actions$, getState) {
 | 
			
		||||
          }
 | 
			
		||||
          return updateUserFlag(username, flag);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(createErrorObservable);
 | 
			
		||||
        .catch(doActionOnError(() => {
 | 
			
		||||
          return updateUserFlag(username, currentValue);
 | 
			
		||||
        }));
 | 
			
		||||
    });
 | 
			
		||||
  return Observable.merge(optimistic$, serverUpdate$);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default combineSagas(
 | 
			
		||||
  updateUserFlagSaga,
 | 
			
		||||
  updateUserEmailSaga,
 | 
			
		||||
  updateUserLangSaga
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								common/app/routes/settings/routes/update-email/Update-Email.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								common/app/routes/settings/routes/update-email/Update-Email.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
import React, { PropTypes } from 'react';
 | 
			
		||||
import { Button, HelpBlock, FormControl, FormGroup } from 'react-bootstrap';
 | 
			
		||||
import { LinkContainer } from 'react-router-bootstrap';
 | 
			
		||||
import { reduxForm } from 'redux-form';
 | 
			
		||||
import { isEmail } from 'validator';
 | 
			
		||||
import { getValidationState } from '../../../../utils/form';
 | 
			
		||||
import { updateMyEmail } from '../../redux/actions';
 | 
			
		||||
 | 
			
		||||
const actions = {
 | 
			
		||||
  updateMyEmail
 | 
			
		||||
};
 | 
			
		||||
const fields = [
 | 
			
		||||
  'email',
 | 
			
		||||
  'duplicate'
 | 
			
		||||
];
 | 
			
		||||
const validateFields = ({ email, duplicate }) => {
 | 
			
		||||
  const errors = {};
 | 
			
		||||
  if (!isEmail('' + email)) {
 | 
			
		||||
    errors.email = 'This email is invalid.';
 | 
			
		||||
  }
 | 
			
		||||
  if (duplicate && email !== duplicate) {
 | 
			
		||||
    errors.duplicate = 'This email does not match the one above.';
 | 
			
		||||
  }
 | 
			
		||||
  return errors;
 | 
			
		||||
};
 | 
			
		||||
const mapStateToProps = state => {
 | 
			
		||||
  const {
 | 
			
		||||
    app: { user: username },
 | 
			
		||||
    entities: { user: userMap }
 | 
			
		||||
  } = state;
 | 
			
		||||
  const { email, emailVerified } = userMap[username] || {};
 | 
			
		||||
  return {
 | 
			
		||||
    initialValues: { email },
 | 
			
		||||
    isEmailThere: !!email,
 | 
			
		||||
    isVerified: !!emailVerified
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class UpdateEmail extends React.Component {
 | 
			
		||||
  constructor(...props) {
 | 
			
		||||
    super(...props);
 | 
			
		||||
    this.handleSubmit = this.handleSubmit.bind(this);
 | 
			
		||||
  }
 | 
			
		||||
  static displayName = 'UpdateEmail';
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    isEmailThere: PropTypes.bool,
 | 
			
		||||
    isVerified: PropTypes.bool,
 | 
			
		||||
    fields: PropTypes.object.isRequired,
 | 
			
		||||
    submitting: PropTypes.bool,
 | 
			
		||||
    handleSubmit: PropTypes.func.isRequired,
 | 
			
		||||
    updateMyEmail: PropTypes.func.isRequired
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleSubmit(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.props.handleSubmit(({ email }) => this.props.updateMyEmail(email))(e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      isEmailThere,
 | 
			
		||||
      isVerified,
 | 
			
		||||
      fields: { email, duplicate },
 | 
			
		||||
      submitting
 | 
			
		||||
    } = this.props;
 | 
			
		||||
    const buttonCopy = !isEmailThere || isVerified ?
 | 
			
		||||
      'Update my Email' :
 | 
			
		||||
      'Verify Email';
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <h2 className='text-center'>Update your email address here:</h2>
 | 
			
		||||
        <form
 | 
			
		||||
          name='update-email'
 | 
			
		||||
          onSubmit={ this.handleSubmit }
 | 
			
		||||
          >
 | 
			
		||||
          <FormGroup
 | 
			
		||||
            bsSize='lg'
 | 
			
		||||
            controlId='email'
 | 
			
		||||
            validationState={ getValidationState(email) }
 | 
			
		||||
            >
 | 
			
		||||
            <FormControl
 | 
			
		||||
              autofocus={ true }
 | 
			
		||||
              placeholder='Enter your new email'
 | 
			
		||||
              type='email'
 | 
			
		||||
              { ...email }
 | 
			
		||||
            />
 | 
			
		||||
            {
 | 
			
		||||
              !email.error ?
 | 
			
		||||
                null :
 | 
			
		||||
                <HelpBlock>{ email.error }</HelpBlock>
 | 
			
		||||
            }
 | 
			
		||||
          </FormGroup>
 | 
			
		||||
          <FormGroup
 | 
			
		||||
            bsSize='lg'
 | 
			
		||||
            controlId='duplicate'
 | 
			
		||||
            validationState={ getValidationState(duplicate) }
 | 
			
		||||
            >
 | 
			
		||||
            <FormControl
 | 
			
		||||
              placeholder='re-type your email address'
 | 
			
		||||
              type='email'
 | 
			
		||||
              { ...duplicate }
 | 
			
		||||
            />
 | 
			
		||||
            {
 | 
			
		||||
              !duplicate.error ?
 | 
			
		||||
                null :
 | 
			
		||||
                <HelpBlock>{ duplicate.error }</HelpBlock>
 | 
			
		||||
            }
 | 
			
		||||
          </FormGroup>
 | 
			
		||||
          <FormGroup>
 | 
			
		||||
            <Button
 | 
			
		||||
              block={ true }
 | 
			
		||||
              bsSize='lg'
 | 
			
		||||
              bsStyle='primary'
 | 
			
		||||
              disabled={ submitting }
 | 
			
		||||
              type='submit'
 | 
			
		||||
              >
 | 
			
		||||
              { buttonCopy }
 | 
			
		||||
            </Button>
 | 
			
		||||
            <div className='button-spacer' />
 | 
			
		||||
            <LinkContainer to='/settings'>
 | 
			
		||||
              <Button
 | 
			
		||||
                block={ true }
 | 
			
		||||
                bsSize='lg'
 | 
			
		||||
                bsStyle='primary'
 | 
			
		||||
                >
 | 
			
		||||
                Go back to Settings
 | 
			
		||||
              </Button>
 | 
			
		||||
            </LinkContainer>
 | 
			
		||||
          </FormGroup>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default reduxForm(
 | 
			
		||||
  {
 | 
			
		||||
    form: 'update-email',
 | 
			
		||||
    fields,
 | 
			
		||||
    validate: validateFields
 | 
			
		||||
  },
 | 
			
		||||
  mapStateToProps,
 | 
			
		||||
  actions
 | 
			
		||||
)(UpdateEmail);
 | 
			
		||||
							
								
								
									
										6
									
								
								common/app/routes/settings/routes/update-email/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								common/app/routes/settings/routes/update-email/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import UpdateEmail from './Update-Email.jsx';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  path: 'update-email',
 | 
			
		||||
  component: UpdateEmail
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										9
									
								
								common/app/utils/combine-sagas.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								common/app/utils/combine-sagas.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import { Observable } from 'rx';
 | 
			
		||||
 | 
			
		||||
export default function combineSagas(...sagas) {
 | 
			
		||||
  return (actions$, getState, deps) => {
 | 
			
		||||
    return Observable.merge(
 | 
			
		||||
      sagas.map(saga => saga(actions$, getState, deps))
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -390,14 +390,14 @@ module.exports = function(User) {
 | 
			
		||||
      lastEmailSentAt.isBefore(fiveMinutesAgo) :
 | 
			
		||||
      true;
 | 
			
		||||
 | 
			
		||||
    if (!isEmail(email)) {
 | 
			
		||||
      return Promise.reject(
 | 
			
		||||
    if (!isEmail('' + email)) {
 | 
			
		||||
      return Observable.throw(
 | 
			
		||||
        new Error('The submitted email not valid.')
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    // email is already associated and verified with this account
 | 
			
		||||
    if (ownEmail && this.emailVerified) {
 | 
			
		||||
      return Promise.reject(new Error(
 | 
			
		||||
      return Observable.throw(new Error(
 | 
			
		||||
        `${email} is already associated with this account.`
 | 
			
		||||
      ));
 | 
			
		||||
    }
 | 
			
		||||
@@ -410,13 +410,13 @@ module.exports = function(User) {
 | 
			
		||||
        `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
 | 
			
		||||
        'a few seconds';
 | 
			
		||||
 | 
			
		||||
      return Promise.reject(new Error(
 | 
			
		||||
      return Observable.throw(new Error(
 | 
			
		||||
        `Please wait ${timeToWait} to resend email verification.`
 | 
			
		||||
      ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return User.doesExist(null, email)
 | 
			
		||||
      .then(exists => {
 | 
			
		||||
    return Observable.fromPromise(User.doesExist(null, email))
 | 
			
		||||
      .flatMap(exists => {
 | 
			
		||||
        // not associated with this account, but is associated with another
 | 
			
		||||
        if (!ownEmail && exists) {
 | 
			
		||||
          return Promise.reject(
 | 
			
		||||
@@ -434,67 +434,35 @@ module.exports = function(User) {
 | 
			
		||||
          this.email = email;
 | 
			
		||||
          this.emailVerified = emailVerified;
 | 
			
		||||
          this.emailVerifyTTL = new Date();
 | 
			
		||||
        })
 | 
			
		||||
        .flatMap(() => {
 | 
			
		||||
          var mailOptions = {
 | 
			
		||||
            type: 'email',
 | 
			
		||||
            to: email,
 | 
			
		||||
            from: 'Team@freecodecamp.com',
 | 
			
		||||
            subject: 'Welcome to Free Code Camp!',
 | 
			
		||||
            protocol: isDev ? null : 'https',
 | 
			
		||||
            host: isDev ? 'localhost' : 'freecodecamp.com',
 | 
			
		||||
            port: isDev ? null : 443,
 | 
			
		||||
            template: path.join(
 | 
			
		||||
              __dirname,
 | 
			
		||||
              '..',
 | 
			
		||||
              '..',
 | 
			
		||||
              'server',
 | 
			
		||||
              'views',
 | 
			
		||||
              'emails',
 | 
			
		||||
              'user-email-verify.ejs'
 | 
			
		||||
            )
 | 
			
		||||
          };
 | 
			
		||||
          return this.verify(mailOptions);
 | 
			
		||||
        })
 | 
			
		||||
        .map(() => dedent`
 | 
			
		||||
          Please check your email.
 | 
			
		||||
          We sent you a link that you can click to verify your email address.
 | 
			
		||||
        `)
 | 
			
		||||
        .catch(error => {
 | 
			
		||||
          debug(error);
 | 
			
		||||
          return Observable.throw(
 | 
			
		||||
            'Oops, something went wrong, please try again later.'
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
        .toPromise();
 | 
			
		||||
      });
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .flatMap(() => {
 | 
			
		||||
        const mailOptions = {
 | 
			
		||||
          type: 'email',
 | 
			
		||||
          to: email,
 | 
			
		||||
          from: 'Team@freecodecamp.com',
 | 
			
		||||
          subject: 'Welcome to Free Code Camp!',
 | 
			
		||||
          protocol: isDev ? null : 'https',
 | 
			
		||||
          host: isDev ? 'localhost' : 'freecodecamp.com',
 | 
			
		||||
          port: isDev ? null : 443,
 | 
			
		||||
          template: path.join(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            '..',
 | 
			
		||||
            '..',
 | 
			
		||||
            'server',
 | 
			
		||||
            'views',
 | 
			
		||||
            'emails',
 | 
			
		||||
            'user-email-verify.ejs'
 | 
			
		||||
          )
 | 
			
		||||
        };
 | 
			
		||||
        return this.verify(mailOptions);
 | 
			
		||||
      })
 | 
			
		||||
      .map(() => dedent`
 | 
			
		||||
        Please check your email.
 | 
			
		||||
        We sent you a link that you can click to verify your email address.
 | 
			
		||||
      `);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  User.remoteMethod(
 | 
			
		||||
    'updateEmail',
 | 
			
		||||
    {
 | 
			
		||||
      isStatic: false,
 | 
			
		||||
      description: 'updates the email of the user object',
 | 
			
		||||
      accepts: [
 | 
			
		||||
        {
 | 
			
		||||
          arg: 'email',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
          required: true
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      returns: [
 | 
			
		||||
        {
 | 
			
		||||
          arg: 'message',
 | 
			
		||||
          type: 'string'
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      http: {
 | 
			
		||||
        path: '/update-email',
 | 
			
		||||
        verb: 'POST'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  User.giveBrowniePoints =
 | 
			
		||||
    function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
 | 
			
		||||
      const findUser = observeMethod(User, 'findOne');
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								server/boot/react.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/boot/react.js
									
									
									
									
										vendored
									
									
								
							@@ -15,7 +15,8 @@ const routes = [
 | 
			
		||||
  '/challenges',
 | 
			
		||||
  '/challenges/*',
 | 
			
		||||
  '/map',
 | 
			
		||||
  '/settings'
 | 
			
		||||
  '/settings',
 | 
			
		||||
  '/settings/*'
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const devRoutes = [];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { ifNoUser401 } from '../utils/middleware';
 | 
			
		||||
import supportedLanguages from '../../common/utils/supported-languages.js';
 | 
			
		||||
 | 
			
		||||
export default function settingsController(app) {
 | 
			
		||||
  const api = app.loopback.Router();
 | 
			
		||||
@@ -15,6 +16,39 @@ export default function settingsController(app) {
 | 
			
		||||
        next
 | 
			
		||||
      );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function updateMyEmail(req, res, next) {
 | 
			
		||||
    const { user, body: { email } } = req;
 | 
			
		||||
    return user.updateEmail(email)
 | 
			
		||||
      .subscribe(
 | 
			
		||||
        (message) => res.json({ message }),
 | 
			
		||||
        next
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateMyLang(req, res, next) {
 | 
			
		||||
    const { user, body: { lang } = {} } = req;
 | 
			
		||||
    const langName = supportedLanguages[lang];
 | 
			
		||||
    const update = { languageTag: lang };
 | 
			
		||||
    if (!supportedLanguages[lang]) {
 | 
			
		||||
      return res.json({
 | 
			
		||||
        message: `${lang} is currently unsupported`
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (user.langaugeTag === lang) {
 | 
			
		||||
      return res.json({
 | 
			
		||||
        message: `Your language is already set to ${langName}`
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return user.update$(update)
 | 
			
		||||
      .subscribe(
 | 
			
		||||
        () => res.json({
 | 
			
		||||
          message: `Your language has been updated to '${langName}'`
 | 
			
		||||
        }),
 | 
			
		||||
        next
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  api.post(
 | 
			
		||||
    '/toggle-lockdown',
 | 
			
		||||
    toggleUserFlag('isLocked')
 | 
			
		||||
@@ -34,5 +68,15 @@ export default function settingsController(app) {
 | 
			
		||||
    ifNoUser401,
 | 
			
		||||
    toggleUserFlag('sendQuincyEmail')
 | 
			
		||||
  );
 | 
			
		||||
  api.post(
 | 
			
		||||
    '/update-my-email',
 | 
			
		||||
    ifNoUser401,
 | 
			
		||||
    updateMyEmail
 | 
			
		||||
  );
 | 
			
		||||
  api.post(
 | 
			
		||||
    '/update-my-lang',
 | 
			
		||||
    ifNoUser401,
 | 
			
		||||
    updateMyLang
 | 
			
		||||
  );
 | 
			
		||||
  app.use(api);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user