From 3060c16dd35d52f52371e956c4229ae38dc5ef36 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 31 Jan 2020 13:20:15 -0800 Subject: [PATCH] BREAKING CHANGES: Converted to use Redux stores. Login now working using Redux. --- client/package.json | 6 + client/src/App/App.container.jsx | 47 +---- client/src/App/App.js | 171 ++++++++---------- .../chat-window/chat-window.container.jsx | 21 ++- .../current-user-dropdown.component.jsx | 46 +++-- .../components/header/header.component.jsx | 30 +-- .../components/header/header.container.jsx | 19 +- .../src/components/header/header.styles.scss | 4 - .../job-detail-lines/job-lines.component.jsx | 1 + .../manage-sign-in-button.component.jsx | 25 ++- .../sign-in-form/sign-in-form.component.jsx | 157 +++++++++------- .../sign-in-form/sign-in-form.container.jsx | 46 ----- client/src/firebase/firebase.utils.js | 10 +- client/src/graphql/apollo-error-handling.js | 1 - client/src/graphql/resolvers.js | 31 ---- client/src/index.js | 20 +- client/src/pages/sign-in/sign-in.page.jsx | 4 +- client/src/redux/root.reducer.js | 24 +++ client/src/redux/root.saga.js | 11 ++ client/src/redux/store.js | 19 ++ client/src/redux/user/user.actions.js | 35 ++++ client/src/redux/user/user.reducer.js | 42 +++++ client/src/redux/user/user.sagas.js | 87 +++++++++ client/src/redux/user/user.selectors.js | 13 ++ client/src/redux/user/user.types.js | 17 ++ client/yarn.lock | 101 ++++++++++- 26 files changed, 628 insertions(+), 360 deletions(-) delete mode 100644 client/src/components/header/header.styles.scss delete mode 100644 client/src/components/sign-in-form/sign-in-form.container.jsx delete mode 100644 client/src/graphql/resolvers.js create mode 100644 client/src/redux/root.reducer.js create mode 100644 client/src/redux/root.saga.js create mode 100644 client/src/redux/store.js create mode 100644 client/src/redux/user/user.actions.js create mode 100644 client/src/redux/user/user.reducer.js create mode 100644 client/src/redux/user/user.sagas.js create mode 100644 client/src/redux/user/user.selectors.js create mode 100644 client/src/redux/user/user.types.js diff --git a/client/package.json b/client/package.json index 6485f4584..de8c9b498 100644 --- a/client/package.json +++ b/client/package.json @@ -26,9 +26,15 @@ "react-image-file-resizer": "^0.2.1", "react-moment": "^0.9.7", "react-number-format": "^4.3.1", + "react-redux": "^7.1.3", "react-router-dom": "^5.1.2", "react-scripts": "3.2.0", "react-trello": "^2.2.3", + "redux": "^4.0.5", + "redux-logger": "^3.0.6", + "redux-persist": "^6.0.0", + "redux-saga": "^1.1.3", + "reselect": "^4.0.0", "styled-components": "^4.4.1", "subscriptions-transport-ws": "^0.9.16" }, diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index fa2168fce..27db52240 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -1,23 +1,20 @@ -import React, { Component } from "react"; - -import App from "./App"; -import Spin from "../components/loading-spinner/loading-spinner.component"; - +import { ApolloLink } from "apollo-boost"; +import { InMemoryCache } from "apollo-cache-inmemory"; import ApolloClient from "apollo-client"; import { split } from "apollo-link"; +import { setContext } from "apollo-link-context"; import { HttpLink } from "apollo-link-http"; +import apolloLogger from "apollo-link-logger"; import { WebSocketLink } from "apollo-link-ws"; import { getMainDefinition } from "apollo-utilities"; -import { InMemoryCache } from "apollo-cache-inmemory"; -import { setContext } from "apollo-link-context"; -import { resolvers, typeDefs } from "../graphql/resolvers"; -import apolloLogger from "apollo-link-logger"; -import { ApolloLink } from "apollo-boost"; +import React, { Component } from "react"; import { ApolloProvider } from "react-apollo"; -import { persistCache } from "apollo-cache-persist"; -import initialState from "../graphql/initial-state"; +import SpinnerComponent from "../components/loading-spinner/loading-spinner.component"; //import { shouldRefreshToken, refreshToken } from "../graphql/middleware"; import errorLink from "../graphql/apollo-error-handling"; +import App from "./App"; + + class AppContainer extends Component { state = { @@ -69,14 +66,8 @@ class AppContainer extends Component { ); const authLink = setContext((_, { headers }) => { - // get the authentication token from local storage if it exists const token = localStorage.getItem("token"); - // return the headers to the context so httpLink can read them if (token) { - // if (shouldRefreshToken) { - // refreshToken(); - // } - return { headers: { ...headers, @@ -99,31 +90,13 @@ class AppContainer extends Component { const client = new ApolloClient({ link: ApolloLink.from(middlewares), cache, - typeDefs, - resolvers, connectToDevTools: true }); - client.writeData({ - data: initialState - }); - - try { - await persistCache({ - cache, - storage: window.sessionStorage, - debug: true - }); - } catch (error) { - console.error("Error restoring Apollo cache", error); - } - this.setState({ client, loaded: true }); - - //Init local state. } componentWillUnmount() {} @@ -132,7 +105,7 @@ class AppContainer extends Component { const { client, loaded } = this.state; if (!loaded) { - return ; + return ; } return ( diff --git a/client/src/App/App.js b/client/src/App/App.js index 5ff998489..ac2111752 100644 --- a/client/src/App/App.js +++ b/client/src/App/App.js @@ -1,22 +1,20 @@ -import React, { useEffect, Suspense, lazy, useState } from "react"; -import { useApolloClient, useQuery } from "@apollo/react-hooks"; -import { Switch, Route, Redirect } from "react-router-dom"; -import firebase from "../firebase/firebase.utils"; +import { useApolloClient } from "@apollo/react-hooks"; import i18next from "i18next"; - -import "./App.css"; - +import React, { lazy, Suspense, useEffect, useState } from "react"; +import { Route, Switch } from "react-router-dom"; +import ErrorBoundary from "../components/error-boundary/error-boundary.component"; //Component Imports import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; -import AlertComponent from "../components/alert/alert.component"; -import ErrorBoundary from "../components/error-boundary/error-boundary.component"; - import { auth } from "../firebase/firebase.utils"; import { UPSERT_USER } from "../graphql/user.queries"; -import { GET_CURRENT_USER, GET_LANGUAGE } from "../graphql/local.queries"; -// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries"; import PrivateRoute from "../utils/private-route"; +import "./App.css"; +import { checkUserSession } from "../redux/user/user.actions"; +import { selectCurrentUser } from "../redux/user/user.selectors"; const LandingPage = lazy(() => import("../pages/landing/landing.page")); const ManagePage = lazy(() => import("../pages/manage/manage.page")); @@ -25,110 +23,85 @@ const Unauthorized = lazy(() => import("../pages/unauthorized/unauthorized.component") ); -export default () => { - const apolloClient = useApolloClient(); - const [loaded, setloaded] = useState(false); +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = dispatch => ({ + checkUserSession: () => dispatch(checkUserSession()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(({ checkUserSession, currentUser }) => { useEffect(() => { - //Run the auth code only on the first render. - const unsubscribeFromAuth = auth.onAuthStateChanged(async user => { - console.log("Auth State Changed."); - setloaded(true); - if (user) { - let token; - token = await user.getIdToken(); - const idTokenResult = await user.getIdTokenResult(); - const hasuraClaim = - idTokenResult.claims["https://hasura.io/jwt/claims"]; - if (!hasuraClaim) { - // Check if refresh is required. - const metadataRef = firebase - .database() - .ref("metadata/" + user.uid + "/refreshTime"); + checkUserSession(); + return () => {}; + }, [checkUserSession]); - metadataRef.on("value", async () => { - // Force refresh to pick up the latest custom claims changes. - token = await user.getIdToken(true); - }); - } + // useEffect(() => { + // //Run the auth code only on the first render. + // const unsubscribeFromAuth = auth.onAuthStateChanged(async user => { + // console.log("onAuthStateChanged: User:", user); + // if (user) { + // let token; + // token = await user.getIdToken(); - //add the bearer token to the headers. - localStorage.setItem("token", token); - const now = new Date(); - window.sessionStorage.setItem(`lastTokenRefreshTime`, now); - // window.sessionStorage.setItem("user", user); + // //add the bearer token to the headers. + // localStorage.setItem("token", token); + // const now = new Date(); + // window.sessionStorage.setItem(`lastTokenRefreshTime`, now); - apolloClient - .mutate({ - mutation: UPSERT_USER, - variables: { authEmail: user.email, authToken: user.uid } - }) - .then() - .catch(error => { - console.log("User login upsert error.", error); - }); + // //token = await user.getIdToken(true); //how to refresh the token. - apolloClient.writeData({ - data: { - currentUser: { - email: user.email, - displayName: user.displayName, - token, - uid: user.uid, - photoUrl: user.photoURL, - __typename: "currentUser" - } - } - }); - } else { - apolloClient.writeData({ data: { currentUser: null } }); - localStorage.removeItem("token"); - } - }); + // apolloClient + // .mutate({ + // mutation: UPSERT_USER, + // variables: { authEmail: user.email, authToken: user.uid } + // }) + // .then() + // .catch(error => { + // console.log("User login upsert error.", error); + // }); - return function cleanup() { - unsubscribeFromAuth(); - }; - }, [apolloClient]); - const HookCurrentUser = useQuery(GET_CURRENT_USER); - const HookLanguage = useQuery(GET_LANGUAGE); + // } else { + // localStorage.removeItem("token"); + // } + // setloaded(true); + // }); - if (!loaded) return ; - if (HookCurrentUser.loading || HookLanguage.loading) - return ; - if (HookCurrentUser.error || HookLanguage.error) - return ( - - ); + // return function cleanup() { + // unsubscribeFromAuth(); + // }; + // }, [apolloClient]); - if (HookLanguage.data.language) - i18next.changeLanguage(HookLanguage.data.language, (err, t) => { + if (false) + i18next.changeLanguage("en_US", (err, t) => { if (err) return console.log("Error encountered when changing languages.", err); }); + console.log("currentUser", currentUser); + if (currentUser.authorized === null) { + //TODO: Translate this. + return ; + } + return (
}> - - - - HookCurrentUser.data.currentUser ? ( - - ) : ( - - ) - } - /> + + + + + @@ -136,4 +109,4 @@ export default () => {
); -}; +}); diff --git a/client/src/components/chat-window/chat-window.container.jsx b/client/src/components/chat-window/chat-window.container.jsx index cd70e1060..940e7b416 100644 --- a/client/src/components/chat-window/chat-window.container.jsx +++ b/client/src/components/chat-window/chat-window.container.jsx @@ -1,8 +1,22 @@ import React, { useState } from "react"; import ChatWindowComponent from "./chat-window.component"; import { Button } from "antd"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectCurrentUser } from "../../redux/user/user.selectors"; -export default function ChatWindowContainer() { +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = dispatch => ({ + // signOutStart: () => dispatch(signOutStart()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(function ChatWindowContainer({ currentUser }) { const [visible, setVisible] = useState(false); return (
@@ -10,9 +24,10 @@ export default function ChatWindowContainer() {
); -} +}); diff --git a/client/src/components/current-user-dropdown/current-user-dropdown.component.jsx b/client/src/components/current-user-dropdown/current-user-dropdown.component.jsx index 5f23bca64..f7735f5d0 100644 --- a/client/src/components/current-user-dropdown/current-user-dropdown.component.jsx +++ b/client/src/components/current-user-dropdown/current-user-dropdown.component.jsx @@ -1,17 +1,29 @@ -import { useApolloClient, useQuery } from "@apollo/react-hooks"; +import { useApolloClient } from "@apollo/react-hooks"; import { Avatar, Col, Dropdown, Icon, Menu, Row } from "antd"; import i18next from "i18next"; import React from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; import { Link } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; import UserImage from "../../assets/User.svg"; -import { GET_CURRENT_USER } from "../../graphql/local.queries"; -import AlertComponent from "../alert/alert.component"; +import { selectCurrentUser } from "../../redux/user/user.selectors"; import SignOut from "../sign-out/sign-out.component"; -export default function CurrentUserDropdown() { +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = dispatch => ({ + // signOutStart: () => dispatch(signOutStart()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(function CurrentUserDropdown({ currentUser }) { const { t } = useTranslation(); - const { loading, error, data } = useQuery(GET_CURRENT_USER); + const client = useApolloClient(); const handleMenuClick = e => { @@ -24,43 +36,39 @@ export default function CurrentUserDropdown() { } }; const menu = ( - + - {t("menus.currentuser.profile")} + {t("menus.currentuser.profile")} - + {t("menus.currentuser.languageselector")} - }> - + } + > + {t("general.languages.english")} - + {t("general.languages.french")} - + {t("general.languages.spanish")} ); - if (loading) return null; - if (error) return ; - - const { currentUser } = data; - return ( - + {currentUser.displayName || t("general.labels.unknown")} @@ -68,4 +76,4 @@ export default function CurrentUserDropdown() { ); -} +}); diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 5a59274c4..0067db0d6 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -6,45 +6,45 @@ import { Link } from "react-router-dom"; import CurrentUserDropdown from "../current-user-dropdown/current-user-dropdown.component"; import GlobalSearch from "../global-search/global-search.component"; import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component"; -import "./header.styles.scss"; export default ({ landingHeader, navItems, selectedNavItem }) => { const apolloClient = useApolloClient(); const { t } = useTranslation(); - + const handleClick = e => { apolloClient.writeData({ data: { selectedNavItem: e.key } }); }; return ( - + + mode="horizontal" + > - - - + + + {t("menus.header.home")} - - - + + + {t("menus.header.activejobs")} - - - + + + {t("menus.header.availablejobs")} diff --git a/client/src/components/header/header.container.jsx b/client/src/components/header/header.container.jsx index 299d55caf..d971b7c46 100644 --- a/client/src/components/header/header.container.jsx +++ b/client/src/components/header/header.container.jsx @@ -1,25 +1,8 @@ import React from "react"; -import "./header.styles.scss"; -import { useQuery } from "react-apollo"; -// //import { -// GET_LANDING_NAV_ITEMS, -// GET_NAV_ITEMS -// } from "../../graphql/metadata.queries"; -import { GET_CURRENT_SELECTED_NAV_ITEM } from "../../graphql/local.queries"; -//import LoadingSpinner from "../loading-spinner/loading-spinner.component"; -//import AlertComponent from "../alert/alert.component"; import HeaderComponent from "./header.component"; export default ({ landingHeader, signedIn }) => { - const hookSelectedNavItem = useQuery(GET_CURRENT_SELECTED_NAV_ITEM); - - const { selectedNavItem } = hookSelectedNavItem.data; - - console.log("selectedNavItem", selectedNavItem); return ( - + ); }; diff --git a/client/src/components/header/header.styles.scss b/client/src/components/header/header.styles.scss deleted file mode 100644 index 3766e1b90..000000000 --- a/client/src/components/header/header.styles.scss +++ /dev/null @@ -1,4 +0,0 @@ -.header{ - text-align: center; - width: 100%; -} \ No newline at end of file diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index 60da20fd3..a02f0bed8 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -132,6 +132,7 @@ export default function JobLinesComponent({ /> ); }} + {...formItemLayout} size="small" pagination={{ position: "bottom", defaultPageSize: 50 }} columns={columns.map(item => ({ ...item }))} diff --git a/client/src/components/manage-sign-in-button/manage-sign-in-button.component.jsx b/client/src/components/manage-sign-in-button/manage-sign-in-button.component.jsx index f2ae7dd19..85a87a58c 100644 --- a/client/src/components/manage-sign-in-button/manage-sign-in-button.component.jsx +++ b/client/src/components/manage-sign-in-button/manage-sign-in-button.component.jsx @@ -4,18 +4,23 @@ import { Link } from "react-router-dom"; import { GET_CURRENT_USER } from "../../graphql/local.queries"; import { Icon } from "antd"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectCurrentUser } from "../../redux/user/user.selectors"; -export default function ManageSignInButton() { - const { - loading, - error, - data: { currentUser } - } = useQuery(GET_CURRENT_USER); +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); - if (loading) return ; - if (error) return error.message; +const mapDispatchToProps = dispatch => ({ + // signOutStart: () => dispatch(signOutStart()) +}); - return currentUser ? ( +export default connect( + mapStateToProps, + mapDispatchToProps +)(function ManageSignInButton({ currentUser }) { + return currentUser.isAuthorized ? (
{" "} @@ -31,4 +36,4 @@ export default function ManageSignInButton() {
); -} +}); diff --git a/client/src/components/sign-in-form/sign-in-form.component.jsx b/client/src/components/sign-in-form/sign-in-form.component.jsx index 2038e12d8..e41aeff39 100644 --- a/client/src/components/sign-in-form/sign-in-form.component.jsx +++ b/client/src/components/sign-in-form/sign-in-form.component.jsx @@ -1,76 +1,97 @@ +import { Button, Form, Icon, Input } from "antd"; import React from "react"; -import { auth } from "../../firebase/firebase.utils"; -import { Form, Icon, Input, Button, Alert } from "antd"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import Logo from "../../assets/logo240.png"; +import { emailSignInStart } from "../../redux/user/user.actions"; +import { + selectCurrentUser, + selectSignInError +} from "../../redux/user/user.selectors"; +import { Redirect } from "react-router-dom"; -class SignInForm extends React.Component { - constructor() { - super(); - this.state = { - errorMessage: null - }; - } +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + signInError: selectSignInError +}); - handleSubmit = e => { - e.preventDefault(); +const mapDispatchToProps = dispatch => ({ + emailSignInStart: (email, password) => + dispatch(emailSignInStart({ email, password })) +}); - this.props.form.validateFields(async (err, values) => { - if (!err) { - const { email, password } = values; - try { - await auth.signInWithEmailAndPassword(email, password); - - this.props.form.resetFields(); - } catch (error) { - this.setState({ ...this.state, errorMessage: error.message }); +export default connect( + mapStateToProps, + mapDispatchToProps +)( + Form.create({ name: "sign_in" })(function SignInComponent({ + form, + emailSignInStart, + currentUser, + signInError + }) { + const handleSubmit = e => { + e.preventDefault(); + form.validateFields(async (err, values) => { + if (!err) { + const { email, password } = values; + emailSignInStart(email, password); + //Try to do the login using a saga here. } - } - }); - }; - - render() { - const { getFieldDecorator } = this.props.form; - const { errorMessage } = this.state; + }); + }; + console.log("currentUser", currentUser); + const { getFieldDecorator } = form; return ( -
- - {getFieldDecorator("email", { - rules: [ - { - type: "email", - message: "Please enter a valid email." - }, - { - required: true, - message: "Please your email." - } - ] - })()} - - - {getFieldDecorator("password", { - rules: [{ required: true, message: "Please enter your password." }] - })( - } - type="password" - placeholder="Password" - /> - )} - - -
Forgot password
- -
- {errorMessage ? : null} - +
+ {currentUser.authorized === true ? : null} + + Bodyshop.app + +
+ + {getFieldDecorator("email", { + rules: [ + { + type: "email", + message: "Please enter a valid email." + }, + { + required: true, + message: "Please your email." + } + ] + })()} + + + {getFieldDecorator("password", { + rules: [ + { required: true, message: "Please enter your password." } + ] + })( + + } + type="password" + placeholder="Password" + /> + )} + + +
Forgot password
+ +
+ {signInError ?
{signInError.message}
: null} +
+
); - } -} -export default Form.create({ name: "sign_in" })(SignInForm); + }) +); diff --git a/client/src/components/sign-in-form/sign-in-form.container.jsx b/client/src/components/sign-in-form/sign-in-form.container.jsx deleted file mode 100644 index 85ca228d4..000000000 --- a/client/src/components/sign-in-form/sign-in-form.container.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import { ApolloConsumer } from "react-apollo"; -import SignInFormComponent from "./sign-in-form.component"; -import { Row, Col, Layout, Typography } from "antd"; -import FooterComponent from "../footer/footer.component"; -import Logo from "../../assets/logo240.png"; - -const { Content, Footer } = Layout; - -export default function SignInFormContainer() { - return ( - - {client => { - return ( - - - - -
- Bodyshop.app -
- - - Bodyshop.app - -
- - - - - -
-
- -
-
- ); - }} -
- ); -} diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js index 4a2a8fae7..56426cfb7 100644 --- a/client/src/firebase/firebase.utils.js +++ b/client/src/firebase/firebase.utils.js @@ -38,6 +38,14 @@ export const firestore = firebase.firestore(); const provider = new firebase.auth.GoogleAuthProvider(); provider.setCustomParameters({ prompt: "select_account" }); -export const signInWithGoogle = () => auth.signInWithPopup(provider); export default firebase; + +export const getCurrentUser = () => { + return new Promise((resolve, reject) => { + const unsubscribe = auth.onAuthStateChanged(userAuth => { + unsubscribe(); + resolve(userAuth); + }, reject); + }); +}; diff --git a/client/src/graphql/apollo-error-handling.js b/client/src/graphql/apollo-error-handling.js index 7b5443ede..28394033b 100644 --- a/client/src/graphql/apollo-error-handling.js +++ b/client/src/graphql/apollo-error-handling.js @@ -1,5 +1,4 @@ import { onError } from "apollo-link-error"; -import { Observable } from "apollo-link"; import { auth } from "../firebase/firebase.utils"; //https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth diff --git a/client/src/graphql/resolvers.js b/client/src/graphql/resolvers.js deleted file mode 100644 index b293bfd8b..000000000 --- a/client/src/graphql/resolvers.js +++ /dev/null @@ -1,31 +0,0 @@ -import { gql } from "apollo-boost"; -import { GET_CURRENT_USER } from "./local.queries"; - -export const typeDefs = gql` - extend type Mutation { - SetCurrentUser(user: User!): User! - } - - extend type User { - email: String! - displayName: String! - token: String! - } - - extend type Jobs { - id: uuid! - } -`; - -export const resolvers = { - Mutation: { - setCurrentUser: (_root, { user }, { cache }) => { - cache.writeQuery({ - query: GET_CURRENT_USER, - data: { currentUser: user } - }); - - return user; - } - } -}; diff --git a/client/src/index.js b/client/src/index.js index b29d04ea2..8cb47c9a0 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -3,16 +3,26 @@ import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import "./translations/i18n"; import * as serviceWorker from "./serviceWorker"; - -import "./index.css"; +import { Provider } from "react-redux"; +import { PersistGate } from "redux-persist/integration/react"; +import { store, persistor } from "./redux/store"; import AppContainer from "./App/App.container"; +import "./index.css"; +import LoadingSpinner from "./components/loading-spinner/loading-spinner.component"; require("dotenv").config(); ReactDOM.render( - - - , + + + } + persistor={persistor} + > + + + + , document.getElementById("root") ); diff --git a/client/src/pages/sign-in/sign-in.page.jsx b/client/src/pages/sign-in/sign-in.page.jsx index 2de375129..15f6566a2 100644 --- a/client/src/pages/sign-in/sign-in.page.jsx +++ b/client/src/pages/sign-in/sign-in.page.jsx @@ -1,6 +1,6 @@ import React from "react"; -import SignInContainer from "../../components/sign-in-form/sign-in-form.container"; +import SignIn from "../../components/sign-in-form/sign-in-form.component"; export default () => { - return ; + return ; }; diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js new file mode 100644 index 000000000..a219a04af --- /dev/null +++ b/client/src/redux/root.reducer.js @@ -0,0 +1,24 @@ +import { combineReducers } from "redux"; +import { persistReducer } from "redux-persist"; +import storage from "redux-persist/lib/storage"; + +import userReducer from "./user/user.reducer"; +// import cartReducer from './cart/cart.reducer'; +// import directoryReducer from './directory/directory.reducer'; +// import shopReducer from './shop/shop.reducer'; + +const persistConfig = { + key: "root", + storage, + //whitelist: ["cart"] + blacklist: ["user"] +}; + +const rootReducer = combineReducers({ + user: userReducer + // cart: cartReducer, + // directory: directoryReducer, + // shop: shopReducer +}); + +export default persistReducer(persistConfig, rootReducer); diff --git a/client/src/redux/root.saga.js b/client/src/redux/root.saga.js new file mode 100644 index 000000000..03f871ee8 --- /dev/null +++ b/client/src/redux/root.saga.js @@ -0,0 +1,11 @@ +import { all, call } from "redux-saga/effects"; + +//List of all Sagas +// import { shopSagas } from "./shop/shop.sagas"; +import { userSagas } from "./user/user.sagas"; +//import { cartSagas } from "./cart/cart.sagas"; + +export default function* rootSaga() { + //All starts all the Sagas concurrently. + yield all([call(userSagas)]); +} diff --git a/client/src/redux/store.js b/client/src/redux/store.js new file mode 100644 index 000000000..736d68033 --- /dev/null +++ b/client/src/redux/store.js @@ -0,0 +1,19 @@ +import { createStore, applyMiddleware } from "redux"; +import { persistStore } from "redux-persist"; +import logger from "redux-logger"; +import createSagaMiddleware from "redux-saga"; +import rootReducer from "./root.reducer"; +import rootSaga from "./root.saga"; + +const sagaMiddleWare = createSagaMiddleware(); +const middlewares = [sagaMiddleWare]; +if (process.env.NODE_ENV === "development") { + middlewares.push(logger); +} + +export const store = createStore(rootReducer, applyMiddleware(...middlewares)); +sagaMiddleWare.run(rootSaga); + +export const persistor = persistStore(store); + +export default { store, persistStore }; diff --git a/client/src/redux/user/user.actions.js b/client/src/redux/user/user.actions.js new file mode 100644 index 000000000..334ad2d8a --- /dev/null +++ b/client/src/redux/user/user.actions.js @@ -0,0 +1,35 @@ +import UserActionTypes from "./user.types"; + +export const signInSuccess = user => ({ + type: UserActionTypes.SIGN_IN_SUCCESS, + payload: user +}); +export const signInFailure = errorMsg => ({ + type: UserActionTypes.SIGN_IN_FAILURE, + payload: errorMsg +}); + +export const emailSignInStart = emailAndPassword => ({ + type: UserActionTypes.EMAIL_SIGN_IN_START, + payload: emailAndPassword +}); + +export const checkUserSession = () => ({ + type: UserActionTypes.CHECK_USER_SESSION +}); + +export const signOutStart = () => ({ + type: UserActionTypes.SIGN_OUT_START +}); +export const signOutSuccess = () => ({ + type: UserActionTypes.SIGN_OUT_SUCCESS +}); + +export const signOutFailure = error => ({ + type: UserActionTypes.SIGN_OUT_FAILURE, + payload: error +}); + +export const unauthorizedUser = () => ({ + type: UserActionTypes.UNAUTHORIZED_USER +}); diff --git a/client/src/redux/user/user.reducer.js b/client/src/redux/user/user.reducer.js new file mode 100644 index 000000000..fa9535d9e --- /dev/null +++ b/client/src/redux/user/user.reducer.js @@ -0,0 +1,42 @@ +import UserActionTypes from "./user.types"; + +const INITIAL_STATE = { + currentUser: { + authorized: null + }, + error: null +}; + +const userReducer = (state = INITIAL_STATE, action) => { + switch (action.type) { + case UserActionTypes.SIGN_IN_SUCCESS: + return { + ...state, + currentUser: action.payload, + error: null + }; + case UserActionTypes.SIGN_OUT_SUCCESS: + return { + ...state, + currentUser: { authorized: false }, + error: null + }; + case UserActionTypes.UNAUTHORIZED_USER: + return { + ...state, + error: null, + currentUser: { authorized: false } + }; + case UserActionTypes.SIGN_IN_FAILURE: + case UserActionTypes.SIGN_OUT_FAILURE: + case UserActionTypes.EMAIL_SIGN_UP_FAILURE: + return { + ...state, + error: action.payload + }; + default: + return state; + } +}; + +export default userReducer; diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js new file mode 100644 index 000000000..f67356042 --- /dev/null +++ b/client/src/redux/user/user.sagas.js @@ -0,0 +1,87 @@ +import { all, call, put, takeLatest } from "redux-saga/effects"; +import { auth, getCurrentUser } from "../../firebase/firebase.utils"; +import { + signInFailure, + signInSuccess, + signOutFailure, + signOutSuccess, + unauthorizedUser +} from "./user.actions"; +import UserActionTypes from "./user.types"; + +// export function* getSnapshotFromUserAuth(userAuth) { +// try { +// const userRef = yield call(createUserProfileDocument, userAuth); +// //const userSnapshot = yield userRef.get(); +// } catch (error) { +// yield put(signInFailure(error)); +// } +// } + +export function* signInWithEmail({ payload: { email, password } }) { + try { + const { user } = yield auth.signInWithEmailAndPassword(email, password); + yield put( + signInSuccess({ + id: user.uid, + email: user.email, + displayName: user.displayName, + authorized: true + }) + ); + } catch (error) { + yield put(signInFailure(error)); + } +} +//This is the listener fo rthe call, and when it finds it, it triggers somethign else. +export function* onEmailSignInStart() { + yield takeLatest(UserActionTypes.EMAIL_SIGN_IN_START, signInWithEmail); +} + +export function* isUserAuthenticated() { + try { + const user = yield getCurrentUser(); + if (!user) { + yield put(unauthorizedUser()); + return; + } + yield put( + signInSuccess({ + id: user.uid, + email: user.email, + displayName: user.displayName, + authorized: true + }) + ); + } catch (error) { + yield put(signInFailure(error)); + } +} + +export function* onCheckUserSession() { + yield takeLatest(UserActionTypes.CHECK_USER_SESSION, isUserAuthenticated); +} + +export function* signOutStart() { + try { + yield auth.signOut(); + yield put(signOutSuccess()); + } catch (error) { + yield put(signOutFailure(error.message)); + } +} + +export function* onSignOutStart() { + yield takeLatest(UserActionTypes.SIGN_OUT_START, signOutStart); +} + +export function* userSagas() { + yield all([ + // call(onGoogleSignInStart), + call(onEmailSignInStart), + call(onCheckUserSession), + call(onSignOutStart) + // call(onEmailSignUpStart), + // call(onEmailSignUpSuccess) + ]); +} diff --git a/client/src/redux/user/user.selectors.js b/client/src/redux/user/user.selectors.js new file mode 100644 index 000000000..ecddf5c33 --- /dev/null +++ b/client/src/redux/user/user.selectors.js @@ -0,0 +1,13 @@ +import { createSelector } from "reselect"; + +const selectUser = state => state.user; + +export const selectCurrentUser = createSelector( + [selectUser], + user => user.currentUser +); + +export const selectSignInError = createSelector( + [selectUser], + user => user.error +); diff --git a/client/src/redux/user/user.types.js b/client/src/redux/user/user.types.js new file mode 100644 index 000000000..07a32eb3b --- /dev/null +++ b/client/src/redux/user/user.types.js @@ -0,0 +1,17 @@ +const UserActionTypes = { + SET_CURRENT_USER: "SET_CURRENT_USER", + GOOGLE_SIGN_IN_START: "GOOGLE_SIGN_IN_START", + SIGN_IN_SUCCESS: "SIGN_IN_SUCCESS", + SIGN_IN_FAILURE: "SIGN_IN_FAILURE", + EMAIL_SIGN_IN_START: "EMAIL_SIGN_IN_START", + CHECK_USER_SESSION: "CHECK_USER_SESSION", + SIGN_OUT_START: "SIGN_OUT_START", + SIGN_OUT_SUCCESS: "SIGN_OUT_SUCCESS", + SIGN_OUT_FAILURE: "SIGN_OUT_FAILURE", + EMAIL_SIGN_UP_START: "EMAIL_SIGN_UP_START", + EMAIL_SIGN_UP_SUCCESS: "EMAIL_SIGN_UP_SUCCESS", + EMAIL_SIGN_UP_FAILURE: "EMAIL_SIGN_UP_FAILURE", + UNAUTHORIZED_USER: "UNAUTHORIZED_USER" + }; + export default UserActionTypes; + \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index d73e081cd..5d1c69033 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1026,6 +1026,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.5.5": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.4.0", "@babel/template@^7.6.0", "@babel/template@^7.7.4": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b" @@ -1569,6 +1576,50 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@redux-saga/core@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4" + integrity sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg== + dependencies: + "@babel/runtime" "^7.6.3" + "@redux-saga/deferred" "^1.1.2" + "@redux-saga/delay-p" "^1.1.2" + "@redux-saga/is" "^1.1.2" + "@redux-saga/symbols" "^1.1.2" + "@redux-saga/types" "^1.1.0" + redux "^4.0.4" + typescript-tuple "^2.2.1" + +"@redux-saga/deferred@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.1.2.tgz#59937a0eba71fff289f1310233bc518117a71888" + integrity sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ== + +"@redux-saga/delay-p@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.1.2.tgz#8f515f4b009b05b02a37a7c3d0ca9ddc157bb355" + integrity sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g== + dependencies: + "@redux-saga/symbols" "^1.1.2" + +"@redux-saga/is@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.2.tgz#ae6c8421f58fcba80faf7cadb7d65b303b97e58e" + integrity sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w== + dependencies: + "@redux-saga/symbols" "^1.1.2" + "@redux-saga/types" "^1.1.0" + +"@redux-saga/symbols@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.2.tgz#216a672a487fc256872b8034835afc22a2d0595d" + integrity sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ== + +"@redux-saga/types@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204" + integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg== + "@svgr/babel-plugin-add-jsx-attribute@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" @@ -10425,6 +10476,18 @@ react-redux@^5.0.7: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-router-dom@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" @@ -10691,7 +10754,19 @@ redux-logger@^3.0.6: dependencies: deep-diff "^0.3.5" -redux@^4.0.0: +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + +redux-saga@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112" + integrity sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw== + dependencies: + "@redux-saga/core" "^1.1.3" + +redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -10888,6 +10963,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -12269,6 +12349,25 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-compare@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425" + integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA== + dependencies: + typescript-logic "^0.0.0" + +typescript-logic@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196" + integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q== + +typescript-tuple@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2" + integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q== + dependencies: + typescript-compare "^0.0.2" + ua-parser-js@^0.7.18: version "0.7.21" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"