From a079f711d4f124a796b8666c22d48f623ee85bca Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 20 Feb 2021 15:33:20 +0200 Subject: [PATCH] refactoring: authentication with react-query. --- client/package.json | 1 + client/src/components/Authentication.js | 12 +- client/src/components/Dashboard/TopbarUser.js | 79 +++-- client/src/components/Guards/PrivateRoute.js | 14 +- .../Authentication/AuthCopyright.js | 6 +- .../containers/Authentication/AuthInsider.js | 4 +- .../containers/Authentication/InviteAccept.js | 276 +---------------- .../Authentication/InviteAcceptForm.js | 100 +++++++ .../Authentication/InviteAcceptFormContent.js | 122 ++++++++ .../Authentication/InviteAcceptProvider.js | 56 ++++ client/src/containers/Authentication/Login.js | 216 +++----------- .../containers/Authentication/LoginForm.js | 77 +++++ .../src/containers/Authentication/Register.js | 281 +++--------------- .../containers/Authentication/RegisterForm.js | 141 +++++++++ .../Authentication/ResetPassword.js | 150 +++------- .../Authentication/ResetPasswordForm.js | 63 ++++ .../Authentication/SendResetPassword.js | 127 +++----- .../Authentication/SendResetPasswordForm.js | 49 +++ .../containers/Authentication/components.js | 49 +++ client/src/containers/Authentication/utils.js | 145 +++++++++ .../withAuthenticationActions.js | 23 +- .../JournalNumber/ReferenceNumberForm.js | 3 +- .../src/containers/Setup/SetupLeftSection.js | 33 +- .../Subscriptions/BillingPlansForm.js | 2 +- client/src/hooks/query/accounts.js | 29 +- client/src/hooks/query/authentication.js | 63 ++++ client/src/hooks/query/bills.js | 26 +- client/src/hooks/query/currencies.js | 41 +-- client/src/hooks/query/customers.js | 20 +- client/src/hooks/query/estimates.js | 28 +- client/src/hooks/query/expenses.js | 34 ++- client/src/hooks/query/financialReports.js | 26 +- client/src/hooks/query/index.js | 5 +- .../src/hooks/query/inventoryAdjustments.js | 12 +- client/src/hooks/query/invite.js | 31 ++ client/src/hooks/query/invoices.js | 26 +- client/src/hooks/query/items.js | 25 +- client/src/hooks/query/itemsCategories.js | 19 +- client/src/hooks/query/manualJournals.js | 22 +- client/src/hooks/query/paymentMades.js | 19 +- client/src/hooks/query/paymentReceives.js | 23 +- client/src/hooks/query/receipts.js | 22 +- client/src/hooks/query/settings.js | 8 +- client/src/hooks/query/users.js | 21 +- client/src/hooks/query/vendors.js | 19 +- client/src/hooks/query/views.js | 29 +- client/src/hooks/state/authentication.js | 41 +++ client/src/hooks/state/globalErrors.js | 17 ++ client/src/hooks/state/index.js | 4 +- client/src/hooks/useRequest.js | 85 ++++++ client/src/routes/dashboard.js | 20 +- client/src/services/axios.js | 16 +- .../authentication/authentication.actions.js | 114 +------ .../globalErrors/globalErrors.actions.js | 14 +- .../src/style/pages/Authentication/Auth.scss | 2 +- client/src/style/variables.scss | 2 +- client/src/utils.js | 27 +- 57 files changed, 1629 insertions(+), 1290 deletions(-) create mode 100644 client/src/containers/Authentication/InviteAcceptForm.js create mode 100644 client/src/containers/Authentication/InviteAcceptFormContent.js create mode 100644 client/src/containers/Authentication/InviteAcceptProvider.js create mode 100644 client/src/containers/Authentication/LoginForm.js create mode 100644 client/src/containers/Authentication/RegisterForm.js create mode 100644 client/src/containers/Authentication/ResetPasswordForm.js create mode 100644 client/src/containers/Authentication/SendResetPasswordForm.js create mode 100644 client/src/containers/Authentication/components.js create mode 100644 client/src/containers/Authentication/utils.js create mode 100644 client/src/hooks/query/authentication.js create mode 100644 client/src/hooks/query/invite.js create mode 100644 client/src/hooks/state/authentication.js create mode 100644 client/src/hooks/state/globalErrors.js create mode 100644 client/src/hooks/useRequest.js diff --git a/client/package.json b/client/package.json index e5d3a7e97..e5e6ee159 100644 --- a/client/package.json +++ b/client/package.json @@ -66,6 +66,7 @@ "react-albus": "^2.0.0", "react-app-polyfill": "^1.0.6", "react-body-classname": "^1.3.1", + "react-content-loader": "^6.0.1", "react-dev-utils": "^10.2.0", "react-dom": "^16.12.0", "react-dropzone": "^11.0.1", diff --git a/client/src/components/Authentication.js b/client/src/components/Authentication.js index f6352b1a1..aac1b8c83 100644 --- a/client/src/components/Authentication.js +++ b/client/src/components/Authentication.js @@ -4,9 +4,8 @@ import BodyClassName from 'react-body-classname'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; import authenticationRoutes from 'routes/authentication'; import { FormattedMessage as T } from 'react-intl'; -import withAuthentication from 'containers/Authentication/withAuthentication'; -import { compose } from 'utils'; import Icon from 'components/Icon'; +import { useIsAuthenticated } from 'hooks/state'; import 'style/pages/Authentication/Auth.scss' @@ -14,14 +13,15 @@ function PageFade(props) { return ; } -function AuthenticationWrapper({ isAuthorized = false, ...rest }) { +export default function AuthenticationWrapper({ ...rest }) { const to = { pathname: '/homepage' }; const location = useLocation(); + const isAuthenticated = useIsAuthenticated(); const locationKey = location.pathname; return ( <> - {isAuthorized ? ( + {isAuthenticated ? ( ) : ( @@ -61,7 +61,3 @@ function AuthenticationWrapper({ isAuthorized = false, ...rest }) { ); } - -export default compose( - withAuthentication(({ isAuthorized }) => ({ isAuthorized })), -)(AuthenticationWrapper); diff --git a/client/src/components/Dashboard/TopbarUser.js b/client/src/components/Dashboard/TopbarUser.js index e14988afb..908bc2f49 100644 --- a/client/src/components/Dashboard/TopbarUser.js +++ b/client/src/components/Dashboard/TopbarUser.js @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { Menu, @@ -10,48 +10,46 @@ import { } from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; -import withAuthentication from 'containers/Authentication/withAuthentication'; -import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions'; +import { firstLettersArgs } from 'utils'; +import { useAuthActions, useAuthUser } from 'hooks/state'; -import { compose, firstLettersArgs } from 'utils'; - -function DashboardTopbarUser({ requestLogout, user }) { +export default function DashboardTopbarUser() { const history = useHistory(); + const { setLogout } = useAuthActions(); + const user = useAuthUser(); - const onClickLogout = useCallback(() => { - requestLogout(); - }, []); - - const userAvatarDropMenu = useMemo( - () => ( - - -
- {user.first_name} {user.last_name} -
-
- : {user.tenant_id} -
- - } - /> - - } - onClick={() => history.push('/preferences')} - /> - } onClick={onClickLogout} /> -
- ), - [onClickLogout], - ); + const onClickLogout = () => { + setLogout(); + }; return ( - + + +
+ {user.first_name} {user.last_name} +
+
+ : {user.tenant_id} +
+ + } + /> + + } + onClick={() => history.push('/preferences')} + /> + } onClick={onClickLogout} /> + + } + position={Position.BOTTOM} + > - - - - {inviteMeta.pending && ( -
- -
- )} - + + + ); } - -export default compose(withAuthenticationActions)(Invite); diff --git a/client/src/containers/Authentication/InviteAcceptForm.js b/client/src/containers/Authentication/InviteAcceptForm.js new file mode 100644 index 000000000..2bc11effc --- /dev/null +++ b/client/src/containers/Authentication/InviteAcceptForm.js @@ -0,0 +1,100 @@ +import React from 'react'; +import { Intent, Position } from '@blueprintjs/core'; +import { Formik } from 'formik'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { isEmpty } from 'lodash'; + +import { useInviteAcceptContext } from './InviteAcceptProvider'; +import { AppToaster } from 'components'; +import { InviteAcceptSchema } from './utils'; +import InviteAcceptFormContent from './InviteAcceptFormContent'; + +export default function InviteAcceptForm() { + const history = useHistory(); + const { formatMessage } = useIntl(); + + // Invite accept context. + const { + inviteAcceptMutate, + inviteMeta, + token, + } = useInviteAcceptContext(); + + // Invite value. + const inviteValue = { + organization_name: '', + invited_email: '', + ...(!isEmpty(inviteMeta) + ? { + invited_email: inviteMeta.email, + organization_name: inviteMeta.organizationName, + } + : {}), + }; + + // Handle form submitting. + const handleSubmit = (values, { setSubmitting, setErrors }) => { + inviteAcceptMutate([values, token]) + .then((response) => { + AppToaster.show({ + message: `Congrats! Your account has been created and invited to + ${inviteValue.organization_name} organization successfully.`, + intent: Intent.SUCCESS, + }); + history.push('/auth/login'); + setSubmitting(false); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) { + AppToaster.show({ + message: formatMessage({ id: 'an_unexpected_error_occurred' }), + intent: Intent.DANGER, + position: Position.BOTTOM, + }); + history.push('/auth/login'); + } + if (errors.find((e) => e.type === 'PHONE_MUMNER.ALREADY.EXISTS')) { + setErrors({ + phone_number: 'This phone number is used in another account.', + }); + } + if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) { + AppToaster.show({ + message: 'An unexpected error occurred', + intent: Intent.DANGER, + position: Position.BOTTOM, + }); + history.push('/auth/login'); + } + setSubmitting(false); + }, + ); + }; + + return ( +
+
+

+ +

+

+ {' '} + {inviteValue.organization_name} +

+
+ + +
+ ); +} diff --git a/client/src/containers/Authentication/InviteAcceptFormContent.js b/client/src/containers/Authentication/InviteAcceptFormContent.js new file mode 100644 index 000000000..993d028e9 --- /dev/null +++ b/client/src/containers/Authentication/InviteAcceptFormContent.js @@ -0,0 +1,122 @@ +import React from 'react'; +import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; +import { Form, ErrorMessage, FastField, useFormikContext } from 'formik'; +import { Link } from 'react-router-dom'; +import { FormattedMessage as T } from 'react-intl'; +import { inputIntent } from 'utils'; +import { Col, Row } from 'components'; +import { useInviteAcceptContext } from './InviteAcceptProvider'; +import { PasswordRevealer } from './components'; +/** + * Invite user form. + */ +export default function InviteUserFormContent() { + // Invite accept context. + const { inviteMeta } = useInviteAcceptContext(); + + // Formik context. + const { isSubmitting } = useFormikContext(); + + return ( +
+ + + + {({ form, field, meta: { error, touched } }) => ( + } + className={'form-group--first_name'} + intent={inputIntent({ error, touched })} + helperText={} + > + + + )} + + + + + + {({ form, field, meta: { error, touched } }) => ( + } + className={'form-group--last_name'} + intent={inputIntent({ error, touched })} + helperText={} + > + + + )} + + + + + + {({ form, field, meta: { error, touched } }) => ( + } + className={'form-group--phone_number'} + intent={inputIntent({ error, touched })} + helperText={} + > + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + labelInfo={} + className={'form-group--password has-password-revealer'} + intent={inputIntent({ error, touched })} + helperText={} + > + + + )} + + +
+

+ {inviteMeta.email},
+ +

+

+
+ + + {' '} + + + {' '} + + +

+
+ +
+ +
+
+ ); +} diff --git a/client/src/containers/Authentication/InviteAcceptProvider.js b/client/src/containers/Authentication/InviteAcceptProvider.js new file mode 100644 index 000000000..6077cd8d0 --- /dev/null +++ b/client/src/containers/Authentication/InviteAcceptProvider.js @@ -0,0 +1,56 @@ +import React, { createContext, useContext } from 'react'; +import { useInviteMetaByToken, useAuthInviteAccept } from 'hooks/query'; +import { InviteAcceptLoading } from './components'; +import { useHistory } from 'react-router-dom'; + +const InviteAcceptContext = createContext(); + +/** + * Invite accept provider. + */ +function InviteAcceptProvider({ token, ...props }) { + // Invite meta by token. + const { + data: inviteMeta, + error: inviteMetaError, + isError: isInviteMetaError, + isFetching: isInviteMetaLoading, + } = useInviteMetaByToken(token, { retry: false }); + + // Invite accept mutate. + const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({ + retry: false, + }); + + // History context. + const history = useHistory(); + + React.useEffect(() => { + if (inviteMetaError) { history.push('/auth/login'); } + }, [history, inviteMetaError]); + + // Provider payload. + const provider = { + token, + inviteMeta, + inviteMetaError, + isInviteMetaError, + isInviteMetaLoading, + inviteAcceptMutate + }; + + if (inviteMetaError) { + return null; + } + + return ( + + { isInviteMetaError } + + + ); +} + +const useInviteAcceptContext = () => useContext(InviteAcceptContext); + +export { InviteAcceptProvider, useInviteAcceptContext }; diff --git a/client/src/containers/Authentication/Login.js b/client/src/containers/Authentication/Login.js index 3fe43f534..c5f23e726 100644 --- a/client/src/containers/Authentication/Login.js +++ b/client/src/containers/Authentication/Login.js @@ -1,132 +1,38 @@ -import React, { useMemo, useState } from 'react'; -import { Link, useHistory } from 'react-router-dom'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { - Button, - InputGroup, - Intent, - FormGroup, - Checkbox, -} from '@blueprintjs/core'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Formik } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; import Toaster from 'components/AppToaster'; -import ErrorMessage from 'components/ErrorMessage'; import AuthInsider from 'containers/Authentication/AuthInsider'; +import { useAuthLogin } from 'hooks/query'; -import Icon from 'components/Icon'; -import { If } from 'components'; +import LoginForm from './LoginForm'; +import { LoginSchema, transformLoginErrorsToToasts } from './utils'; -import withAuthenticationActions from './withAuthenticationActions'; - -import { compose } from 'utils'; - -const ERRORS_TYPES = { - INVALID_DETAILS: 'INVALID_DETAILS', - USER_INACTIVE: 'USER_INACTIVE', - LOGIN_TO_MANY_ATTEMPTS: 'LOGIN_TO_MANY_ATTEMPTS', -}; -function Login({ requestLogin }) { - const { formatMessage } = useIntl(); - const history = useHistory(); - const [shown, setShown] = useState(false); - const passwordRevealer = () => { - setShown(!shown); - }; - - // Validation schema. - const loginValidationSchema = Yup.object().shape({ - crediential: Yup.string() - .required() - .email() - .label(formatMessage({ id: 'email' })), - password: Yup.string() - .required() - .min(4) - .label(formatMessage({ id: 'password' })), - }); - - // Formik validation schema and submit handler. - const { - touched, - errors, - handleSubmit, - getFieldProps, - isSubmitting, - } = useFormik({ - initialValues: { - crediential: '', - password: '', - }, - validationSchema: loginValidationSchema, - onSubmit: (values, { setSubmitting }) => { - requestLogin({ - crediential: values.crediential, - password: values.password, +/** + * Login page. + */ +export default function Login() { + const { mutateAsync: loginMutate } = useAuthLogin(); + + const handleSubmit = (values, { setSubmitting }) => { + loginMutate({ + crediential: values.crediential, + password: values.password, + }) + .then(() => { + setSubmitting(false); }) - .then(() => { - setSubmitting(false); - }) - .catch((errors) => { - const toastBuilders = []; - if (errors.find((e) => e.type === ERRORS_TYPES.INVALID_DETAILS)) { - toastBuilders.push({ - message: formatMessage({ - id: 'email_and_password_entered_did_not_match', - }), - intent: Intent.DANGER, - }); - } - if (errors.find((e) => e.type === ERRORS_TYPES.USER_INACTIVE)) { - toastBuilders.push({ - message: formatMessage({ - id: 'the_user_has_been_suspended_from_admin', - }), - intent: Intent.DANGER, - }); - } - if ( - errors.find((e) => e.type === ERRORS_TYPES.LOGIN_TO_MANY_ATTEMPTS) - ) { - toastBuilders.push({ - message: formatMessage({ - id: 'your_account_has_been_locked', - }), - intent: Intent.DANGER, - }); - } - toastBuilders.forEach((builder) => { - Toaster.show(builder); - }); - setSubmitting(false); - }); - }, - }); + .catch(({ response: { data: { errors } } }) => { + const toastBuilders = transformLoginErrorsToToasts(errors); - const passwordRevealerTmp = useMemo( - () => ( - passwordRevealer()}> - - <> - {' '} - - - - - - - <> - {' '} - - - - - - - ), - [shown, passwordRevealer], - ); + toastBuilders.forEach((builder) => { + Toaster.show(builder); + }); + setSubmitting(false); + }); + }; return ( @@ -142,59 +48,15 @@ function Login({ requestLogin }) { -
- } - intent={errors.crediential && touched.crediential && Intent.DANGER} - helperText={ - - } - className={'form-group--crediential'} - > - - - - } - labelInfo={passwordRevealerTmp} - intent={errors.password && touched.password && Intent.DANGER} - helperText={ - - } - className={'form-group--password has-password-revealer'} - > - - - -
- - - -
- -
- -
-
+
); -} - -export default compose(withAuthenticationActions)(Login); +} \ No newline at end of file diff --git a/client/src/containers/Authentication/LoginForm.js b/client/src/containers/Authentication/LoginForm.js new file mode 100644 index 000000000..45fb79f44 --- /dev/null +++ b/client/src/containers/Authentication/LoginForm.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { + Button, + InputGroup, + Intent, + FormGroup, + Checkbox, +} from '@blueprintjs/core'; +import { Form, ErrorMessage, FastField } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { inputIntent } from 'utils'; +import { PasswordRevealer } from './components'; + +/** + * Login form. + */ +export default function LoginForm({ + isSubmitting +}) { + return ( +
+ + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--crediential'} + > + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--password has-password-revealer'} + > + + + )} + + +
+ + + +
+ +
+ +
+
+ ); +} diff --git a/client/src/containers/Authentication/Register.js b/client/src/containers/Authentication/Register.js index c34cc82ba..973208228 100644 --- a/client/src/containers/Authentication/Register.js +++ b/client/src/containers/Authentication/Register.js @@ -1,56 +1,28 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { Row, Col } from 'react-grid-system'; +import React, { useMemo } from 'react'; +import { Formik } from 'formik'; import { Link, useHistory } from 'react-router-dom'; import { - Button, - InputGroup, Intent, - FormGroup, - Spinner, } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; import AppToaster from 'components/AppToaster'; import AuthInsider from 'containers/Authentication/AuthInsider'; +import { useAuthLogin, useAuthRegister } from '../../hooks/query/authentication'; -import ErrorMessage from 'components/ErrorMessage'; -import Icon from 'components/Icon'; -import { If } from 'components'; -import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions'; +import RegisterForm from './RegisterForm'; +import { RegisterSchema, transformRegisterErrorsToForm } from './utils'; -import { compose } from 'utils'; - -function RegisterUserForm({ requestRegister, requestLogin }) { +/** + * Register form. + */ +export default function RegisterUserForm() { const { formatMessage } = useIntl(); const history = useHistory(); - const [shown, setShown] = useState(false); - const passwordRevealer = useCallback(() => { - setShown(!shown); - }, [shown]); - - const ValidationSchema = Yup.object().shape({ - first_name: Yup.string() - .required() - .label(formatMessage({ id: 'first_name_' })), - last_name: Yup.string() - .required() - .label(formatMessage({ id: 'last_name_' })), - email: Yup.string() - .email() - .required() - .label(formatMessage({ id: 'email' })), - phone_number: Yup.string() - .matches() - .required() - .label(formatMessage({ id: 'phone_number_' })), - password: Yup.string() - .min(4) - .required() - .label(formatMessage({ id: 'password' })), - }); + const { mutateAsync: authLoginMutate } = useAuthLogin(); + const { mutateAsync: authRegisterMutate } = useAuthRegister(); + const initialValues = useMemo( () => ({ first_name: '', @@ -58,85 +30,37 @@ function RegisterUserForm({ requestRegister, requestLogin }) { email: '', phone_number: '', password: '', + country: 'LY', }), [], ); - const { - errors, - touched, - handleSubmit, - getFieldProps, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - validationSchema: ValidationSchema, - initialValues: { - ...initialValues, - country: 'LY', - }, - onSubmit: (values, { setSubmitting, setErrors }) => { - requestRegister(values) - .then((response) => { - requestLogin({ - crediential: values.email, - password: values.password, - }) - .then(() => { - history.push('/register/subscription'); - setSubmitting(false); - }) - .catch((errors) => { - AppToaster.show({ - message: formatMessage({ id: 'something_wentwrong' }), - intent: Intent.SUCCESS, - }); - }); + const handleSubmit = (values, { setSubmitting, setErrors }) => { + authRegisterMutate(values) + .then((response) => { + authLoginMutate({ + crediential: values.email, + password: values.password, }) - .catch((errors) => { - if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) { - setErrors({ - phone_number: formatMessage({ - id: 'the_phone_number_already_used_in_another_account', - }), + .then(() => { + history.push('/register/subscription'); + setSubmitting(false); + }) + .catch(({ response: { data: { errors } } }) => { + AppToaster.show({ + message: formatMessage({ id: 'something_wentwrong' }), + intent: Intent.SUCCESS, }); - } - if (errors.some((e) => e.type === 'EMAIL.EXISTS')) { - setErrors({ - email: formatMessage({ - id: 'the_email_already_used_in_another_account', - }), - }); - } - setSubmitting(false); - }); - }, - }); - - const passwordRevealerTmp = useMemo( - () => ( - passwordRevealer()}> - - <> - {' '} - - - - - - - <> - {' '} - - - - - - - ), - [shown, passwordRevealer], - ); + }); + }) + .catch(({ response: { data: { errors } } }) => { + const formErrors = transformRegisterErrorsToForm(errors); + setErrors(formErrors); + setSubmitting(false); + }); + }; + return (
@@ -146,136 +70,17 @@ function RegisterUserForm({ requestRegister, requestLogin }) { - {' '}
-
- - - } - intent={ - errors.first_name && touched.first_name && Intent.DANGER - } - helperText={ - - } - className={'form-group--first-name'} - > - - - - - - } - intent={errors.last_name && touched.last_name && Intent.DANGER} - helperText={ - - } - className={'form-group--last-name'} - > - - - - - - } - intent={ - errors.phone_number && touched.phone_number && Intent.DANGER - } - helperText={ - - } - className={'form-group--phone-number'} - > - - - - } - intent={errors.email && touched.email && Intent.DANGER} - helperText={ - - } - className={'form-group--email'} - > - - - - } - labelInfo={passwordRevealerTmp} - intent={errors.password && touched.password && Intent.DANGER} - helperText={ - - } - className={'form-group--password has-password-revealer'} - > - - - -
-

-
- - - {' '} - - - {' '} - - -

-
- -
- -
-
- - -
- -
-
+
); -} - -export default compose(withAuthenticationActions)(RegisterUserForm); +} \ No newline at end of file diff --git a/client/src/containers/Authentication/RegisterForm.js b/client/src/containers/Authentication/RegisterForm.js new file mode 100644 index 000000000..194652b53 --- /dev/null +++ b/client/src/containers/Authentication/RegisterForm.js @@ -0,0 +1,141 @@ +import React from 'react'; +import { + Button, + InputGroup, + Intent, + FormGroup, + Spinner +} from '@blueprintjs/core'; +import { ErrorMessage, FastField, Form } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { Link } from 'react-router-dom'; +import { Row, Col, If } from 'components'; + +import { inputIntent } from 'utils'; + +/** + * Register form. + */ +export default function RegisterForm({ + isSubmitting, +}) { + return ( +
+ + + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--first-name'} + > + + + )} + + + + + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--last-name'} + > + + + )} + + + + + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--phone-number'} + > + + + )} + + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--email'} + > + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + // labelInfo={passwordRevealerTmp} + intent={inputIntent({ error, touched })} + helperText={ + + } + className={'form-group--password has-password-revealer'} + > + + + )} + + +
+

+
+ + + {' '} + + + {' '} + + +

+
+ +
+ +
+ + +
+ +
+
+
+ ); +} diff --git a/client/src/containers/Authentication/ResetPassword.js b/client/src/containers/Authentication/ResetPassword.js index c57a1863d..8116e60bb 100644 --- a/client/src/containers/Authentication/ResetPassword.js +++ b/client/src/containers/Authentication/ResetPassword.js @@ -1,40 +1,31 @@ import React, { useMemo } from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; - +import { Formik } from 'formik'; import { - Button, - InputGroup, Intent, - FormGroup, Position, } from '@blueprintjs/core'; import { Link, useParams, useHistory } from 'react-router-dom'; import { FormattedMessage as T, useIntl } from 'react-intl'; +import { useAuthResetPassword } from 'hooks/query'; + import AppToaster from 'components/AppToaster'; -import ErrorMessage from 'components/ErrorMessage'; import AuthInsider from 'containers/Authentication/AuthInsider'; -import withAuthenticationActions from './withAuthenticationActions'; -import { compose } from 'utils'; - -function ResetPassword({ requestResetPassword }) { +import ResetPasswordForm from './ResetPasswordForm'; +import { ResetPasswordSchema } from './utils'; +/** + * Reset password page. + */ +export default function ResetPassword() { const { formatMessage } = useIntl(); const { token } = useParams(); const history = useHistory(); - const ValidationSchema = Yup.object().shape({ - password: Yup.string() - .min(4) - .required() - .label(formatMessage({ id: 'password' })), - confirm_password: Yup.string() - .oneOf([Yup.ref('password'), null]) - .required() - .label(formatMessage({ id: 'confirm_password' })), - }); + // Authentication reset password. + const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword(); + // Initial values of the form. const initialValues = useMemo( () => ({ password: '', @@ -43,42 +34,30 @@ function ResetPassword({ requestResetPassword }) { [], ); - const { - touched, - errors, - handleSubmit, - getFieldProps, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - validationSchema: ValidationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting }) => { - requestResetPassword(values, token) - .then((response) => { + // Handle the form submitting. + const handleSubmit = (values, { setSubmitting }) => { + authResetPasswordMutate([token, values]) + .then((response) => { + AppToaster.show({ + message: formatMessage('password_successfully_updated'), + intent: Intent.DANGER, + position: Position.BOTTOM, + }); + history.push('/auth/login'); + setSubmitting(false); + }) + .catch(({ response: { data: { errors } } }) => { + if (errors.find((e) => e.type === 'TOKEN_INVALID')) { AppToaster.show({ - message: formatMessage('password_successfully_updated'), + message: formatMessage({ id: 'an_unexpected_error_occurred' }), intent: Intent.DANGER, position: Position.BOTTOM, }); history.push('/auth/login'); - setSubmitting(false); - }) - .catch((errors) => { - if (errors.find((e) => e.type === 'TOKEN_INVALID')) { - AppToaster.show({ - message: formatMessage('an_unexpected_error_occurred'), - intent: Intent.DANGER, - position: Position.BOTTOM, - }); - history.push('/auth/login'); - } - setSubmitting(false); - }); - }, - }); + } + setSubmitting(false); + }); + }; return ( @@ -93,66 +72,13 @@ function ResetPassword({ requestResetPassword }) { -
- } - intent={errors.password && touched.password && Intent.DANGER} - helperText={ - - } - className={'form-group--password'} - > - - - - } - labelInfo={'(again):'} - intent={ - errors.confirm_password && - touched.confirm_password && - Intent.DANGER - } - helperText={ - - } - className={'form-group--confirm-password'} - > - - - -
- -
-
+
); -} - -export default compose(withAuthenticationActions)(ResetPassword); +} \ No newline at end of file diff --git a/client/src/containers/Authentication/ResetPasswordForm.js b/client/src/containers/Authentication/ResetPasswordForm.js new file mode 100644 index 000000000..30a7bb446 --- /dev/null +++ b/client/src/containers/Authentication/ResetPasswordForm.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; +import { Form, ErrorMessage, FastField } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { inputIntent } from 'utils'; + +/** + * Reset password form. + */ +export default function ResetPasswordForm({ isSubmitting }) { + return ( +
+ + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--password'} + > + + + )} + + + + {({ form, field, meta: { error, touched } }) => ( + } + labelInfo={'(again):'} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--confirm-password'} + > + + + )} + + +
+ +
+
+ ); +} diff --git a/client/src/containers/Authentication/SendResetPassword.js b/client/src/containers/Authentication/SendResetPassword.js index 637304e29..b467e3cc5 100644 --- a/client/src/containers/Authentication/SendResetPassword.js +++ b/client/src/containers/Authentication/SendResetPassword.js @@ -1,75 +1,60 @@ import React, { useMemo } from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; +import { Formik } from 'formik'; import { FormattedMessage as T, useIntl } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; -import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core'; +import { Intent } from '@blueprintjs/core'; -import { compose } from 'utils'; +import { useAuthSendResetPassword } from 'hooks/query'; +import Toaster from 'components/AppToaster'; +import SendResetPasswordForm from './SendResetPasswordForm'; +import { SendResetPasswordSchema, transformSendResetPassErrorsToToasts } from './utils'; import AppToaster from 'components/AppToaster'; -import ErrorMessage from 'components/ErrorMessage'; - import AuthInsider from 'containers/Authentication/AuthInsider'; -import withAuthenticationActions from './withAuthenticationActions'; - -function SendResetPassword({ requestSendResetPassword }) { +/** + * Send reset password page. + */ +export default function SendResetPassword({ requestSendResetPassword }) { const { formatMessage } = useIntl(); const history = useHistory(); - // Validation schema. - const ValidationSchema = Yup.object().shape({ - crediential: Yup.string() - .required() - .email().label(formatMessage({ id: 'email' })), - }); + const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword(); + // Initial values. const initialValues = useMemo( () => ({ crediential: '', }), - [] + [], ); - // Formik validation - const { - errors, - touched, - handleSubmit, - getFieldProps, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - validationSchema: ValidationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting }) => { - requestSendResetPassword(values.crediential) - .then((response) => { - AppToaster.show({ - message: formatMessage({id:'check_your_email_for_a_link_to_reset'}), - intent: Intent.SUCCESS, - }); - history.push('/auth/login'); - setSubmitting(false); - }) - .catch((errors) => { - if (errors.find((e) => e.type === 'EMAIL.NOT.REGISTERED')) { - AppToaster.show({ - message: formatMessage({id:'we_couldn_t_find_your_account_with_that_email'}), - intent: Intent.DANGER, - }); - } - setSubmitting(false); + // Handle form submitting. + const handleSubmit = (values, { setSubmitting }) => { + sendResetPasswordMutate({ email: values.crediential }) + .then((response) => { + AppToaster.show({ + message: formatMessage({ + id: 'check_your_email_for_a_link_to_reset', + }), + intent: Intent.SUCCESS, }); - }, - }); + history.push('/auth/login'); + setSubmitting(false); + }) + .catch(({ response: { data: { errors } } }) => { + const toastBuilders = transformSendResetPassErrorsToToasts(errors); + + toastBuilders.forEach((builder) => { + Toaster.show(builder); + }); + setSubmitting(false); + }); + }; return ( -
+

@@ -79,38 +64,14 @@ function SendResetPassword({ requestSendResetPassword }) {

-
- - } - className={'form-group--crediential'} - > - - - -
- -
-
- -