diff --git a/.env-template b/.env-template
index 469225c32..a48fa19db 100644
--- a/.env-template
+++ b/.env-template
@@ -21,6 +21,11 @@ SHARETRIBE_SDK_CLIENT_SECRET=
REACT_APP_SHARETRIBE_MARKETPLACE_CURRENCY=USD
REACT_APP_CANONICAL_ROOT_URL=http://localhost:3000
+# Social logins && SSO
+# If the app or client id is not set the auhtentication option is not shown in FTW
+REACT_APP_FACEBOOK_APP_ID=
+FACEBOOK_APP_SECRET=
+
# This is overwritten by configuration in .env.development and
# .env.test. In production deployments use env variable and set it to
# 'production'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf7148ab9..b9952c5a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,20 @@ https://github.com/sharetribe/flex-template-web/
## Upcoming version 2020-XX-XX
+## [v8.4.0] 2020-10-15
+
+### Updates from upstream (FTW-daily v6.4.0)
+
+- [add] Add Facebook login as a first step towards supporting social logins and SSO in FTW. This PR
+ introduces new endpoints createUserWithIdp and loginWithIdp and strategy for logging in with
+ Facebook. See the PR for the more detailed view of the changes. #1364
+- [fix] Fix missing proptype warnings in TransactionPage and TransactionPanel tests. #1363
+- [fix] Improve error handling by passing error details forward instead of creating a new error that
+ hides the details when making API call to FTW server. #1361
+- [fix] Remove duplicate page schema from body. #1355
+
+[v8.4.0]: https://github.com/sharetribe/ftw-hourly/compare/v8.3.1...v8.4.0
+
## [v8.3.1] 2020-08-19
- [fix] Fix popup-button in SelectSingleFilterPopup.css and adjust Footer with correct baselines.
diff --git a/package.json b/package.json
index 23051004f..e8c6f787e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "app",
- "version": "8.3.1",
+ "version": "8.4.0",
"private": true,
"license": "Apache-2.0",
"dependencies": {
@@ -36,6 +36,8 @@
"moment-timezone": "^0.5.26",
"object.entries": "^1.1.2",
"object.values": "^1.1.1",
+ "passport": "^0.4.1",
+ "passport-facebook": "^3.0.0",
"path-to-regexp": "^6.1.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.1",
@@ -54,7 +56,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"seedrandom": "^3.0.5",
- "sharetribe-flex-sdk": "1.12.0",
+ "sharetribe-flex-sdk": "1.13.0",
"sharetribe-scripts": "3.1.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.9",
diff --git a/server/api-util/sdk.js b/server/api-util/sdk.js
index f2d6ff185..757c2f20f 100644
--- a/server/api-util/sdk.js
+++ b/server/api-util/sdk.js
@@ -55,13 +55,16 @@ exports.deserialize = str => {
exports.handleError = (res, error) => {
console.error(error);
if (error.status && error.statusText && error.data) {
+ const { status, statusText, data } = error;
+
// JS SDK error
res
.status(error.status)
.json({
- status: error.status,
- statusText: error.statusText,
- data: error.data,
+ name: 'Local API request failed',
+ status,
+ statusText,
+ data,
})
.end();
} else {
diff --git a/server/api/auth/createUserWithIdp.js b/server/api/auth/createUserWithIdp.js
new file mode 100644
index 000000000..6270d674e
--- /dev/null
+++ b/server/api/auth/createUserWithIdp.js
@@ -0,0 +1,81 @@
+const http = require('http');
+const https = require('https');
+const sharetribeSdk = require('sharetribe-flex-sdk');
+const { handleError, serialize, typeHandlers } = require('../../api-util/sdk');
+
+const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
+const CLIENT_SECRET = process.env.SHARETRIBE_SDK_CLIENT_SECRET;
+const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
+const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
+const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
+
+const FACBOOK_APP_ID = process.env.REACT_APP_FACEBOOK_APP_ID;
+const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
+
+const FACEBOOK_IDP_ID = 'facebook';
+const GOOGLE_IDP_ID = 'google';
+
+// Instantiate HTTP(S) Agents with keepAlive set to true.
+// This will reduce the request time for consecutive requests by
+// reusing the existing TCP connection, thus eliminating the time used
+// for setting up new TCP connections.
+const httpAgent = new http.Agent({ keepAlive: true });
+const httpsAgent = new https.Agent({ keepAlive: true });
+
+const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
+
+module.exports = (req, res) => {
+ const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
+ clientId: CLIENT_ID,
+ req,
+ res,
+ secure: USING_SSL,
+ });
+
+ const sdk = sharetribeSdk.createInstance({
+ transitVerbose: TRANSIT_VERBOSE,
+ clientId: CLIENT_ID,
+ clientSecret: CLIENT_SECRET,
+ httpAgent,
+ httpsAgent,
+ tokenStore,
+ typeHandlers,
+ ...baseUrl,
+ });
+
+ const { idpToken, idpId, ...rest } = req.body;
+
+ // Choose the idpClientId based on which authentication method is used.
+ const idpClientId =
+ idpId === FACEBOOK_IDP_ID ? FACBOOK_APP_ID : idpId === GOOGLE_IDP_ID ? GOOGLE_CLIENT_ID : null;
+
+ sdk.currentUser
+ .createWithIdp({ idpId: FACEBOOK_IDP_ID, idpClientId, idpToken, ...rest })
+ .then(() =>
+ // After the user is created, we need to call loginWithIdp endpoint
+ // so that the user will be logged in.
+ sdk.loginWithIdp({
+ idpId,
+ idpClientId: `${idpClientId}`,
+ idpToken: `${idpToken}`,
+ })
+ )
+ .then(apiResponse => {
+ const { status, statusText, data } = apiResponse;
+ res
+ .clearCookie('st-authinfo')
+ .status(status)
+ .set('Content-Type', 'application/transit+json')
+ .send(
+ serialize({
+ status,
+ statusText,
+ data,
+ })
+ )
+ .end();
+ })
+ .catch(e => {
+ handleError(res, e);
+ });
+};
diff --git a/server/api/auth/facebook.js b/server/api/auth/facebook.js
new file mode 100644
index 000000000..a5380b46f
--- /dev/null
+++ b/server/api/auth/facebook.js
@@ -0,0 +1,78 @@
+const passport = require('passport');
+const passportFacebook = require('passport-facebook');
+const loginWithIdp = require('./loginWithIdp');
+
+const radix = 10;
+const PORT = parseInt(process.env.REACT_APP_DEV_API_SERVER_PORT, radix);
+const rootUrl = process.env.REACT_APP_CANONICAL_ROOT_URL;
+const clientID = process.env.REACT_APP_FACEBOOK_APP_ID;
+const clientSecret = process.env.FACEBOOK_APP_SECRET;
+
+const FacebookStrategy = passportFacebook.Strategy;
+let callbackURL = null;
+
+const useDevApiServer = process.env.NODE_ENV === 'development' && !!PORT;
+
+if (useDevApiServer) {
+ callbackURL = `http://localhost:${PORT}/api/auth/facebook/callback`;
+} else {
+ callbackURL = `${rootUrl}/api/auth/facebook/callback`;
+}
+
+const strategyOptions = {
+ clientID,
+ clientSecret,
+ callbackURL,
+ profileFields: ['id', 'name', 'emails'],
+ passReqToCallback: true,
+};
+
+const verifyCallback = (req, accessToken, refreshToken, profile, done) => {
+ const { email, first_name, last_name } = profile._json;
+ const state = req.query.state;
+ const queryParams = JSON.parse(state);
+
+ const { from, defaultReturn, defaultConfirm } = queryParams;
+
+ const userData = {
+ email,
+ firstName: first_name,
+ lastName: last_name,
+ accessToken,
+ refreshToken,
+ from,
+ defaultReturn,
+ defaultConfirm,
+ };
+
+ done(null, userData);
+};
+
+// ClientId is required when adding a new Facebook strategy to passport
+if (clientID) {
+ passport.use(new FacebookStrategy(strategyOptions, verifyCallback));
+}
+
+exports.authenticateFacebook = (req, res, next) => {
+ const from = req.query.from ? req.query.from : null;
+ const defaultReturn = req.query.defaultReturn ? req.query.defaultReturn : null;
+ const defaultConfirm = req.query.defaultConfirm ? req.query.defaultConfirm : null;
+
+ const params = {
+ ...(!!from && { from }),
+ ...(!!defaultReturn && { defaultReturn }),
+ ...(!!defaultConfirm && { defaultConfirm }),
+ };
+
+ const paramsAsString = JSON.stringify(params);
+
+ passport.authenticate('facebook', { scope: ['email'], state: paramsAsString })(req, res, next);
+};
+
+// Use custom callback for calling loginWithIdp enpoint
+// to log in the user to Flex with the data from Facebook
+exports.authenticateFacebookCallback = (req, res, next) => {
+ passport.authenticate('facebook', function(err, user) {
+ loginWithIdp(err, user, req, res, clientID, 'facebook');
+ })(req, res, next);
+};
diff --git a/server/api/auth/loginWithIdp.js b/server/api/auth/loginWithIdp.js
new file mode 100644
index 000000000..75a75f89d
--- /dev/null
+++ b/server/api/auth/loginWithIdp.js
@@ -0,0 +1,126 @@
+const http = require('http');
+const https = require('https');
+const sharetribeSdk = require('sharetribe-flex-sdk');
+const sdkUtils = require('../../api-util/sdk');
+
+const CLIENT_ID = process.env.REACT_APP_SHARETRIBE_SDK_CLIENT_ID;
+const CLIENT_SECRET = process.env.SHARETRIBE_SDK_CLIENT_SECRET;
+const TRANSIT_VERBOSE = process.env.REACT_APP_SHARETRIBE_SDK_TRANSIT_VERBOSE === 'true';
+const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
+const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL;
+const rootUrl = process.env.REACT_APP_CANONICAL_ROOT_URL;
+
+// Instantiate HTTP(S) Agents with keepAlive set to true.
+// This will reduce the request time for consecutive requests by
+// reusing the existing TCP connection, thus eliminating the time used
+// for setting up new TCP connections.
+const httpAgent = new http.Agent({ keepAlive: true });
+const httpsAgent = new https.Agent({ keepAlive: true });
+
+const baseUrl = BASE_URL ? { baseUrl: BASE_URL } : {};
+
+module.exports = (err, user, req, res, clientID, idpId) => {
+ if (err) {
+ console.error(err);
+
+ // Save error details to cookie so that we can show
+ // relevant information in the frontend
+ return res
+ .cookie(
+ 'st-autherror',
+ {
+ status: err.status,
+ code: err.code,
+ message: err.message,
+ },
+ {
+ maxAge: 15 * 60 * 1000, // 15 minutes
+ }
+ )
+ .redirect(`${rootUrl}/login#`);
+ }
+
+ if (!user) {
+ console.error('Failed to fetch user details from identity provider!');
+
+ // Save error details to cookie so that we can show
+ // relevant information in the frontend
+ return res
+ .cookie(
+ 'st-autherror',
+ {
+ status: 'Bad Request',
+ code: 400,
+ message: 'Failed to fetch user details from identity provider!',
+ },
+ {
+ maxAge: 15 * 60 * 1000, // 15 minutes
+ }
+ )
+ .redirect(`${rootUrl}/login#`);
+ }
+
+ const { from, defaultReturn, defaultConfirm } = user;
+
+ const tokenStore = sharetribeSdk.tokenStore.expressCookieStore({
+ clientId: CLIENT_ID,
+ req,
+ res,
+ secure: USING_SSL,
+ });
+
+ const sdk = sharetribeSdk.createInstance({
+ transitVerbose: TRANSIT_VERBOSE,
+ clientId: CLIENT_ID,
+ clientSecret: CLIENT_SECRET,
+ httpAgent,
+ httpsAgent,
+ tokenStore,
+ typeHandlers: sdkUtils.typeHandlers,
+ ...baseUrl,
+ });
+
+ return sdk
+ .loginWithIdp({
+ idpId: 'facebook',
+ idpClientId: clientID,
+ idpToken: user ? user.accessToken : null,
+ })
+ .then(response => {
+ if (response.status === 200) {
+ // If the user was authenticated, redirect back to to LandingPage
+ // We need to add # to the end of the URL because otherwise Facebook
+ // login will add their defaul #_#_ which breaks the routing in frontend.
+
+ if (from) {
+ res.redirect(`${rootUrl}${from}#`);
+ } else {
+ res.redirect(`${rootUrl}${defaultReturn}#`);
+ }
+ }
+ })
+ .catch(() => {
+ // If authentication fails, we want to create a new user with idp
+ // For this we will need to pass some information to frontend so
+ // that we can use that information in createUserWithIdp api call.
+ // The createUserWithIdp api call is triggered from frontend
+ // after showing a confirm page to user
+
+ res.cookie(
+ 'st-authinfo',
+ {
+ email: user.email,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ idpToken: `${user.accessToken}`,
+ idpId,
+ from,
+ },
+ {
+ maxAge: 15 * 60 * 1000, // 15 minutes
+ }
+ );
+
+ res.redirect(`${rootUrl}${defaultConfirm}#`);
+ });
+};
diff --git a/server/apiRouter.js b/server/apiRouter.js
index 1a09735cb..4d21d277f 100644
--- a/server/apiRouter.js
+++ b/server/apiRouter.js
@@ -16,6 +16,10 @@ const transactionLineItems = require('./api/transaction-line-items');
const initiatePrivileged = require('./api/initiate-privileged');
const transitionPrivileged = require('./api/transition-privileged');
+const createUserWithIdp = require('./api/auth/createUserWithIdp');
+
+const { authenticateFacebook, authenticateFacebookCallback } = require('./api/auth/facebook');
+
const router = express.Router();
// ================ API router middleware: ================ //
@@ -50,4 +54,19 @@ router.post('/transaction-line-items', transactionLineItems);
router.post('/initiate-privileged', initiatePrivileged);
router.post('/transition-privileged', transitionPrivileged);
+// Create user with identity provider (e.g. Facebook or Google)
+// This endpoint is called to create a new user after user has confirmed
+// they want to continue with the data fetched from IdP (e.g. name and email)
+router.post('/auth/create-user-with-idp', createUserWithIdp);
+
+// Facebook authentication endpoints
+
+// This endpoint is called when user wants to initiate authenticaiton with Facebook
+router.get('/auth/facebook', authenticateFacebook);
+
+// This is the route for callback URL the user is redirected after authenticating
+// with Facebook. In this route a Passport.js custom callback is used for calling
+// loginWithIdp endpoint in Flex API to authenticate user to Flex
+router.get('/auth/facebook/callback', authenticateFacebookCallback);
+
module.exports = router;
diff --git a/server/index.js b/server/index.js
index fe00a801e..1450068df 100644
--- a/server/index.js
+++ b/server/index.js
@@ -30,6 +30,7 @@ const enforceSsl = require('express-enforces-ssl');
const path = require('path');
const sharetribeSdk = require('sharetribe-flex-sdk');
const sitemap = require('express-sitemap');
+const passport = require('passport');
const auth = require('./auth');
const apiRouter = require('./apiRouter');
const renderer = require('./renderer');
@@ -145,6 +146,12 @@ if (!dev) {
}
}
+// Initialize Passport.js (http://www.passportjs.org/)
+// Passport is authentication middleware for Node.js
+// We use passport to enable authenticating with
+// a 3rd party identity provider (e.g. Facebook or Google)
+app.use(passport.initialize());
+
// Server-side routes that do not render the application
app.use('/api', apiRouter);
diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css
index 1cb34644d..fe78cd381 100644
--- a/src/components/Button/Button.css
+++ b/src/components/Button/Button.css
@@ -106,3 +106,36 @@
.secondaryButton .checkmark {
stroke: var(--matterColorAnti);
}
+
+/* Social logins && SSO buttons */
+
+.socialButtonRoot {
+ @apply --marketplaceButtonStyles;
+ min-height: 48px;
+ background-color: var(--matterColorLight);
+
+ color: var(--matterColorDark);
+ font-weight: var(--fontWeightMedium);
+ font-size: 14px;
+
+ border: 1px solid #d2d2d2;
+ border-radius: 4px;
+
+ /* We need to add this to position the icon inside button */
+ position: relative;
+
+ @media (--viewportMedium) {
+ padding: 0;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: var(--matterColorLight);
+ }
+ &:disabled {
+ background-color: var(--matterColorNegative);
+ }
+}
+
+.socialButton {
+}
diff --git a/src/components/Button/Button.js b/src/components/Button/Button.js
index f934c25af..b0de63bce 100644
--- a/src/components/Button/Button.js
+++ b/src/components/Button/Button.js
@@ -100,3 +100,10 @@ export const InlineTextButton = props => {
return ;
};
InlineTextButton.displayName = 'InlineTextButton';
+
+export const SocialLoginButton = props => {
+ const classes = classNames(props.rootClassName || css.socialButtonRoot, css.socialButton);
+ return ;
+};
+
+SocialLoginButton.displayName = 'SocialLoginButton';
diff --git a/src/components/Page/Page.js b/src/components/Page/Page.js
index cee20b5e5..013ee43e4 100644
--- a/src/components/Page/Page.js
+++ b/src/components/Page/Page.js
@@ -43,6 +43,14 @@ class PageComponent extends Component {
// handling both dragover and drop events.
document.addEventListener('dragover', preventDefault);
document.addEventListener('drop', preventDefault);
+
+ // Remove duplicated server-side rendered page schema.
+ // It's in
to improve initial rendering performance,
+ // but after web app is initialized, react-helmet-async operates with
+ const pageSchema = document.getElementById('page-schema');
+ if (pageSchema) {
+ pageSchema.remove();
+ }
}
componentWillUnmount() {
@@ -197,7 +205,7 @@ class PageComponent extends Component {
{metaTags}
-
diff --git a/src/components/index.js b/src/components/index.js
index 88dc91fae..f4e38f248 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -60,7 +60,7 @@ export { default as ValidationError } from './ValidationError/ValidationError';
// First components that include only atomic components //
//////////////////////////////////////////////////////////
-export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton } from './Button/Button';
+export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton, SocialLoginButton } from './Button/Button';
export { default as CookieConsent } from './CookieConsent/CookieConsent';
export { default as ImageCarousel } from './ImageCarousel/ImageCarousel';
export { default as ImageFromFile } from './ImageFromFile/ImageFromFile';
diff --git a/src/config.js b/src/config.js
index 6b6b5f547..6c8f9e610 100644
--- a/src/config.js
+++ b/src/config.js
@@ -101,11 +101,14 @@ const siteInstagramPage = null;
// Facebook page is used in SEO schema (http://schema.org/Organization)
const siteFacebookPage = 'https://www.facebook.com/Sharetribe/';
+// Social logins & SSO
+
+// Note: Facebook app id is also used for tracking:
// Facebook counts shares with app or page associated by this id
// Currently it is unset, but you can read more about fb:app_id from
// https://developers.facebook.com/docs/sharing/webmasters#basic
// You should create one to track social sharing in Facebook
-const facebookAppId = null;
+const facebookAppId = process.env.REACT_APP_FACEBOOK_APP_ID;
const maps = {
mapboxAccessToken: process.env.REACT_APP_MAPBOX_ACCESS_TOKEN,
diff --git a/src/containers/AuthenticationPage/AuthenticationPage.css b/src/containers/AuthenticationPage/AuthenticationPage.css
index 20927b580..92ea1e7c1 100644
--- a/src/containers/AuthenticationPage/AuthenticationPage.css
+++ b/src/containers/AuthenticationPage/AuthenticationPage.css
@@ -42,6 +42,11 @@
.form {
margin-top: 24px;
+ /* We don't want the form to take the whole space so that on mobile view
+ the social login buttons will be after the sign up button
+ and not in the bottom of the page */
+ flex-grow: 0;
+
@media (--viewportMedium) {
margin-top: 30px;
padding-top: 2px;
@@ -147,3 +152,63 @@
.error {
@apply --marketplaceModalErrorStyles;
}
+
+/* ================ Social logins & SSO ================ */
+
+.signupWithIdpTitle {
+ @apply --marketplaceModalTitleStyles;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 16px;
+ color: var(--matterColorDark);
+
+ @media (--viewportMedium) {
+ margin-top: 6px;
+ }
+}
+
+.confirmInfoText {
+ @apply --marketplaceH4FontStyles;
+}
+
+.buttonIcon {
+ position: absolute;
+ left: 0;
+ margin-left: 20px;
+}
+
+.socialButtonsOr {
+ width: 100%;
+ height: 32px;
+ margin: 28px 0 20px 0;
+ text-align: center;
+ position: relative;
+ background-color: var(--matterColorLight);
+
+ &:after {
+ content: '';
+ width: 100%;
+ border-bottom: solid 1px #d2d2d2;
+ position: absolute;
+ left: 0;
+ top: 50%;
+ z-index: 1;
+ }
+}
+
+@media (--viewportMedium) {
+ .socialButtonsOr {
+ height: 34px;
+ margin: 15px 0;
+ }
+}
+
+.socialButtonsOrText {
+ background-color: var(--matterColorLight);
+ width: auto;
+ display: inline-block;
+ z-index: 3;
+ padding: 0 20px 0 20px;
+ position: relative;
+ margin: 0;
+}
diff --git a/src/containers/AuthenticationPage/AuthenticationPage.js b/src/containers/AuthenticationPage/AuthenticationPage.js
index 477937ad1..c9a5579db 100644
--- a/src/containers/AuthenticationPage/AuthenticationPage.js
+++ b/src/containers/AuthenticationPage/AuthenticationPage.js
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter, Redirect } from 'react-router-dom';
+import Cookies from 'js-cookie';
+import routeConfiguration from '../../routeConfiguration';
+import { pathByRouteName } from '../../util/routes';
+import { apiBaseUrl } from '../../util/api';
import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
import classNames from 'classnames';
import config from '../../config';
@@ -19,6 +23,7 @@ import {
LinkTabNavHorizontal,
IconEmailSent,
InlineTextButton,
+ SocialLoginButton,
IconClose,
LayoutSingleColumn,
LayoutWrapperTopbar,
@@ -28,20 +33,36 @@ import {
Modal,
TermsOfService,
} from '../../components';
-import { LoginForm, SignupForm } from '../../forms';
+import { ConfirmSignupForm, LoginForm, SignupForm } from '../../forms';
import { TopbarContainer } from '../../containers';
-import { login, authenticationInProgress, signup } from '../../ducks/Auth.duck';
+import { login, authenticationInProgress, signup, signupWithIdp } from '../../ducks/Auth.duck';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { manageDisableScrolling } from '../../ducks/UI.duck';
import css from './AuthenticationPage.css';
+import { FacebookLogo } from './socialLoginLogos';
export class AuthenticationPageComponent extends Component {
constructor(props) {
super(props);
- this.state = { tosModalOpen: false };
+ this.state = {
+ tosModalOpen: false,
+ authError: Cookies.get('st-autherror')
+ ? JSON.parse(Cookies.get('st-autherror').replace('j:', ''))
+ : null,
+ authInfo: Cookies.get('st-authinfo')
+ ? JSON.parse(Cookies.get('st-authinfo').replace('j:', ''))
+ : null,
+ };
+ }
+
+ componentDidMount() {
+ // Remove the autherror cookie once the content is saved to state
+ // because we don't want to show the error message e.g. after page refresh
+ Cookies.remove('st-autherror');
}
+
render() {
const {
authInProgress,
@@ -54,14 +75,21 @@ export class AuthenticationPageComponent extends Component {
signupError,
submitLogin,
submitSignup,
+ confirmError,
+ submitSinguoWithIdp,
tab,
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
onManageDisableScrolling,
} = this.props;
+
+ const isConfirm = tab === 'confirm';
const isLogin = tab === 'login';
- const from = location.state && location.state.from ? location.state.from : null;
+ const locationFrom = location.state && location.state.from ? location.state.from : null;
+ const authinfoFrom =
+ this.state.authInfo && this.state.authInfo.from ? this.state.authInfo.from : null;
+ const from = locationFrom ? locationFrom : authinfoFrom ? authinfoFrom : null;
const user = ensureCurrentUser(currentUser);
const currentUserLoaded = !!user.id;
@@ -95,6 +123,16 @@ export class AuthenticationPageComponent extends Component {
);
+ const confirmErrorMessage = confirmError ? (
+
+ {isSignupEmailTakenError(confirmError) ? (
+
+ ) : (
+
+ )}
+
+ ) : null;
+
// eslint-disable-next-line no-confusing-arrow
const errorMessage = (error, message) => (error ? message : null);
const loginOrSignupError = isLogin
@@ -136,10 +174,100 @@ export class AuthenticationPageComponent extends Component {
submitSignup(params);
};
- const formContent = (
+ const handleSubmitConfirm = values => {
+ const { idpToken, email, firstName, lastName, idpId } = this.state.authInfo;
+ const { email: newEmail, firstName: newFirstName, lastName: newLastName } = values;
+
+ // Pass email, fistName or lastName to Flex API only if user has edited them
+ // sand they can't be fetched directly from idp provider (e.g. Facebook)
+
+ const authParams = {
+ ...(newEmail !== email && { email: newEmail }),
+ ...(newFirstName !== firstName && { firstName: newFirstName }),
+ ...(newLastName !== lastName && { lastName: newLastName }),
+ };
+
+ submitSinguoWithIdp({
+ idpToken,
+ idpId,
+ ...authParams,
+ });
+ };
+
+ const authWithFacebook = () => {
+ const routes = routeConfiguration();
+ const baseUrl = apiBaseUrl();
+
+ // Route where the user should be returned after authentication
+ // This is used e.g. with EditListingPage and ListingPage
+ const fromParam = from ? `from=${from}` : '';
+
+ // Default route where user is returned after successfull authentication
+ const defaultReturn = pathByRouteName('LandingPage', routes);
+ const defaultReturnParam = defaultReturn ? `&defaultReturn=${defaultReturn}` : '';
+
+ // Route for confirming user data before creating a new user
+ const defaultConfirm = pathByRouteName('ConfirmPage', routes);
+ const defaultConfirmParam = defaultConfirm ? `&defaultConfirm=${defaultConfirm}` : '';
+
+ window.location.href = `${baseUrl}/api/auth/facebook?${fromParam}${defaultReturnParam}${defaultConfirmParam}`;
+ };
+
+ const idp = this.state.authInfo
+ ? this.state.authInfo.idpId.replace(/^./, str => str.toUpperCase())
+ : null;
+
+ // Form for confirming information frm IdP (e.g. Facebook)
+ // before new user is created to Flex
+ const confirmForm = (
+
+
+
+
+
+
+
+
+ {confirmErrorMessage}
+
this.setState({ tosModalOpen: true })}
+ authInfo={this.state.authInfo}
+ />
+
+ );
+
+ // Social login buttons
+ const showSocialLogins = !!process.env.REACT_APP_FACEBOOK_APP_ID;
+ const facebookButtonText = isLogin ? (
+
+ ) : (
+
+ );
+
+ const socialLoginButtonsMaybe = showSocialLogins ? (
+
+
+
+
+
+
+
+
authWithFacebook()}>
+ {FacebookLogo}
+ {facebookButtonText}
+
+
+ ) : null;
+
+ // Tabs for SignupForm and LoginForm
+ const authenticationForms = (
{loginOrSignupError}
+
{isLogin ? (
) : (
@@ -150,9 +278,13 @@ export class AuthenticationPageComponent extends Component {
onOpenTermsOfService={() => this.setState({ tosModalOpen: true })}
/>
)}
+
+ {socialLoginButtonsMaybe}
);
+ const formContent = isConfirm ? confirmForm : authenticationForms;
+
const name = user.attributes.profile.firstName;
const email = {user.attributes.email};
@@ -265,8 +397,10 @@ AuthenticationPageComponent.defaultProps = {
currentUser: null,
loginError: null,
signupError: null,
+ confirmError: null,
tab: 'signup',
sendVerificationEmailError: null,
+ showSocialLoginsForTests: false,
};
const { bool, func, object, oneOf, shape } = PropTypes;
@@ -278,9 +412,11 @@ AuthenticationPageComponent.propTypes = {
loginError: propTypes.error,
scrollingDisabled: bool.isRequired,
signupError: propTypes.error,
+ confirmError: propTypes.error,
+
submitLogin: func.isRequired,
submitSignup: func.isRequired,
- tab: oneOf(['login', 'signup']),
+ tab: oneOf(['login', 'signup', 'confirm']),
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: propTypes.error,
@@ -295,7 +431,7 @@ AuthenticationPageComponent.propTypes = {
};
const mapStateToProps = state => {
- const { isAuthenticated, loginError, signupError } = state.Auth;
+ const { isAuthenticated, loginError, signupError, confirmError } = state.Auth;
const { currentUser, sendVerificationEmailInProgress, sendVerificationEmailError } = state.user;
return {
authInProgress: authenticationInProgress(state),
@@ -304,6 +440,7 @@ const mapStateToProps = state => {
loginError,
scrollingDisabled: isScrollingDisabled(state),
signupError,
+ confirmError,
sendVerificationEmailInProgress,
sendVerificationEmailError,
};
@@ -312,6 +449,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
submitLogin: ({ email, password }) => dispatch(login(email, password)),
submitSignup: params => dispatch(signup(params)),
+ submitSinguoWithIdp: params => dispatch(signupWithIdp(params)),
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
diff --git a/src/containers/AuthenticationPage/AuthenticationPage.test.js b/src/containers/AuthenticationPage/AuthenticationPage.test.js
index 5cf622635..834835fd0 100644
--- a/src/containers/AuthenticationPage/AuthenticationPage.test.js
+++ b/src/containers/AuthenticationPage/AuthenticationPage.test.js
@@ -6,6 +6,12 @@ import { AuthenticationPageComponent } from './AuthenticationPage';
const noop = () => null;
describe('AuthenticationPageComponent', () => {
+ // We need to overwrite REACT_APP_FACEBOOK_APP_ID before running the test
+ // to make sure it's same in local environment and in CI
+ beforeEach(() => {
+ process.env = Object.assign(process.env, { REACT_APP_FACEBOOK_APP_ID: '' });
+ });
+
it('matches snapshot', () => {
const props = {
history: { push: noop },
diff --git a/src/containers/AuthenticationPage/socialLoginLogos.js b/src/containers/AuthenticationPage/socialLoginLogos.js
new file mode 100644
index 000000000..ab63f3453
--- /dev/null
+++ b/src/containers/AuthenticationPage/socialLoginLogos.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+// This is Facebook's logo, you are not allowed to change its color
+export const FacebookLogo = (
+
+);
diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js
index dc0c1857a..7ad286b3a 100644
--- a/src/containers/CheckoutPage/CheckoutPage.js
+++ b/src/containers/CheckoutPage/CheckoutPage.js
@@ -958,7 +958,14 @@ const CheckoutPage = compose(
injectIntl
)(CheckoutPageComponent);
-CheckoutPage.setInitialValues = initialValues => setInitialValues(initialValues);
+CheckoutPage.setInitialValues = (initialValues, saveToSessionStorage = false) => {
+ if (saveToSessionStorage) {
+ const { listing, bookingData, bookingDates } = initialValues;
+ storeData(bookingData, bookingDates, listing, null, STORAGE_KEY);
+ }
+
+ return setInitialValues(initialValues);
+};
CheckoutPage.displayName = 'CheckoutPage';
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js b/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js
index 94d661bc5..8b4d87578 100644
--- a/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.duck.js
@@ -12,6 +12,10 @@ export const SAVE_PHONE_NUMBER_ERROR = 'app/ContactDetailsPage/SAVE_PHONE_NUMBER
export const SAVE_CONTACT_DETAILS_CLEAR = 'app/ContactDetailsPage/SAVE_CONTACT_DETAILS_CLEAR';
+export const RESET_PASSWORD_REQUEST = 'app/ContactDetailsPage/RESET_PASSWORD_REQUEST';
+export const RESET_PASSWORD_SUCCESS = 'app/ContactDetailsPage/RESET_PASSWORD_SUCCESS';
+export const RESET_PASSWORD_ERROR = 'app/ContactDetailsPage/RESET_PASSWORD_ERROR';
+
// ================ Reducer ================ //
const initialState = {
@@ -19,6 +23,8 @@ const initialState = {
savePhoneNumberError: null,
saveContactDetailsInProgress: false,
contactDetailsChanged: false,
+ resetPasswordInProgress: false,
+ resetPasswordError: null,
};
export default function reducer(state = initialState, action = {}) {
@@ -48,6 +54,14 @@ export default function reducer(state = initialState, action = {}) {
contactDetailsChanged: false,
};
+ case RESET_PASSWORD_REQUEST:
+ return { ...state, resetPasswordInProgress: true, resetPasswordError: null };
+ case RESET_PASSWORD_SUCCESS:
+ return { ...state, resetPasswordInProgress: false };
+ case RESET_PASSWORD_ERROR:
+ console.error(payload); // eslint-disable-line no-console
+ return { ...state, resetPasswordInProgress: false, resetPasswordError: payload };
+
default:
return state;
}
@@ -70,6 +84,16 @@ export const savePhoneNumberError = error => ({
export const saveContactDetailsClear = () => ({ type: SAVE_CONTACT_DETAILS_CLEAR });
+export const resetPasswordRequest = () => ({ type: RESET_PASSWORD_REQUEST });
+
+export const resetPasswordSuccess = () => ({ type: RESET_PASSWORD_SUCCESS });
+
+export const resetPasswordError = e => ({
+ type: RESET_PASSWORD_ERROR,
+ error: true,
+ payload: e,
+});
+
// ================ Thunks ================ //
/**
@@ -217,3 +241,11 @@ export const saveContactDetails = params => (dispatch, getState, sdk) => {
return dispatch(savePhoneNumber({ phoneNumber }));
}
};
+
+export const resetPassword = email => (dispatch, getState, sdk) => {
+ dispatch(resetPasswordRequest());
+ return sdk.passwordReset
+ .request({ email })
+ .then(() => dispatch(resetPasswordSuccess()))
+ .catch(e => dispatch(resetPasswordError(storableError(e))));
+};
diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.js b/src/containers/ContactDetailsPage/ContactDetailsPage.js
index 57c651670..4239276b5 100644
--- a/src/containers/ContactDetailsPage/ContactDetailsPage.js
+++ b/src/containers/ContactDetailsPage/ContactDetailsPage.js
@@ -20,7 +20,11 @@ import { ContactDetailsForm } from '../../forms';
import { TopbarContainer } from '../../containers';
import { isScrollingDisabled } from '../../ducks/UI.duck';
-import { saveContactDetails, saveContactDetailsClear } from './ContactDetailsPage.duck';
+import {
+ saveContactDetails,
+ saveContactDetailsClear,
+ resetPassword,
+} from './ContactDetailsPage.duck';
import css from './ContactDetailsPage.css';
export const ContactDetailsPageComponent = props => {
@@ -37,6 +41,9 @@ export const ContactDetailsPageComponent = props => {
sendVerificationEmailError,
onResendVerificationEmail,
onSubmitContactDetails,
+ onResetPassword,
+ resetPasswordInProgress,
+ resetPasswordError,
intl,
} = props;
@@ -52,12 +59,15 @@ export const ContactDetailsPageComponent = props => {
savePhoneNumberError={savePhoneNumberError}
currentUser={currentUser}
onResendVerificationEmail={onResendVerificationEmail}
+ onResetPassword={onResetPassword}
onSubmit={values => onSubmitContactDetails({ ...values, currentEmail, currentPhoneNumber })}
onChange={onChange}
inProgress={saveContactDetailsInProgress}
ready={contactDetailsChanged}
sendVerificationEmailInProgress={sendVerificationEmailInProgress}
sendVerificationEmailError={sendVerificationEmailError}
+ resetPasswordInProgress={resetPasswordInProgress}
+ resetPasswordError={resetPasswordError}
/>
) : null;
@@ -96,6 +106,8 @@ ContactDetailsPageComponent.defaultProps = {
savePhoneNumberError: null,
currentUser: null,
sendVerificationEmailError: null,
+ resetPasswordInProgress: false,
+ resetPasswordError: null,
};
ContactDetailsPageComponent.propTypes = {
@@ -111,6 +123,8 @@ ContactDetailsPageComponent.propTypes = {
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: propTypes.error,
onResendVerificationEmail: func.isRequired,
+ resetPasswordInProgress: bool,
+ resetPasswordError: propTypes.error,
// from injectIntl
intl: intlShape.isRequired,
@@ -129,6 +143,8 @@ const mapStateToProps = state => {
savePhoneNumberError,
saveContactDetailsInProgress,
contactDetailsChanged,
+ resetPasswordInProgress,
+ resetPasswordError,
} = state.ContactDetailsPage;
return {
saveEmailError,
@@ -140,6 +156,8 @@ const mapStateToProps = state => {
scrollingDisabled: isScrollingDisabled(state),
sendVerificationEmailInProgress,
sendVerificationEmailError,
+ resetPasswordInProgress,
+ resetPasswordError,
};
};
@@ -147,6 +165,7 @@ const mapDispatchToProps = dispatch => ({
onChange: () => dispatch(saveContactDetailsClear()),
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
onSubmitContactDetails: values => dispatch(saveContactDetails(values)),
+ onResetPassword: values => dispatch(resetPassword(values)),
});
const ContactDetailsPage = compose(
diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js
index e58c0741a..0e6a49765 100644
--- a/src/containers/ListingPage/ListingPage.js
+++ b/src/containers/ListingPage/ListingPage.js
@@ -123,10 +123,13 @@ export class ListingPageComponent extends Component {
confirmPaymentError: null,
};
+ const saveToSessionStorage = !this.props.currentUser;
+
const routes = routeConfiguration();
// Customize checkout page state with current listing and selected bookingDates
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routes);
- callSetInitialValues(setInitialValues, initialValues);
+
+ callSetInitialValues(setInitialValues, initialValues, saveToSessionStorage);
// Clear previous Stripe errors from store if there is any
onInitializeCardPaymentData();
@@ -602,7 +605,8 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
- callSetInitialValues: (setInitialValues, values) => dispatch(setInitialValues(values)),
+ callSetInitialValues: (setInitialValues, values, saveToSessionStorage) =>
+ dispatch(setInitialValues(values, saveToSessionStorage)),
onFetchTransactionLineItems: (bookingData, listingId, isOwnListing) =>
dispatch(fetchTransactionLineItems(bookingData, listingId, isOwnListing)),
onSendEnquiry: (listingId, message) => dispatch(sendEnquiry(listingId, message)),
diff --git a/src/containers/PasswordChangePage/PasswordChangePage.duck.js b/src/containers/PasswordChangePage/PasswordChangePage.duck.js
index 669495a79..1efb1bca0 100644
--- a/src/containers/PasswordChangePage/PasswordChangePage.duck.js
+++ b/src/containers/PasswordChangePage/PasswordChangePage.duck.js
@@ -8,12 +8,18 @@ export const CHANGE_PASSWORD_ERROR = 'app/PasswordChangePage/CHANGE_PASSWORD_ERR
export const CHANGE_PASSWORD_CLEAR = 'app/PasswordChangePage/CHANGE_PASSWORD_CLEAR';
+export const RESET_PASSWORD_REQUEST = 'app/PasswordChangePage/RESET_PASSWORD_REQUEST';
+export const RESET_PASSWORD_SUCCESS = 'app/PasswordChangePage/RESET_PASSWORD_SUCCESS';
+export const RESET_PASSWORD_ERROR = 'app/PasswordChangePage/RESET_PASSWORD_ERROR';
+
// ================ Reducer ================ //
const initialState = {
changePasswordError: null,
changePasswordInProgress: false,
passwordChanged: false,
+ resetPasswordInProgress: false,
+ resetPasswordError: null,
};
export default function reducer(state = initialState, action = {}) {
@@ -34,6 +40,14 @@ export default function reducer(state = initialState, action = {}) {
case CHANGE_PASSWORD_CLEAR:
return { ...initialState };
+ case RESET_PASSWORD_REQUEST:
+ return { ...state, resetPasswordInProgress: true, resetPasswordError: null };
+ case RESET_PASSWORD_SUCCESS:
+ return { ...state, resetPasswordInProgress: false };
+ case RESET_PASSWORD_ERROR:
+ console.error(payload); // eslint-disable-line no-console
+ return { ...state, resetPasswordInProgress: false, resetPasswordError: payload };
+
default:
return state;
}
@@ -51,6 +65,16 @@ export const changePasswordError = error => ({
export const changePasswordClear = () => ({ type: CHANGE_PASSWORD_CLEAR });
+export const resetPasswordRequest = () => ({ type: RESET_PASSWORD_REQUEST });
+
+export const resetPasswordSuccess = () => ({ type: RESET_PASSWORD_SUCCESS });
+
+export const resetPasswordError = e => ({
+ type: RESET_PASSWORD_ERROR,
+ error: true,
+ payload: e,
+});
+
// ================ Thunks ================ //
export const changePassword = params => (dispatch, getState, sdk) => {
@@ -67,3 +91,11 @@ export const changePassword = params => (dispatch, getState, sdk) => {
throw e;
});
};
+
+export const resetPassword = email => (dispatch, getState, sdk) => {
+ dispatch(resetPasswordRequest());
+ return sdk.passwordReset
+ .request({ email })
+ .then(() => dispatch(resetPasswordSuccess()))
+ .catch(e => dispatch(resetPasswordError(storableError(e))));
+};
diff --git a/src/containers/PasswordChangePage/PasswordChangePage.js b/src/containers/PasswordChangePage/PasswordChangePage.js
index ea8b54791..cd80ce957 100644
--- a/src/containers/PasswordChangePage/PasswordChangePage.js
+++ b/src/containers/PasswordChangePage/PasswordChangePage.js
@@ -18,7 +18,7 @@ import {
import { PasswordChangeForm } from '../../forms';
import { TopbarContainer } from '../../containers';
-import { changePassword, changePasswordClear } from './PasswordChangePage.duck';
+import { changePassword, changePasswordClear, resetPassword } from './PasswordChangePage.duck';
import css from './PasswordChangePage.css';
export const PasswordChangePageComponent = props => {
@@ -28,6 +28,9 @@ export const PasswordChangePageComponent = props => {
currentUser,
onChange,
onSubmitChangePassword,
+ onResetPassword,
+ resetPasswordInProgress,
+ resetPasswordError,
passwordChanged,
scrollingDisabled,
intl,
@@ -41,6 +44,9 @@ export const PasswordChangePageComponent = props => {
currentUser={currentUser}
onSubmit={onSubmitChangePassword}
onChange={onChange}
+ onResetPassword={onResetPassword}
+ resetPasswordInProgress={resetPasswordInProgress}
+ resetPasswordError={resetPasswordError}
inProgress={changePasswordInProgress}
ready={passwordChanged}
/>
@@ -79,6 +85,8 @@ export const PasswordChangePageComponent = props => {
PasswordChangePageComponent.defaultProps = {
changePasswordError: null,
currentUser: null,
+ resetPasswordInProgress: false,
+ resetPasswordError: null,
};
const { bool, func } = PropTypes;
@@ -91,6 +99,8 @@ PasswordChangePageComponent.propTypes = {
onSubmitChangePassword: func.isRequired,
passwordChanged: bool.isRequired,
scrollingDisabled: bool.isRequired,
+ resetPasswordInProgress: bool,
+ resetPasswordError: propTypes.error,
// from injectIntl
intl: intlShape.isRequired,
@@ -102,6 +112,8 @@ const mapStateToProps = state => {
changePasswordError,
changePasswordInProgress,
passwordChanged,
+ resetPasswordInProgress,
+ resetPasswordError,
} = state.PasswordChangePage;
const { currentUser } = state.user;
return {
@@ -110,12 +122,15 @@ const mapStateToProps = state => {
currentUser,
passwordChanged,
scrollingDisabled: isScrollingDisabled(state),
+ resetPasswordInProgress,
+ resetPasswordError,
};
};
const mapDispatchToProps = dispatch => ({
onChange: () => dispatch(changePasswordClear()),
onSubmitChangePassword: values => dispatch(changePassword(values)),
+ onResetPassword: values => dispatch(resetPassword(values)),
});
const PasswordChangePage = compose(
diff --git a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap b/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
index 69350a455..56fab8d81 100644
--- a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
+++ b/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
@@ -64,6 +64,8 @@ exports[`PasswordChangePage matches snapshot 1`] = `
onChange={[Function]}
onSubmit={[Function]}
ready={false}
+ resetPasswordError={null}
+ resetPasswordInProgress={false}
/>
diff --git a/src/ducks/Auth.duck.js b/src/ducks/Auth.duck.js
index 173b5fec1..db11c1ecf 100644
--- a/src/ducks/Auth.duck.js
+++ b/src/ducks/Auth.duck.js
@@ -1,5 +1,6 @@
import isEmpty from 'lodash/isEmpty';
import { clearCurrentUser, fetchCurrentUser } from './user.duck';
+import { createUserWithIdp } from '../util/api';
import { storableError } from '../util/errors';
import * as log from '../util/log';
@@ -22,6 +23,10 @@ export const SIGNUP_REQUEST = 'app/Auth/SIGNUP_REQUEST';
export const SIGNUP_SUCCESS = 'app/Auth/SIGNUP_SUCCESS';
export const SIGNUP_ERROR = 'app/Auth/SIGNUP_ERROR';
+export const CONFIRM_REQUEST = 'app/Auth/CONFIRM_REQUEST';
+export const CONFIRM_SUCCESS = 'app/Auth/CONFIRM_SUCCESS';
+export const CONFIRM_ERROR = 'app/Auth/CONFIRM_ERROR';
+
// Generic user_logout action that can be handled elsewhere
// E.g. src/reducers.js clears store as a consequence
export const USER_LOGOUT = 'app/USER_LOGOUT';
@@ -48,6 +53,10 @@ const initialState = {
// signup
signupError: null,
signupInProgress: false,
+
+ // confirm (create use with idp)
+ confirmErro: null,
+ confirmInProgress: false,
};
export default function reducer(state = initialState, action = {}) {
@@ -90,6 +99,13 @@ export default function reducer(state = initialState, action = {}) {
case SIGNUP_ERROR:
return { ...state, signupInProgress: false, signupError: payload };
+ case CONFIRM_REQUEST:
+ return { ...state, confirmInProgress: true, loginError: null, confirmError: null };
+ case CONFIRM_SUCCESS:
+ return { ...state, confirmInProgress: false, isAuthenticated: true };
+ case CONFIRM_ERROR:
+ return { ...state, confirmInProgress: false, confirmError: payload };
+
default:
return state;
}
@@ -119,6 +135,10 @@ export const signupRequest = () => ({ type: SIGNUP_REQUEST });
export const signupSuccess = () => ({ type: SIGNUP_SUCCESS });
export const signupError = error => ({ type: SIGNUP_ERROR, payload: error, error: true });
+export const confirmRequest = () => ({ type: CONFIRM_REQUEST });
+export const confirmSuccess = () => ({ type: CONFIRM_SUCCESS });
+export const confirmError = error => ({ type: CONFIRM_ERROR, payload: error, error: true });
+
export const userLogout = () => ({ type: USER_LOGOUT });
// ================ Thunks ================ //
@@ -200,3 +220,16 @@ export const signup = params => (dispatch, getState, sdk) => {
});
});
};
+
+export const signupWithIdp = params => (dispatch, getState, sdk) => {
+ dispatch(confirmRequest());
+ return createUserWithIdp(params)
+ .then(res => {
+ return dispatch(confirmSuccess());
+ })
+ .then(() => dispatch(fetchCurrentUser()))
+ .catch(e => {
+ log.error(e, 'create-user-with-idp-failed', { params });
+ return dispatch(confirmError(storableError(e)));
+ });
+};
diff --git a/src/forms/ConfirmSignupForm/ConfirmSignupForm.css b/src/forms/ConfirmSignupForm/ConfirmSignupForm.css
new file mode 100644
index 000000000..c75f298f7
--- /dev/null
+++ b/src/forms/ConfirmSignupForm/ConfirmSignupForm.css
@@ -0,0 +1,54 @@
+@import '../../marketplace.css';
+
+.root {
+ @apply --marketplaceModalFormRootStyles;
+
+ justify-content: flex-start;
+
+ @media (--viewportMedium) {
+ justify-content: space-between;
+ }
+}
+
+.name {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 24px;
+
+ @media (--viewportMedium) {
+ margin-top: 32px;
+ }
+}
+
+.firstNameRoot {
+ width: calc(34% - 9px);
+}
+
+.lastNameRoot {
+ width: calc(66% - 9px);
+}
+
+.password {
+ @apply --marketplaceModalPasswordMargins;
+}
+
+.bottomWrapper {
+ @apply --marketplaceModalBottomWrapper;
+}
+
+.bottomWrapperText {
+ @apply --marketplaceModalBottomWrapperText;
+}
+
+.termsText {
+ @apply --marketplaceModalHelperText;
+}
+
+.termsLink {
+ @apply --marketplaceModalHelperLink;
+
+ &:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+}
diff --git a/src/forms/ConfirmSignupForm/ConfirmSignupForm.js b/src/forms/ConfirmSignupForm/ConfirmSignupForm.js
new file mode 100644
index 000000000..24cd1876d
--- /dev/null
+++ b/src/forms/ConfirmSignupForm/ConfirmSignupForm.js
@@ -0,0 +1,174 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { compose } from 'redux';
+import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
+import { Form as FinalForm } from 'react-final-form';
+import classNames from 'classnames';
+import * as validators from '../../util/validators';
+import { Form, PrimaryButton, FieldTextInput } from '../../components';
+
+import css from './ConfirmSignupForm.css';
+
+const KEY_CODE_ENTER = 13;
+
+const ConfirmSignupFormComponent = props => (
+ {
+ const {
+ rootClassName,
+ className,
+ formId,
+ handleSubmit,
+ inProgress,
+ invalid,
+ intl,
+ onOpenTermsOfService,
+ authInfo,
+ } = fieldRenderProps;
+
+ // email
+ const emailLabel = intl.formatMessage({
+ id: 'ConfirmSignupForm.emailLabel',
+ });
+ const emailPlaceholder = intl.formatMessage({
+ id: 'ConfirmSignupForm.emailPlaceholder',
+ });
+ const emailRequiredMessage = intl.formatMessage({
+ id: 'ConfirmSignupForm.emailRequired',
+ });
+ const emailRequired = validators.required(emailRequiredMessage);
+ const emailInvalidMessage = intl.formatMessage({
+ id: 'ConfirmSignupForm.emailInvalid',
+ });
+ const emailValid = validators.emailFormatValid(emailInvalidMessage);
+
+ // firstName
+ const firstNameLabel = intl.formatMessage({
+ id: 'ConfirmSignupForm.firstNameLabel',
+ });
+ const firstNamePlaceholder = intl.formatMessage({
+ id: 'ConfirmSignupForm.firstNamePlaceholder',
+ });
+ const firstNameRequiredMessage = intl.formatMessage({
+ id: 'ConfirmSignupForm.firstNameRequired',
+ });
+ const firstNameRequired = validators.required(firstNameRequiredMessage);
+
+ // lastName
+ const lastNameLabel = intl.formatMessage({
+ id: 'ConfirmSignupForm.lastNameLabel',
+ });
+ const lastNamePlaceholder = intl.formatMessage({
+ id: 'ConfirmSignupForm.lastNamePlaceholder',
+ });
+ const lastNameRequiredMessage = intl.formatMessage({
+ id: 'ConfirmSignupForm.lastNameRequired',
+ });
+ const lastNameRequired = validators.required(lastNameRequiredMessage);
+
+ const classes = classNames(rootClassName || css.root, className);
+ const submitInProgress = inProgress;
+ const submitDisabled = invalid || submitInProgress;
+
+ const handleTermsKeyUp = e => {
+ // Allow click action with keyboard like with normal links
+ if (e.keyCode === KEY_CODE_ENTER) {
+ onOpenTermsOfService();
+ }
+ };
+ const termsLink = (
+
+
+
+ );
+
+ // If authInfo is not available we should not show the ConfirmForm
+ if (!authInfo) {
+ return;
+ }
+
+ // Initial values from idp provider
+ const { email, firstName, lastName, idpId } = authInfo;
+
+ return (
+
+ );
+ }}
+ />
+);
+
+ConfirmSignupFormComponent.defaultProps = { inProgress: false };
+
+const { bool, func } = PropTypes;
+
+ConfirmSignupFormComponent.propTypes = {
+ inProgress: bool,
+
+ onOpenTermsOfService: func.isRequired,
+
+ // from injectIntl
+ intl: intlShape.isRequired,
+};
+
+const ConfirmSignupForm = compose(injectIntl)(ConfirmSignupFormComponent);
+ConfirmSignupForm.displayName = 'ConfirmSignupForm';
+
+export default ConfirmSignupForm;
diff --git a/src/forms/ContactDetailsForm/ContactDetailsForm.js b/src/forms/ContactDetailsForm/ContactDetailsForm.js
index a2b0caa0b..7341b5f4b 100644
--- a/src/forms/ContactDetailsForm/ContactDetailsForm.js
+++ b/src/forms/ContactDetailsForm/ContactDetailsForm.js
@@ -22,9 +22,10 @@ const SHOW_EMAIL_SENT_TIMEOUT = 2000;
class ContactDetailsFormComponent extends Component {
constructor(props) {
super(props);
- this.state = { showVerificationEmailSentMessage: false };
+ this.state = { showVerificationEmailSentMessage: false, showResetPasswordMessage: false };
this.emailSentTimeoutId = null;
this.handleResendVerificationEmail = this.handleResendVerificationEmail.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
this.submittedValues = {};
}
@@ -43,6 +44,12 @@ class ContactDetailsFormComponent extends Component {
});
}
+ handleResetPassword() {
+ this.setState({ showResetPasswordMessage: true });
+ const email = this.props.currentUser.attributes.email;
+ this.props.onResetPassword(email);
+ }
+
render() {
return (
+
+
+ );
+
+ const resendPasswordLink = (
+
+
+
+ );
+
+ const resetPasswordLink =
+ this.state.showResetPasswordMessage || resetPasswordInProgress ? (
+ <>
+ {currentUser.attributes.email},
+ }}
+ />{' '}
+ {resendPasswordLink}
+ >
+ ) : (
+ sendPasswordLink
+ );
+
const classes = classNames(rootClassName || css.root, className);
const submittedOnce = Object.keys(this.submittedValues).length > 0;
const pristineSinceLastSubmit = submittedOnce && isEqual(values, this.submittedValues);
@@ -299,6 +334,11 @@ class ContactDetailsFormComponent extends Component {
+
+
+
+
+ );
+
+ const resendPasswordLink = (
+
+
+
+ );
+
+ const resetPasswordLink =
+ this.state.showResetPasswordMessage || resetPasswordInProgress ? (
+ <>
+ {currentUser.attributes.email},
+ }}
+ />{' '}
+ {resendPasswordLink}
+ >
+ ) : (
+ sendPasswordLink
+ );
+
return (