Merge pull request #430 from sharetribe/contactdetailspage

Contactdetailspage
This commit is contained in:
Vesa Luusua 2017-09-25 15:56:35 +03:00 committed by GitHub
commit cf1937c203
15 changed files with 447 additions and 13 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#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab",
"source-map-support": "^0.4.15",
"url": "^0.11.0"
},

View file

@ -36,10 +36,11 @@
.contentWrapper {
/* Expand to the full remaining width of the viewport */
flex: 1;
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 24px;
@media (--viewportLarge) {
padding: 118px 15vw 82px 82px;

View file

@ -0,0 +1,120 @@
@import '../../marketplace.css';
:root {
--infoTextStyles: {
@apply --marketplaceH4FontStyles;
color: var(--successColor);
display: inline-block;
padding-left: 18px;
background-repeat: no-repeat;
background-position: top 7px left;
margin-top: 11px;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 14px;
margin-bottom: 1px;
}
}
}
.root {}
.emailSection {
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;
}
}
.emailVerified {
@apply --infoTextStyles;
background-image: url('data:image/svg+xml;utf8,<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M6.52 9.098c-.163.188-.397.3-.646.31h-.032c-.238 0-.466-.094-.635-.263L2.783 6.732c-.353-.35-.354-.92-.003-1.273.35-.353.92-.354 1.272-.004L5.794 7.19l4.59-5.278C9.287.738 7.73 0 6 0 2.686 0 0 2.686 0 6c0 3.313 2.686 6 6 6 3.313 0 6-2.687 6-6 0-.91-.21-1.772-.573-2.545L6.52 9.098z" fill="%232ECC71" fill-rule="evenodd"/></svg>');
}
.emailUnverified {
@apply --infoTextStyles;
color: var(--matterColorAnti);
background-image: url('data:image/svg+xml;utf8,<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M6.52 9.098c-.163.188-.397.3-.646.31h-.032c-.238 0-.466-.094-.635-.263L2.783 6.732c-.353-.35-.354-.92-.003-1.273.35-.353.92-.354 1.272-.004L5.794 7.19l4.59-5.278C9.287.738 7.73 0 6 0 2.686 0 0 2.686 0 6c0 3.313 2.686 6 6 6 3.313 0 6-2.687 6-6 0-.91-.21-1.772-.573-2.545L6.52 9.098z" fill="%23F00" fill-rule="evenodd"/></svg>');
}
.pendingEmailUnverified {
@apply --infoTextStyles;
color: var(--failColor);
width: 100%;
background-image: url('data:image/svg+xml;utf8,<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M6.52 9.098c-.163.188-.397.3-.646.31h-.032c-.238 0-.466-.094-.635-.263L2.783 6.732c-.353-.35-.354-.92-.003-1.273.35-.353.92-.354 1.272-.004L5.794 7.19l4.59-5.278C9.287.738 7.73 0 6 0 2.686 0 0 2.686 0 6c0 3.313 2.686 6 6 6 3.313 0 6-2.687 6-6 0-.91-.21-1.772-.573-2.545L6.52 9.098z" fill="%23F00" fill-rule="evenodd"/></svg>');
}
.checkInbox {
color: var(--matterColor);
}
.emailStyle {
font-weight: var(--fontWeightBold);
word-wrap: break-word;
}
.helperLink {
color: var(--matterColor);
border-bottom: solid 1px var(--matterColor);
line-height: 20px;
margin: 0;
cursor: pointer;
@media (--viewportMedium) {
margin: 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,178 @@
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 * as validators from '../../util/validators';
import { ensureCurrentUser } from '../../util/data';
import { PrimaryButton, TextInputField } from '../../components';
import css from './ContactDetailsForm.css';
const ContactDetailsFormComponent = props => {
const {
rootClassName,
className,
currentUser,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
onResendVerificationEmail,
pristine,
} = props;
const user = ensureCurrentUser(currentUser);
if (!user.id) {
return null;
}
// email
const emailLabel = intl.formatMessage({
id: 'ContactDetailsForm.emailLabel',
});
const { email, emailVerified, pendingEmail } = user.attributes;
const emailPlaceholder = email || '';
const emailRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
// Email status info: unverified, verified and pending email (aka changed unverified email)
let emailVerifiedInfo = null;
if (emailVerified && !pendingEmail && pristine) {
// Current email is verified and there's no pending unverified email
emailVerifiedInfo = (
<span className={css.emailVerified}>
<FormattedMessage id="ContactDetailsForm.emailVerified" />
</span>
);
} 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>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
emailVerifiedInfo = (
<span className={css.emailUnverified}>
<FormattedMessage id="ContactDetailsForm.emailUnverified" values={{ resendEmailLink }} />
</span>
);
} else if (pendingEmail) {
// Current email has been tried to change, but the new address is not yet verified
const pendingEmailStyled = <span className={css.emailStyle}>{pendingEmail}</span>;
const pendingEmailCheckInbox = (
<span className={css.checkInbox}>
<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>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
emailVerifiedInfo = (
<span className={css.pendingEmailUnverified}>
<FormattedMessage
id="ContactDetailsForm.pendingEmailUnverified"
values={{ pendingEmailCheckInbox, resendPendingEmailLink }}
/>
</span>
);
}
// password
const passwordLabel = intl.formatMessage({
id: 'ContactDetailsForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'ContactDetailsForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordRequired',
});
const passwordRequired = validators.required(passwordRequiredMessage);
const classes = classNames(rootClassName || css.root, className);
const submitDisabled = invalid || submitting || inProgress;
return (
<form className={classes} onSubmit={handleSubmit}>
<div className={css.emailSection}>
<TextInputField
type="email"
name="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={emailRequired}
/>
{emailVerifiedInfo}
</div>
<div className={css.confirmChangesSection}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="ContactDetailsForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="ContactDetailsForm.confirmChangesInfo" />
</p>
<TextInputField
className={css.password}
type="password"
name="currentPassword"
id={`${form}.currentPassword`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordRequired}
/>
</div>
<div className={css.bottomWrapper}>
<PrimaryButton className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="ContactDetailsForm.saveChanges" />
</PrimaryButton>
</div>
</form>
);
};
ContactDetailsFormComponent.defaultProps = {
rootClassName: null,
className: null,
inProgress: false,
};
const { bool, func, string } = PropTypes;
ContactDetailsFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
inProgress: bool,
intl: intlShape.isRequired,
onResendVerificationEmail: func.isRequired,
};
const defaultFormName = 'ContactDetailsForm';
const ContactDetailsForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
ContactDetailsFormComponent
);
export default ContactDetailsForm;

View file

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

View file

@ -0,0 +1,56 @@
import { currentUserShowSuccess } from '../../ducks/user.duck';
// ================ Action types ================ //
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';
// ================ Reducer ================ //
const initialState = {
changeEmailError: null,
changeEmailInProgress: false,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case CHANGE_EMAIL_REQUEST:
return { ...state, changeEmailInProgress: true, changeEmailError: null };
case CHANGE_EMAIL_SUCCESS:
return { ...state, changeEmailInProgress: false };
case CHANGE_EMAIL_ERROR:
return { ...state, changeEmailInProgress: false, changeEmailError: payload };
default:
return state;
}
}
// ================ Action creators ================ //
export const changeEmailRequest = () => ({ type: CHANGE_EMAIL_REQUEST });
export const changeEmailSuccess = () => ({ type: CHANGE_EMAIL_SUCCESS });
export const changeEmailError = error => ({
type: CHANGE_EMAIL_ERROR,
payload: error,
error: true,
});
// ================ Thunks ================ //
export const changeEmail = params =>
(dispatch, getState, sdk) => {
dispatch(changeEmailRequest());
const { email, currentPassword } = params;
return sdk.currentUser
.changeEmail({ email, currentPassword }, { expand: true })
.then(response => {
const currentUser = response.data.data;
dispatch(changeEmailSuccess());
dispatch(currentUserShowSuccess(currentUser));
})
.catch(e => dispatch(changeEmailError(e)));
};

View file

@ -4,7 +4,8 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { ensureCurrentUser } from '../../util/data';
import { fetchCurrentUser, sendVerificationEmail } from '../../ducks/user.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import {
@ -17,7 +18,9 @@ import {
TopbarWrapper,
UserNav,
} from '../../components';
import { ContactDetailsForm } from '../../containers';
import { changeEmail } from './ContactDetailsPage.duck';
import css from './ContactDetailsPage.css';
export const ContactDetailsPageComponent = props => {
@ -37,6 +40,7 @@ export const ContactDetailsPageComponent = props => {
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
onSubmitChangeEmail,
} = props;
const tabs = [
@ -56,6 +60,19 @@ export const ContactDetailsPageComponent = props => {
},
];
const user = ensureCurrentUser(currentUser);
const email = user.attributes.email || '';
const changeEmailForm = user.id
? <ContactDetailsForm
className={css.form}
initialValues={{ email }}
currentUser={currentUser}
onResendVerificationEmail={onResendVerificationEmail}
onSubmit={onSubmitChangeEmail}
inProgress={authInProgress}
/>
: null;
return (
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Contact details">
<LayoutSideNavigation>
@ -82,7 +99,12 @@ export const ContactDetailsPageComponent = props => {
<TabNav rootClassName={css.tabs} tabRootClassName={css.tab} tabs={tabs} />
</SideNavWrapper>
<ContentWrapper>
Main content
<div className={css.content}>
<h1 className={css.title}>
<FormattedMessage id="ContactDetailsPage.title" />
</h1>
{changeEmailForm}
</div>
</ContentWrapper>
</LayoutSideNavigation>
</PageLayout>
@ -111,6 +133,7 @@ ContactDetailsPageComponent.propTypes = {
notificationCount: number,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
onSubmitChangeEmail: func.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: instanceOf(Error),
onResendVerificationEmail: func.isRequired,
@ -154,10 +177,16 @@ const mapDispatchToProps = dispatch => ({
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
onSubmitChangeEmail: values => dispatch(changeEmail(values)),
});
const ContactDetailsPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(
ContactDetailsPageComponent
);
ContactDetailsPage.loadData = () => {
// Since verify email happens in separate tab, current user's data might be updated
return fetchCurrentUser();
};
export default ContactDetailsPage;

View file

@ -19,6 +19,7 @@ describe('ContactDetailsPage', () => {
onManageDisableScrolling={noop}
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
onSubmitChangeEmail={noop}
/>
);
expect(tree).toMatchSnapshot();

View file

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

View file

@ -16,8 +16,9 @@ const EmailVerificationFormComponent = props => {
verificationError,
} = props;
const email = <strong>{currentUser.attributes.email}</strong>;
const name = currentUser.attributes.profile.firstName;
const { email, emailVerified, pendingEmail, profile } = currentUser.attributes;
const emailToVerify = <strong>{pendingEmail || email}</strong>;
const name = profile.firstName;
const errorMessage = (
<div className={css.error}>
@ -37,7 +38,10 @@ const EmailVerificationFormComponent = props => {
</h1>
<p className={css.modalMessage}>
<FormattedMessage id="EmailVerificationForm.finishAccountSetup" values={{ email }} />
<FormattedMessage
id="EmailVerificationForm.finishAccountSetup"
values={{ email: emailToVerify }}
/>
</p>
{verificationError ? errorMessage : null}
@ -82,7 +86,7 @@ const EmailVerificationFormComponent = props => {
</div>
);
return currentUser.attributes.emailVerified ? alreadyVerified : verifyEmail;
return emailVerified && !pendingEmail ? alreadyVerified : verifyEmail;
};
EmailVerificationFormComponent.defaultProps = {

View file

@ -18,7 +18,6 @@ import {
UserNav,
} from '../../components';
import css from './PasswordChangePage.css';
export const PasswordChangePageComponent = props => {

View file

@ -2,6 +2,7 @@ import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
import BookingDatesForm from './BookingDatesForm/BookingDatesForm';
import ChangeAccountPasswordForm from './ChangeAccountPasswordForm/ChangeAccountPasswordForm';
import CheckoutPage from './CheckoutPage/CheckoutPage';
import ContactDetailsForm from './ContactDetailsForm/ContactDetailsForm';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
import EditListingLocationForm from './EditListingLocationForm/EditListingLocationForm';
@ -41,6 +42,7 @@ export {
BookingDatesForm,
ChangeAccountPasswordForm,
CheckoutPage,
ContactDetailsForm,
ContactDetailsPage,
EditListingDescriptionForm,
EditListingLocationForm,

View file

@ -239,6 +239,7 @@ const routesConfiguration = [
exact: true,
name: 'ContactDetailsPage',
component: props => <ContactDetailsPage {...props} />,
loadData: params => ContactDetailsPage.loadData(params),
},
{
path: '/account/change-password',

View file

@ -48,8 +48,22 @@
"CheckoutPage.priceBreakdownTitle": "Booking breakdown",
"CheckoutPage.speculateTransactionError": "Failed to fetch breakdown information.",
"CheckoutPage.title": "Book {listingTitle}",
"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.emailUnverified": "You haven't verified your email yet. {resendEmailLink}",
"ContactDetailsForm.emailVerified": "Your email is verified.",
"ContactDetailsForm.passwordLabel": "Current password",
"ContactDetailsForm.passwordPlaceholder": "Enter your current password…",
"ContactDetailsForm.passwordRequired": "This field 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",
"ContactDetailsPage.emailTabTitle": "Email",
"ContactDetailsPage.passwordTabTitle": "Password",
"ContactDetailsPage.title": "Email settings",
"DateInput.clearDate": "Clear Date",
"DateInput.closeDatePicker": "Close",
"DateInput.defaultPlaceholder": "Date input",

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#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5":
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab":
version "0.0.1"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#0bc2a2e82be2c5eadbc22e511a750a2a91dc0fd5"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#2c7f3d323c5e7a7c4c93ef38962c236859b83bab"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"