diff --git a/package.json b/package.json
index 502c2a96..86ab7823 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/components/LayoutSideNavigation/LayoutSideNavigation.css b/src/components/LayoutSideNavigation/LayoutSideNavigation.css
index b4133d73..11c238d9 100644
--- a/src/components/LayoutSideNavigation/LayoutSideNavigation.css
+++ b/src/components/LayoutSideNavigation/LayoutSideNavigation.css
@@ -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;
diff --git a/src/containers/ContactDetailsForm/ContactDetailsForm.css b/src/containers/ContactDetailsForm/ContactDetailsForm.css
new file mode 100644
index 00000000..6af9fcab
--- /dev/null
+++ b/src/containers/ContactDetailsForm/ContactDetailsForm.css
@@ -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,');
+}
+
+.emailUnverified {
+ @apply --infoTextStyles;
+ color: var(--matterColorAnti);
+ background-image: url('data:image/svg+xml;utf8,');
+}
+
+.pendingEmailUnverified {
+ @apply --infoTextStyles;
+ color: var(--failColor);
+ width: 100%;
+ background-image: url('data:image/svg+xml;utf8,');
+}
+
+.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;
+ }
+}
diff --git a/src/containers/ContactDetailsForm/ContactDetailsForm.js b/src/containers/ContactDetailsForm/ContactDetailsForm.js
new file mode 100644
index 00000000..4c8d74e1
--- /dev/null
+++ b/src/containers/ContactDetailsForm/ContactDetailsForm.js
@@ -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 = (
+
+
+
+ );
+ } 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 = (
+
+
+
+ );
+ /* eslint-enable jsx-a11y/no-static-element-interactions */
+
+ emailVerifiedInfo = (
+
+
+
+ );
+ } else if (pendingEmail) {
+ // Current email has been tried to change, but the new address is not yet verified
+
+ const pendingEmailStyled = {pendingEmail};
+ const pendingEmailCheckInbox = (
+
+
+
+ );
+
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
+ const resendPendingEmailLink = (
+
+
+
+ );
+ /* eslint-enable jsx-a11y/no-static-element-interactions */
+
+ emailVerifiedInfo = (
+
+
+
+ );
+ }
+
+ // 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 (
+
+ );
+};
+
+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;
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.css b/src/containers/ContactDetailsPage/ContactDetailsPage.css
index e8fc3e33..0ca79c21 100644
--- a/src/containers/ContactDetailsPage/ContactDetailsPage.css
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.css
@@ -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;
+ }
+}
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js b/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js
new file mode 100644
index 00000000..b795f32e
--- /dev/null
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js
@@ -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)));
+ };
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.js b/src/containers/ContactDetailsPage/ContactDetailsPage.js
index 4f9c2ee2..2e250a9f 100644
--- a/src/containers/ContactDetailsPage/ContactDetailsPage.js
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.js
@@ -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
+ ?
+ : null;
+
return (
@@ -82,7 +99,12 @@ export const ContactDetailsPageComponent = props => {
- Main content
+
+
+
+
+ {changeEmailForm}
+
@@ -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;
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.test.js b/src/containers/ContactDetailsPage/ContactDetailsPage.test.js
index 0c8698bb..ea6a1ba8 100644
--- a/src/containers/ContactDetailsPage/ContactDetailsPage.test.js
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.test.js
@@ -19,6 +19,7 @@ describe('ContactDetailsPage', () => {
onManageDisableScrolling={noop}
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
+ onSubmitChangeEmail={noop}
/>
);
expect(tree).toMatchSnapshot();
diff --git a/src/containers/ContactDetailsPage/__snapshots__/ContactDetailsPage.test.js.snap b/src/containers/ContactDetailsPage/__snapshots__/ContactDetailsPage.test.js.snap
index d0e59094..371df3d6 100644
--- a/src/containers/ContactDetailsPage/__snapshots__/ContactDetailsPage.test.js.snap
+++ b/src/containers/ContactDetailsPage/__snapshots__/ContactDetailsPage.test.js.snap
@@ -72,7 +72,13 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
- Main content
+
+
+
+
+
diff --git a/src/containers/EmailVerificationForm/EmailVerificationForm.js b/src/containers/EmailVerificationForm/EmailVerificationForm.js
index a06477ea..88488790 100644
--- a/src/containers/EmailVerificationForm/EmailVerificationForm.js
+++ b/src/containers/EmailVerificationForm/EmailVerificationForm.js
@@ -16,8 +16,9 @@ const EmailVerificationFormComponent = props => {
verificationError,
} = props;
- const email = {currentUser.attributes.email};
- const name = currentUser.attributes.profile.firstName;
+ const { email, emailVerified, pendingEmail, profile } = currentUser.attributes;
+ const emailToVerify = {pendingEmail || email};
+ const name = profile.firstName;
const errorMessage = (
@@ -37,7 +38,10 @@ const EmailVerificationFormComponent = props => {
-
+
{verificationError ? errorMessage : null}
@@ -82,7 +86,7 @@ const EmailVerificationFormComponent = props => {
);
- return currentUser.attributes.emailVerified ? alreadyVerified : verifyEmail;
+ return emailVerified && !pendingEmail ? alreadyVerified : verifyEmail;
};
EmailVerificationFormComponent.defaultProps = {
diff --git a/src/containers/PasswordChangePage/PasswordChangePage.js b/src/containers/PasswordChangePage/PasswordChangePage.js
index ee3741f9..a9e190eb 100644
--- a/src/containers/PasswordChangePage/PasswordChangePage.js
+++ b/src/containers/PasswordChangePage/PasswordChangePage.js
@@ -18,7 +18,6 @@ import {
UserNav,
} from '../../components';
-
import css from './PasswordChangePage.css';
export const PasswordChangePageComponent = props => {
diff --git a/src/containers/index.js b/src/containers/index.js
index 5f603a22..ee58a89f 100644
--- a/src/containers/index.js
+++ b/src/containers/index.js
@@ -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,
diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js
index 44ed1451..66797e34 100644
--- a/src/routesConfiguration.js
+++ b/src/routesConfiguration.js
@@ -239,6 +239,7 @@ const routesConfiguration = [
exact: true,
name: 'ContactDetailsPage',
component: props => ,
+ loadData: params => ContactDetailsPage.loadData(params),
},
{
path: '/account/change-password',
diff --git a/src/translations/en.json b/src/translations/en.json
index 03b32ea8..bd0414fa 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -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 haven’t 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",
diff --git a/yarn.lock b/yarn.lock
index 681b604e..f44aa1f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"