Merge pull request #332 from sharetribe/email-verification

Email verification
This commit is contained in:
Mikko Koski 2017-08-08 10:21:16 +03:00 committed by GitHub
commit 51ef686068
12 changed files with 439 additions and 4 deletions

View file

@ -32,7 +32,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#a086f37c765cf5b6034bc5f4788788237485c863",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#bb03aefbb50afbb05b3a3592f434b52e383255df",
"source-map-support": "^0.4.15",
"url": "^0.11.0"
},

View file

@ -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 = {};

View file

@ -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);
}

View file

@ -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 = <strong>{currentUser.attributes.email}</strong>;
const errorMessage = (
<div className={css.error}>
<FormattedMessage id="EmailVerificationForm.verificationFailed" />
</div>
);
const verifyEmail = (
<div className={css.root}>
<div>
<h2>
<FormattedMessage id="EmailVerificationForm.verifyEmailAddress" />
</h2>
<p>
<FormattedMessage id="EmailVerificationForm.finishAccountSetup" values={{ email }} />
</p>
{verificationError ? errorMessage : null}
</div>
<form onSubmit={handleSubmit}>
<Field component="input" type="hidden" name="verificationToken" />
<Button className={css.button} type="submit" disabled={submitting || inProgress}>
{submitting || inProgress
? <FormattedMessage id="EmailVerificationForm.verifying" />
: <FormattedMessage id="EmailVerificationForm.verify" />}
</Button>
</form>
</div>
);
const alreadyVerified = (
<div className={css.root}>
<div>
<h2>
<FormattedMessage id="EmailVerificationForm.emailVerified" />
</h2>
<p>
<FormattedMessage
id="EmailVerificationForm.yourEmailAddressIsVerified"
values={{ email }}
/>
</p>
</div>
<NamedLink className={css.button} name="LandingPage">
<FormattedMessage id="EmailVerificationForm.okHomepage" />
</NamedLink>
</div>
);
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.isRequired,
verificationError: instanceOf(Error),
};
const defaultFormName = 'EmailVerificationForm';
const EmailVerificationForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
EmailVerificationFormComponent
);
export default EmailVerificationForm;

View file

@ -0,0 +1,39 @@
@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);
}

View file

@ -0,0 +1,166 @@
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';
/**
Parse verification token from URL
Returns stringified token, if the token is provided.
Returns `null` if verification token is not provided.
Please note that we need to explicitely stringify the token, because
the unwanted result of the `parse` method is that it automatically
parses the token to number.
*/
const parseVerificationToken = location => {
const urlParams = parse(location.search);
const verificationToken = urlParams.t;
if (verificationToken) {
return `${verificationToken}`;
}
return null;
};
export const EmailVerificationPageComponent = props => {
const {
authInfoError,
authInProgress,
currentUser,
currentUserHasListings,
intl,
isAuthenticated,
logoutError,
notificationCount,
onLogout,
onManageDisableScrolling,
scrollingDisabled,
submitVerification,
emailVerificationInProgress,
verificationError,
} = props;
const title = intl.formatMessage({
id: 'EmailVerificationPage.title',
});
const initialValues = { verificationToken: parseVerificationToken(location) };
return (
<PageLayout
title={title}
authInfoError={authInfoError}
logoutError={logoutError}
scrollingDisabled={scrollingDisabled}
>
<Topbar
isAuthenticated={isAuthenticated}
authInProgress={authInProgress}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
notificationCount={notificationCount}
history={history}
location={location}
onLogout={onLogout}
onManageDisableScrolling={onManageDisableScrolling}
/>
<div className={css.root}>
<div className={css.content}>
{currentUser
? <EmailVerificationForm
initialValues={initialValues}
onSubmit={submitVerification}
currentUser={currentUser}
inProgress={emailVerificationInProgress}
verificationError={verificationError}
/>
: <FormattedMessage id="EmailVerificationPage.loadingUserInformation" />}
</div>
</div>
</PageLayout>
);
};
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,
emailVerificationInProgress: 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),
emailVerificationInProgress: 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;

View file

@ -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,

View file

@ -0,0 +1,69 @@
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.
return sdk.users
.verifyEmail({ verificationToken })
.then(() => dispatch(verificationSuccess()))
.then(() => dispatch(fetchCurrentUser()))
.catch(e => dispatch(verificationError(e)));
};

View file

@ -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,
};

View file

@ -20,6 +20,7 @@ import {
SearchPage,
SecurityPage,
StyleguidePage,
EmailVerificationPage,
} from './containers';
const RedirectToLandingPage = () => <NamedRedirect name="LandingPage" />;
@ -319,6 +320,17 @@ const routesConfiguration = [
name: 'NotFoundPage',
component: props => <NotFoundPage {...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 => <EmailVerificationPage {...props} />,
},
];
export default routesConfiguration;

View file

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

View file

@ -6170,9 +6170,9 @@ sharetribe-scripts@0.9.2:
optionalDependencies:
fsevents "1.0.17"
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a086f37c765cf5b6034bc5f4788788237485c863":
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#bb03aefbb50afbb05b3a3592f434b52e383255df":
version "0.0.1"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#a086f37c765cf5b6034bc5f4788788237485c863"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#bb03aefbb50afbb05b3a3592f434b52e383255df"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"