diff --git a/app/soapbox/features/auth_login/components/captcha.js b/app/soapbox/features/auth_login/components/captcha.js
deleted file mode 100644
index bcdaf74a4..000000000
--- a/app/soapbox/features/auth_login/components/captcha.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { Map as ImmutableMap } from 'immutable';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import { fetchCaptcha } from 'soapbox/actions/auth';
-import { TextInput } from 'soapbox/features/forms';
-
-const noOp = () => {};
-
-export default @connect()
-class CaptchaField extends React.Component {
-
- static propTypes = {
- onChange: PropTypes.func,
- onFetch: PropTypes.func,
- onFetchFail: PropTypes.func,
- onClick: PropTypes.func,
- dispatch: PropTypes.func,
- refreshInterval: PropTypes.number,
- idempotencyKey: PropTypes.string,
- }
-
- static defaultProps = {
- onChange: noOp,
- onFetch: noOp,
- onFetchFail: noOp,
- onClick: noOp,
- refreshInterval: 5*60*1000, // 5 minutes, Pleroma default
- }
-
- state = {
- captcha: ImmutableMap(),
- refresh: undefined,
- }
-
- startRefresh = () => {
- const { refreshInterval } = this.props;
- if (refreshInterval) {
- const refresh = setInterval(this.fetchCaptcha, refreshInterval);
- this.setState({ refresh });
- }
- }
-
- endRefresh = () => {
- clearInterval(this.state.refresh);
- }
-
- forceRefresh = () => {
- this.fetchCaptcha();
- this.endRefresh();
- this.startRefresh();
- }
-
- fetchCaptcha = () => {
- const { dispatch, onFetch, onFetchFail } = this.props;
- dispatch(fetchCaptcha()).then(response => {
- const captcha = ImmutableMap(response.data);
- this.setState({ captcha });
- onFetch(captcha);
- }).catch(error => {
- onFetchFail(error);
- });
- }
-
- componentDidMount() {
- this.fetchCaptcha();
- this.startRefresh(); // Refresh periodically
- }
-
- componentWillUnmount() {
- this.endRefresh();
- }
-
- componentDidUpdate(prevProps) {
- if (this.props.idempotencyKey !== prevProps.idempotencyKey) {
- this.forceRefresh();
- }
- }
-
- render() {
- const { captcha } = this.state;
- const { onChange, onClick, ...props } = this.props;
-
- switch(captcha.get('type')) {
- case 'native':
- return (
-
- );
- case 'none':
- default:
- return null;
- }
- }
-
-}
-
-export const NativeCaptchaField = ({ captcha, onChange, onClick, name, value }) => (
-
-
})
-
-
-);
-
-NativeCaptchaField.propTypes = {
- captcha: ImmutablePropTypes.map.isRequired,
- onChange: PropTypes.func,
- onClick: PropTypes.func,
- name: PropTypes.string,
- value: PropTypes.string,
-};
diff --git a/app/soapbox/features/auth_login/components/captcha.tsx b/app/soapbox/features/auth_login/components/captcha.tsx
new file mode 100644
index 000000000..c45010dac
--- /dev/null
+++ b/app/soapbox/features/auth_login/components/captcha.tsx
@@ -0,0 +1,118 @@
+import { Map as ImmutableMap } from 'immutable';
+import React, { useState, useEffect } from 'react';
+import { FormattedMessage } from 'react-intl';
+
+import { fetchCaptcha } from 'soapbox/actions/auth';
+import { TextInput } from 'soapbox/features/forms';
+import { useAppDispatch } from 'soapbox/hooks';
+
+const noOp = () => {};
+
+interface ICaptchaField {
+ name?: string,
+ value: string,
+ onChange?: React.ChangeEventHandler,
+ onFetch?: (captcha: ImmutableMap) => void,
+ onFetchFail?: (error: Error) => void,
+ onClick?: React.MouseEventHandler,
+ refreshInterval?: number,
+ idempotencyKey: string,
+}
+
+const CaptchaField: React.FC = ({
+ name,
+ value,
+ onChange = noOp,
+ onFetch = noOp,
+ onFetchFail = noOp,
+ onClick = noOp,
+ refreshInterval = 5*60*1000, // 5 minutes, Pleroma default
+ idempotencyKey,
+}) => {
+ const dispatch = useAppDispatch();
+
+ const [captcha, setCaptcha] = useState(ImmutableMap());
+ const [refresh, setRefresh] = useState(undefined);
+
+ const getCaptcha = () => {
+ dispatch(fetchCaptcha()).then(response => {
+ const captcha = ImmutableMap(response.data);
+ setCaptcha(captcha);
+ onFetch(captcha);
+ }).catch((error: Error) => {
+ onFetchFail(error);
+ });
+ };
+
+ const startRefresh = () => {
+ if (refreshInterval) {
+ const newRefresh = setInterval(getCaptcha, refreshInterval);
+ setRefresh(newRefresh);
+ }
+ };
+
+ const endRefresh = () => {
+ if (refresh) {
+ clearInterval(refresh);
+ }
+ };
+
+ useEffect(() => {
+ getCaptcha();
+ endRefresh();
+ startRefresh(); // Refresh periodically
+
+ return () => {
+ endRefresh();
+ };
+ }, [idempotencyKey]);
+
+ switch(captcha.get('type')) {
+ case 'native':
+ return (
+
+ );
+ case 'none':
+ default:
+ return null;
+ }
+};
+
+interface INativeCaptchaField {
+ captcha: ImmutableMap,
+ onChange: React.ChangeEventHandler,
+ onClick: React.MouseEventHandler,
+ name?: string,
+ value: string,
+}
+
+const NativeCaptchaField: React.FC = ({ captcha, onChange, onClick, name, value }) => (
+
+
})
+
+
+);
+
+export {
+ CaptchaField as default,
+ NativeCaptchaField,
+};