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.",