diff --git a/src/app.test.js b/src/app.test.js index c937fdcc..55adf053 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -80,6 +80,7 @@ describe('Application', () => { '/account/contact-details': defaultAuthPath, '/account/payout-preferences': defaultAuthPath, '/account/security': defaultAuthPath, + '/email_verification': defaultAuthPath, }; forEach(urlRedirects, (redirectPath, url) => { const context = {}; diff --git a/src/containers/EmailVerificationForm/EmailVerificationForm.css b/src/containers/EmailVerificationForm/EmailVerificationForm.css new file mode 100644 index 00000000..3bc64e06 --- /dev/null +++ b/src/containers/EmailVerificationForm/EmailVerificationForm.css @@ -0,0 +1,25 @@ +@import '../../marketplace.css'; + +.root { + display: flex; + flex-direction: column; + flex: 1; + justify-content: space-between; + height: 100%; +} + +.button { + @apply --marketplaceButtonStyles; + + margin-top: 24px; + align-self: stretch; + + @media (--viewportMedium) { + margin-top: 72px; + } +} + +.error { + margin-top: 24px; + color: var(--failColor); +} diff --git a/src/containers/EmailVerificationForm/EmailVerificationForm.js b/src/containers/EmailVerificationForm/EmailVerificationForm.js new file mode 100644 index 00000000..6319084d --- /dev/null +++ b/src/containers/EmailVerificationForm/EmailVerificationForm.js @@ -0,0 +1,99 @@ +import React, { PropTypes } from 'react'; +import { compose } from 'redux'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import { reduxForm, Field, propTypes as formPropTypes } from 'redux-form'; +import { Button, NamedLink } from '../../components'; + +import * as propTypes from '../../util/propTypes'; +import css from './EmailVerificationForm.css'; +const EmailVerificationFormComponent = props => { + const { + currentUser, + submitting, + inProgress, + handleSubmit, + verificationError, + } = props; + + const email = {currentUser.attributes.email}; + + const errorMessage = ( +
+ +
+ ); + + const verifyEmail = ( +
+
+

+ +

+ +

+ +

+ + {verificationError ? errorMessage : null} + +
+ +
+ + + + +
+ ); + + const alreadyVerified = ( +
+
+

+ +

+ +

+ +

+ +
+ + + + +
+ ); + + return currentUser.attributes.emailVerified ? alreadyVerified : verifyEmail; +}; +EmailVerificationFormComponent.defaultProps = { + currentUser: null, + inProgress: false, + verificationError: null, +}; +const { + instanceOf, + bool, +} = PropTypes; + +EmailVerificationFormComponent.propTypes = { + ...formPropTypes, + inProgress: bool, + currentUser: propTypes.currentUser, + verificationError: instanceOf(Error), +}; +const defaultFormName = 'EmailVerificationForm'; + +const EmailVerificationForm = compose(reduxForm({ form: defaultFormName }), injectIntl)( + EmailVerificationFormComponent +); +export default EmailVerificationForm; diff --git a/src/containers/EmailVerificationPage/EmailVerificationPage.css b/src/containers/EmailVerificationPage/EmailVerificationPage.css new file mode 100644 index 00000000..42cc8248 --- /dev/null +++ b/src/containers/EmailVerificationPage/EmailVerificationPage.css @@ -0,0 +1,40 @@ +@import '../../marketplaceFonts.css'; +@import '../../marketplace.css'; + +.root { + @apply --backgroundImage; + + /* PageLayout is using flex: EmailVerificationPage's .root takes all available space */ + flex-grow: 1; + + /* EmailVerificationPage's root uses flexbox */ + display: flex; + + @media (--viewportMedium) { + justify-content: center; + align-items: flex-start; + } +} + +.content { + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 30px 24px 98px 24px; + background-color: var(--matterColorLight); + border-radius: 2px; + + @media (--viewportMedium) { + flex-basis: 480px; + flex-grow: 0; + min-height: 573px; + padding: 60px; + margin-top: 7.5vh; + margin-bottom: 7.5vh; + } +} + +.error { + margin-top: 24px; + color: var(--failColor); +} diff --git a/src/containers/EmailVerificationPage/EmailVerificationPage.js b/src/containers/EmailVerificationPage/EmailVerificationPage.js new file mode 100644 index 00000000..81edd925 --- /dev/null +++ b/src/containers/EmailVerificationPage/EmailVerificationPage.js @@ -0,0 +1,142 @@ +import React, { PropTypes } from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; +import * as propTypes from '../../util/propTypes'; +import { PageLayout, Topbar } from '../../components'; +import { EmailVerificationForm } from '../../containers'; +import { authenticationInProgress } from '../../ducks/Auth.duck'; +import { verify, verificationInProgress } from '../../ducks/EmailVerification.duck'; +import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; +import { parse } from '../../util/urlHelpers'; +import css from './EmailVerificationPage.css'; +export const EmailVerificationPageComponent = props => { + const { + authInfoError, + authInProgress, + currentUser, + currentUserHasListings, + intl, + isAuthenticated, + logoutError, + notificationCount, + onLogout, + onManageDisableScrolling, + scrollingDisabled, + submitVerification, + verifInProgress, + verificationError, + } = props; + const title = intl.formatMessage({ + id: 'EmailVerificationPage.title', + }); + + const urlParams = parse(location.search); + + const initialValues = { verificationToken: urlParams.t }; + + return ( + + +
+
+ + {currentUser + ? + : } +
+
+ +
+ ); +}; + +EmailVerificationPageComponent.defaultProps = { + authInfoError: null, + currentUser: null, + logoutError: null, + notificationCount: 0, + verificationError: null, +}; +const { bool, func, instanceOf, number } = PropTypes; +EmailVerificationPageComponent.propTypes = { + authInfoError: instanceOf(Error), + authInProgress: bool.isRequired, + currentUser: propTypes.currentUser, + currentUserHasListings: bool.isRequired, + isAuthenticated: bool.isRequired, + logoutError: instanceOf(Error), + notificationCount: number, + onLogout: func.isRequired, + onManageDisableScrolling: func.isRequired, + scrollingDisabled: bool.isRequired, + submitVerification: func.isRequired, + verifInProgress: bool.isRequired, + verificationError: instanceOf(Error), + intl: intlShape.isRequired, +}; +const mapStateToProps = state => { + const { + authInfoError, + isAuthenticated, + logoutError, + } = state.Auth; + const { + currentUser, + currentUserHasListings, + currentUserNotificationCount: notificationCount, + } = state.user; + const { + verificationError, + } = state.EmailVerification; + return { + authInfoError, + authInProgress: authenticationInProgress(state), + verifInProgress: verificationInProgress(state), + verificationError, + currentUser, + currentUserHasListings, + isAuthenticated, + logoutError, + notificationCount, + scrollingDisabled: isScrollingDisabled(state), + }; +}; + +const mapDispatchToProps = dispatch => ({ + onManageDisableScrolling: (componentId, disableScrolling) => + dispatch(manageDisableScrolling(componentId, disableScrolling)), + submitVerification: ({ verificationToken }) => { + return dispatch(verify(verificationToken)); + }, +}); + +const EmailVerificationPage = compose( + connect(mapStateToProps, mapDispatchToProps), + withRouter, + injectIntl +)(EmailVerificationPageComponent); + +export default EmailVerificationPage; diff --git a/src/containers/index.js b/src/containers/index.js index 23909df8..890a69e4 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -10,6 +10,8 @@ import EditListingPage from './EditListingPage/EditListingPage'; import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm'; import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm'; import EditProfilePage from './EditProfilePage/EditProfilePage'; +import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage'; +import EmailVerificationForm from './EmailVerificationForm/EmailVerificationForm'; import InboxPage from './InboxPage/InboxPage'; import LandingPage from './LandingPage/LandingPage'; import ListingPage from './ListingPage/ListingPage'; @@ -45,6 +47,8 @@ export { EditListingPhotosForm, EditListingPricingForm, EditProfilePage, + EmailVerificationPage, + EmailVerificationForm, InboxPage, LandingPage, ListingPage, diff --git a/src/ducks/EmailVerification.duck.js b/src/ducks/EmailVerification.duck.js new file mode 100644 index 00000000..71977865 --- /dev/null +++ b/src/ducks/EmailVerification.duck.js @@ -0,0 +1,70 @@ +import { fetchCurrentUser } from './user.duck'; + +// ================ Action types ================ // + +export const VERIFICATION_REQUEST = 'app/EmailVerification/VERIFICATION_REQUEST'; +export const VERIFICATION_SUCCESS = 'app/EmailVerification/VERIFICATION_SUCCESS'; +export const VERIFICATION_ERROR = 'app/EmailVerification/VERIFICATION_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + isVerified: false, + + // verification + verificationError: null, + verificationInProgress: false, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case VERIFICATION_REQUEST: + return { + ...state, + verificationInProgress: true, + verificationError: null, + }; + case VERIFICATION_SUCCESS: + return { ...state, verificationInProgress: false, isVerified: true }; + case VERIFICATION_ERROR: + return { ...state, verificationInProgress: false, verificationError: payload }; + default: + return state; + } +} + +// ================ Selectors ================ // + +export const verificationInProgress = state => { + return state.EmailVerification.verificationInProgress; +}; + +// ================ Action creators ================ // + +export const verificationRequest = () => ({ type: VERIFICATION_REQUEST }); +export const verificationSuccess = () => ({ type: VERIFICATION_SUCCESS }); +export const verificationError = error => ({ + type: VERIFICATION_ERROR, + payload: error, + error: true, +}); + +// ================ Thunks ================ // + +export const verify = verificationToken => + (dispatch, getState, sdk) => { + if (verificationInProgress(getState())) { + return Promise.reject(new Error('Email verification already in progress')); + } + dispatch(verificationRequest()); + + // Note that the thunk does not reject when the verification fails, it + // just dispatches the login error action. + const verifyParams = { verificationToken: `${verificationToken}` }; + return sdk.users + .verifyEmail(verifyParams) + .then(() => dispatch(verificationSuccess())) + .then(() => dispatch(fetchCurrentUser())) + .catch(e => dispatch(verificationError(e))); + }; diff --git a/src/ducks/index.js b/src/ducks/index.js index 84bf0358..183c2afa 100644 --- a/src/ducks/index.js +++ b/src/ducks/index.js @@ -11,5 +11,15 @@ import LocationFilter from './LocationFilter.duck'; import UI from './UI.duck'; import marketplaceData from './marketplaceData.duck'; import user from './user.duck'; +import EmailVerification from './EmailVerification.duck'; -export { form, Auth, FlashNotification, LocationFilter, UI, marketplaceData, user }; +export { + form, + Auth, + FlashNotification, + LocationFilter, + UI, + marketplaceData, + user, + EmailVerification, +}; diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js index 8d613ab4..b77d9d29 100644 --- a/src/routesConfiguration.js +++ b/src/routesConfiguration.js @@ -20,6 +20,7 @@ import { SearchPage, SecurityPage, StyleguidePage, + EmailVerificationPage, } from './containers'; const RedirectToLandingPage = () => ; @@ -319,6 +320,17 @@ const routesConfiguration = [ name: 'NotFoundPage', component: props => , }, + + // Do not change this path! + // + // The API expects that the Starter App implements /email_verification endpoint + { + path: '/email_verification', + auth: true, + exact: true, + name: 'EmailVerificationPage', + component: props => , + }, ]; export default routesConfiguration; diff --git a/src/translations/en.json b/src/translations/en.json index daf04e26..232daac2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -78,6 +78,16 @@ "EditListingPricingForm.pricePerNight": "Price per night in euros", "EditListingPricingForm.priceRequired": "You need to add a valid price.", "EditListingPricingPanel.title": "How much does it cost?", + "EmailVerificationPage.title": "Verify your email address", + "EmailVerificationPage.loadingUserInformation": "Loading user information...", + "EmailVerificationForm.verifyEmailAddress": "Verify your email address", + "EmailVerificationForm.finishAccountSetup": "To finish setting up your Saunatime account, we sent an email to {email} to make sure this email is yours.", + "EmailVerificationForm.verify": "Verify my email", + "EmailVerificationForm.verifying": "Verifying...", + "EmailVerificationForm.emailVerified": "Your email address is verified", + "EmailVerificationForm.yourEmailAddressIsVerified": "Your email address {email} is verified.", + "EmailVerificationForm.okHomepage": "Ok, got it. Take me to the homepage", + "EmailVerificationForm.verificationFailed": "Could not verify the email address.", "HeroSection.mobileSearchButtonText": "Search saunas", "HeroSection.subTitle": "The largest online community to rent saunas in Finland.", "HeroSection.title": "Book saunas everywhere.",