Merge pull request #433 from sharetribe/change-email-errors

Change email errors
This commit is contained in:
Vesa Luusua 2017-09-25 19:47:05 +03:00 committed by GitHub
commit 48ebdc2efb
12 changed files with 141 additions and 25 deletions

View file

@ -21,9 +21,6 @@
flex-direction: column;
width: 100%;
/* Hide Topbar's box-shadow when side nav is stacked below Topbar as a new bar */
z-index: calc(var(--zIndexTopbar));
@media (--viewportLarge) {
flex-direction: row;
justify-content: center;

View file

@ -154,7 +154,9 @@ class TopbarComponent extends Component {
const {
className,
rootClassName,
desktopClassName,
mobileRootClassName,
mobileClassName,
isAuthenticated,
authInProgress,
currentUser,
@ -233,7 +235,7 @@ class TopbarComponent extends Component {
return (
<div className={classes}>
<div className={classNames(mobileRootClassName || css.container)}>
<div className={classNames(mobileRootClassName || css.container, mobileClassName)}>
<Button rootClassName={css.menu} onClick={this.handleMobileMenuOpen}>
<MenuIcon
className={css.menuIcon}
@ -253,6 +255,7 @@ class TopbarComponent extends Component {
</div>
<div className={css.desktop}>
<TopbarDesktop
className={desktopClassName}
currentUserHasListings={currentUserHasListings}
currentUser={currentUser}
currentPage={currentPage}
@ -336,7 +339,9 @@ class TopbarComponent extends Component {
TopbarComponent.defaultProps = {
className: null,
rootClassName: null,
desktopClassName: null,
mobileRootClassName: null,
mobileClassName: null,
notificationCount: 0,
currentUser: null,
currentUserHasOrders: null,
@ -349,7 +354,9 @@ const { arrayOf, bool, func, instanceOf, number, shape, string } = PropTypes;
TopbarComponent.propTypes = {
className: string,
rootClassName: string,
desktopClassName: string,
mobileRootClassName: string,
mobileClassName: string,
isAuthenticated: bool.isRequired,
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,

View file

@ -32,8 +32,7 @@ const TopbarDesktop = props => {
initialSearchFormValues,
} = props;
const rootClass = rootClassName || css.root;
const classes = classNames(rootClass, className);
const classes = classNames(rootClassName || css.root, className);
const search = (
<TopbarSearchForm

View file

@ -78,12 +78,22 @@
line-height: 20px;
margin: 0;
cursor: pointer;
opacity: 1;
transition: opacity var(--transitionStyleButton);
@media (--viewportMedium) {
margin: 0;
}
}
.resendClicked {
opacity: 0;
}
.tooMany {
color: var(--matterColorAnti);
}
.confirmChangesTitle {
/* Font */
color: var(--matterColorAnti);

View file

@ -5,6 +5,10 @@ import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import classNames from 'classnames';
import * as validators from '../../util/validators';
import { ensureCurrentUser } from '../../util/data';
import {
isForbiddenChangeEmailError,
isTooManyEmailVerificationRequestsError,
} from '../../util/errors';
import { PrimaryButton, TextInputField } from '../../components';
import css from './ContactDetailsForm.css';
@ -13,6 +17,7 @@ const ContactDetailsFormComponent = props => {
const {
rootClassName,
className,
changeEmailError,
currentUser,
form,
handleSubmit,
@ -22,6 +27,9 @@ const ContactDetailsFormComponent = props => {
invalid,
onResendVerificationEmail,
pristine,
ready,
sendVerificationEmailError,
sendVerificationEmailInProgress,
} = props;
const user = ensureCurrentUser(currentUser);
@ -55,12 +63,22 @@ const ContactDetailsFormComponent = props => {
);
} else if (!emailVerified && !pendingEmail) {
// Current email is unverified. This is the email given in sign up form
/* eslint-disable jsx-a11y/no-static-element-interactions */
const resendEmailLink = (
<span className={css.helperLink} onClick={onResendVerificationEmail} role="button">
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>
const tooManyVerificationRequests = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
);
const resendClasses = classNames(css.helperLink, {
[css.resendClicked]: sendVerificationEmailInProgress,
});
/* eslint-disable jsx-a11y/no-static-element-interactions */
const resendEmailLink = tooManyVerificationRequests
? <span className={css.tooMany}>
<FormattedMessage id="ContactDetailsForm.tooManyVerificationRequests" />
</span>
: <span className={resendClasses} onClick={onResendVerificationEmail} role="button">
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>;
/* eslint-enable jsx-a11y/no-static-element-interactions */
emailVerifiedInfo = (
@ -74,16 +92,27 @@ const ContactDetailsFormComponent = props => {
const pendingEmailStyled = <span className={css.emailStyle}>{pendingEmail}</span>;
const pendingEmailCheckInbox = (
<span className={css.checkInbox}>
<FormattedMessage id="ContactDetailsForm.pendingEmailCheckInbox" values={{ pendingEmail: pendingEmailStyled}} />
<FormattedMessage
id="ContactDetailsForm.pendingEmailCheckInbox"
values={{ pendingEmail: pendingEmailStyled }}
/>
</span>
);
/* eslint-disable jsx-a11y/no-static-element-interactions */
const resendPendingEmailLink = (
<span className={css.helperLink} onClick={onResendVerificationEmail} role="button">
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>
const tooManyVerificationRequests = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
);
const resendClasses = classNames(css.helperLink, {
[css.resendClicked]: sendVerificationEmailInProgress,
});
/* eslint-disable jsx-a11y/no-static-element-interactions */
const resendPendingEmailLink = tooManyVerificationRequests
? <span className={css.tooMany}>
<FormattedMessage id="ContactDetailsForm.tooManyVerificationRequests" />
</span>
: <span className={resendClasses} onClick={onResendVerificationEmail} role="button">
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>;
/* eslint-enable jsx-a11y/no-static-element-interactions */
emailVerifiedInfo = (
@ -106,8 +135,16 @@ const ContactDetailsFormComponent = props => {
const passwordRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordRequired',
});
const passwordRequired = validators.required(passwordRequiredMessage);
const passwordFailedMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordFailed',
});
const passwordErrorText = isForbiddenChangeEmailError(changeEmailError)
? passwordFailedMessage
: null;
const classes = classNames(rootClassName || css.root, className);
const submitDisabled = invalid || submitting || inProgress;
@ -141,10 +178,17 @@ const ContactDetailsFormComponent = props => {
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordRequired}
customErrorText={passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
<PrimaryButton className={css.submitButton} type="submit" disabled={submitDisabled}>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="ContactDetailsForm.saveChanges" />
</PrimaryButton>
</div>
@ -155,18 +199,25 @@ const ContactDetailsFormComponent = props => {
ContactDetailsFormComponent.defaultProps = {
rootClassName: null,
className: null,
changeEmailError: null,
inProgress: false,
sendVerificationEmailError: null,
sendVerificationEmailInProgress: false,
};
const { bool, func, string } = PropTypes;
const { bool, func, instanceOf, string } = PropTypes;
ContactDetailsFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
changeEmailError: instanceOf(Error),
inProgress: bool,
intl: intlShape.isRequired,
onResendVerificationEmail: func.isRequired,
ready: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
sendVerificationEmailInProgress: bool,
};
const defaultFormName = 'ContactDetailsForm';

View file

@ -13,6 +13,11 @@
}
.desktopTopbar,
.mobileTopbar {
box-shadow: none;
}
.tabs {
display: flex;
flex-direction: row;

View file

@ -6,23 +6,34 @@ export const CHANGE_EMAIL_REQUEST = 'app/ContactDetailsPage/CHANGE_EMAIL_REQUEST
export const CHANGE_EMAIL_SUCCESS = 'app/ContactDetailsPage/CHANGE_EMAIL_SUCCESS';
export const CHANGE_EMAIL_ERROR = 'app/ContactDetailsPage/CHANGE_EMAIL_ERROR';
export const CHANGE_EMAIL_CLEAR = 'app/ContactDetailsPage/CHANGE_EMAIL_CLEAR';
// ================ Reducer ================ //
const initialState = {
changeEmailError: null,
changeEmailInProgress: false,
emailChanged: false,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case CHANGE_EMAIL_REQUEST:
return { ...state, changeEmailInProgress: true, changeEmailError: null };
return { ...state, changeEmailInProgress: true, changeEmailError: null, emailChanged: false };
case CHANGE_EMAIL_SUCCESS:
return { ...state, changeEmailInProgress: false };
return { ...state, changeEmailInProgress: false, emailChanged: true };
case CHANGE_EMAIL_ERROR:
return { ...state, changeEmailInProgress: false, changeEmailError: payload };
case CHANGE_EMAIL_CLEAR:
return {
...state,
changeEmailInProgress: false,
changeEmailError: null,
emailChanged: false,
};
default:
return state;
}
@ -38,6 +49,8 @@ export const changeEmailError = error => ({
error: true,
});
export const changeEmailClear = () => ({ type: CHANGE_EMAIL_CLEAR });
// ================ Thunks ================ //
export const changeEmail = params =>

View file

@ -20,21 +20,25 @@ import {
} from '../../components';
import { ContactDetailsForm } from '../../containers';
import { changeEmail } from './ContactDetailsPage.duck';
import { changeEmail, changeEmailClear } from './ContactDetailsPage.duck';
import css from './ContactDetailsPage.css';
export const ContactDetailsPageComponent = props => {
const {
authInfoError,
authInProgress,
changeEmailError,
changeEmailInProgress,
currentUser,
currentUserHasListings,
currentUserHasOrders,
emailChanged,
history,
isAuthenticated,
location,
logoutError,
notificationCount,
onChange,
onLogout,
onManageDisableScrolling,
sendVerificationEmailInProgress,
@ -66,10 +70,15 @@ export const ContactDetailsPageComponent = props => {
? <ContactDetailsForm
className={css.form}
initialValues={{ email }}
changeEmailError={changeEmailError}
currentUser={currentUser}
onResendVerificationEmail={onResendVerificationEmail}
onSubmit={onSubmitChangeEmail}
inProgress={authInProgress}
onChange={onChange}
inProgress={changeEmailInProgress}
ready={emailChanged}
sendVerificationEmailInProgress={sendVerificationEmailInProgress}
sendVerificationEmailError={sendVerificationEmailError}
/>
: null;
@ -83,9 +92,11 @@ export const ContactDetailsPageComponent = props => {
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
currentUserHasOrders={currentUserHasOrders}
desktopClassName={css.desktopTopbar}
history={history}
isAuthenticated={isAuthenticated}
location={location}
mobileClassName={css.mobileTopbar}
notificationCount={notificationCount}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
@ -113,6 +124,7 @@ export const ContactDetailsPageComponent = props => {
ContactDetailsPageComponent.defaultProps = {
authInfoError: null,
changeEmailError: null,
currentUser: null,
currentUserHasOrders: null,
logoutError: null,
@ -125,12 +137,16 @@ const { bool, func, instanceOf, number, object, shape } = PropTypes;
ContactDetailsPageComponent.propTypes = {
authInfoError: instanceOf(Error),
authInProgress: bool.isRequired,
changeEmailError: instanceOf(Error),
changeEmailInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
currentUserHasOrders: bool,
emailChanged: bool.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),
notificationCount: number,
onChange: func.isRequired,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
onSubmitChangeEmail: func.isRequired,
@ -157,12 +173,16 @@ const mapStateToProps = state => {
sendVerificationEmailInProgress,
sendVerificationEmailError,
} = state.user;
const { changeEmailError, changeEmailInProgress, emailChanged } = state.ContactDetailsPage;
return {
authInfoError,
authInProgress: authenticationInProgress(state),
changeEmailError,
changeEmailInProgress,
currentUser,
currentUserHasListings,
currentUserHasOrders,
emailChanged,
notificationCount,
isAuthenticated,
logoutError,
@ -173,6 +193,7 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
onChange: () => dispatch(changeEmailClear()),
onLogout: historyPush => dispatch(logout(historyPush)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),

View file

@ -15,11 +15,14 @@ describe('ContactDetailsPage', () => {
authInProgress={false}
currentUserHasListings={false}
isAuthenticated={false}
onChange={noop}
onLogout={noop}
onManageDisableScrolling={noop}
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
onSubmitChangeEmail={noop}
changeEmailInProgress={false}
emailChanged={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -4,6 +4,7 @@
* https://github.com/erikras/ducks-modular-redux
*/
import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage.duck';
import EditListingPage from './EditListingPage/EditListingPage.duck';
import InboxPage from './InboxPage/InboxPage.duck';
import ListingPage from './ListingPage/ListingPage.duck';
@ -17,6 +18,7 @@ import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.du
export {
CheckoutPage,
ContactDetailsPage,
EditListingPage,
InboxPage,
ListingPage,

View file

@ -51,16 +51,18 @@
"ContactDetailsForm.confirmChangesInfo": "To change your email, you need to enter your current password.",
"ContactDetailsForm.confirmChangesTitle": "Confirm your changes",
"ContactDetailsForm.emailLabel": "Your email",
"ContactDetailsForm.emailRequired": "This field is required",
"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.passwordLabel": "Current password",
"ContactDetailsForm.passwordPlaceholder": "Enter your current password…",
"ContactDetailsForm.passwordRequired": "This field is required",
"ContactDetailsForm.passwordRequired": "Password is required",
"ContactDetailsForm.pendingEmailCheckInbox": "Check your inbox to verify {pendingEmail} now.",
"ContactDetailsForm.pendingEmailUnverified": "You have changed your email address but havent verified the new address yet. {pendingEmailCheckInbox} {resendPendingEmailLink}",
"ContactDetailsForm.resendEmailVerificationText": "Resend verification email.",
"ContactDetailsForm.saveChanges": "Save changes",
"ContactDetailsForm.tooManyVerificationRequests": "Too many email verification requests sent.",
"ContactDetailsPage.emailTabTitle": "Email",
"ContactDetailsPage.passwordTabTitle": "Password",
"ContactDetailsPage.title": "Email settings",

View file

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