| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import { useEffect, useRef, useState, useLayoutEffect } from 'react'; | |
| import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; | |
| import { useTranslation } from 'react-i18next'; | |
| import ReactDOM from 'react-dom/client'; | |
| import type { | |
| FormValue, | |
| ImgCodeRes, | |
| CaptchaKey, | |
| FieldError, | |
| ImgCodeReq, | |
| } from './interface'; | |
| export interface Props { | |
| captchaKey: CaptchaKey; | |
| commonProps?: any; | |
| } | |
| const checkImgCode = ({ | |
| captchaKey, | |
| commonProps, | |
| }: Props) => { | |
| return new Promise<ImgCodeRes>((resolve) => { | |
| fetch(`/answer/api/v1/user/action/record?action=${captchaKey}`, { | |
| headers: { | |
| ...commonProps?.headers | |
| } | |
| }) | |
| .then((resp) => { | |
| return resp.json(); | |
| }) | |
| .then((data) => { | |
| console.log('checkImgCode', data); | |
| resolve(data.data); | |
| }); | |
| }); | |
| }; | |
| type SubmitCallback = { | |
| (): void; | |
| }; | |
| const Index = ({ | |
| captchaKey, | |
| commonProps, | |
| }: Props) => { | |
| const refRoot = useRef<ReactDOM.Root | null>(null); | |
| if (refRoot.current === null) { | |
| refRoot.current = ReactDOM.createRoot(document.createElement('div')); | |
| } | |
| const { t } = useTranslation('plugin', { | |
| keyPrefix: 'basic_captcha.frontend', | |
| }); | |
| const refKey = useRef<CaptchaKey>(captchaKey); | |
| const refCallback = useRef<SubmitCallback>(); | |
| const pending = useRef(false); | |
| const autoInitCaptchaData = /email/i.test(refKey.current); | |
| const [stateShow, setStateShow] = useState(false); | |
| const [captcha, setCaptcha] = useState<ImgCodeRes>({ | |
| captcha_id: '', | |
| captcha_img: '', | |
| verify: false, | |
| }); | |
| const [imgCode, setImgCode] = useState<FormValue>({ | |
| value: '', | |
| isInvalid: false, | |
| errorMsg: '', | |
| }); | |
| const refCaptcha = useRef(captcha); | |
| const refImgCode = useRef(imgCode); | |
| const fetchCaptchaData = () => { | |
| pending.current = true; | |
| checkImgCode({ | |
| captchaKey: refKey.current, | |
| commonProps, | |
| }) | |
| .then((resp) => { | |
| setCaptcha(resp); | |
| }) | |
| .finally(() => { | |
| pending.current = false; | |
| }); | |
| }; | |
| const resetCapture = () => { | |
| setCaptcha({ | |
| captcha_id: '', | |
| captcha_img: '', | |
| verify: false, | |
| }); | |
| }; | |
| const resetImgCode = () => { | |
| setImgCode({ | |
| value: '', | |
| isInvalid: false, | |
| errorMsg: '', | |
| }); | |
| }; | |
| const resetCallback = () => { | |
| refCallback.current = undefined; | |
| }; | |
| const show = () => { | |
| if (!stateShow) { | |
| setStateShow(true); | |
| } | |
| }; | |
| /** | |
| * There are some cases where the React scheduler cancels the execution of some functions, | |
| * which prevents them from closing properly: | |
| * for example, if the parent component uninstalls the child component directly, | |
| * and the `captchaModal.close()` call is inside the child component. | |
| * In this case, call `await captchaModal.close()` and wait for the close action to complete. | |
| */ | |
| const close = () => { | |
| setStateShow(false); | |
| resetCapture(); | |
| resetImgCode(); | |
| resetCallback(); | |
| const p = new Promise<void>((resolve) => { | |
| setTimeout(resolve, 50); | |
| }); | |
| return p; | |
| }; | |
| const handleCaptchaError = (fel: FieldError[] = []) => { | |
| console.log('handleCaptchaError', fel); | |
| const captchaErr = fel.find((o) => { | |
| return o.error_field === 'captcha_code'; | |
| }); | |
| const ri = refImgCode.current; | |
| if (captchaErr) { | |
| /** | |
| * `imgCode.value` No value but a validation error is received, | |
| * indicating that it is the first time the interface has returned a CAPTCHA error, | |
| * triggering the CAPTCHA logic. There is no need to display the error message at this point. | |
| */ | |
| if (ri.value) { | |
| setImgCode({ | |
| ...ri, | |
| isInvalid: true, | |
| errorMsg: captchaErr.error_msg, | |
| }); | |
| } | |
| fetchCaptchaData(); | |
| show(); | |
| } else { | |
| close(); | |
| } | |
| // Assist business logic in filtering CAPTCHA error messages when necessary | |
| return captchaErr; | |
| }; | |
| const handleChange = (evt) => { | |
| evt.preventDefault(); | |
| setImgCode({ | |
| value: evt.target.value || '', | |
| isInvalid: false, | |
| errorMsg: '', | |
| }); | |
| }; | |
| const getCaptcha = () => { | |
| const rc = refCaptcha.current; | |
| const ri = refImgCode.current; | |
| const r = { | |
| verify: !!rc?.verify, | |
| captcha_id: rc?.captcha_id, | |
| captcha_code: ri.value, | |
| }; | |
| return r; | |
| }; | |
| const resolveCaptchaReq = (req: ImgCodeReq) => { | |
| const r = getCaptcha(); | |
| if (r.verify) { | |
| req.captcha_code = r.captcha_code; | |
| req.captcha_id = r.captcha_id; | |
| } | |
| }; | |
| const handleSubmit = (evt) => { | |
| evt.preventDefault(); | |
| if (!imgCode.value) { | |
| return; | |
| } | |
| if (refCallback.current) { | |
| refCallback.current(); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (autoInitCaptchaData) { | |
| fetchCaptchaData(); | |
| } | |
| }, []); | |
| useLayoutEffect(() => { | |
| refImgCode.current = imgCode; | |
| refCaptcha.current = captcha; | |
| }, [captcha, imgCode]); | |
| useEffect(() => { | |
| refRoot.current?.render( | |
| <Modal | |
| size="sm" | |
| title="Captcha" | |
| show={stateShow} | |
| onHide={() => close()} | |
| centered> | |
| <Modal.Header closeButton> | |
| <Modal.Title as="h5">{t('title')}</Modal.Title> | |
| </Modal.Header> | |
| <Modal.Body> | |
| <Form noValidate onSubmit={handleSubmit}> | |
| <Form.Group controlId="code" className="mb-3"> | |
| <div className="mb-3 p-2 d-flex align-items-center justify-content-center bg-light rounded-2"> | |
| <img | |
| src={captcha?.captcha_img} | |
| alt="captcha img" | |
| width="auto" | |
| height="60px" | |
| /> | |
| </div> | |
| <InputGroup> | |
| <Form.Control | |
| type="text" | |
| autoComplete="off" | |
| placeholder={t('placeholder')} | |
| isInvalid={imgCode?.isInvalid} | |
| onChange={handleChange} | |
| value={imgCode.value} | |
| /> | |
| <Button | |
| onClick={fetchCaptchaData} | |
| variant="outline-secondary" | |
| title={t('refresh', { keyPrefix: 'btns' })} | |
| style={{ | |
| borderTopRightRadius: '0.375rem', | |
| borderBottomRightRadius: '0.375rem', | |
| }}> | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| width="16" | |
| height="16" | |
| fill="currentColor" | |
| className="bi bi-arrow-repeat" | |
| viewBox="0 0 16 16"> | |
| <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41m-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9" /> | |
| <path | |
| fillRule="evenodd" | |
| d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5 5 0 0 0 8 3M3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9z" | |
| /> | |
| </svg> | |
| </Button> | |
| <Form.Control.Feedback type="invalid"> | |
| {imgCode?.errorMsg} | |
| </Form.Control.Feedback> | |
| </InputGroup> | |
| </Form.Group> | |
| <div className="d-grid"> | |
| <Button type="submit" disabled={!imgCode.value}> | |
| {t('verify')} | |
| </Button> | |
| </div> | |
| </Form> | |
| </Modal.Body> | |
| </Modal>, | |
| ); | |
| }); | |
| const r = { | |
| close, | |
| show, | |
| check: (submitFunc: SubmitCallback) => { | |
| if (pending.current) { | |
| return false; | |
| } | |
| refCallback.current = submitFunc; | |
| if (captcha?.verify) { | |
| show(); | |
| return false; | |
| } | |
| return submitFunc(); | |
| }, | |
| getCaptcha, | |
| resolveCaptchaReq, | |
| fetchCaptchaData, | |
| handleCaptchaError, | |
| }; | |
| return r; | |
| }; | |
| export default Index; |