diff --git a/src/app.test.js b/src/app.test.js
index 4a082fcb..29df13af 100644
--- a/src/app.test.js
+++ b/src/app.test.js
@@ -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 = {};
diff --git a/src/components/KeysIcon/KeysIcon.js b/src/components/KeysIcon/KeysIcon.js
new file mode 100644
index 00000000..559d2fbd
--- /dev/null
+++ b/src/components/KeysIcon/KeysIcon.js
@@ -0,0 +1,57 @@
+import React, { PropTypes } from 'react';
+
+const KeysIcon = props => {
+ const { className } = props;
+ return (
+
+ );
+};
+
+KeysIcon.defaultProps = { className: null };
+
+const { string } = PropTypes;
+
+KeysIcon.propTypes = {
+ className: string,
+};
+
+export default KeysIcon;
diff --git a/src/components/KeysIconSuccess/KeysIconSuccess.js b/src/components/KeysIconSuccess/KeysIconSuccess.js
new file mode 100644
index 00000000..d4d450ec
--- /dev/null
+++ b/src/components/KeysIconSuccess/KeysIconSuccess.js
@@ -0,0 +1,57 @@
+import React, { PropTypes } from 'react';
+
+const KeysIconSuccess = props => {
+ const { className } = props;
+ return (
+
+ );
+};
+
+KeysIconSuccess.defaultProps = { className: null };
+
+const { string } = PropTypes;
+
+KeysIconSuccess.propTypes = {
+ className: string,
+};
+
+export default KeysIconSuccess;
diff --git a/src/components/index.js b/src/components/index.js
index 7c381a91..1307d98a 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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,
diff --git a/src/containers/ChangePasswordForm/ChangePasswordForm.example.js b/src/containers/ChangePasswordForm/ChangePasswordForm.example.js
deleted file mode 100644
index 0e9de9b6..00000000
--- a/src/containers/ChangePasswordForm/ChangePasswordForm.example.js
+++ /dev/null
@@ -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',
-};
diff --git a/src/containers/ChangePasswordForm/ChangePasswordForm.js b/src/containers/ChangePasswordForm/ChangePasswordForm.js
deleted file mode 100644
index d990777a..00000000
--- a/src/containers/ChangePasswordForm/ChangePasswordForm.js
+++ /dev/null
@@ -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 (
-
- );
-};
-
-ChangePasswordForm.propTypes = { ...formPropTypes };
-
-const defaultFormName = 'ChangePasswordForm';
-
-export default reduxForm({ form: defaultFormName })(ChangePasswordForm);
diff --git a/src/containers/ChangePasswordForm/ChangePasswordForm.test.js b/src/containers/ChangePasswordForm/ChangePasswordForm.test.js
deleted file mode 100644
index e936ab66..00000000
--- a/src/containers/ChangePasswordForm/ChangePasswordForm.test.js
+++ /dev/null
@@ -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();
- expect(tree).toMatchSnapshot();
- });
-});
diff --git a/src/containers/ChangePasswordForm/__snapshots__/ChangePasswordForm.test.js.snap b/src/containers/ChangePasswordForm/__snapshots__/ChangePasswordForm.test.js.snap
deleted file mode 100644
index 8c64f698..00000000
--- a/src/containers/ChangePasswordForm/__snapshots__/ChangePasswordForm.test.js.snap
+++ /dev/null
@@ -1,37 +0,0 @@
-exports[`ChangePasswordForm matches snapshot 1`] = `
-
-`;
diff --git a/src/containers/PasswordChangePage/PasswordChangePage.js b/src/containers/PasswordChangePage/PasswordChangePage.js
deleted file mode 100644
index f42e3052..00000000
--- a/src/containers/PasswordChangePage/PasswordChangePage.js
+++ /dev/null
@@ -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 (
-
-
-
-
- );
-};
-
-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;
diff --git a/src/containers/PasswordChangePage/PasswordChangePage.test.js b/src/containers/PasswordChangePage/PasswordChangePage.test.js
deleted file mode 100644
index 91a08dbc..00000000
--- a/src/containers/PasswordChangePage/PasswordChangePage.test.js
+++ /dev/null
@@ -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(
-
- );
- expect(tree).toMatchSnapshot();
- });
-});
diff --git a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap b/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
deleted file mode 100644
index b1d71c1d..00000000
--- a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
+++ /dev/null
@@ -1,27 +0,0 @@
-exports[`ContactDetailsPage matches snapshot 1`] = `
-
-
-
-
-`;
diff --git a/src/containers/PasswordResetForm/PasswordResetForm.css b/src/containers/PasswordResetForm/PasswordResetForm.css
new file mode 100644
index 00000000..4377786e
--- /dev/null
+++ b/src/containers/PasswordResetForm/PasswordResetForm.css
@@ -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;
+}
diff --git a/src/containers/PasswordResetForm/PasswordResetForm.example.js b/src/containers/PasswordResetForm/PasswordResetForm.example.js
new file mode 100644
index 00000000..a4da189a
--- /dev/null
+++ b/src/containers/PasswordResetForm/PasswordResetForm.example.js
@@ -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',
+};
diff --git a/src/containers/PasswordResetForm/PasswordResetForm.js b/src/containers/PasswordResetForm/PasswordResetForm.js
new file mode 100644
index 00000000..c13aa85b
--- /dev/null
+++ b/src/containers/PasswordResetForm/PasswordResetForm.js
@@ -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 (
+
+ );
+};
+
+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;
diff --git a/src/containers/PasswordResetPage/PasswordResetPage.css b/src/containers/PasswordResetPage/PasswordResetPage.css
new file mode 100644
index 00000000..850c1b93
--- /dev/null
+++ 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
new file mode 100644
index 00000000..ed943760
--- /dev/null
+++ b/src/containers/PasswordResetPage/PasswordResetPage.js
@@ -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 = (
+
+ );
+
+ 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,
+};
+
+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;
diff --git a/src/containers/index.js b/src/containers/index.js
index 7bef4382..2ede665f 100644
--- a/src/containers/index.js
+++ b/src/containers/index.js
@@ -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,
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/examples.js b/src/examples.js
index 20614923..5425c9cd 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -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,
diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js
index 63ccc7d6..4cc74989 100644
--- a/src/routesConfiguration.js
+++ b/src/routesConfiguration.js
@@ -11,8 +11,8 @@ import {
ManageListingsPage,
NotFoundPage,
OrderPage,
- PasswordChangePage,
PasswordForgottenPage,
+ PasswordResetPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
@@ -156,12 +156,6 @@ const routesConfiguration = [
name: 'PasswordForgottenPage',
component: props => ,
},
- {
- path: '/password/change',
- exact: true,
- name: 'PasswordChangePage',
- component: props => ,
- },
{
path: '/inbox',
auth: true,
@@ -302,6 +296,16 @@ const routesConfiguration = [
component: 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 => ,
+ },
+
// Do not change this path!
//
// The API expects that the Starter App implements /verify-email endpoint
diff --git a/src/translations/en.json b/src/translations/en.json
index d8832ac2..37a23ada 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -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",
diff --git a/src/util/validators.js b/src/util/validators.js
index 6533748e..d37c63d5 100644
--- a/src/util/validators.js
+++ b/src/util/validators.js
@@ -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;