From 388a08e7326e2b748c1cfe10a98265ead5160f5e Mon Sep 17 00:00:00 2001 From: Kimmo Puputti Date: Fri, 8 Sep 2017 15:17:39 +0300 Subject: [PATCH] Password reset page --- .../PasswordResetForm/PasswordResetForm.js | 13 +- .../PasswordResetPage/PasswordResetPage.css | 70 +++++++ .../PasswordResetPage.duck.js | 51 +++++ .../PasswordResetPage/PasswordResetPage.js | 191 +++++++++++++----- src/containers/reducers.js | 6 +- src/translations/en.json | 7 + 6 files changed, 286 insertions(+), 52 deletions(-) create mode 100644 src/containers/PasswordResetPage/PasswordResetPage.duck.js diff --git a/src/containers/PasswordResetForm/PasswordResetForm.js b/src/containers/PasswordResetForm/PasswordResetForm.js index 2f762bd8..c13aa85b 100644 --- a/src/containers/PasswordResetForm/PasswordResetForm.js +++ b/src/containers/PasswordResetForm/PasswordResetForm.js @@ -8,6 +8,8 @@ import * as validators from '../../util/validators'; import css from './PasswordResetForm.css'; +const PASSWORD_MIN_LENGTH = 8; + const PasswordResetFormComponent = props => { const { rootClassName, @@ -30,7 +32,16 @@ const PasswordResetFormComponent = props => { const passwordRequiredMessage = intl.formatMessage({ id: 'PasswordResetForm.passwordRequired', }); + const passwordMinLengthMessage = intl.formatMessage( + { + id: 'PasswordResetForm.passwordTooShort', + }, + { + minLength: PASSWORD_MIN_LENGTH, + } + ); const passwordRequired = validators.required(passwordRequiredMessage); + const passwordMinLength = validators.minLength(passwordMinLengthMessage, PASSWORD_MIN_LENGTH); const classes = classNames(rootClassName || css.root, className); const submitDisabled = invalid || submitting || inProgress; @@ -44,7 +55,7 @@ const PasswordResetFormComponent = props => { id={`${form}.password`} label={passwordLabel} placeholder={passwordPlaceholder} - validate={passwordRequired} + validate={[passwordRequired, passwordMinLength]} /> diff --git a/src/containers/PasswordResetPage/PasswordResetPage.css b/src/containers/PasswordResetPage/PasswordResetPage.css index e69de29b..850c1b93 100644 --- a/src/containers/PasswordResetPage/PasswordResetPage.css +++ b/src/containers/PasswordResetPage/PasswordResetPage.css @@ -0,0 +1,70 @@ +@import '../../marketplace.css'; + +.root { + @apply --backgroundImage; + + flex-grow: 1; + display: flex; + + @media (--viewportMedium) { + justify-content: center; + align-items: flex-start; + } +} + +.content { + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 30px 24px 98px 24px; + background-color: var(--matterColorLight); + border-radius: 2px; + + @media (--viewportMedium) { + flex-basis: 480px; + flex-grow: 0; + min-height: 573px; + padding: 60px; + margin-top: 7.5vh; + margin-bottom: 7.5vh; + } +} + +.error { + color: var(--failColor); +} + +.mainHeading { + margin-top: 26px; + + @media (--viewportMedium) { + margin-top: 23px; + } +} + +.helpText { + margin-top: 12px; + + @media (--viewportMedium) { + margin-top: 15px; + } +} + +.form { + flex: 1; + + display: flex; + flex-direction: column; + margin-top: 37px; + + @media (--viewportMedium) { + margin-top: 38px; + } +} + +.buttonLink { + @apply --marketplaceButtonStylesPrimary; + + /* Pull the button to the bottom of the flex container */ + margin-top: auto; +} diff --git a/src/containers/PasswordResetPage/PasswordResetPage.duck.js b/src/containers/PasswordResetPage/PasswordResetPage.duck.js new file mode 100644 index 00000000..3fa8d2e7 --- /dev/null +++ b/src/containers/PasswordResetPage/PasswordResetPage.duck.js @@ -0,0 +1,51 @@ +// ================ Action types ================ // + +export const RESET_PASSWORD_REQUEST = 'app/PasswordResetPage/RESET_PASSWORD_REQUEST'; +export const RESET_PASSWORD_SUCCESS = 'app/PasswordResetPage/RESET_PASSWORD_SUCCESS'; +export const RESET_PASSWORD_ERROR = 'app/PasswordResetPage/RESET_PASSWORD_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + resetPasswordInProgress: false, + resetPasswordError: null, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case RESET_PASSWORD_REQUEST: + return { ...state, resetPasswordInProgress: true, resetPasswordError: null }; + case RESET_PASSWORD_SUCCESS: + return { ...state, resetPasswordInProgress: false }; + case RESET_PASSWORD_ERROR: + console.error(payload); // eslint-disable-line no-console + return { ...state, resetPasswordInProgress: false, resetPasswordError: payload }; + default: + return state; + } +} + +// ================ Action creators ================ // + +export const resetPasswordRequest = () => ({ type: RESET_PASSWORD_REQUEST }); + +export const resetPasswordSuccess = () => ({ type: RESET_PASSWORD_SUCCESS }); + +export const resetPasswordError = e => ({ + type: RESET_PASSWORD_ERROR, + error: true, + payload: e, +}); + +// ================ Thunks ================ // + +export const resetPassword = (email, passwordResetToken, newPassword) => + (dispatch, getState, sdk) => { + dispatch(resetPasswordRequest()); + const params = { email, passwordResetToken, newPassword }; + return sdk.passwordReset + .reset(params) + .then(() => dispatch(resetPasswordSuccess())) + .catch(e => dispatch(resetPasswordError(e))); + }; diff --git a/src/containers/PasswordResetPage/PasswordResetPage.js b/src/containers/PasswordResetPage/PasswordResetPage.js index 96761729..ed943760 100644 --- a/src/containers/PasswordResetPage/PasswordResetPage.js +++ b/src/containers/PasswordResetPage/PasswordResetPage.js @@ -1,70 +1,155 @@ -import React, { PropTypes } from 'react'; +import React, { PropTypes, Component } from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; -import { injectIntl, intlShape } from 'react-intl'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import * as propTypes from '../../util/propTypes'; -import { PageLayout, Topbar } from '../../components'; +import { PageLayout, Topbar, NamedLink, KeysIcon, KeysIconSuccess } from '../../components'; +import { PasswordResetForm } from '../../containers'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; +import { parse } from '../../util/urlHelpers'; +import { resetPassword } from './PasswordResetPage.duck'; -// import css from './PasswordResetPage.css'; +import css from './PasswordResetPage.css'; -export const PasswordResetPageComponent = props => { - const { - authInfoError, - authInProgress, - currentUser, - currentUserHasListings, - intl, - isAuthenticated, - logoutError, - notificationCount, - onLogout, - onManageDisableScrolling, - scrollingDisabled, - location, - history, - } = props; +const { bool, func, instanceOf, number, object, shape, string } = PropTypes; - const title = intl.formatMessage({ - id: 'PasswordResetPage.title', - }); - - return ( - - - -

TODO

- -
- ); +const parseUrlParams = location => { + const params = parse(location.search); + const { t: token, e: email } = params; + return { token, email }; }; +export class PasswordResetPageComponent extends Component { + constructor(props) { + super(props); + this.state = { newPasswordSubmitted: false }; + } + render() { + const { + authInfoError, + authInProgress, + currentUser, + currentUserHasListings, + intl, + isAuthenticated, + logoutError, + notificationCount, + onLogout, + onManageDisableScrolling, + scrollingDisabled, + location, + history, + resetPasswordInProgress, + resetPasswordError, + onSubmitPassword, + } = this.props; + + const title = intl.formatMessage({ + id: 'PasswordResetPage.title', + }); + + const { token, email } = parseUrlParams(location); + const paramsValid = !!(token && email); + + const handleSubmit = values => { + const { password } = values; + this.setState({ newPasswordSubmitted: false }); + onSubmitPassword(email, token, password).then(() => { + this.setState({ newPasswordSubmitted: true }); + }); + }; + + const paramsErrorContent = ( +
+

+ +

+
+ ); + + const resetFormContent = ( +
+ +

+ +

+

+ +

+ {resetPasswordError + ?

+ +

+ : null} + +
+ ); + + const resetDoneContent = ( +
+ +

+ +

+

+ +

+ + + +
+ ); + + let content; + + if (!paramsValid) { + content = paramsErrorContent; + } else if (!resetPasswordError && this.state.newPasswordSubmitted) { + content = resetDoneContent; + } else { + content = resetFormContent; + } + + return ( + + +
+ {content} +
+
+ ); + } +} + PasswordResetPageComponent.defaultProps = { authInfoError: null, currentUser: null, logoutError: null, notificationCount: 0, + resetPasswordError: null, }; -const { bool, func, instanceOf, number, object, shape, string } = PropTypes; - PasswordResetPageComponent.propTypes = { authInfoError: instanceOf(Error), authInProgress: bool.isRequired, @@ -77,6 +162,10 @@ PasswordResetPageComponent.propTypes = { onManageDisableScrolling: func.isRequired, scrollingDisabled: bool.isRequired, + resetPasswordInProgress: bool.isRequired, + resetPasswordError: instanceOf(Error), + onSubmitPassword: func.isRequired, + // from withRouter history: object.isRequired, location: shape({ @@ -98,6 +187,7 @@ const mapStateToProps = state => { currentUserHasListings, currentUserNotificationCount: notificationCount, } = state.user; + const { resetPasswordInProgress, resetPasswordError } = state.PasswordResetPage; return { authInfoError, authInProgress: authenticationInProgress(state), @@ -107,6 +197,8 @@ const mapStateToProps = state => { logoutError, notificationCount, scrollingDisabled: isScrollingDisabled(state), + resetPasswordInProgress, + resetPasswordError, }; }; @@ -114,6 +206,7 @@ const mapDispatchToProps = dispatch => ({ onLogout: () => dispatch(logout()), onManageDisableScrolling: (componentId, disableScrolling) => dispatch(manageDisableScrolling(componentId, disableScrolling)), + onSubmitPassword: (email, token, password) => dispatch(resetPassword(email, token, password)), }); const PasswordResetPage = compose( diff --git a/src/containers/reducers.js b/src/containers/reducers.js index c0d47bc4..40fe13fa 100644 --- a/src/containers/reducers.js +++ b/src/containers/reducers.js @@ -7,11 +7,12 @@ import CheckoutPage from './CheckoutPage/CheckoutPage.duck'; import EditListingPage from './EditListingPage/EditListingPage.duck'; import InboxPage from './InboxPage/InboxPage.duck'; import ListingPage from './ListingPage/ListingPage.duck'; +import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck'; import OrderPage from './OrderPage/OrderPage.duck'; +import PasswordResetPage from './PasswordResetPage/PasswordResetPage.duck'; +import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck'; import SalePage from './SalePage/SalePage.duck'; import SearchPage from './SearchPage/SearchPage.duck'; -import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck'; -import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck'; export { CheckoutPage, @@ -20,6 +21,7 @@ export { ListingPage, ManageListingsPage, OrderPage, + PasswordResetPage, ProfileSettingsPage, SalePage, SearchPage, diff --git a/src/translations/en.json b/src/translations/en.json index acaa752f..37a23ada 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -208,7 +208,14 @@ "PasswordResetForm.passwordLabel": "Your new password", "PasswordResetForm.passwordPlaceholder": "Enter your new password...", "PasswordResetForm.passwordRequired": "This field is required", + "PasswordResetForm.passwordTooShort": "Password should be at least {minLength} characters", "PasswordResetForm.submitButtonText": "Reset password", + "PasswordResetPage.helpText": "Please provide a new password.", + "PasswordResetPage.loginButtonText": "Log in", + "PasswordResetPage.mainHeading": "Reset your password", + "PasswordResetPage.passwordChangedHeading": "Password changed", + "PasswordResetPage.passwordChangedHelpText": "Your password has been changed successfully.", + "PasswordResetPage.resetFailed": "Reset failed. Please try again.", "PasswordResetPage.title": "Reset password", "PayoutDetailsForm.addressTitle": "Address", "PayoutDetailsForm.bankDetails": "Bank details",