Merge pull request #383 from sharetribe/reset-password

Reset password
This commit is contained in:
Kimmo Puputti 2017-09-11 16:46:20 +03:00 committed by GitHub
commit 2347354014
23 changed files with 614 additions and 257 deletions

View file

@ -45,8 +45,8 @@ describe('Application', () => {
'/signup': 'Sign up',
'/password': 'Request new password',
'/password/forgotten': 'Request new password',
'/password/change': 'Type new password',
'/this-url-should-not-be-found': 'Page not found',
'/reset-password?t=token&e=email': 'Reset password',
};
forEach(urlTitles, (title, url) => {
const context = {};

View file

@ -0,0 +1,57 @@
import React, { PropTypes } from 'react';
const KeysIcon = props => {
const { className } = props;
return (
<svg
className={className}
width="50px"
height="57px"
viewBox="0 0 50 57"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<g
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
>
<g transform="translate(-538.000000, -240.000000)" stroke="#C0392B">
<g transform="translate(540.000000, 242.000000)">
<g>
<path
d="M25,19.65 C25,12.6626 19.4022727,7 12.5,7 C5.59772727,7 0,12.6626 0,19.65 C0,25.4391 3.84772727,30.3082 9.09090909,31.8101 L9.09090909,34.6 L6.81818182,36.8977 L9.09090909,39.2 L9.09090909,41.5 L6.81818182,43.8 L9.09090909,46.1 L9.09090909,49.55 L12.5,53 L15.9090909,49.55 L15.9090909,31.8101 C21.1522727,30.3082 25,25.4391 25,19.65 L25,19.65 Z"
strokeWidth="2.5"
/>
<path
d="M16,15.5 C16,17.432 14.432,19 12.5,19 C10.568,19 9,17.432 9,15.5 C9,13.568 10.568,12 12.5,12 C14.432,12 16,13.568 16,15.5 L16,15.5 Z"
strokeWidth="2"
/>
<path
d="M16,33.460821 C19.8584615,35.6660894 24.6353846,35.6843714 28.5192308,33.5362343 L31,35.7163649 L31,39.1442433 L34.4615385,39.1442433 L35.6153846,40.3440007 L35.6153846,43.7147478 L38.6546154,43.7147478 L41.1053846,46 L46,46 L46,41.15298 L33.4138462,28.6892143 C36.1138462,23.9061813 35.4192308,17.7474266 31.3138462,13.6819628 C28.42,10.8162565 24.4784615,9.62335484 20.71,10.1032578"
strokeWidth="2.5"
/>
<path
d="M13,15 L13,4.61538462 C13,2.06769231 15.016,0 17.5,0 C19.984,0 22,2.06769231 22,4.61538462 L22,9.81230769"
strokeWidth="2.5"
/>
</g>
</g>
</g>
</g>
</svg>
);
};
KeysIcon.defaultProps = { className: null };
const { string } = PropTypes;
KeysIcon.propTypes = {
className: string,
};
export default KeysIcon;

View file

@ -0,0 +1,57 @@
import React, { PropTypes } from 'react';
const KeysIconSuccess = props => {
const { className } = props;
return (
<svg className={className} width="52" height="60" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2 2)" fill="none" fillRule="evenodd">
<path
d="M25 19.6C25 12.6 19.4 7 12.5 7 5.5 7 0 12.7 0 19.6c0 5.8 3.8 10.7 9 12.2v2.8L7 37 9 39v2.3L7 43.8 9 46v3.5l3.5 3.5 3.4-3.5V31.8c5.2-1.5 9-6.4 9-12.2z"
stroke="#C0392B"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 15.5c0 2-1.6 3.5-3.5 3.5-2 0-3.5-1.6-3.5-3.5 0-2 1.6-3.5 3.5-3.5 2 0 3.5 1.6 3.5 3.5z"
stroke="#C0392B"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 33.5c4 2.2 8.6 2.2 12.5 0l2.5 2.2V39h3.5l1 1.3v3.4h3.2L41 46h5v-4.8L33.4 28.7c2.7-4.8 2-11-2-15-3-3-7-4-10.7-3.6"
stroke="#C0392B"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13 15V4.6C13 2 15 0 17.5 0S22 2 22 4.6v5.2"
stroke="#C0392B"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle stroke="#FFF" strokeWidth="2" fill="#2ECC71" cx="35.5" cy="43.5" r="13.5" />
<path
stroke="#FFF"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
d="M42 41l-8 8-4-4"
/>
</g>
</svg>
);
};
KeysIconSuccess.defaultProps = { className: null };
const { string } = PropTypes;
KeysIconSuccess.propTypes = {
className: string,
};
export default KeysIconSuccess;

View file

@ -19,6 +19,8 @@ import FilterPanel from './FilterPanel/FilterPanel';
import HeroSection from './HeroSection/HeroSection';
import ImageCarousel from './ImageCarousel/ImageCarousel';
import ImageFromFile from './ImageFromFile/ImageFromFile';
import KeysIcon from './KeysIcon/KeysIcon';
import KeysIconSuccess from './KeysIconSuccess/KeysIconSuccess';
import ListingCard from './ListingCard/ListingCard';
import LocationAutocompleteInput, {
LocationAutocompleteInputField,
@ -88,6 +90,8 @@ export {
ImageCarousel,
ImageFromFile,
InlineTextButton,
KeysIcon,
KeysIconSuccess,
ListingCard,
LocationAutocompleteInput,
LocationAutocompleteInputField,

View file

@ -1,12 +0,0 @@
/* eslint-disable no-console */
import ChangePasswordForm from './ChangePasswordForm';
export const Empty = {
component: ChangePasswordForm,
props: {
onSubmit(values) {
console.log('submit new password form values:', values);
},
},
group: 'forms',
};

View file

@ -1,22 +0,0 @@
import React from 'react';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Button } from '../../components';
const ChangePasswordForm = props => {
const { handleSubmit, pristine, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="newPassword1">New password</label>
<Field name="newPassword1" component="input" type="password" />
<label htmlFor="newPassword2">New password, again</label>
<Field name="newPassword2" component="input" type="password" />
<Button type="submit" disabled={pristine || submitting}>Change password</Button>
</form>
);
};
ChangePasswordForm.propTypes = { ...formPropTypes };
const defaultFormName = 'ChangePasswordForm';
export default reduxForm({ form: defaultFormName })(ChangePasswordForm);

View file

@ -1,10 +0,0 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import ChangePasswordForm from './ChangePasswordForm';
describe('ChangePasswordForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(<ChangePasswordForm />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,37 +0,0 @@
exports[`ChangePasswordForm matches snapshot 1`] = `
<form
onSubmit={[Function]}>
<label
htmlFor="newPassword1">
New password
</label>
<input
name="newPassword1"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
type="password"
value="" />
<label
htmlFor="newPassword2">
New password, again
</label>
<input
name="newPassword2"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
type="password"
value="" />
<button
className=""
disabled={true}
type="submit">
Change password
</button>
</form>
`;

View file

@ -1,107 +0,0 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import * as propTypes from '../../util/propTypes';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import { PageLayout, Topbar } from '../../components';
import { ChangePasswordForm } from '../../containers';
const changePassword = values => {
// eslint-disable-next-line no-console
console.log('submit with values:', values);
};
export const PasswordChangePageComponent = props => {
const {
authInfoError,
authInProgress,
currentUser,
currentUserHasListings,
history,
isAuthenticated,
location,
logoutError,
notificationCount,
onLogout,
onManageDisableScrolling,
} = props;
return (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Type new password">
<Topbar
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
history={history}
isAuthenticated={isAuthenticated}
location={location}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
<ChangePasswordForm onSubmit={changePassword} />
</PageLayout>
);
};
PasswordChangePageComponent.defaultProps = {
authInfoError: null,
currentUser: null,
logoutError: null,
notificationCount: 0,
};
const { bool, func, instanceOf, number, object, shape } = PropTypes;
PasswordChangePageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
notificationCount: number,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
location: shape({ state: object }).isRequired,
};
const mapStateToProps = state => {
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
// Topbar needs user info.
const {
currentUser,
currentUserHasListings,
currentUserNotificationCount: notificationCount,
} = state.user;
return {
authInfoError,
authInProgress: authenticationInProgress(state),
currentUser,
currentUserHasListings,
currentUserNotificationCount: notificationCount,
isAuthenticated,
logoutError,
scrollingDisabled: isScrollingDisabled(state),
};
};
const mapDispatchToProps = dispatch => ({
onLogout: historyPush => dispatch(logout(historyPush)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
});
const PasswordChangePage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(
PasswordChangePageComponent
);
export default PasswordChangePage;

View file

@ -1,24 +0,0 @@
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { PasswordChangePageComponent } from './PasswordChangePage';
const noop = () => null;
describe('ContactDetailsPage', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<PasswordChangePageComponent
params={{ displayName: 'my-shop' }}
history={{ push: noop }}
location={{ search: '' }}
scrollingDisabled={false}
authInProgress={false}
currentUserHasListings={false}
isAuthenticated={false}
onLogout={noop}
onManageDisableScrolling={noop}
/>
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,27 +0,0 @@
exports[`ContactDetailsPage matches snapshot 1`] = `
<withRouter(PageLayout)
authInfoError={null}
logoutError={null}
title="Type new password">
<Topbar
authInProgress={false}
currentUser={null}
currentUserHasListings={false}
history={
Object {
"push": [Function],
}
}
isAuthenticated={false}
location={
Object {
"search": "",
}
}
notificationCount={0}
onLogout={[Function]}
onManageDisableScrolling={[Function]} />
<ReduxForm
onSubmit={[Function]} />
</withRouter(PageLayout)>
`;

View file

@ -0,0 +1,15 @@
.root {
}
.passwordInput {
/* Leave space between the input and the button below when the
viewport height is small */
margin-bottom: 24px;
}
.submitButton {
/* In a flexbox context, this makes the button position itself to the
bottom */
margin-top: auto;
}

View file

@ -0,0 +1,12 @@
/* eslint-disable no-console */
import PasswordResetForm from './PasswordResetForm';
export const Empty = {
component: PasswordResetForm,
props: {
onSubmit(values) {
console.log('submit with values:', values);
},
},
group: 'forms',
};

View file

@ -0,0 +1,89 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import { PrimaryButton, TextInputField } from '../../components';
import * as validators from '../../util/validators';
import css from './PasswordResetForm.css';
const PASSWORD_MIN_LENGTH = 8;
const PasswordResetFormComponent = props => {
const {
rootClassName,
className,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
} = props;
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordResetForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordResetForm.passwordPlaceholder',
});
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;
return (
<form className={classes} onSubmit={handleSubmit}>
<TextInputField
className={css.passwordInput}
type="password"
name="password"
id={`${form}.password`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={[passwordRequired, passwordMinLength]}
/>
<PrimaryButton className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="PasswordResetForm.submitButtonText" />
</PrimaryButton>
</form>
);
};
PasswordResetFormComponent.defaultProps = {
rootClassName: null,
className: null,
inProgress: false,
};
const { string, bool } = PropTypes;
PasswordResetFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
inProgress: bool,
intl: intlShape.isRequired,
};
const defaultFormName = 'PasswordResetForm';
const PasswordResetForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
PasswordResetFormComponent
);
export default PasswordResetForm;

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

@ -0,0 +1,218 @@
import React, { PropTypes, Component } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import * as propTypes from '../../util/propTypes';
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';
const { bool, func, instanceOf, number, object, shape, string } = PropTypes;
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,
};
PasswordResetPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
notificationCount: number,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
scrollingDisabled: bool.isRequired,
resetPasswordInProgress: bool.isRequired,
resetPasswordError: instanceOf(Error),
onSubmitPassword: func.isRequired,
// from withRouter
history: object.isRequired,
location: shape({
search: string,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const {
authInfoError,
isAuthenticated,
logoutError,
} = state.Auth;
const {
currentUser,
currentUserHasListings,
currentUserNotificationCount: notificationCount,
} = state.user;
const { resetPasswordInProgress, resetPasswordError } = state.PasswordResetPage;
return {
authInfoError,
authInProgress: authenticationInProgress(state),
currentUser,
currentUserHasListings,
isAuthenticated,
logoutError,
notificationCount,
scrollingDisabled: isScrollingDisabled(state),
resetPasswordInProgress,
resetPasswordError,
};
};
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(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
injectIntl
)(PasswordResetPageComponent);
export default PasswordResetPage;

View file

@ -1,7 +1,6 @@
import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
import BookingDatesForm from './BookingDatesForm/BookingDatesForm';
import ChangeAccountPasswordForm from './ChangeAccountPasswordForm/ChangeAccountPasswordForm';
import ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
import CheckoutPage from './CheckoutPage/CheckoutPage';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
@ -9,8 +8,8 @@ import EditListingLocationForm from './EditListingLocationForm/EditListingLocati
import EditListingPage from './EditListingPage/EditListingPage';
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
import EmailVerificationForm from './EmailVerificationForm/EmailVerificationForm';
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
import InboxPage from './InboxPage/InboxPage';
import LandingPage from './LandingPage/LandingPage';
import ListingPage from './ListingPage/ListingPage';
@ -19,9 +18,10 @@ import LoginForm from './LoginForm/LoginForm';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
import NotFoundPage from './NotFoundPage/NotFoundPage';
import OrderPage from './OrderPage/OrderPage';
import PasswordChangePage from './PasswordChangePage/PasswordChangePage';
import PasswordForgottenForm from './PasswordForgottenForm/PasswordForgottenForm';
import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage';
import PasswordResetForm from './PasswordResetForm/PasswordResetForm';
import PasswordResetPage from './PasswordResetPage/PasswordResetPage';
import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm';
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
import ProfilePage from './ProfilePage/ProfilePage';
@ -39,7 +39,6 @@ export {
AuthenticationPage,
BookingDatesForm,
ChangeAccountPasswordForm,
ChangePasswordForm,
CheckoutPage,
ContactDetailsPage,
EditListingDescriptionForm,
@ -47,8 +46,8 @@ export {
EditListingPage,
EditListingPhotosForm,
EditListingPricingForm,
EmailVerificationPage,
EmailVerificationForm,
EmailVerificationPage,
InboxPage,
LandingPage,
ListingPage,
@ -57,9 +56,10 @@ export {
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordForgottenForm,
PasswordForgottenPage,
PasswordResetForm,
PasswordResetPage,
PayoutDetailsForm,
PayoutPreferencesPage,
ProfilePage,

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

@ -32,7 +32,6 @@ import * as TopbarDesktop from './components/TopbarDesktop/TopbarDesktop.example
import * as BookingDatesForm from './containers/BookingDatesForm/BookingDatesForm.example';
import * as ChangeAccountPasswordForm
from './containers/ChangeAccountPasswordForm/ChangeAccountPasswordForm.example';
import * as ChangePasswordForm from './containers/ChangePasswordForm/ChangePasswordForm.example';
import * as Colors from './containers/StyleguidePage/Colors.example';
import * as EditListingDescriptionForm
from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example';
@ -45,6 +44,7 @@ import * as EditListingPricingForm
import * as LoginForm from './containers/LoginForm/LoginForm.example';
import * as PasswordForgottenForm
from './containers/PasswordForgottenForm/PasswordForgottenForm.example';
import * as PasswordResetForm from './containers/PasswordResetForm/PasswordResetForm.example';
import * as PayoutDetailsForm from './containers/PayoutDetailsForm/PayoutDetailsForm.example';
import * as SignupForm from './containers/SignupForm/SignupForm.example';
import * as StripePaymentForm from './containers/StripePaymentForm/StripePaymentForm.example';
@ -57,7 +57,6 @@ export {
BookingDatesForm,
Button,
ChangeAccountPasswordForm,
ChangePasswordForm,
Colors,
CurrencyInputField,
DateInputField,
@ -80,6 +79,7 @@ export {
NamedLink,
PaginationLinks,
PasswordForgottenForm,
PasswordResetForm,
PayoutDetailsForm,
ResponsiveImage,
SelectField,

View file

@ -11,8 +11,8 @@ import {
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordForgottenPage,
PasswordResetPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
@ -156,12 +156,6 @@ const routesConfiguration = [
name: 'PasswordForgottenPage',
component: props => <PasswordForgottenPage {...props} />,
},
{
path: '/password/change',
exact: true,
name: 'PasswordChangePage',
component: props => <PasswordChangePage {...props} />,
},
{
path: '/inbox',
auth: true,
@ -302,6 +296,16 @@ const routesConfiguration = [
component: props => <NotFoundPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /reset-password endpoint
{
path: '/reset-password',
exact: true,
name: 'PasswordResetPage',
component: props => <PasswordResetPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /verify-email endpoint

View file

@ -205,6 +205,18 @@
"PaginationLinks.next": "Next page",
"PaginationLinks.previous": "Previous page",
"PaginationLinks.toPage": "Go to page {page}",
"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",
"PayoutDetailsForm.birthdayDatePlaceholder": "dd",

View file

@ -14,6 +14,11 @@ export const required = message =>
return value ? VALID : message;
};
export const minLength = (message, minimumLength) =>
value => {
return value && value.length >= minimumLength ? VALID : message;
};
export const maxLength = (message, maximumLength) =>
value => {
return !value || value.length <= maximumLength ? VALID : message;