@@ -11,6 +11,4 @@ function AuthCopyright() {
);
-}
-
-export default AuthCopyright;
+}
\ No newline at end of file
diff --git a/client/src/containers/Authentication/AuthInsider.js b/client/src/containers/Authentication/AuthInsider.js
index 5626f58a1..4df1d7348 100644
--- a/client/src/containers/Authentication/AuthInsider.js
+++ b/client/src/containers/Authentication/AuthInsider.js
@@ -1,7 +1,9 @@
import React from 'react';
-import Icon from 'components/Icon';
import AuthCopyright from './AuthCopyright';
+/**
+ * Authentication insider page.
+ */
export default function AuthInsider({
logo = true,
copyright = true,
diff --git a/client/src/containers/Authentication/InviteAccept.js b/client/src/containers/Authentication/InviteAccept.js
index bedc61291..5a5267835 100644
--- a/client/src/containers/Authentication/InviteAccept.js
+++ b/client/src/containers/Authentication/InviteAccept.js
@@ -1,276 +1,20 @@
-import React, { useCallback, useMemo, useState } from 'react';
-import * as Yup from 'yup';
-import { useFormik } from 'formik';
-import {
- Button,
- InputGroup,
- Intent,
- FormGroup,
- Position,
- Spinner,
-} from '@blueprintjs/core';
+import React from 'react';
import { useParams } from 'react-router-dom';
-import { Row, Col } from 'react-grid-system';
-import { Link, useHistory } from 'react-router-dom';
-import { FormattedMessage as T, useIntl } from 'react-intl';
-
-
-import AppToaster from 'components/AppToaster';
-import ErrorMessage from 'components/ErrorMessage';
-
-import Icon from 'components/Icon';
-import { If } from 'components';
-import useAsync from 'hooks/async';
+import InviteAcceptForm from './InviteAcceptForm';
import AuthInsider from 'containers/Authentication/AuthInsider';
+import { InviteAcceptProvider } from './InviteAcceptProvider';
-import withAuthenticationActions from './withAuthenticationActions';
-
-import { compose } from 'utils';
-
-function Invite({ requestInviteAccept, requestInviteMetaByToken }) {
- const { formatMessage } = useIntl();
+/**
+ * Authentication invite page.
+ */
+export default function Invite() {
const { token } = useParams();
- 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_'})),
- phone_number: Yup.string()
- .matches()
- .required()
- .label(formatMessage({id:'phone_number'})),
- password: Yup.string()
- .min(4)
- .required().label(formatMessage({id:'password'}))
- });
-
- const inviteMeta = useAsync(() => {
- return requestInviteMetaByToken(token);
- });
-
- const inviteErrors = inviteMeta.error || [];
- const inviteValue = {
- organization_name: '',
- invited_email: '',
- ...(inviteMeta.value ? inviteMeta.value.data.data : {}),
- };
-
- if (inviteErrors.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');
- }
-
- const initialValues = useMemo(
- () => ({
- first_name: '',
- last_name: '',
- phone_number: '',
- password: '',
- }),
- []
- );
-
- const {
- touched,
- errors,
- handleSubmit,
- getFieldProps,
- isSubmitting,
- } = useFormik({
- enableReinitialize: true,
- validationSchema: ValidationSchema,
- initialValues: {
- ...initialValues,
- },
- onSubmit: (values, { setSubmitting, setErrors }) => {
- requestInviteAccept(values, token)
- .then((response) => {
- AppToaster.show({
- message: `Congrats! Your account has been created and invited to
-
${inviteValue.organization_name} organization successfully.`,
- intent: Intent.SUCCESS,
- });
- setSubmitting(false);
- })
- .catch((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.',
- });
- }
- setSubmitting(false);
- });
- },
- });
-
- const passwordRevealerTmp = useMemo(
- () => (
-
passwordRevealer()}>
-
- <>
- {' '}
-
-
-
- >
-
-
- <>
- {' '}
-
-
-
- >
-
-
- ),
- [shown, passwordRevealer]
- );
return (
-
-
-
-
-
-
-
- {inviteValue.organization_name}
-
-
-
-
-
- {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 (
+
+ );
+}
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 }) {
-