|
1 | | -import React, { useRef } from 'react'; |
| 1 | +import React, { useRef, useState } from 'react'; |
2 | 2 | import { Link, RouteComponentProps } from 'react-router-dom'; |
3 | | -import login, { LoginData } from '@/api/auth/login'; |
| 3 | +import login from '@/api/auth/login'; |
4 | 4 | import LoginFormContainer from '@/components/auth/LoginFormContainer'; |
5 | | -import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy'; |
6 | | -import { ApplicationStore } from '@/state'; |
7 | | -import { FormikProps, withFormik } from 'formik'; |
| 5 | +import { useStoreState } from 'easy-peasy'; |
| 6 | +import { Formik, FormikHelpers } from 'formik'; |
8 | 7 | import { object, string } from 'yup'; |
9 | 8 | import Field from '@/components/elements/Field'; |
10 | | -import { httpErrorToHuman } from '@/api/http'; |
11 | | -import { FlashMessage } from '@/state/flashes'; |
12 | | -import ReCAPTCHA from 'react-google-recaptcha'; |
13 | 9 | import tw from 'twin.macro'; |
14 | 10 | import Button from '@/components/elements/Button'; |
| 11 | +import Reaptcha from 'reaptcha'; |
| 12 | +import useFlash from '@/plugins/useFlash'; |
15 | 13 |
|
16 | | -type OwnProps = RouteComponentProps & { |
17 | | - clearFlashes: ActionCreator<void>; |
18 | | - addFlash: ActionCreator<FlashMessage>; |
| 14 | +interface Values { |
| 15 | + username: string; |
| 16 | + password: string; |
19 | 17 | } |
20 | 18 |
|
21 | | -const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => { |
22 | | - const ref = useRef<ReCAPTCHA | null>(null); |
23 | | - const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha); |
| 19 | +const LoginContainer = ({ history }: RouteComponentProps) => { |
| 20 | + const ref = useRef<Reaptcha>(null); |
| 21 | + const [ token, setToken ] = useState(''); |
24 | 22 |
|
25 | | - const submit = (e: React.FormEvent<HTMLFormElement>) => { |
26 | | - e.preventDefault(); |
| 23 | + const { clearFlashes, clearAndAddHttpError } = useFlash(); |
| 24 | + const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); |
27 | 25 |
|
28 | | - if (ref.current && !values.recaptchaData) { |
29 | | - return ref.current.execute(); |
30 | | - } |
31 | | - |
32 | | - handleSubmit(e); |
33 | | - }; |
34 | | - |
35 | | - return ( |
36 | | - <React.Fragment> |
37 | | - {ref.current && ref.current.render()} |
38 | | - <LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}> |
39 | | - <Field |
40 | | - type={'text'} |
41 | | - label={'Username or Email'} |
42 | | - id={'username'} |
43 | | - name={'username'} |
44 | | - light |
45 | | - /> |
46 | | - <div css={tw`mt-6`}> |
47 | | - <Field |
48 | | - type={'password'} |
49 | | - label={'Password'} |
50 | | - id={'password'} |
51 | | - name={'password'} |
52 | | - light |
53 | | - /> |
54 | | - </div> |
55 | | - <div css={tw`mt-6`}> |
56 | | - <Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}> |
57 | | - Login |
58 | | - </Button> |
59 | | - </div> |
60 | | - {recaptchaEnabled && |
61 | | - <ReCAPTCHA |
62 | | - ref={ref} |
63 | | - size={'invisible'} |
64 | | - sitekey={siteKey || '_invalid_key'} |
65 | | - onChange={token => { |
66 | | - ref.current && ref.current.reset(); |
67 | | - setFieldValue('recaptchaData', token); |
68 | | - submitForm(); |
69 | | - }} |
70 | | - onExpired={() => setFieldValue('recaptchaData', null)} |
71 | | - /> |
72 | | - } |
73 | | - <div css={tw`mt-6 text-center`}> |
74 | | - <Link |
75 | | - to={'/auth/password'} |
76 | | - css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`} |
77 | | - > |
78 | | - Forgot password? |
79 | | - </Link> |
80 | | - </div> |
81 | | - </LoginFormContainer> |
82 | | - </React.Fragment> |
83 | | - ); |
84 | | -}; |
| 26 | + const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { |
| 27 | + clearFlashes(); |
85 | 28 |
|
86 | | -const EnhancedForm = withFormik<OwnProps, LoginData>({ |
87 | | - displayName: 'LoginContainerForm', |
88 | | - |
89 | | - mapPropsToValues: () => ({ |
90 | | - username: '', |
91 | | - password: '', |
92 | | - recaptchaData: null, |
93 | | - }), |
94 | | - |
95 | | - validationSchema: () => object().shape({ |
96 | | - username: string().required('A username or email must be provided.'), |
97 | | - password: string().required('Please enter your account password.'), |
98 | | - }), |
| 29 | + // If there is no token in the state yet, request the token and then abort this submit request |
| 30 | + // since it will be re-submitted when the recaptcha data is returned by the component. |
| 31 | + if (recaptchaEnabled && !token) { |
| 32 | + ref.current!.execute().catch(error => console.error(error)); |
| 33 | + return; |
| 34 | + } |
99 | 35 |
|
100 | | - handleSubmit: (values, { props, setFieldValue, setSubmitting }) => { |
101 | | - props.clearFlashes(); |
102 | | - login(values) |
| 36 | + login({ ...values, recaptchaData: token }) |
103 | 37 | .then(response => { |
104 | 38 | if (response.complete) { |
105 | 39 | // @ts-ignore |
106 | 40 | window.location = response.intended || '/'; |
107 | 41 | return; |
108 | 42 | } |
109 | 43 |
|
110 | | - props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken }); |
| 44 | + history.replace('/auth/login/checkpoint', { token: response.confirmationToken }); |
111 | 45 | }) |
112 | 46 | .catch(error => { |
113 | 47 | console.error(error); |
114 | 48 |
|
115 | 49 | setSubmitting(false); |
116 | | - setFieldValue('recaptchaData', null); |
117 | | - props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); |
| 50 | + clearAndAddHttpError({ error }); |
118 | 51 | }); |
119 | | - }, |
120 | | -})(LoginContainer); |
121 | | - |
122 | | -export default (props: RouteComponentProps) => { |
123 | | - const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); |
| 52 | + }; |
124 | 53 |
|
125 | 54 | return ( |
126 | | - <EnhancedForm |
127 | | - {...props} |
128 | | - addFlash={addFlash} |
129 | | - clearFlashes={clearFlashes} |
130 | | - /> |
| 55 | + <Formik |
| 56 | + onSubmit={onSubmit} |
| 57 | + initialValues={{ username: '', password: '' }} |
| 58 | + validationSchema={object().shape({ |
| 59 | + username: string().required('A username or email must be provided.'), |
| 60 | + password: string().required('Please enter your account password.'), |
| 61 | + })} |
| 62 | + > |
| 63 | + {({ isSubmitting, setSubmitting, submitForm }) => ( |
| 64 | + <LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}> |
| 65 | + <Field |
| 66 | + type={'text'} |
| 67 | + label={'Username or Email'} |
| 68 | + id={'username'} |
| 69 | + name={'username'} |
| 70 | + light |
| 71 | + /> |
| 72 | + <div css={tw`mt-6`}> |
| 73 | + <Field |
| 74 | + type={'password'} |
| 75 | + label={'Password'} |
| 76 | + id={'password'} |
| 77 | + name={'password'} |
| 78 | + light |
| 79 | + /> |
| 80 | + </div> |
| 81 | + <div css={tw`mt-6`}> |
| 82 | + <Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}> |
| 83 | + Login |
| 84 | + </Button> |
| 85 | + </div> |
| 86 | + {recaptchaEnabled && |
| 87 | + <Reaptcha |
| 88 | + ref={ref} |
| 89 | + size={'invisible'} |
| 90 | + sitekey={siteKey || '_invalid_key'} |
| 91 | + onVerify={response => { |
| 92 | + setToken(response); |
| 93 | + submitForm(); |
| 94 | + }} |
| 95 | + onExpire={() => { |
| 96 | + setSubmitting(false); |
| 97 | + setToken(''); |
| 98 | + }} |
| 99 | + /> |
| 100 | + } |
| 101 | + <div css={tw`mt-6 text-center`}> |
| 102 | + <Link |
| 103 | + to={'/auth/password'} |
| 104 | + css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`} |
| 105 | + > |
| 106 | + Forgot password? |
| 107 | + </Link> |
| 108 | + </div> |
| 109 | + </LoginFormContainer> |
| 110 | + )} |
| 111 | + </Formik> |
131 | 112 | ); |
132 | 113 | }; |
| 114 | + |
| 115 | +export default LoginContainer; |
0 commit comments