Merge pull request #434 from sharetribe/change-password

Change password
This commit is contained in:
Vesa Luusua 2017-09-25 23:08:49 +03:00 committed by GitHub
commit 2d6f52fc99
14 changed files with 419 additions and 10 deletions

View file

@ -34,7 +34,7 @@
"redux-thunk": "^2.2.0",
"sanitize.css": "^5.0.0",
"sharetribe-scripts": "0.9.2",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#e2fe4c3be061bfbefdb2db1fa5ed2a8a5d4d4ffe",
"source-map-support": "^0.4.15",
"url": "^0.11.0"
},

View file

@ -56,6 +56,6 @@
@media (--viewportMedium) {
margin-top: 2px;
margin-bottom: 31px;
margin-bottom: 47px;
}
}

View file

@ -0,0 +1,60 @@
@import '../../marketplace.css';
.root {}
.newPasswordSection {
margin-top: 16px;
margin-bottom: 46px;
padding-top: 6px;
@media (--viewportMedium) {
margin-bottom: 56px;
}
}
.confirmChangesSection {
flex-grow: 1;
display: flex;
flex-direction: column;
margin-bottom: 60px;
padding: 0;
@media (--viewportLarge) {
padding: 4px 0 0 0;
}
}
.confirmChangesTitle {
/* Font */
color: var(--matterColorAnti);
margin-top: 0;
margin-bottom: 13px;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 14px;
}
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 20px;
}
}
.confirmChangesInfo {
margin-top: 0;
margin-bottom: 37px;
@media (--viewportMedium) {
margin-bottom: 46px;
}
}
.bottomWrapper {
margin-top: 46px;
@media (--viewportMedium) {
margin-top: 96px;
}
}

View file

@ -0,0 +1,182 @@
import React, { Component, 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 * as validators from '../../util/validators';
import { ensureCurrentUser } from '../../util/data';
import { isForbiddenChangePasswordError } from '../../util/errors';
import { PrimaryButton, TextInputField } from '../../components';
import css from './PasswordChangeForm.css';
const RESET_TIMEOUT = 800;
class PasswordChangeFormComponent extends Component {
componentWillUnmount() {
window.clearTimeout(this.resetTimeoutId);
}
render() {
const {
rootClassName,
className,
changePasswordError,
currentUser,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
ready,
reset,
} = this.props;
const user = ensureCurrentUser(currentUser);
if (!user.id) {
return null;
}
// New password
const newPasswordLabel = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordLabel',
});
const newPasswordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordPlaceholder',
});
const newPasswordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordRequired',
});
const newPasswordRequired = validators.required(newPasswordRequiredMessage);
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordChangeForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordRequired',
});
const passwordRequired = validators.required(passwordRequiredMessage);
const passwordFailedMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordFailed',
});
const passwordErrorText = isForbiddenChangePasswordError(changePasswordError)
? passwordFailedMessage
: null;
const classes = classNames(rootClassName || css.root, className);
const submitDisabled = invalid || submitting || inProgress;
return (
<form
className={classes}
onSubmit={values => {
handleSubmit(values).then(() => {
this.resetTimeoutId = window.setTimeout(reset, RESET_TIMEOUT);
});
}}
>
<div className={css.newPasswordSection}>
<TextInputField
type="password"
name="newPassword"
id={`${form}.newPassword`}
label={newPasswordLabel}
placeholder={newPasswordPlaceholder}
validate={[newPasswordRequired, passwordMinLength, passwordMaxLength]}
/>
</div>
<div className={css.confirmChangesSection}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="PasswordChangeForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="PasswordChangeForm.confirmChangesInfo" />
</p>
<TextInputField
className={css.password}
type="password"
name="currentPassword"
id={`${form}.currentPassword`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={[passwordRequired, passwordMinLength, passwordMaxLength]}
customErrorText={passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="PasswordChangeForm.saveChanges" />
</PrimaryButton>
</div>
</form>
);
}
}
PasswordChangeFormComponent.defaultProps = {
rootClassName: null,
className: null,
changePasswordError: null,
inProgress: false,
};
const { bool, instanceOf, string } = PropTypes;
PasswordChangeFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
changePasswordError: instanceOf(Error),
inProgress: bool,
intl: intlShape.isRequired,
ready: bool.isRequired,
};
const defaultFormName = 'PasswordChangeForm';
const PasswordChangeForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
PasswordChangeFormComponent
);
export default PasswordChangeForm;

View file

@ -1,5 +1,23 @@
@import '../../marketplace.css';
.content {
@media (--viewportMedium) {
margin: 32px auto 0 auto;
max-width: 564px;
}
@media (--viewportLarge) {
margin: 0;
}
}
.desktopTopbar,
.mobileTopbar {
box-shadow: none;
}
.tabs {
display: flex;
flex-direction: row;
@ -13,7 +31,7 @@
@media (--viewportLarge) {
min-height: auto;
flex-direction: column;
margin-top: 4px;
margin-top: 28px;
}
}
@ -31,3 +49,13 @@
margin-left: 0;
}
}
.title {
margin-top: 4px;
margin-bottom: 19px;
@media (--viewportMedium) {
margin-top: 2px;
margin-bottom: 47px;
}
}

View file

@ -0,0 +1,66 @@
// ================ Action types ================ //
export const CHANGE_PASSWORD_REQUEST = 'app/PasswordChangePage/CHANGE_PASSWORD_REQUEST';
export const CHANGE_PASSWORD_SUCCESS = 'app/PasswordChangePage/CHANGE_PASSWORD_SUCCESS';
export const CHANGE_PASSWORD_ERROR = 'app/PasswordChangePage/CHANGE_PASSWORD_ERROR';
export const CHANGE_PASSWORD_CLEAR = 'app/PasswordChangePage/CHANGE_PASSWORD_CLEAR';
// ================ Reducer ================ //
const initialState = {
changePasswordError: null,
changePasswordInProgress: false,
passwordChanged: false,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case CHANGE_PASSWORD_REQUEST:
return {
...state,
changePasswordInProgress: true,
changePasswordError: null,
passwordChanged: false,
};
case CHANGE_PASSWORD_SUCCESS:
return { ...state, changePasswordInProgress: false, passwordChanged: true };
case CHANGE_PASSWORD_ERROR:
return { ...state, changePasswordInProgress: false, changePasswordError: payload };
case CHANGE_PASSWORD_CLEAR:
return { ...initialState };
default:
return state;
}
}
// ================ Action creators ================ //
export const changePasswordRequest = () => ({ type: CHANGE_PASSWORD_REQUEST });
export const changePasswordSuccess = () => ({ type: CHANGE_PASSWORD_SUCCESS });
export const changePasswordError = error => ({
type: CHANGE_PASSWORD_ERROR,
payload: error,
error: true,
});
export const changePasswordClear = () => ({ type: CHANGE_PASSWORD_CLEAR });
// ================ Thunks ================ //
export const changePassword = params =>
(dispatch, getState, sdk) => {
dispatch(changePasswordRequest());
const { newPassword, currentPassword } = params;
return sdk.currentUser
.changePassword({ newPassword, currentPassword })
.then(() => dispatch(changePasswordSuccess()))
.catch(e => {
dispatch(changePasswordError(e));
throw e;
});
};

View file

@ -17,13 +17,17 @@ import {
TopbarWrapper,
UserNav,
} from '../../components';
import { PasswordChangeForm } from '../../containers';
import { changePassword, changePasswordClear } from './PasswordChangePage.duck';
import css from './PasswordChangePage.css';
export const PasswordChangePageComponent = props => {
const {
authInfoError,
authInProgress,
changePasswordError,
changePasswordInProgress,
currentUser,
currentUserHasListings,
currentUserHasOrders,
@ -32,8 +36,11 @@ export const PasswordChangePageComponent = props => {
location,
logoutError,
notificationCount,
onChange,
onLogout,
onManageDisableScrolling,
onSubmitChangePassword,
passwordChanged,
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
@ -56,6 +63,18 @@ export const PasswordChangePageComponent = props => {
},
];
const changePasswordForm = currentUser && currentUser.id
? <PasswordChangeForm
className={css.form}
changePasswordError={changePasswordError}
currentUser={currentUser}
onSubmit={onSubmitChangePassword}
onChange={onChange}
inProgress={changePasswordInProgress}
ready={passwordChanged}
/>
: null;
return (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Contact details">
<LayoutSideNavigation>
@ -82,7 +101,12 @@ export const PasswordChangePageComponent = props => {
<TabNav rootClassName={css.tabs} tabRootClassName={css.tab} tabs={tabs} />
</SideNavWrapper>
<ContentWrapper>
Main content
<div className={css.content}>
<h1 className={css.title}>
<FormattedMessage id="PasswordChangePage.title" />
</h1>
{changePasswordForm}
</div>
</ContentWrapper>
</LayoutSideNavigation>
</PageLayout>
@ -91,6 +115,7 @@ export const PasswordChangePageComponent = props => {
PasswordChangePageComponent.defaultProps = {
authInfoError: null,
changePasswordError: null,
currentUser: null,
currentUserHasOrders: null,
logoutError: null,
@ -103,14 +128,19 @@ const { bool, func, instanceOf, number, object, shape } = PropTypes;
PasswordChangePageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
changePasswordError: instanceOf(Error),
changePasswordInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
currentUserHasOrders: bool,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
notificationCount: number,
onChange: func.isRequired,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
onSubmitChangePassword: func.isRequired,
passwordChanged: bool.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
onResendVerificationEmail: func.isRequired,
@ -126,6 +156,11 @@ const mapStateToProps = state => {
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
// Topbar needs user info.
const {
changePasswordError,
changePasswordInProgress,
passwordChanged,
} = state.PasswordChangePage;
const {
currentUser,
currentUserHasListings,
@ -137,12 +172,15 @@ const mapStateToProps = state => {
return {
authInfoError,
authInProgress: authenticationInProgress(state),
changePasswordError,
changePasswordInProgress,
currentUser,
currentUserHasListings,
currentUserHasOrders,
notificationCount,
isAuthenticated,
logoutError,
passwordChanged,
scrollingDisabled: isScrollingDisabled(state),
sendVerificationEmailInProgress,
sendVerificationEmailError,
@ -150,10 +188,12 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
onChange: () => dispatch(changePasswordClear()),
onLogout: historyPush => dispatch(logout(historyPush)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
onSubmitChangePassword: values => dispatch(changePassword(values)),
});
const PasswordChangePage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(

View file

@ -15,10 +15,14 @@ describe('PasswordChangePage', () => {
authInProgress={false}
currentUserHasListings={false}
isAuthenticated={false}
onChange={noop}
onLogout={noop}
onManageDisableScrolling={noop}
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
onSubmitChangePassword={noop}
changePasswordInProgress={false}
passwordChanged={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -72,7 +72,13 @@ exports[`PasswordChangePage matches snapshot 1`] = `
<ContentWrapper
className={null}
rootClassName={null}>
Main content
<div>
<h1>
<FormattedMessage
id="PasswordChangePage.title"
values={Object {}} />
</h1>
</div>
</ContentWrapper>
</LayoutSideNavigation>
</withRouter(PageLayout)>

View file

@ -19,6 +19,7 @@ import LoginForm from './LoginForm/LoginForm';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
import NotFoundPage from './NotFoundPage/NotFoundPage';
import OrderPage from './OrderPage/OrderPage';
import PasswordChangeForm from './PasswordChangeForm/PasswordChangeForm';
import PasswordChangePage from './PasswordChangePage/PasswordChangePage';
import PasswordRecoveryForm from './PasswordRecoveryForm/PasswordRecoveryForm';
import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage';
@ -59,6 +60,7 @@ export {
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangeForm,
PasswordChangePage,
PasswordRecoveryForm,
PasswordRecoveryPage,

View file

@ -10,11 +10,12 @@ 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 PasswordChangePage from './PasswordChangePage/PasswordChangePage.duck';
import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.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 PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.duck';
export {
CheckoutPage,
@ -24,9 +25,10 @@ export {
ListingPage,
ManageListingsPage,
OrderPage,
PasswordChangePage,
PasswordRecoveryPage,
PasswordResetPage,
ProfileSettingsPage,
SalePage,
SearchPage,
PasswordRecoveryPage,
};

View file

@ -54,7 +54,7 @@
"ContactDetailsForm.emailRequired": "Email is required",
"ContactDetailsForm.emailUnverified": "You haven't verified your email yet. {resendEmailLink}",
"ContactDetailsForm.emailVerified": "Your email is verified.",
"ContactDetailsForm.passwordFailed": "You entered wrong password",
"ContactDetailsForm.passwordFailed": "Please double-check your password",
"ContactDetailsForm.passwordLabel": "Current password",
"ContactDetailsForm.passwordPlaceholder": "Enter your current password…",
"ContactDetailsForm.passwordRequired": "Password is required",
@ -240,8 +240,21 @@
"PaginationLinks.next": "Next page",
"PaginationLinks.previous": "Previous page",
"PaginationLinks.toPage": "Go to page {page}",
"PasswordChangeForm.confirmChangesInfo": "To change your password, you need to enter your current password.",
"PasswordChangeForm.confirmChangesTitle": "Confirm your changes",
"PasswordChangeForm.newPasswordLabel": "New password",
"PasswordChangeForm.newPasswordPlaceholder": "Enter your new password",
"PasswordChangeForm.newPasswordRequired": "New password is required",
"PasswordChangeForm.passwordFailed": "Please double-check your current password",
"PasswordChangeForm.passwordLabel": "Current password",
"PasswordChangeForm.passwordPlaceholder": "Enter your current password…",
"PasswordChangeForm.passwordRequired": "Current password is required",
"PasswordChangeForm.passwordTooLong": "Password should be at most {maxLength} characters",
"PasswordChangeForm.passwordTooShort": "Password should be at least {minLength} characters",
"PasswordChangeForm.saveChanges": "Save changes",
"PasswordChangePage.emailTabTitle": "Email",
"PasswordChangePage.passwordTabTitle": "Password",
"PasswordChangePage.title": "Password settings",
"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",

View file

@ -94,3 +94,9 @@ export const isTransactionInitiateListingNotFoundError = apiError => {
* is due to giving wrong password.
*/
export const isForbiddenChangeEmailError = apiError => apiError && apiError.status === 403;
/**
* Check if the given API error (from `sdk.currentUser.changePassword(params)`)
* is due to giving wrong password.
*/
export const isForbiddenChangePasswordError = apiError => apiError && apiError.status === 403;

View file

@ -6128,9 +6128,9 @@ sharetribe-scripts@0.9.2:
optionalDependencies:
fsevents "1.0.17"
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab":
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#e2fe4c3be061bfbefdb2db1fa5ed2a8a5d4d4ffe":
version "0.0.1"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#e2fe4c3be061bfbefdb2db1fa5ed2a8a5d4d4ffe"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"