diff --git a/api/src/controllers/userController.js b/api/src/controllers/userController.js index 8306594e6..d8c3d00e6 100644 --- a/api/src/controllers/userController.js +++ b/api/src/controllers/userController.js @@ -155,15 +155,18 @@ const registerUser = async (req, res) => { }) } + const responseData = {} + if (isCognito) { await registerCognito(username, password) + responseData.message = 'Code for sign up confirmation was sent to your email' + responseData.forSignUpConfirmation = true } else { await registerLocal(username, password) + responseData.message = 'The user has been registered' } - res.status(201).send({ - message: 'The user has been registered' - }) + res.status(201).send(responseData) } catch (err) { res.status(409).json({ error: err.message }) } diff --git a/package.json b/package.json index e7cd96ad6..de507d0a0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", - "aws-amplify": "^3.3.27", + "aws-amplify": "^4.3.21", "axios": "^0.21.1", "classnames": "^2.3.1", "dayjs": "^1.10.7", diff --git a/public/index.html b/public/index.html index 5583c169a..8cd2bf10b 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,7 @@
+
diff --git a/src/App.jsx b/src/App.jsx index efeb1df5d..49c220d44 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,16 +5,20 @@ import { QueryClientProvider } from "react-query"; import customParseFormat from "dayjs/plugin/customParseFormat"; import { Switch, BrowserRouter as Router, Route } from "react-router-dom"; -import { SignInPage } from "screens/auth/sign-in"; -import { SignUpPage } from "screens/auth/sign-up"; -import { ForgotPasswordPage } from "screens/auth/forgot-password"; -import { AdditionalInfoPage } from "screens/auth/additional-info"; +import { + SignInPage, + SignUpPage, + ConfirmEmailPage, + ForgotPasswordPage, + AdditionalInfoPage, +} from "screens/auth"; import { PrivateRoute } from "components/privateRoute"; import { SideBarNavigation } from "common/side-bar-navigation"; import { Routes } from "routes"; import ScrollToTop from "utils/scrollToTop"; +import { Routes as PathRoutes } from "constants/routes"; import { SearchBarProvider } from "providers/search-bar"; import { queryClient } from "./reactQuery"; @@ -43,14 +47,21 @@ function App() { - - - + + + + diff --git a/src/actions/auth.js b/src/actions/auth.js index 8a7907d1c..d33e61349 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -2,17 +2,7 @@ import Amplify, { Auth } from "aws-amplify"; import { api } from "api"; import { authConfig } from "config/amplify"; -import { getErrorMessage } from "utils/error"; -import { setCurrentUser } from "store/user/slices"; -import { - USER_LOGOUT, - USER_LOGIN_FAIL, - USER_LOGIN_SUCCESS, - ACCESS_TOKEN_SUCCESS, -} from "constants/userConstants"; - -// import { news } from "./community"; -// import { visitCommunity } from "./communityActions"; +import { USER_LOGOUT } from "constants/userConstants"; const isCognito = process.env.REACT_APP_AUTH_METHOD === "cognito"; @@ -20,7 +10,6 @@ if (isCognito) { Amplify.configure({ Auth: { ...authConfig } }); } -// TODO: Move to store/thunk when reduxjs/toolkit will be setuped const makeLogout = (dispatch) => { // dispatch({ type: USER_DETAILS_FAIL, payload: message }); localStorage.clear(); @@ -39,106 +28,6 @@ export const logout = () => async (dispatch) => { } }; -// TODO: Move to store/thunk when reduxjs/toolkit will be setuped -export const getAccessToken = () => async (dispatch) => { - try { - const response = await api.auth.getToken(); - - if (response.status !== 201) { - makeLogout(dispatch); - return Promise.reject(); - } - - dispatch({ type: ACCESS_TOKEN_SUCCESS, payload: true }); - return Promise.resolve(); - } catch (error) { - makeLogout(dispatch); - return Promise.reject(error); - } -}; - -// TODO: Move to store/thunk when reduxjs/toolkit will be setuped -export const login = - ({ name, password }) => - async (dispatch) => { - try { - let authData = {}; - let response; - if (isCognito) { - response = await Auth.signIn(name, password); - const id = response?.attributes?.sub || ""; - authData = { - id, - token: response?.signInUserSession?.idToken?.jwtToken || "", - }; - await api.auth.login({ id }); - } else { - response = await api.auth.login({ username: name, password }); - authData = response.data; - } - - localStorage.setItem("userInfo", JSON.stringify(authData)); - - const profile = await api.user.get({ id: authData.id }); - dispatch(setCurrentUser({ ...response, ...profile?.data?.results })); - - // await getAccessToken()(dispatch); // access token is already received in login response - // const community = await news()(dispatch); - // await visitCommunity(community.id)(dispatch); - - dispatch({ type: USER_LOGIN_SUCCESS, payload: response }); - return Promise.resolve(response); - } catch (error) { - dispatch({ type: USER_LOGIN_FAIL, payload: getErrorMessage(error) }); - return Promise.reject(error); - } - }; - -export const register = - ({ name, password }) => - async (dispatch) => { - try { - await api.auth.register({ username: name, password }); - - // no auto login for cognito since it needs to confirm email with a code - if (!isCognito) { - await login({ name, password })(dispatch); - } - - return Promise.resolve(); - } catch (error) { - return Promise.reject(error); - } - }; - -// TODO: Why there is no functionality to request code without cognito? -export const requestCode = async (username) => { - try { - let response; - if (isCognito) { - const data = await api.auth.forgotPassword(username); - response = - data.data.details.CodeDeliveryDetails.AttributeName.split("_").join( - " " - ); - } - return Promise.resolve(response); - } catch (error) { - return Promise.reject(error); - } -}; - -export const resetPassword = async ({ username, code, password }) => { - try { - if (isCognito) { - await api.auth.forgotPasswordSubmit(username, code, password); - } - return Promise.resolve(); - } catch (error) { - return Promise.reject(error); - } -}; - export const changePassword = async ({ oldPassword, newPassword }) => { try { if (isCognito) { diff --git a/src/api/auth.js b/src/api/auth.js index dc367cbee..257ba3c62 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -21,8 +21,11 @@ export const forgotPasswordSubmit = ({ username, code, newPassword }) => newPassword, }); -export const confirmSignup = ({ username, code }) => +export const confirmEmail = ({ email, code }) => apiInstance.post("/users/confirm-sign-up", { - username, code, + username: email, }); + +export const resendEmailCode = ({ email }) => + apiInstance.post("/users/resend-sign-up-code", { username: email }); diff --git a/src/assets/icons/congratulations.svg b/src/assets/icons/congratulations.svg deleted file mode 100644 index 749fff465..000000000 --- a/src/assets/icons/congratulations.svg +++ /dev/null @@ -1,364 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/images/congratulations.png b/src/assets/images/congratulations.png new file mode 100644 index 000000000..1d9e0b385 Binary files /dev/null and b/src/assets/images/congratulations.png differ diff --git a/src/assets/images/email.png b/src/assets/images/email.png new file mode 100644 index 000000000..a7ec57949 Binary files /dev/null and b/src/assets/images/email.png differ diff --git a/src/common/checkbox/index.jsx b/src/common/checkbox/index.jsx index bd89a5042..3ac463feb 100644 --- a/src/common/checkbox/index.jsx +++ b/src/common/checkbox/index.jsx @@ -6,7 +6,13 @@ import { Icon } from "common/icon"; import "./styles.scss"; -export const Checkbox = ({ value = false, onChange, title, error }) => { +export const Checkbox = ({ + title, + error, + onChange, + children, + value = false, +}) => { return (
@@ -15,7 +21,12 @@ export const Checkbox = ({ value = false, onChange, title, error }) => {
- {title &&

{title}

} + {(title || children) && ( +
+ {title &&
{title}
} + {children && children} +
+ )} ); }; diff --git a/src/common/checkbox/styles.scss b/src/common/checkbox/styles.scss index 1e348aa1d..2df7f31aa 100644 --- a/src/common/checkbox/styles.scss +++ b/src/common/checkbox/styles.scss @@ -1,12 +1,13 @@ +@import "src/scss/mixins"; @import "src/scss/variables"; .pf-checkbox { cursor: pointer; user-select: none; - display: flex; - align-items: center; - flex-direction: row; + height: 24px; + gap: $gap-smallest; + @include flexRow(center, center); input[type="checkbox"] { height: 0; @@ -37,17 +38,19 @@ &-error { border-color: #da3443; } - } - &:hover .checkbox { - border-color: #cadcd6; + &:hover { + border-color: #cadcd6; + } } - .checkbox-title { - color: #eeefef; - margin-left: 10px; - user-select: none; - font: $font-body; + .checkbox-title-container { + gap: 4px; + @include flexRow(center, flex-start); + + h5 { + user-select: none; + } } input[type="checkbox"]:checked + .checkbox { diff --git a/src/common/icon/index.jsx b/src/common/icon/index.jsx index 897e30ef8..5fe267259 100644 --- a/src/common/icon/index.jsx +++ b/src/common/icon/index.jsx @@ -41,7 +41,6 @@ import { ReactComponent as GamburgerIcon } from "assets/icons/gamburger.svg"; import { ReactComponent as LogoutIcon } from "assets/icons/logout.svg"; import { ReactComponent as TrashIcon } from "assets/icons/trash.svg"; import { ReactComponent as CropIcon } from "assets/icons/crop.svg"; -import { ReactComponent as CongratulationsIcon } from "assets/icons/congratulations.svg"; import { ReactComponent as UsersIcon } from "assets/icons/users.svg"; import { ReactComponent as SearchIcon } from "assets/icons/search.svg"; import { ReactComponent as PlusIcon } from "assets/icons/plus.svg"; @@ -79,9 +78,6 @@ const getIcon = (iconName) => { case "camera": return ; - case "congratulations": - return ; - case "cross": return ; diff --git a/src/common/image/index.jsx b/src/common/image/index.jsx new file mode 100644 index 000000000..cd489cb90 --- /dev/null +++ b/src/common/image/index.jsx @@ -0,0 +1,28 @@ +import CongratulationsImage from "assets/images/congratulations.png"; +import EmailImage from "assets/images/email.png"; + +const getImageByName = (name) => { + switch (name) { + case "email": + return EmailImage; + + case "congratulations": + return CongratulationsImage; + + default: + return null; + } +}; + +export const Image = ({ image, className }) => { + return ( + + ); +}; diff --git a/src/common/input/input-component/index.jsx b/src/common/input/input-component/index.jsx index 407dc65fc..c0140f5c1 100644 --- a/src/common/input/input-component/index.jsx +++ b/src/common/input/input-component/index.jsx @@ -26,6 +26,18 @@ export const InputComponent = ({ ); } + if (type === "email-code") { + return ( + onChange(event.target.value)} + {...props} + /> + ); + } + if (type === "currency") { return ( { - return ( -
-
- -
-
- ); -}; - export const ComponentLoader = ({ width = "100%", height = "100%" }) => { return (
diff --git a/src/common/modal/index.jsx b/src/common/modal/index.jsx index 780545db2..4c35815fe 100644 --- a/src/common/modal/index.jsx +++ b/src/common/modal/index.jsx @@ -17,6 +17,18 @@ export const Modal = ({ visible, children, modalRef }) => { ); }; +export const LoaderModal = ({ visible, children, modalRef }) => { + if (!visible) return null; + + return ( + +
+ {children} +
+
+ ); +}; + export const CommonModal = ({ visible, title, onClose, children }) => { if (!visible) return null; diff --git a/src/common/modal/styles.scss b/src/common/modal/styles.scss index 6d88eb0b3..c93de3735 100644 --- a/src/common/modal/styles.scss +++ b/src/common/modal/styles.scss @@ -1,18 +1,11 @@ @import "src/scss/mixins"; .portal-modal-container { + @include position(fixed, 0, 0, 0, 0); + @include flexColumn(center, center); + backdrop-filter: blur(4px); background-color: rgba(0, 0, 0, 0.3); - - top: 0; - left: 0; - right: 0; - bottom: 0; - position: fixed; - - display: flex; - align-items: center; - justify-content: center; } .common-modal-container { @@ -22,17 +15,13 @@ border-radius: 4px; background-color: #191b1d; - display: flex; - flex-direction: column; + @include flexColumn(auto, auto); .top-container { width: 100%; height: 56px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; margin-bottom: 24px; + @include flexRow(center, space-between); } @include breakpoint("mobile") { @@ -41,3 +30,15 @@ max-width: unset; } } + +.app-portal-loader-container { + width: 100%; + height: 100%; + + z-index: 1000; + @include flexColumn(center, center); + @include position(fixed, 0, 0, 0, 0); + + backdrop-filter: blur(8px); + background-color: rgba(0, 0, 0, 0.5); +} diff --git a/src/components/auth/buttons-container/index.jsx b/src/components/auth/buttons-container/index.jsx new file mode 100644 index 000000000..b8e5fa75f --- /dev/null +++ b/src/components/auth/buttons-container/index.jsx @@ -0,0 +1,8 @@ +import "./styles.scss"; + +export const ButtonsContainer = ({ label, children }) => ( +
+ {label &&
{label}
} +
{children}
+
+); diff --git a/src/components/auth/buttons-container/styles.scss b/src/components/auth/buttons-container/styles.scss new file mode 100644 index 000000000..490dec555 --- /dev/null +++ b/src/components/auth/buttons-container/styles.scss @@ -0,0 +1,20 @@ +@import "src/scss/mixins"; +@import "src/scss/variables"; + +.auth-action-buttons-container { + width: 100%; + gap: $gap-medium; + @include flexColumn(center, flex-start); + + .buttons-container { + width: 100%; + gap: $gap-medium; + @include flexRow(center, space-between); + } + + @include breakpoint("mobile", "minMax") { + .buttons-container { + @include flexColumn(center, flex-start); + } + } +} diff --git a/src/components/auth/footer-container/index.jsx b/src/components/auth/footer-container/index.jsx new file mode 100644 index 000000000..1bdbc9fc1 --- /dev/null +++ b/src/components/auth/footer-container/index.jsx @@ -0,0 +1,8 @@ +import "./styles.scss"; + +export const FooterContainer = ({ title, children }) => ( +
+ {title &&
{title}
} + {children && children} +
+); diff --git a/src/components/auth/footer-container/styles.scss b/src/components/auth/footer-container/styles.scss new file mode 100644 index 000000000..0b3f51d5e --- /dev/null +++ b/src/components/auth/footer-container/styles.scss @@ -0,0 +1,12 @@ +@import "src/scss/mixins"; +@import "src/scss/variables"; + +.auth-footer-container { + width: 100%; + gap: $gap-smallest; + @include flexRow(center, center); + + @include breakpoint("mobile", "minMax") { + @include flexColumn(center, center); + } +} diff --git a/src/components/auth/index.js b/src/components/auth/index.js new file mode 100644 index 000000000..094ec4105 --- /dev/null +++ b/src/components/auth/index.js @@ -0,0 +1,4 @@ +export { Placeholder } from "./placeholder"; +export { InputsContainer } from "./inputs-container"; +export { FooterContainer } from "./footer-container"; +export { ButtonsContainer } from "./buttons-container"; diff --git a/src/components/auth/inputs-container/index.jsx b/src/components/auth/inputs-container/index.jsx new file mode 100644 index 000000000..c546e9d3d --- /dev/null +++ b/src/components/auth/inputs-container/index.jsx @@ -0,0 +1,14 @@ +import { InputField } from "common/input"; + +import "./styles.scss"; + +export const InputsContainer = ({ inputs, children }) => { + return ( +
+ {inputs.map((data, index) => ( + + ))} + {children &&
{children}
} +
+ ); +}; diff --git a/src/components/auth/inputs-container/styles.scss b/src/components/auth/inputs-container/styles.scss new file mode 100644 index 000000000..0216f7e24 --- /dev/null +++ b/src/components/auth/inputs-container/styles.scss @@ -0,0 +1,19 @@ +@import "src/scss/mixins"; +@import "src/scss/variables"; + +.auth-inputs-container { + width: 100%; + gap: $gap-medium; + @include flexColumn(center, center); + + .bottom-container { + width: 100%; + gap: $gap-smallest; + @include flexRow(center, space-between); + + button { + height: 24px; + width: fit-content; + } + } +} diff --git a/src/components/auth/placeholder/index.jsx b/src/components/auth/placeholder/index.jsx new file mode 100644 index 000000000..3d4f1a11d --- /dev/null +++ b/src/components/auth/placeholder/index.jsx @@ -0,0 +1,12 @@ +import { Image } from "common/image"; + +import "./styles.scss"; + +export const Placeholder = ({ image, title }) => { + return ( +
+ {image && } + {title &&
{title}
} +
+ ); +}; diff --git a/src/components/auth/placeholder/styles.scss b/src/components/auth/placeholder/styles.scss new file mode 100644 index 000000000..a414893e8 --- /dev/null +++ b/src/components/auth/placeholder/styles.scss @@ -0,0 +1,19 @@ +@import "src/scss/mixins"; +@import "src/scss/variables"; + +.auth-placeholder-container { + width: 100%; + gap: $gap-big; + @include flexColumn(center, center); + + img { + max-height: 256px; + object-fit: contain; + } + + @include breakpoint("mobile") { + img { + max-height: unset; + } + } +} diff --git a/src/components/privateRoute/index.jsx b/src/components/privateRoute/index.jsx index d8aeb90c5..9fa42136b 100644 --- a/src/components/privateRoute/index.jsx +++ b/src/components/privateRoute/index.jsx @@ -1,63 +1,49 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useAlert } from "react-alert"; import { Route, Redirect } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; -import { Loader } from "common/loader"; +import { Routes } from "constants/routes"; +import { useStateIfMounted } from "hooks"; +import { selectIsAuthed } from "store/user/selectors"; import { getCurrentUserThunk } from "store/user/thunks"; -import { selectCurrentUser } from "store/user/selectors"; -// import { checkAndUpdateToken } from "../../actions/userAction"; - -export const PrivateRoute = ({ component: Component, ...rest }) => { +const CheckAuthRoute = ({ isAuthed }) => { const alert = useAlert(); const dispatch = useDispatch(); - const currentUser = useSelector(selectCurrentUser); - - const [isAuthed, setIsAuthed] = useState(false); - const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - if (!currentUser) { - setIsAuthed(false); - setIsLoading(true); - } - if (currentUser) { - setIsAuthed(true); - setIsLoading(false); - } - }, [currentUser]); + const [isRequesting, setIsRequesting] = useStateIfMounted(true); useEffect(async () => { - if (!isAuthed && isLoading) { - try { - const response = await getCurrentUserThunk()(dispatch); - setIsAuthed(response.isAuthed); - setIsLoading(false); - } catch (error) { - if (error) alert.error(error); - setIsAuthed(false); - setIsLoading(false); - } + try { + await dispatch(getCurrentUserThunk()); + } catch (error) { + if (error) alert.error(error); + } finally { + setIsRequesting(false); } - }, [isAuthed, isLoading]); + }, []); + + if (!isAuthed && isRequesting) return <>; - // const hasAccess = () => { - // const userInfo = window.localStorage.getItem("userInfo"); - // console.log(userInfo); - // return userInfo && dispatch(checkAndUpdateToken()); - // }; + if (!isAuthed && !isRequesting) return ; - if (isLoading) { - return ; - } + return null; +}; + +export const PrivateRoute = ({ component: Component, ...rest }) => { + const isAuthed = useSelector(selectIsAuthed); return ( - isAuthed ? : + isAuthed ? ( + + ) : ( + + ) } /> ); diff --git a/src/constants/routes.js b/src/constants/routes.js index 676e61b51..224ed43a7 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -1,6 +1,14 @@ const news = "/news"; const courses = "/courses"; +const Auth = { + Login: "/login", + Register: "/register", + ConfirmEmail: "/confirm-email", + ForgotPassword: "/forgot-password", + AdditionalInfo: "/additional-info", +}; + const News = { Home: news, Article: `${news}/:id`, @@ -16,4 +24,4 @@ const Courses = { Members: `${courses}/:id/members`, }; -export const Routes = { News, Courses }; +export const Routes = { Auth, News, Courses }; diff --git a/src/index.jsx b/src/index.jsx index d892db37e..890ab60ce 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -3,10 +3,12 @@ import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import { Provider as AlertProvider } from "react-alert"; +import { store } from "store"; +import { LoaderProvider } from "providers"; import { Alert, alertOptions } from "common/alert"; import App from "./App"; -import { store } from "./store"; + import reportWebVitals from "./reportWebVitals"; import "./scss/styles.scss"; @@ -14,7 +16,9 @@ import "./scss/styles.scss"; ReactDOM.render( - + + + , document.getElementById("app") diff --git a/src/layout/auth/index.jsx b/src/layout/auth/index.jsx index ed31d1344..3345def0e 100644 --- a/src/layout/auth/index.jsx +++ b/src/layout/auth/index.jsx @@ -1,7 +1,6 @@ import { Formik, Form } from "formik"; import { Icon } from "common/icon"; -import { Loader } from "common/loader"; import { BlurContainer } from "layout/blur-container"; import "./styles.scss"; @@ -14,7 +13,6 @@ export const AuthLayout = ({ initialValues, withLogo = true, validationSchema, - isLoading = false, enableReinitialize = false, }) => { return ( @@ -57,8 +55,6 @@ export const AuthLayout = ({
)} - - {isLoading && } ); }; diff --git a/src/layout/auth/styles.scss b/src/layout/auth/styles.scss index 9ab670077..1479ca067 100644 --- a/src/layout/auth/styles.scss +++ b/src/layout/auth/styles.scss @@ -7,8 +7,7 @@ overflow: hidden; background-color: transparent; - display: flex; - flex-direction: row; + @include flexRow(); .left-container { width: 40%; @@ -18,26 +17,19 @@ overflow-y: scroll; background-color: rgba(20, 20, 20, 0.6); - display: flex; - flex-direction: column; - align-items: center; + @include flexColumn(center, auto); .auth-scroll-container { gap: 80px; width: 80%; - - display: flex; - flex-direction: column; - justify-content: flex-start; + padding-bottom: 16px; + @include flexColumn(auto, flex-start); .logo-container { width: 100%; height: 120px; flex-shrink: 0; - - display: flex; - align-items: flex-end; - justify-content: flex-start; + @include flexColumn(flex-start, flex-end); .left-logo-icon { width: 100%; @@ -57,66 +49,7 @@ .form-container { gap: 40px; - display: flex; - flex-direction: column; - - .inputs-container { - gap: 24px; - display: flex; - flex-direction: column; - } - - .image-container { - width: 100%; - padding: 12px 24px; - display: flex; - align-items: center; - justify-content: center; - } - - .row-container { - gap: 24px; - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; - } - - .terms-checkbox-container { - display: flex; - flex-direction: row; - - .link-container { - display: flex; - align-items: center; - flex-direction: row; - - p { - user-select: none; - color: #eeefef; - font: $font-body; - margin-left: 10px; - margin-right: 4px; - } - } - } - - .socials-container { - width: 100%; - gap: 24px; - display: flex; - align-items: center; - flex-direction: column; - } - - .footer { - gap: 8px; - margin-bottom: 10px; - display: flex; - flex-wrap: wrap; - flex-direction: row; - justify-content: center; - } + @include flexColumn(); } } } @@ -128,8 +61,7 @@ background-color: transparent; display: flex; - align-items: center; - justify-content: center; + @include flexColumn(center, center); .right-logo-icon { width: 80%; @@ -167,8 +99,7 @@ .logo-container { height: 72px; - align-items: center; - justify-content: center; + @include flexColumn(center, center); .left-logo-icon { height: 24px; @@ -200,12 +131,6 @@ .auth-scroll-container { width: 100%; padding: 0px 16px; - - .form-container { - .row-container { - flex-wrap: wrap; - } - } } } } diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 000000000..88bd70e13 --- /dev/null +++ b/src/providers/index.js @@ -0,0 +1,2 @@ +export { LoaderProvider } from "./loader"; +export { SearchBarProvider, SearchBarContext } from "./search-bar"; diff --git a/src/providers/loader/index.jsx b/src/providers/loader/index.jsx new file mode 100644 index 000000000..c2005d5d7 --- /dev/null +++ b/src/providers/loader/index.jsx @@ -0,0 +1,31 @@ +import { useSelector } from "react-redux"; +import Lottie from "lottie-react"; + +import { LoaderModal } from "common/modal"; +import { selectIsLoading } from "store/loader/selectors"; +import loaderAnimation from "assets/animations/loader.json"; + +const options = { + loop: true, + autoplay: true, + rendererSettings: { + preserveAspectRatio: "xMidYMid slice", + }, +}; + +export const LoaderProvider = ({ children }) => { + const isLoading = useSelector(selectIsLoading); + + return ( + <> + + + + {children} + + ); +}; diff --git a/src/routes/index.jsx b/src/routes/index.jsx index e56470364..1c27b3e7d 100644 --- a/src/routes/index.jsx +++ b/src/routes/index.jsx @@ -120,6 +120,11 @@ export const Routes = () => { component={CoursesListPage} path={PathRoutes.Courses.Home} /> + { const history = useHistory(); const dispatch = useDispatch(); - const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState(AdditionalStep.Info); const onSubmit = async (values) => { try { - setIsLoading(true); + dispatch(setIsLoading(true)); const payload = configurePayload(values); await updateUserInfo(payload)(dispatch); if (step === AdditionalStep.Info) { setStep(AdditionalStep.Avatar); - setIsLoading(false); } else { - setIsLoading(false); history.replace("/news"); } } catch (error) { - setIsLoading(false); alert.error(getErrorMessage(error)); + } finally { + dispatch(setIsLoading(false)); } }; @@ -63,7 +62,6 @@ export const AdditionalInfoPage = () => { { > {({ dirty }) => ( <> - {step === AdditionalStep.Info && ( -
- {inputs.map((item) => ( - - ))} -
- )} + {step === AdditionalStep.Info && } {step === AdditionalStep.Avatar && ( )} -
+ { disabled={!dirty} variant="primary" /> -
+ )}
diff --git a/src/screens/auth/confirm-email/config.js b/src/screens/auth/confirm-email/config.js new file mode 100644 index 000000000..82be2d282 --- /dev/null +++ b/src/screens/auth/confirm-email/config.js @@ -0,0 +1,43 @@ +import * as yup from "yup"; + +import { emailCode } from "utils/validators"; + +const model = { + code: { + name: "code", + required: true, + type: "email-code", + placeholder: "Code", + }, +}; + +export const inputs = [model.code]; + +export const validationSchema = yup.object().shape({ + [model.code.name]: emailCode.required("Code is required"), +}); + +export const initialValues = { + [model.code.name]: "", +}; + +export const Variant = { + Confirm: "Confirm", + Success: "Success", +}; + +export const Image = { + [Variant.Confirm]: "email", + [Variant.Success]: "congratulations", +}; + +export const Title = { + [Variant.Confirm]: "Sign Up", + [Variant.Success]: "Congratulations!", +}; + +export const Subtitle = { + [Variant.Success]: null, + [Variant.Confirm]: + "Please check your mail. Enter the code you received at the email.", +}; diff --git a/src/screens/auth/confirm-email/index.jsx b/src/screens/auth/confirm-email/index.jsx new file mode 100644 index 000000000..030b574a6 --- /dev/null +++ b/src/screens/auth/confirm-email/index.jsx @@ -0,0 +1,141 @@ +import { useEffect, useState, useCallback } from "react"; +import { useAlert } from "react-alert"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; + +import { AuthLayout } from "layout/auth"; +import { ActionButton } from "common/buttons/action-button"; +import { + Placeholder, + InputsContainer, + ButtonsContainer, +} from "components/auth"; + +import { Routes } from "constants/routes"; +import { getErrorMessage } from "utils/error"; +import { + confirmEmailThunk, + requestConfirmEmailCodeThunk, +} from "store/user/thunks"; + +import { + Title, + Image, + inputs, + Variant, + Subtitle, + initialValues, + validationSchema, +} from "./config"; + +export const ConfirmEmailPage = () => { + const alert = useAlert(); + const history = useHistory(); + const dispatch = useDispatch(); + + const [variant, setVariant] = useState(Variant.Confirm); + const [isFromRegister, setIsFromRegister] = useState(true); + const [data, setData] = useState({ email: "", password: "" }); + + useEffect(() => { + const state = history.location?.state || {}; + + if (!state.email) { + history.replace(Routes.Auth.Login); + return; + } + + setVariant(state.variant || Variant.Confirm); + setIsFromRegister(state.isFromRegister || false); + setData({ email: state.email || "", password: state.password || "" }); + }, [history.location]); + + const handleSubmit = useCallback( + async ({ code }) => { + try { + await dispatch( + confirmEmailThunk({ + code, + email: data.email, + password: data.password, + }) + ); + + if (isFromRegister) { + setVariant(Variant.Success); + } else { + history.push(Routes.News.Home); + } + } catch (error) { + alert.error(getErrorMessage(error)); + } + }, + [data, isFromRegister] + ); + + const handleResendClick = useCallback(async () => { + try { + await dispatch( + requestConfirmEmailCodeThunk({ + email: data.email, + }) + ); + alert.success(`We've sent new confirmation code at ${data.email}`); + } catch (error) { + alert.error(getErrorMessage(error)); + } + }, [data.email]); + + return ( + + {({ dirty }) => ( + <> + + + {variant === Variant.Confirm && ( + <> + +
+ + + + + + )} + + {variant === Variant.Success && isFromRegister && ( + + history.replace(Routes.News.Home)} + /> + history.replace(Routes.Auth.AdditionalInfo)} + /> + + )} + + )} + + ); +}; diff --git a/src/screens/auth/forgot-password/config.js b/src/screens/auth/forgot-password/config.js index 1adc9f37f..50c7f3790 100644 --- a/src/screens/auth/forgot-password/config.js +++ b/src/screens/auth/forgot-password/config.js @@ -1,5 +1,7 @@ import * as Yup from "yup"; +import { password, repeatPassword } from "utils/validators"; + export const model = { username: { name: "username", @@ -19,53 +21,49 @@ export const model = { password: { name: "password", + type: "password", icon: "lock", placeholder: "New Password", }, confirmPassword: { name: "confirmPassword", + type: "password", icon: "lock", placeholder: "Confirm Password", }, }; -const { username, codeRequested, code, password, confirmPassword } = model; - const isRequiredField = (value, schema, message) => value ? schema.required(message) : schema.optional(); export const validationSchema = Yup.object().shape({ - [username.name]: Yup.string().required("Username is required field!"), + [model.username.name]: Yup.string().required("Username is required field!"), - [codeRequested.name]: Yup.bool().optional(), + [model.codeRequested.name]: Yup.bool().optional(), - [code.name]: Yup.string().when([codeRequested.name], (value, schema) => - isRequiredField(value, schema, "Code is required field!") + [model.code.name]: Yup.string().when( + [model.codeRequested.name], + (value, schema) => isRequiredField(value, schema, "Code is required field!") ), - [password.name]: Yup.string() - .min(6, "Password must be at least 6 characters") - .when([codeRequested.name], (value, schema) => + [model.password.name]: password(8).when( + [model.codeRequested.name], + (value, schema) => isRequiredField(value, schema, "Password is required field!") - ), + ), - [confirmPassword.name]: Yup.string() - .min(6, "Password must be at least 6 characters") - .test( - "passwords-match", - "Passwords must match", - (value, context) => context.parent.password === value - ) - .when([codeRequested.name], (value, schema) => + [model.confirmPassword.name]: repeatPassword(8).when( + [model.codeRequested.name], + (value, schema) => isRequiredField(value, schema, "Confirm Password is required field!") - ), + ), }); export const initialValues = { - [username.name]: "", - [codeRequested.name]: false, - [code.name]: "", - [password.name]: "", - [confirmPassword.name]: "", + [model.code.name]: "", + [model.username.name]: "", + [model.password.name]: "", + [model.confirmPassword.name]: "", + [model.codeRequested.name]: false, }; diff --git a/src/screens/auth/forgot-password/index.jsx b/src/screens/auth/forgot-password/index.jsx index 8388d4759..f296be8c4 100644 --- a/src/screens/auth/forgot-password/index.jsx +++ b/src/screens/auth/forgot-password/index.jsx @@ -1,14 +1,18 @@ -import React, { useState } from "react"; import { useAlert } from "react-alert"; +import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { TextLink } from "common/links"; import { AuthLayout } from "layout/auth"; -import { InputField } from "common/input"; import { ActionButton } from "common/buttons/action-button"; +import { + FooterContainer, + InputsContainer, + ButtonsContainer, +} from "components/auth"; import { getErrorMessage } from "utils/error"; -import { requestCode, resetPassword } from "actions/auth"; +import { requestCodeThunk, resetPasswordThunk } from "store/user/thunks"; import { validationSchema, initialValues, model } from "./config"; import { @@ -20,8 +24,7 @@ import { export const ForgotPasswordPage = () => { const alert = useAlert(); const history = useHistory(); - - const [isLoading, setIsLoading] = useState(false); + const dispatch = useDispatch(); const handleFormSubmit = async (values, actions) => { const { codeRequested, username, code, password } = values; @@ -31,21 +34,22 @@ export const ForgotPasswordPage = () => { } try { - setIsLoading(true); - if (!codeRequested) { - const response = await requestCode(username); + const response = await dispatch(requestCodeThunk(username)); actions.setFieldValue(model.codeRequested.name, true); - if (response) alert.success(`Code has been sent to ${response}!`); - else alert.success("Code has been sent!"); - setIsLoading(false); + + alert.success( + response + ? `Code has been sent to ${response}!` + : "Code has been sent!" + ); } else { - await resetPassword({ username, code, password }); + await dispatch(resetPasswordThunk({ username, code, password })); + alert.success("Password has been successfully changed!"); history.push("/login"); } } catch (error) { - setIsLoading(false); alert.error(getErrorMessage(error)); } }; @@ -62,20 +66,25 @@ export const ForgotPasswordPage = () => { } try { - setIsLoading(true); - const response = await requestCode(values.username); - if (response) alert.success(`Code has been sent to ${response}!`); - else alert.success("Code has been sent!"); + const response = await dispatch(requestCodeThunk(values.username)); + + alert.success( + response ? `Code has been sent to ${response}!` : "Code has been sent!" + ); } catch (error) { alert.error(getErrorMessage(error)); - } finally { - setIsLoading(false); } }; + const generateInputs = (values) => { + const { username, code, password, confirmPassword } = model; + return isCodeRequested(values) + ? [username, code, password, confirmPassword] + : [username]; + }; + return ( { > {({ values, setFieldValue }) => ( <> -
- - - {values[model.codeRequested.name] && ( - <> - - - - - )} - -
- handleCodeResend(values, setFieldValue)} - /> - - -
-
- -
-
Go back to
+ + + + handleCodeResend(values, setFieldValue)} + /> + + + + + -
+ )}
diff --git a/src/screens/auth/index.js b/src/screens/auth/index.js new file mode 100644 index 000000000..dc16da28d --- /dev/null +++ b/src/screens/auth/index.js @@ -0,0 +1,5 @@ +export { SignUpPage } from "./sign-up"; +export { SignInPage } from "./sign-in"; +export { ConfirmEmailPage } from "./confirm-email"; +export { ForgotPasswordPage } from "./forgot-password"; +export { AdditionalInfoPage } from "./additional-info"; diff --git a/src/screens/auth/sign-in/config.js b/src/screens/auth/sign-in/config.js index b63e699fb..8b4c234d4 100644 --- a/src/screens/auth/sign-in/config.js +++ b/src/screens/auth/sign-in/config.js @@ -5,13 +5,14 @@ export const model = { name: "username", icon: "person", required: true, - placeholder: "Username", + placeholder: "Username or Email", }, password: { name: "password", icon: "lock", required: true, + type: "password", placeholder: "Password", }, }; @@ -25,3 +26,5 @@ export const initialValues = { [model.username.name]: "", [model.password.name]: "", }; + +export const inputs = [model.username, model.password]; diff --git a/src/screens/auth/sign-in/helpers.js b/src/screens/auth/sign-in/helpers.js new file mode 100644 index 000000000..3d75da3b8 --- /dev/null +++ b/src/screens/auth/sign-in/helpers.js @@ -0,0 +1,3 @@ +export const isNonConfirmedError = (error) => { + return error && error.code === "UserNotConfirmedException"; +}; diff --git a/src/screens/auth/sign-in/index.jsx b/src/screens/auth/sign-in/index.jsx index d0d0d10f5..51cce0665 100644 --- a/src/screens/auth/sign-in/index.jsx +++ b/src/screens/auth/sign-in/index.jsx @@ -1,18 +1,25 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { useAlert } from "react-alert"; import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { TextLink } from "common/links"; import { AuthLayout } from "layout/auth"; -import { InputField } from "common/input"; import { Checkbox } from "common/checkbox"; import { ActionButton } from "common/buttons/action-button"; - -import { login } from "actions/auth"; +import { + InputsContainer, + FooterContainer, + ButtonsContainer, +} from "components/auth"; + +import { api } from "api"; +import { Routes } from "constants/routes"; import { getErrorMessage } from "utils/error"; +import { loginThunk } from "store/user/thunks"; -import { model, validationSchema, initialValues } from "./config"; +import { isNonConfirmedError } from "./helpers"; +import { validationSchema, initialValues, inputs } from "./config"; // TODO: Implement Remember me; @@ -22,7 +29,6 @@ export const SignInPage = () => { const dispatch = useDispatch(); const [remember, setRemember] = useState(false); - const [isLoading, setIsLoading] = useState(false); const onGoogleLogin = () => { // Auth.federatedSignIn({ provider: "Google" }); @@ -34,77 +40,71 @@ export const SignInPage = () => { const handleFormSubmit = async ({ username, password }) => { try { - setIsLoading(true); - await login({ name: username, password })(dispatch); - history.push("/news"); + await dispatch(loginThunk({ name: username, password })); + history.push(Routes.News.Home); } catch (error) { - setIsLoading(false); alert.error(getErrorMessage(error)); + + if (isNonConfirmedError(error)) { + await api.auth.resendEmailCode({ email: username }); + history.push({ + pathname: Routes.Auth.ConfirmEmail, + state: { email: username, password, isFromRegister: false }, + }); + } } }; return ( {() => ( <> -
- - - -
- setRemember(!remember)} - /> - - -
-
+ + setRemember(!remember)} + /> + + + -
-
Sign In with services
- -
- - - -
-
- -
-
Don't have an account yet?
+ + + + + + -
+ )}
diff --git a/src/screens/auth/sign-up/config.js b/src/screens/auth/sign-up/config.js index bb3ce8013..7e08e7274 100644 --- a/src/screens/auth/sign-up/config.js +++ b/src/screens/auth/sign-up/config.js @@ -1,15 +1,18 @@ import * as Yup from "yup"; +import { password } from "utils/validators"; + export const model = { - username: { - name: "username", + email: { + name: "email", icon: "person", required: true, - placeholder: "Username", + placeholder: "Email", }, password: { name: "password", + type: "password", icon: "lock", required: true, placeholder: "Password", @@ -20,16 +23,18 @@ export const model = { }, }; +export const inputs = [model.email, model.password]; + export const validationSchema = Yup.object().shape({ - [model.username.name]: Yup.string().required("Username is required field!"), - [model.password.name]: Yup.string() - .min(8, "Password must be at least 8 characters") - .required("Password is required field!"), + [model.email.name]: Yup.string() + .email("Email is not valid") + .required("Username is required field!"), + [model.password.name]: password(8).required("Password is required field!"), [model.agrre.name]: Yup.bool().isTrue().required(), }); export const initialValues = { - [model.username.name]: "", + [model.email.name]: "", [model.password.name]: "", [model.agrre.name]: false, }; diff --git a/src/screens/auth/sign-up/index.jsx b/src/screens/auth/sign-up/index.jsx index 527a61c9b..620d40be9 100644 --- a/src/screens/auth/sign-up/index.jsx +++ b/src/screens/auth/sign-up/index.jsx @@ -1,28 +1,28 @@ -import React, { useState } from "react"; import { useAlert } from "react-alert"; import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; -import { Icon } from "common/icon"; import { TextLink } from "common/links"; import { AuthLayout } from "layout/auth"; -import { InputField } from "common/input"; import { CheckboxField } from "common/checkbox"; import { ActionButton } from "common/buttons/action-button"; +import { + FooterContainer, + InputsContainer, + ButtonsContainer, +} from "components/auth"; -import { register } from "actions/auth"; +import { Routes } from "constants/routes"; import { getErrorMessage } from "utils/error"; +import { registerThunk, loginThunk } from "store/user/thunks"; -import { model, validationSchema, initialValues } from "./config"; +import { model, validationSchema, initialValues, inputs } from "./config"; export const SignUpPage = () => { const alert = useAlert(); const history = useHistory(); const dispatch = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - const [isSidngedUp, setIsSignedUp] = useState(false); - const onGoogleLogin = () => { // Auth.federatedSignIn({ provider: "Google" }); }; @@ -31,105 +31,71 @@ export const SignUpPage = () => { // Auth.federatedSignIn({ provider: "Facebook" }); }; - const handleFormSubmit = async ({ username, password }) => { + const handleFormSubmit = async ({ email, password }) => { try { - setIsLoading(true); - await register({ name: username, password })(dispatch); - setIsSignedUp(true); + const { confirmEmail } = await dispatch( + registerThunk({ name: email, password }) + ); + + const pathname = Routes.Auth.ConfirmEmail; + const state = { + email, + password, + variant: "Confirm", + isFromRegister: true, + }; + + if (!confirmEmail) { + await dispatch(loginThunk({ name: email, password })); + state.variant = "Success"; + } + + history.push({ pathname, state }); } catch (error) { alert.error(getErrorMessage(error)); - } finally { - setIsLoading(false); } }; return ( {() => ( <> - {isSidngedUp && ( - <> -
- -
- -
- history.replace("/news")} - /> - - history.replace("/additional-info")} - /> -
- - )} - - {!isSidngedUp && ( - <> -
- - - -
-
- - -
-

I agree with

- -
-
-
-
- - - -
-
Sign In with services
- -
- - - -
-
- -
-
Already have an account?
- -
- - )} + + + + + + + + + + + + + + + + + )}
diff --git a/src/screens/courses/course/main-info/index.jsx b/src/screens/courses/course/main-info/index.jsx index 276b7d5f9..535f9eb98 100644 --- a/src/screens/courses/course/main-info/index.jsx +++ b/src/screens/courses/course/main-info/index.jsx @@ -27,7 +27,7 @@ export const CourseMainInfo = ({ ? `$${parseFloat(parseFloat(price) / 100).toFixed(2)}` : "$00.00"; - const courseMembers = `${members.length || 0} people tried`; + const courseMembers = `${members?.length || 0} people tried`; const handleMoreOptionClick = (option) => { if (option === MoreOption.Review && onAddReview) onAddReview(); diff --git a/src/store/index.js b/src/store/index.js index dd0c4af87..fc62f4098 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -109,6 +109,7 @@ import { import { newsReducer } from "./news/slices"; import { userReducer } from "./user/slices"; import { coursesReducer } from "./courses"; +import { loaderReducer } from "./loader/slices"; const reducer = combineReducers({ listEvents: eventListReducer, @@ -188,6 +189,7 @@ const reducer = combineReducers({ news: newsReducer, user: userReducer, courses: coursesReducer, + loader: loaderReducer, }); const userInfoFromStorage = window.localStorage.getItem("userInfo") diff --git a/src/store/loader/selectors.js b/src/store/loader/selectors.js new file mode 100644 index 000000000..18d5c7954 --- /dev/null +++ b/src/store/loader/selectors.js @@ -0,0 +1,8 @@ +import { createSelector } from "reselect"; + +const loader = (store) => store.loader; + +export const selectIsLoading = createSelector( + [loader], + (store) => store.isLoading +); diff --git a/src/store/loader/slices.js b/src/store/loader/slices.js new file mode 100644 index 000000000..dd11c6785 --- /dev/null +++ b/src/store/loader/slices.js @@ -0,0 +1,20 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { isLoading: false }; + +const slice = createSlice({ + name: "loader", + initialState, + reducers: { + setIsLoading: (state, { payload }) => { + state.isLoading = payload; + }, + }, +}); + +const { + reducer: loaderReducer, + actions: { setIsLoading }, +} = slice; + +export { loaderReducer, setIsLoading }; diff --git a/src/store/user/selectors.js b/src/store/user/selectors.js index cd7444bd1..dbae35e69 100644 --- a/src/store/user/selectors.js +++ b/src/store/user/selectors.js @@ -6,3 +6,8 @@ export const selectCurrentUser = createSelector( [selectUserStore], (store) => store.currentProfile ); + +export const selectIsAuthed = createSelector( + [selectUserStore], + (store) => store.isAuthed +); diff --git a/src/store/user/slices.js b/src/store/user/slices.js index edb08e4da..5c247ea55 100644 --- a/src/store/user/slices.js +++ b/src/store/user/slices.js @@ -1,6 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; -const initialState = { currentProfile: null }; +const initialState = { isAuthed: false, currentProfile: null }; const userSlice = createSlice({ name: "user", @@ -9,12 +9,15 @@ const userSlice = createSlice({ setCurrentUser: (state, { payload }) => { state.currentProfile = { ...payload }; }, + setIsAuthed: (state, { payload }) => { + state.isAuthed = payload; + }, }, }); const { reducer: userReducer, - actions: { setCurrentUser }, + actions: { setCurrentUser, setIsAuthed }, } = userSlice; -export { userReducer, setCurrentUser }; +export { userReducer, setCurrentUser, setIsAuthed }; diff --git a/src/store/user/thunks.js b/src/store/user/thunks.js index cebb1a4d5..9f44aab8e 100644 --- a/src/store/user/thunks.js +++ b/src/store/user/thunks.js @@ -1,10 +1,79 @@ +import { Auth, Amplify } from "aws-amplify"; + import { api } from "api"; -import { getErrorMessage } from "utils/error"; +import { authConfig } from "config/amplify"; +import { setIsLoading } from "store/loader/slices"; + +import { setCurrentUser, setIsAuthed } from "./slices"; + +const isCognito = process.env.REACT_APP_AUTH_METHOD === "cognito"; + +if (isCognito) { + Amplify.configure({ Auth: { ...authConfig } }); +} + +export const registerThunk = + ({ name, password }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); + await api.auth.register({ username: name, password }); + + if (isCognito) { + return Promise.resolve({ confirmEmail: true }); + } + + return Promise.resolve({ confirmEmail: false }); + } catch (error) { + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; + +export const loginThunk = + ({ name, password }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); -import { setCurrentUser } from "./slices"; + let data; + let username = name; + + if (isCognito) { + if (username.includes("@")) username = username.replace(/@/g, ""); + const response = await Auth.signIn(username, password); + + data = { + id: response?.attributes?.sub || "", + token: response?.signInUserSession?.idToken?.jwtToken || "", + }; + } else { + const response = await api.auth.login({ username, password }); + data = { ...response.data }; + } + + localStorage.setItem("userInfo", JSON.stringify(data)); + const authData = isCognito ? { id: data.id } : { username, password }; + + await api.auth.login(authData); + const user = await api.user.get({ id: data.id }); + const profile = { ...user?.data?.results }; + + dispatch(setIsAuthed(true)); + dispatch(setCurrentUser(profile)); + + return Promise.resolve(); + } catch (error) { + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; export const getCurrentUserThunk = () => async (dispatch) => { try { + dispatch(setIsLoading(true)); const storage = JSON.parse(window.localStorage.getItem("userInfo")); if (!storage || !storage.id) { @@ -12,10 +81,106 @@ export const getCurrentUserThunk = () => async (dispatch) => { } const response = await api.user.get({ id: storage.id }); + + dispatch(setIsAuthed(true)); dispatch(setCurrentUser({ ...response?.data?.results })); - return Promise.resolve({ isAuthed: true }); + return Promise.resolve(); + } catch (error) { + dispatch(setIsAuthed(false)); + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } +}; + +// TODO: Why there is no functionality to request code without cognito? +export const requestCodeThunk = (username) => async (dispatch) => { + try { + dispatch(setIsLoading(true)); + + let response; + if (isCognito) { + const data = await api.auth.forgotPassword(username); + response = + data.data.details.CodeDeliveryDetails.AttributeName.split("_").join( + " " + ); + } + return Promise.resolve(response); } catch (error) { - return Promise.reject(getErrorMessage(error)); + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); } }; + +export const resetPasswordThunk = + ({ username, code, password }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); + if (isCognito) { + await api.auth.forgotPasswordSubmit(username, code, password); + } + return Promise.resolve(); + } catch (error) { + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; + +export const changePasswordThunk = + ({ oldPassword, newPassword }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); + + if (isCognito) { + const user = await Auth.currentAuthenticatedUser(); + await Auth.changePassword(user, oldPassword, newPassword); + } else { + await api.auth.changePassword({ oldPassword, newPassword }); + } + + return Promise.resolve(); + } catch (error) { + // TODO: From backend receive wrong error object; + // TODO: Backend_Bug: Always incorrect password error, but password has been changed; + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; + +export const confirmEmailThunk = + ({ code, email, password }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); + + await api.auth.confirmEmail({ email, code }); + await dispatch(loginThunk({ name: email, password })); + + return Promise.resolve(); + } catch (error) { + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; + +export const requestConfirmEmailCodeThunk = + ({ email }) => + async (dispatch) => { + try { + dispatch(setIsLoading(true)); + await api.auth.resendEmailCode({ email }); + return Promise.resolve(); + } catch (error) { + return Promise.reject(error); + } finally { + dispatch(setIsLoading(false)); + } + }; diff --git a/src/utils/validators.js b/src/utils/validators.js new file mode 100644 index 000000000..fd08fb728 --- /dev/null +++ b/src/utils/validators.js @@ -0,0 +1,24 @@ +import * as yup from "yup"; + +export const emailCode = yup.string().test({ + name: "email-code", + message: "Must be 6 digits", + test: (value) => !value.includes("_"), +}); + +export const password = (characters) => { + const message = `Password must be at least ${characters} characters`; + return yup.string().min(characters, message); +}; + +export const repeatPassword = (characters) => { + const message = `Password must be at least ${characters} characters`; + return yup + .string() + .min(characters, message) + .test( + "passwords-match", + "Passwords must match", + (value, context) => context.parent.password === value + ); +};