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;