Merge pull request #382 from sharetribe/request-new-password

Request new password
This commit is contained in:
Hannu Lyytikäinen 2017-09-15 11:02:00 +03:00 committed by GitHub
commit fdc86022d7
24 changed files with 814 additions and 228 deletions

View file

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

View file

@ -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 {
<div className={classes}>
{label ? <label htmlFor={id}>{label}</label> : null}
{isTextarea ? <ExpandingTextarea {...inputProps} /> : <input {...inputProps} />}
<ValidationError fieldMeta={meta} />
<ValidationError fieldMeta={fieldMeta} />
</div>
);
}
@ -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,

View file

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

View file

@ -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}
/>
</div>
<p className={css.bottomWrapper}>
<NamedLink name="PasswordRecoveryPage" className={css.recoveryLink}>
<FormattedMessage id="LoginForm.forgotPassword" />
{' '}
<span className={css.recoveryLinkHelp}>
<FormattedMessage id="LoginForm.forgotPasswordHelp" />
</span>
</NamedLink>
</p>
<Button className={css.button} type="submit" disabled={submitDisabled}>
<FormattedMessage id="LoginForm.logIn" />
</Button>

View file

@ -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(<LoginForm />);
const flattenedRoutes = flattenRoutes(routesConfiguration);
const tree = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<LoginForm />
</RoutesProvider>
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -42,6 +42,26 @@ exports[`LoginForm matches snapshot 1`] = `
value="" />
</div>
</div>
<p
className={undefined}>
<a
className=""
href="/recover-password"
onClick={[Function]}
style={Object {}}
title={null}>
<span>
Forgot your password?
</span>
<span
className={undefined}>
<span>
No problem.
</span>
</span>
</a>
</p>
<button
className=""
disabled={true}

View file

@ -1,21 +0,0 @@
import React from 'react';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Button } from '../../components';
const PasswordForgottenForm = props => {
const { handleSubmit, pristine, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<Field name="email" component="input" type="email" />
<p>We will send you instructions to your email.</p>
<Button type="submit" disabled={pristine || submitting}>Send</Button>
</form>
);
};
PasswordForgottenForm.propTypes = { ...formPropTypes };
const defaultFormName = 'PasswordForgottenForm';
export default reduxForm({ form: defaultFormName })(PasswordForgottenForm);

View file

@ -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(<PasswordForgottenForm />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,27 +0,0 @@
exports[`PasswordForgottenForm matches snapshot 1`] = `
<form
onSubmit={[Function]}>
<label
htmlFor="email">
Email
</label>
<input
name="email"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
type="email"
value="" />
<p>
We will send you instructions to your email.
</p>
<button
className=""
disabled={true}
type="submit">
Send
</button>
</form>
`;

View file

@ -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 (
<PageLayout
authInfoError={authInfoError}
logoutError={logoutError}
title="Request new password"
>
<Topbar
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
currentUserHasOrders={currentUserHasOrders}
history={history}
isAuthenticated={isAuthenticated}
location={location}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
onResendVerificationEmail={onResendVerificationEmail}
sendVerificationEmailInProgress={sendVerificationEmailInProgress}
sendVerificationEmailError={sendVerificationEmailError}
/>
<PasswordForgottenForm onSubmit={sendPasswordResetEmail} />
</PageLayout>
);
};
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;

View file

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

View file

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

View file

@ -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 = (
<NamedLink name="LoginPage" className={css.loginLink}>
<FormattedMessage id="PasswordRecoveryForm.loginLinkText" />
</NamedLink>
);
return (
<form className={classes} onSubmit={handleSubmit}>
<TextInputField
className={css.email}
type="email"
name="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={emailRequired}
customErrorText={customErrorText}
/>
<div className={css.bottomWrapper}>
<p className={css.bottomText}>
<FormattedMessage id="PasswordRecoveryForm.loginLinkInfo" values={{ loginLink }} />
</p>
<PrimaryButton className={css.submitButton} type="submit" disabled={buttonDisabled}>
<FormattedMessage id="PasswordRecoveryForm.sendInstructions" />
</PrimaryButton>
</div>
</form>
);
};
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;

View file

@ -0,0 +1,53 @@
import React, { PropTypes } from 'react';
const DoorIcon = props => {
const { className } = props;
return (
<svg
className={className}
width="52"
height="59"
viewBox="0 0 52 59"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth="2.5" fill="none" fillRule="evenodd" strokeLinejoin="round">
<path
stroke="#C0392B"
fill="#C0392B"
strokeLinecap="round"
d="M28.182 42.364H50V26H28.182z"
/>
<path d="M39.09 38v-4.364" stroke="#FFF" strokeLinecap="round" />
<path
d="M31.455 26v-2.455c0-4.067 3.176-7.363 7.09-7.363 3.915 0 7.09 3.296 7.09 7.363V26"
stroke="#C0392B"
strokeLinecap="round"
/>
<path
d="M40.182 32.545c0 .603-.49 1.09-1.09 1.09-.603 0-1.092-.487-1.092-1.09 0-.602.49-1.09 1.09-1.09.603 0 1.092.488 1.092 1.09z"
stroke="#FFF"
/>
<path
stroke="#C0392B"
strokeLinecap="round"
d="M23.818 7.455h19.637V14M43.455 47.818v4.364H23.818M2 52.798l21.818 4.838V2L2 6.838z"
/>
<path
d="M19.455 30.91c0 2.107-1.71 3.817-3.82 3.817-2.106 0-3.817-1.71-3.817-3.818 0-2.11 1.71-3.82 3.818-3.82s3.82 1.71 3.82 3.82z"
stroke="#C0392B"
strokeLinecap="round"
/>
</g>
</svg>
);
};
DoorIcon.defaultProps = { className: null };
const { string } = PropTypes;
DoorIcon.propTypes = {
className: string,
};
export default DoorIcon;

View file

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

View file

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

View file

@ -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 = (
<InlineTextButton className={css.bottomLink} onClick={() => onSubmitEmail(submittedEmail)}>
<FormattedMessage id="PasswordRecoveryPage.resendEmailLinkText" />
</InlineTextButton>
);
const fixEmailLink = (
<InlineTextButton className={css.bottomLink} onClick={onRetypeEmail}>
<FormattedMessage id="PasswordRecoveryPage.fixEmailLinkText" />
</InlineTextButton>
);
const submitEmailContent = (
<div className={css.submitEmailContent}>
<KeysIcon />
<h1 className={css.title}>
<FormattedMessage id="PasswordRecoveryPage.forgotPasswordTitle" />
</h1>
<p className={css.message}>
<FormattedMessage id="PasswordRecoveryPage.forgotPasswordMessage" />
</p>
<PasswordRecoveryForm
onChange={onChange}
onSubmit={values => onSubmitEmail(values.email)}
initialValues={{ email: initialEmail }}
recoveryError={recoveryError}
/>
</div>
);
const submittedEmailText = passwordRequested
? <span className={css.submittedEmail}>{initialEmail}</span>
: <span className={css.submittedEmail}>{submittedEmail}</span>;
const emailSubmittedContent = (
<div className={css.emailSubmittedContent}>
<KeysIcon />
<h1 className={css.title}>
<FormattedMessage id="PasswordRecoveryPage.emailSubmittedTitle" />
</h1>
<p className={css.message}>
<FormattedMessage
id="PasswordRecoveryPage.emailSubmittedMessage"
values={{ submittedEmailText }}
/>
</p>
<div className={css.bottomWrapper}>
<p className={css.bottomInfo}>
{recoveryInProgress
? <FormattedMessage id="PasswordRecoveryPage.resendingEmailInfo" />
: <FormattedMessage
id="PasswordRecoveryPage.resendEmailInfo"
values={{ resendEmailLink }}
/>}
</p>
<p className={css.bottomInfo}>
<FormattedMessage id="PasswordRecoveryPage.fixEmailInfo" values={{ fixEmailLink }} />
</p>
</div>
</div>
);
const initialEmailText = <span className={css.submittedEmail}>{initialEmail}</span>;
const emailNotVerifiedContent = (
<div className={css.emailNotVerifiedContent}>
<DoorIcon />
<h1 className={css.title}>
<FormattedMessage id="PasswordRecoveryPage.emailNotVerifiedTitle" />
</h1>
<p className={css.message}>
<FormattedMessage
id="PasswordRecoveryPage.emailNotVerifiedMessage"
values={{ initialEmailText }}
/>
</p>
<p className={css.emailNotVerifiedBottomText}>
<FormattedMessage id="PasswordRecoveryPage.emailNotVerifiedContactAdmin" />
</p>
</div>
);
const genericErrorContent = (
<div className={css.genericErrorContent}>
<KeysIcon />
<h1 className={css.title}>
<FormattedMessage id="PasswordRecoveryPage.actionFailedTitle" />
</h1>
<p>
<FormattedMessage id="PasswordRecoveryPage.actionFailedMessage" />
</p>
</div>
);
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 (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title={title}>
<Topbar
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
currentUserHasOrders={currentUserHasOrders}
history={history}
isAuthenticated={isAuthenticated}
location={location}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
onResendVerificationEmail={onResendVerificationEmail}
sendVerificationEmailInProgress={sendVerificationEmailInProgress}
sendVerificationEmailError={sendVerificationEmailError}
/>
<div className={css.root}>
{content}
</div>
</PageLayout>
);
};
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;

View file

@ -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(
<PasswordForgottenPageComponent
<PasswordRecoveryPageComponent
params={{ displayName: 'my-shop' }}
history={{ push: noop }}
location={{ search: '' }}
@ -19,6 +20,12 @@ describe('ContactDetailsPage', () => {
onManageDisableScrolling={noop}
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
recoveryInProgress={false}
passwordRequested={false}
onChange={noop}
onSubmitEmail={noop}
onRetypeEmail={noop}
intl={fakeIntl}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -2,7 +2,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
<withRouter(PageLayout)
authInfoError={null}
logoutError={null}
title="Request new password">
title="PasswordRecoveryPage.title">
<Topbar
authInProgress={false}
currentUser={null}
@ -25,7 +25,30 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
onResendVerificationEmail={[Function]}
sendVerificationEmailError={null}
sendVerificationEmailInProgress={false} />
<ReduxForm
onSubmit={[Function]} />
<div>
<div>
<KeysIcon
className={null} />
<h1>
<FormattedMessage
id="PasswordRecoveryPage.forgotPasswordTitle"
values={Object {}} />
</h1>
<p>
<FormattedMessage
id="PasswordRecoveryPage.forgotPasswordMessage"
values={Object {}} />
</p>
<ReduxForm
initialValues={
Object {
"email": null,
}
}
onChange={[Function]}
onSubmit={[Function]}
recoveryError={null} />
</div>
</div>
</withRouter(PageLayout)>
`;

View file

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

View file

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

View file

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

View file

@ -11,8 +11,8 @@ import {
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordForgottenPage,
PasswordResetPage,
PasswordRecoveryPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
@ -145,16 +145,10 @@ const routesConfiguration = [
component: props => <AuthenticationPage {...props} tab="signup" />,
},
{
path: '/password',
path: '/recover-password',
exact: true,
name: 'PasswordPage',
component: props => <PasswordForgottenPage {...props} />,
},
{
path: '/password/forgotten',
exact: true,
name: 'PasswordForgottenPage',
component: props => <PasswordForgottenPage {...props} />,
name: 'PasswordRecoveryPage',
component: props => <PasswordRecoveryPage {...props} />,
},
{
path: '/inbox',

View file

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