Password reset page

This commit is contained in:
Kimmo Puputti 2017-09-08 15:17:39 +03:00
parent c7db98a668
commit 388a08e732
6 changed files with 286 additions and 52 deletions

View file

@ -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]}
/>
<PrimaryButton className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="PasswordResetForm.submitButtonText" />

View file

@ -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;
}

View file

@ -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)));
};

View file

@ -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 (
<PageLayout
title={title}
authInfoError={authInfoError}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
>
<Topbar
isAuthenticated={isAuthenticated}
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
notificationCount={notificationCount}
history={history}
location={location}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
<p>TODO</p>
</PageLayout>
);
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 = (
<div className={css.content}>
<p className={css.error}>
<FormattedMessage id="PasswordResetPage.invalidUrlParams" />
</p>
</div>
);
const resetFormContent = (
<div className={css.content}>
<KeysIcon />
<h1 className={css.mainHeading}>
<FormattedMessage id="PasswordResetPage.mainHeading" />
</h1>
<p className={css.helpText}>
<FormattedMessage id="PasswordResetPage.helpText" />
</p>
{resetPasswordError
? <p className={css.error}>
<FormattedMessage id="PasswordResetPage.resetFailed" />
</p>
: null}
<PasswordResetForm
className={css.form}
onSubmit={handleSubmit}
inProgress={resetPasswordInProgress}
/>
</div>
);
const resetDoneContent = (
<div className={css.content}>
<KeysIconSuccess />
<h1 className={css.mainHeading}>
<FormattedMessage id="PasswordResetPage.passwordChangedHeading" />
</h1>
<p className={css.helpText}>
<FormattedMessage id="PasswordResetPage.passwordChangedHelpText" />
</p>
<NamedLink name="LoginPage" className={css.buttonLink}>
<FormattedMessage id="PasswordResetPage.loginButtonText" />
</NamedLink>
</div>
);
let content;
if (!paramsValid) {
content = paramsErrorContent;
} else if (!resetPasswordError && this.state.newPasswordSubmitted) {
content = resetDoneContent;
} else {
content = resetFormContent;
}
return (
<PageLayout
title={title}
authInfoError={authInfoError}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
>
<Topbar
isAuthenticated={isAuthenticated}
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
notificationCount={notificationCount}
history={history}
location={location}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
<div className={css.root}>
{content}
</div>
</PageLayout>
);
}
}
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(

View file

@ -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,

View file

@ -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",