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 ( +
+
+ + {emailVerifiedInfo} +
+ +
+

+ +

+

+ +

+ + +
+
+ + + +
+
+ ); +}; + +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"