diff --git a/src/app.test.js b/src/app.test.js index 29df13af..a06a30bd 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -43,8 +43,7 @@ describe('Application', () => { '/u/1234': 'Profile page with display name: 1234', '/login': 'Login', '/signup': 'Sign up', - '/password': 'Request new password', - '/password/forgotten': 'Request new password', + '/recover-password': 'Request a new password', '/this-url-should-not-be-found': 'Page not found', '/reset-password?t=token&e=email': 'Reset password', }; diff --git a/src/components/TextInputField/TextInputField.js b/src/components/TextInputField/TextInputField.js index 6039ab0c..7ff8a1a6 100644 --- a/src/components/TextInputField/TextInputField.js +++ b/src/components/TextInputField/TextInputField.js @@ -17,6 +17,7 @@ class TextInputFieldComponent extends Component { rootClassName, className, clearOnUnmount, + customErrorText, id, label, type, @@ -33,9 +34,13 @@ class TextInputFieldComponent extends Component { const { valid, invalid, touched, error } = meta; const isTextarea = type === 'textarea'; + const errorText = customErrorText || error; + // Error message and input error styles are only shown if the // field has been touched and the validation has failed. - const hasError = touched && invalid && error; + const hasError = touched && invalid && errorText; + + const fieldMeta = { touched, error: errorText }; const inputClasses = classNames(css.input, { [css.inputSuccess]: valid, @@ -50,7 +55,7 @@ class TextInputFieldComponent extends Component {
{label ? : null} {isTextarea ? : } - +
); } @@ -60,6 +65,7 @@ TextInputFieldComponent.defaultProps = { rootClassName: null, className: null, clearOnUnmount: false, + customErrorText: null, id: null, label: null, }; @@ -72,6 +78,10 @@ TextInputFieldComponent.propTypes = { clearOnUnmount: bool, + // Error message that can be manually passed to input field, + // overrides default validation message + customErrorText: string, + // Label is optional, but if it is given, an id is also required so // the label can reference the input in the `for` attribute id: string, diff --git a/src/containers/LoginForm/LoginForm.css b/src/containers/LoginForm/LoginForm.css index 1b17d7aa..98c11d52 100644 --- a/src/containers/LoginForm/LoginForm.css +++ b/src/containers/LoginForm/LoginForm.css @@ -10,17 +10,23 @@ .password { margin-top: 24px; + margin-bottom: 24px; @media (--viewportMedium) { margin-top: 30px; } } -.button { - margin-top: 24px; - align-self: stretch; - - @media (--viewportMedium) { - margin-top: 72px; - } +.bottomWrapper { + margin-top: auto; + text-align: center; +} + +.recoveryLink { + @apply --marketplaceH5FontStyles; + color: var(--matterColor); +} + +.recoveryLinkHelp { + color: var(--matterColorAnti); } diff --git a/src/containers/LoginForm/LoginForm.js b/src/containers/LoginForm/LoginForm.js index 6f48781b..3699ddc4 100644 --- a/src/containers/LoginForm/LoginForm.js +++ b/src/containers/LoginForm/LoginForm.js @@ -3,7 +3,7 @@ import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { reduxForm, propTypes as formPropTypes } from 'redux-form'; import classNames from 'classnames'; -import { Button, TextInputField } from '../../components'; +import { Button, TextInputField, NamedLink } from '../../components'; import * as validators from '../../util/validators'; import css from './LoginForm.css'; @@ -68,6 +68,15 @@ const LoginFormComponent = props => { validate={passwordRequired} /> +

+ + + {' '} + + + + +

diff --git a/src/containers/LoginForm/LoginForm.test.js b/src/containers/LoginForm/LoginForm.test.js index bffa3a3b..c7aa9db3 100644 --- a/src/containers/LoginForm/LoginForm.test.js +++ b/src/containers/LoginForm/LoginForm.test.js @@ -1,10 +1,18 @@ import React from 'react'; import { renderDeep } from '../../util/test-helpers'; +import { RoutesProvider } from '../../components'; +import routesConfiguration from '../../routesConfiguration'; +import { flattenRoutes } from '../../util/routes'; import LoginForm from './LoginForm'; describe('LoginForm', () => { it('matches snapshot', () => { - const tree = renderDeep(); + const flattenedRoutes = flattenRoutes(routesConfiguration); + const tree = renderDeep( + + + + ); expect(tree).toMatchSnapshot(); }); }); diff --git a/src/containers/LoginForm/__snapshots__/LoginForm.test.js.snap b/src/containers/LoginForm/__snapshots__/LoginForm.test.js.snap index 72311e2c..9bba618b 100644 --- a/src/containers/LoginForm/__snapshots__/LoginForm.test.js.snap +++ b/src/containers/LoginForm/__snapshots__/LoginForm.test.js.snap @@ -42,6 +42,26 @@ exports[`LoginForm matches snapshot 1`] = ` value="" /> +

+ + + Forgot your password? + + + + + No problem. + + + +

- - ); -}; - -PasswordForgottenForm.propTypes = { ...formPropTypes }; - -const defaultFormName = 'PasswordForgottenForm'; - -export default reduxForm({ form: defaultFormName })(PasswordForgottenForm); diff --git a/src/containers/PasswordForgottenForm/PasswordForgottenForm.test.js b/src/containers/PasswordForgottenForm/PasswordForgottenForm.test.js deleted file mode 100644 index 64f14cd0..00000000 --- a/src/containers/PasswordForgottenForm/PasswordForgottenForm.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { renderDeep } from '../../util/test-helpers'; -import PasswordForgottenForm from './PasswordForgottenForm'; - -describe('PasswordForgottenForm', () => { - it('matches snapshot', () => { - const tree = renderDeep(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/src/containers/PasswordForgottenForm/__snapshots__/PasswordForgottenForm.test.js.snap b/src/containers/PasswordForgottenForm/__snapshots__/PasswordForgottenForm.test.js.snap deleted file mode 100644 index abe77199..00000000 --- a/src/containers/PasswordForgottenForm/__snapshots__/PasswordForgottenForm.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -exports[`PasswordForgottenForm matches snapshot 1`] = ` -
- - -

- We will send you instructions to your email. -

- -
-`; diff --git a/src/containers/PasswordForgottenPage/PasswordForgottenPage.js b/src/containers/PasswordForgottenPage/PasswordForgottenPage.js deleted file mode 100644 index 6da79ab7..00000000 --- a/src/containers/PasswordForgottenPage/PasswordForgottenPage.js +++ /dev/null @@ -1,133 +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 { sendVerificationEmail } from '../../ducks/user.duck'; -import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; -import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; -import { PageLayout, Topbar } from '../../components'; -import { PasswordForgottenForm } from '../../containers'; - -const sendPasswordResetEmail = values => { - // eslint-disable-next-line no-console - console.log('submit with values:', values); -}; - -export const PasswordForgottenPageComponent = props => { - const { - authInfoError, - authInProgress, - currentUser, - currentUserHasListings, - currentUserHasOrders, - history, - isAuthenticated, - location, - logoutError, - notificationCount, - onLogout, - onManageDisableScrolling, - sendVerificationEmailInProgress, - sendVerificationEmailError, - onResendVerificationEmail, - } = props; - - return ( - - - - - ); -}; - -PasswordForgottenPageComponent.defaultProps = { - authInfoError: null, - currentUser: null, - currentUserHasOrders: null, - logoutError: null, - notificationCount: 0, - sendVerificationEmailError: null, -}; - -const { bool, func, instanceOf, number, object, shape } = PropTypes; - -PasswordForgottenPageComponent.propTypes = { - authInfoError: instanceOf(Error), - authInProgress: bool.isRequired, - currentUser: propTypes.currentUser, - currentUserHasListings: bool.isRequired, - currentUserHasOrders: bool, - isAuthenticated: bool.isRequired, - logoutError: instanceOf(Error), - notificationCount: number, - onLogout: func.isRequired, - onManageDisableScrolling: func.isRequired, - sendVerificationEmailInProgress: bool.isRequired, - sendVerificationEmailError: instanceOf(Error), - onResendVerificationEmail: 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, - currentUserHasOrders, - currentUserNotificationCount: notificationCount, - sendVerificationEmailInProgress, - sendVerificationEmailError, - } = state.user; - return { - authInfoError, - authInProgress: authenticationInProgress(state), - currentUser, - currentUserHasListings, - currentUserHasOrders, - currentUserNotificationCount: notificationCount, - isAuthenticated, - logoutError, - scrollingDisabled: isScrollingDisabled(state), - sendVerificationEmailInProgress, - sendVerificationEmailError, - }; -}; - -const mapDispatchToProps = dispatch => ({ - onLogout: historyPush => dispatch(logout(historyPush)), - onManageDisableScrolling: (componentId, disableScrolling) => - dispatch(manageDisableScrolling(componentId, disableScrolling)), - onResendVerificationEmail: () => dispatch(sendVerificationEmail()), -}); - -const PasswordForgottenPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)( - PasswordForgottenPageComponent -); - -export default PasswordForgottenPage; diff --git a/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.css b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.css new file mode 100644 index 00000000..a4c925e5 --- /dev/null +++ b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.css @@ -0,0 +1,40 @@ +@import '../../marketplace.css'; + +.root { +display: flex; + flex-direction: column; + flex: 1; + justify-content: space-between; + height: 100%; + min-height: 306px; +} + +.email { + margin-top: 37px; + + @media (--viewportMedium) { + margin-top: 38px; + } +} + +.bottomWrapper { + margin-top: auto; +} + +.bottomText { + @apply --marketplaceH5FontStyles; + color: var(--matterColorAnti); + margin: 0; + + @media (--viewportMedium) { + margin: 0; + } +} + +.submitButton { + margin-top: 26px; +} + +.loginLink { + color: var(--matterColor); +} diff --git a/src/containers/PasswordForgottenForm/PasswordForgottenForm.example.js b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.example.js similarity index 65% rename from src/containers/PasswordForgottenForm/PasswordForgottenForm.example.js rename to src/containers/PasswordRecoveryForm/PasswordRecoveryForm.example.js index 6b864451..9d4bb450 100644 --- a/src/containers/PasswordForgottenForm/PasswordForgottenForm.example.js +++ b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.example.js @@ -1,8 +1,8 @@ /* eslint-disable no-console */ -import PasswordForgottenForm from './PasswordForgottenForm'; +import PasswordRecoveryForm from './PasswordRecoveryForm'; export const Empty = { - component: PasswordForgottenForm, + component: PasswordRecoveryForm, props: { onSubmit(values) { console.log('submit forgotten password email:', values); diff --git a/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js new file mode 100644 index 00000000..8370fe36 --- /dev/null +++ b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js @@ -0,0 +1,101 @@ +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, NamedLink } from '../../components'; +import * as validators from '../../util/validators'; + +import css from './PasswordRecoveryForm.css'; + +const isNotFoundError = error => error && error.status === 404; + +const PasswordRecoveryFormComponent = props => { + const { + rootClassName, + className, + handleSubmit, + pristine, + submitting, + form, + initialValues, + intl, + recoveryError, + } = props; + + // email + const emailLabel = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailLabel', + }); + const emailPlaceholder = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailPlaceholder', + }); + const emailRequiredMessage = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailRequired', + }); + const emailNotFoundMessage = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailNotFound', + }); + + const emailRequired = validators.required(emailRequiredMessage); + // In case a given email is not found, pass a custom error message + // to be rendered with the input component + const customErrorText = isNotFoundError(recoveryError) ? emailNotFoundMessage : null; + const initialEmail = initialValues ? initialValues.email : null; + const buttonDisabled = (pristine && !initialEmail) || submitting; + const classes = classNames(rootClassName || css.root, className); + + const loginLink = ( + + + + ); + + return ( +
+ + +
+

+ +

+ + + +
+ + ); +}; + +PasswordRecoveryFormComponent.defaultProps = { + rootClassName: null, + className: null, + recoveryError: null, +}; + +const { instanceOf, string } = PropTypes; + +PasswordRecoveryFormComponent.propTypes = { + ...formPropTypes, + rootClassName: string, + className: string, + recoveryError: instanceOf(Error), + intl: intlShape.isRequired, +}; + +const defaultFormName = 'PasswordRecoveryForm'; + +const PasswordRecoveryForm = compose(reduxForm({ form: defaultFormName }), injectIntl)( + PasswordRecoveryFormComponent +); + +export default PasswordRecoveryForm; diff --git a/src/containers/PasswordRecoveryPage/DoorIcon.js b/src/containers/PasswordRecoveryPage/DoorIcon.js new file mode 100644 index 00000000..5bed0738 --- /dev/null +++ b/src/containers/PasswordRecoveryPage/DoorIcon.js @@ -0,0 +1,53 @@ +import React, { PropTypes } from 'react'; + +const DoorIcon = props => { + const { className } = props; + return ( + + + + + + + + + + + ); +}; + +DoorIcon.defaultProps = { className: null }; + +const { string } = PropTypes; + +DoorIcon.propTypes = { + className: string, +}; + +export default DoorIcon; diff --git a/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.css b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.css new file mode 100644 index 00000000..b655d83e --- /dev/null +++ b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.css @@ -0,0 +1,118 @@ +@import '../../marketplace.css'; + +:root { + --baseContent: { + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 32px 24px 98px 24px; + background-color: var(--matterColorLight); + border-radius: 2px; + + @media (--viewportMedium) { + flex-basis: 480px; + flex-grow: 0; + padding: 55px; + margin-top: 7.5vh; + margin-bottom: 7.5vh; + } + } +} + +.root { + @apply --backgroundImage; + + display: flex; + flex: 1; + + @media (--viewportMedium) { + justify-content: center; + align-items: flex-start; + } +} + +.submitEmailContent { + @apply --baseContent; +} + +.emailSubmittedContent { + @apply --baseContent; + padding-bottom: 99px; + + @media (--viewportMedium) { + padding-bottom: 62px; + min-height: 508px; + } +} + +.emailNotVerifiedContent { + @apply --baseContent; + padding-top: 30px; + + @media (--viewportMedium) { + padding-top: 53px; + padding-bottom: 80px; + } +} + +.genericErrorContent { + @apply --baseContent; +} + +.keysIconRoot { + width: 100%; + height: 100%; +} + +.title { + @apply --marketplaceModalTitle; + margin-top: 31px; + margin-bottom: 0px; +} + +.message { + margin-top: 23px; + margin-bottom: 0px; + + @media (--viewportMedium) { + margin-top: 24px; + margin-bottom: 0px; + } +} + +.submittedEmail { + font-weight: bold; +} + +.emailNotVerifiedBottomText { + margin-top: 30px; + margin-bottom: 0px; + + @media (--viewportMedium) { + margin-top: 32px; + } +} + +.bottomWrapper { + margin-top: auto; +} + +.bottomInfo { + @apply --marketplaceH5FontStyles; + margin: 0; + color: var(--matterColorAnti); + + @media (--viewportMedium) { + margin: 0; + } +} + +.bottomLink { + @apply --marketplaceH5FontStyles; + margin: 0; + color: var(--matterColor); + + @media (--viewportMedium) { + margin: 0; + } +} diff --git a/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.duck.js b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.duck.js new file mode 100644 index 00000000..a2ec3b7c --- /dev/null +++ b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.duck.js @@ -0,0 +1,80 @@ +// ================ Action types ================ // + +export const RECOVERY_REQUEST = 'app/PasswordRecoveryPage/RECOVERY_REQUEST'; +export const RECOVERY_SUCCESS = 'app/PasswordRecoveryPage/RECOVERY_SUCCESS'; +export const RECOVERY_ERROR = 'app/PasswordRecoveryPage/RECOVERY_ERROR'; +export const RETYPE_EMAIL = 'app/PasswordRecoveryPage/RETYPE_EMAIL'; +export const CLEAR_RECOVERY_ERROR = 'app/PasswordRecoveryPage/CLEAR_RECOVERY_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + initialEmail: null, + submittedEmail: null, + recoveryError: null, + recoveryInProgress: false, + passwordRequested: false, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case RECOVERY_REQUEST: + return { + ...state, + submittedEmail: null, + recoveryInProgress: true, + recoveryError: null, + }; + case RECOVERY_SUCCESS: + return { + ...state, + submittedEmail: payload.email, + initialEmail: payload.email, + recoveryInProgress: false, + passwordRequested: true, + }; + case RECOVERY_ERROR: + return { + ...state, + recoveryInProgress: false, + recoveryError: payload.error, + initialEmail: payload.email, + }; + case RETYPE_EMAIL: + return { + ...state, + initialEmail: state.submittedEmail, + submittedEmail: null, + passwordRequested: false, + }; + case CLEAR_RECOVERY_ERROR: + return { ...state, recoveryError: null }; + default: + return state; + } +} + +// ================ Action creators ================ // + +export const passwordRecoveryRequest = () => ({ type: RECOVERY_REQUEST }); +export const passwordRecoverySuccess = email => ({ type: RECOVERY_SUCCESS, payload: { email } }); +export const passwordRecoveryError = (error, email) => ({ + type: RECOVERY_ERROR, + payload: { error, email }, + error: true, +}); +export const retypePasswordRecoveryEmail = () => ({ type: RETYPE_EMAIL }); +export const clearPasswordRecoveryError = () => ({ type: CLEAR_RECOVERY_ERROR }); + +// ================ Thunks ================ // + +export const recoverPassword = email => + (dispatch, getState, sdk) => { + dispatch(passwordRecoveryRequest()); + + return sdk.passwordReset + .request({ email }) + .then(() => dispatch(passwordRecoverySuccess(email))) + .catch(error => dispatch(passwordRecoveryError(error, email))); + }; diff --git a/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.js b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.js new file mode 100644 index 00000000..afcc2f1e --- /dev/null +++ b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.js @@ -0,0 +1,283 @@ +import React, { PropTypes } 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 { sendVerificationEmail } from '../../ducks/user.duck'; +import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; +import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; +import { recoverPassword, retypePasswordRecoveryEmail, clearPasswordRecoveryError } from './PasswordRecoveryPage.duck'; +import { PageLayout, Topbar, InlineTextButton, KeysIcon } from '../../components'; +import { PasswordRecoveryForm } from '../../containers'; + +import DoorIcon from './DoorIcon'; +import css from './PasswordRecoveryPage.css'; + +export const PasswordRecoveryPageComponent = props => { + const { + authInfoError, + authInProgress, + currentUser, + currentUserHasListings, + currentUserHasOrders, + history, + isAuthenticated, + location, + logoutError, + notificationCount, + onLogout, + onManageDisableScrolling, + sendVerificationEmailInProgress, + sendVerificationEmailError, + onResendVerificationEmail, + initialEmail, + submittedEmail, + recoveryError, + recoveryInProgress, + passwordRequested, + onChange, + onSubmitEmail, + onRetypeEmail, + intl, + } = props; + + const title = intl.formatMessage({ + id: 'PasswordRecoveryPage.title', + }); + + const resendEmailLink = ( + onSubmitEmail(submittedEmail)}> + + + ); + + const fixEmailLink = ( + + + + ); + + const submitEmailContent = ( +
+ +

+ +

+

+ +

+ onSubmitEmail(values.email)} + initialValues={{ email: initialEmail }} + recoveryError={recoveryError} + /> +
+ ); + + const submittedEmailText = passwordRequested + ? {initialEmail} + : {submittedEmail}; + + const emailSubmittedContent = ( +
+ +

+ +

+

+ +

+
+

+ {recoveryInProgress + ? + : } +

+

+ +

+
+
+ ); + + const initialEmailText = {initialEmail}; + const emailNotVerifiedContent = ( +
+ +

+ +

+

+ +

+

+ +

+
+ ); + + const genericErrorContent = ( +
+ +

+ +

+

+ +

+
+ ); + + let content; + if (recoveryError && recoveryError.status === 409) { + content = emailNotVerifiedContent; + } else if (recoveryError && recoveryError.status === 404) { + content = submitEmailContent; + } else if (recoveryError) { + content = genericErrorContent; + } else if (submittedEmail || passwordRequested) { + content = emailSubmittedContent; + } else { + content = submitEmailContent; + } + + return ( + + +
+ {content} +
+ +
+ ); +}; + +PasswordRecoveryPageComponent.defaultProps = { + authInfoError: null, + currentUser: null, + currentUserHasOrders: null, + logoutError: null, + notificationCount: 0, + sendVerificationEmailError: null, + initialEmail: null, + submittedEmail: null, + recoveryError: null, +}; + +const { bool, func, instanceOf, number, object, shape, string } = PropTypes; + +PasswordRecoveryPageComponent.propTypes = { + authInfoError: instanceOf(Error), + authInProgress: bool.isRequired, + currentUser: propTypes.currentUser, + currentUserHasListings: bool.isRequired, + currentUserHasOrders: bool, + isAuthenticated: bool.isRequired, + logoutError: instanceOf(Error), + notificationCount: number, + onLogout: func.isRequired, + onManageDisableScrolling: func.isRequired, + sendVerificationEmailInProgress: bool.isRequired, + sendVerificationEmailError: instanceOf(Error), + onResendVerificationEmail: func.isRequired, + initialEmail: string, + submittedEmail: string, + recoveryError: instanceOf(Error), + recoveryInProgress: bool.isRequired, + passwordRequested: bool.isRequired, + onChange: func.isRequired, + onSubmitEmail: func.isRequired, + onRetypeEmail: func.isRequired, + + // from injectIntl + intl: intlShape.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, + currentUserHasOrders, + currentUserNotificationCount: notificationCount, + sendVerificationEmailInProgress, + sendVerificationEmailError, + } = state.user; + + const { + initialEmail, + submittedEmail, + recoveryError, + recoveryInProgress, + passwordRequested, + } = state.PasswordRecoveryPage; + return { + authInfoError, + authInProgress: authenticationInProgress(state), + currentUser, + currentUserHasListings, + currentUserHasOrders, + currentUserNotificationCount: notificationCount, + isAuthenticated, + logoutError, + scrollingDisabled: isScrollingDisabled(state), + sendVerificationEmailInProgress, + sendVerificationEmailError, + initialEmail, + submittedEmail, + recoveryError, + recoveryInProgress, + passwordRequested, + }; +}; + +const mapDispatchToProps = dispatch => ({ + onLogout: historyPush => dispatch(logout(historyPush)), + onManageDisableScrolling: (componentId, disableScrolling) => + dispatch(manageDisableScrolling(componentId, disableScrolling)), + onResendVerificationEmail: () => dispatch(sendVerificationEmail()), + onChange: () => dispatch(clearPasswordRecoveryError()), + onSubmitEmail: email => dispatch(recoverPassword(email)), + onRetypeEmail: () => dispatch(retypePasswordRecoveryEmail()), +}); + +const PasswordRecoveryPage = compose( + connect(mapStateToProps, mapDispatchToProps), + withRouter, + injectIntl +)(PasswordRecoveryPageComponent); + +export default PasswordRecoveryPage; diff --git a/src/containers/PasswordForgottenPage/PasswordForgottenPage.test.js b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js similarity index 67% rename from src/containers/PasswordForgottenPage/PasswordForgottenPage.test.js rename to src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js index dc2badb0..efe43fde 100644 --- a/src/containers/PasswordForgottenPage/PasswordForgottenPage.test.js +++ b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js @@ -1,13 +1,14 @@ import React from 'react'; import { renderShallow } from '../../util/test-helpers'; -import { PasswordForgottenPageComponent } from './PasswordForgottenPage'; +import { fakeIntl } from '../../util/test-data'; +import { PasswordRecoveryPageComponent } from './PasswordRecoveryPage'; const noop = () => null; describe('ContactDetailsPage', () => { it('matches snapshot', () => { const tree = renderShallow( - { onManageDisableScrolling={noop} sendVerificationEmailInProgress={false} onResendVerificationEmail={noop} + recoveryInProgress={false} + passwordRequested={false} + onChange={noop} + onSubmitEmail={noop} + onRetypeEmail={noop} + intl={fakeIntl} /> ); expect(tree).toMatchSnapshot(); diff --git a/src/containers/PasswordForgottenPage/__snapshots__/PasswordForgottenPage.test.js.snap b/src/containers/PasswordRecoveryPage/__snapshots__/PasswordRecoveryPage.test.js.snap similarity index 52% rename from src/containers/PasswordForgottenPage/__snapshots__/PasswordForgottenPage.test.js.snap rename to src/containers/PasswordRecoveryPage/__snapshots__/PasswordRecoveryPage.test.js.snap index 12e085e7..1f0e0c37 100644 --- a/src/containers/PasswordForgottenPage/__snapshots__/PasswordForgottenPage.test.js.snap +++ b/src/containers/PasswordRecoveryPage/__snapshots__/PasswordRecoveryPage.test.js.snap @@ -2,7 +2,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = ` + title="PasswordRecoveryPage.title"> - +
+
+ +

+ +

+

+ +

+ +
+
`; diff --git a/src/containers/index.js b/src/containers/index.js index 2ede665f..e12a506e 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -18,10 +18,10 @@ import LoginForm from './LoginForm/LoginForm'; import ManageListingsPage from './ManageListingsPage/ManageListingsPage'; import NotFoundPage from './NotFoundPage/NotFoundPage'; import OrderPage from './OrderPage/OrderPage'; -import PasswordForgottenForm from './PasswordForgottenForm/PasswordForgottenForm'; -import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage'; import PasswordResetForm from './PasswordResetForm/PasswordResetForm'; import PasswordResetPage from './PasswordResetPage/PasswordResetPage'; +import PasswordRecoveryForm from './PasswordRecoveryForm/PasswordRecoveryForm'; +import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage'; import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm'; import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage'; import ProfilePage from './ProfilePage/ProfilePage'; @@ -56,10 +56,10 @@ export { ManageListingsPage, NotFoundPage, OrderPage, - PasswordForgottenForm, - PasswordForgottenPage, PasswordResetForm, PasswordResetPage, + PasswordRecoveryForm, + PasswordRecoveryPage, PayoutDetailsForm, PayoutPreferencesPage, ProfilePage, diff --git a/src/containers/reducers.js b/src/containers/reducers.js index 40fe13fa..614d4b8a 100644 --- a/src/containers/reducers.js +++ b/src/containers/reducers.js @@ -13,6 +13,7 @@ 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 PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.duck'; export { CheckoutPage, @@ -25,4 +26,5 @@ export { ProfileSettingsPage, SalePage, SearchPage, + PasswordRecoveryPage, }; diff --git a/src/examples.js b/src/examples.js index 5425c9cd..50727c19 100644 --- a/src/examples.js +++ b/src/examples.js @@ -42,9 +42,9 @@ import * as EditListingPhotosForm import * as EditListingPricingForm from './containers/EditListingPricingForm/EditListingPricingForm.example'; 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 PasswordRecoveryForm + from './containers/PasswordRecoveryForm/PasswordRecoveryForm.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'; @@ -78,8 +78,8 @@ export { ModalInMobile, NamedLink, PaginationLinks, - PasswordForgottenForm, PasswordResetForm, + PasswordRecoveryForm, PayoutDetailsForm, ResponsiveImage, SelectField, diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js index 4cc74989..d1056a52 100644 --- a/src/routesConfiguration.js +++ b/src/routesConfiguration.js @@ -11,8 +11,8 @@ import { ManageListingsPage, NotFoundPage, OrderPage, - PasswordForgottenPage, PasswordResetPage, + PasswordRecoveryPage, PayoutPreferencesPage, ProfilePage, ProfileSettingsPage, @@ -145,16 +145,10 @@ const routesConfiguration = [ component: props => , }, { - path: '/password', + path: '/recover-password', exact: true, - name: 'PasswordPage', - component: props => , - }, - { - path: '/password/forgotten', - exact: true, - name: 'PasswordForgottenPage', - component: props => , + name: 'PasswordRecoveryPage', + component: props => , }, { path: '/inbox', diff --git a/src/translations/en.json b/src/translations/en.json index 01047086..44e97b0f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -166,7 +166,9 @@ "LoginForm.emailLabel": "Email", "LoginForm.emailPlaceholder": "john.doe@example.com", "LoginForm.emailRequired": "This field is required", + "LoginForm.forgotPassword": "Forgot your password?", "LoginForm.logIn": "Log in", + "LoginForm.forgotPasswordHelp": "No problem.", "LoginForm.passwordLabel": "Password", "LoginForm.passwordPlaceholder": "Enter your password…", "LoginForm.passwordRequired": "This field is required", @@ -223,6 +225,28 @@ "PasswordResetPage.passwordChangedHelpText": "Your password has been changed successfully.", "PasswordResetPage.resetFailed": "Reset failed. Please try again.", "PasswordResetPage.title": "Reset password", + "PasswordRecoveryForm.emailLabel": "Email", + "PasswordRecoveryForm.emailNotFound": "Hmm. We didn't find an account with that email. Please double-check your email and try again.", + "PasswordRecoveryForm.emailPlaceholder": "john.doe@example.com", + "PasswordRecoveryForm.emailRequired": "This field is required", + "PasswordRecoveryForm.loginLinkText": "Go log in.", + "PasswordRecoveryForm.loginLinkInfo": "Suddenly remembered your password? {loginLink}", + "PasswordRecoveryForm.sendInstructions": "Send instructions", + "PasswordRecoveryPage.actionFailedTitle": "Whoops!", + "PasswordRecoveryPage.actionFailedMessage": "Something went wrong. Please refresh the page and try again.", + "PasswordRecoveryPage.emailNotVerifiedTitle": "We have bad news", + "PasswordRecoveryPage.emailNotVerifiedMessage": "Unfortunately, we cannot recover your password, because your email {initialEmailText} hasn't been verified.", + "PasswordRecoveryPage.emailNotVerifiedContactAdmin": "To resolve the issue, please contact Saunatime administrators.", + "PasswordRecoveryPage.emailSubmittedTitle": "Check your inbox", + "PasswordRecoveryPage.emailSubmittedMessage": "We have sent the instructions for resetting your password to {submittedEmailText}.", + "PasswordRecoveryPage.fixEmailLinkText": "Fix it.", + "PasswordRecoveryPage.fixEmailInfo": "Whoops, typo in your email? {fixEmailLink}", + "PasswordRecoveryPage.forgotPasswordTitle": "Forgot your password?", + "PasswordRecoveryPage.forgotPasswordMessage": "No worries! Please enter the email address you used when signing up and we'll send you instructions on how to set a new password.", + "PasswordRecoveryPage.resendEmailLinkText": "Send another email.", + "PasswordRecoveryPage.resendEmailInfo": "Didn't get the email? {resendEmailLink}", + "PasswordRecoveryPage.resendingEmailInfo": "Resending instructions...", + "PasswordRecoveryPage.title": "Request a new password", "PayoutDetailsForm.addressTitle": "Address", "PayoutDetailsForm.bankDetails": "Bank details", "PayoutDetailsForm.birthdayDatePlaceholder": "dd",