diff --git a/frontend/app/Router.js b/frontend/app/Router.js
index 600e4ba1a..10aac6cf8 100644
--- a/frontend/app/Router.js
+++ b/frontend/app/Router.js
@@ -13,7 +13,7 @@ import { withStore } from 'App/mstore';
import APIClient from './api_client';
import * as routes from './routes';
import { OB_DEFAULT_TAB, isRoute } from 'App/routes';
-import Signup from './components/Signup/Signup';
+import Signup from 'Components/Signup';
import { fetchTenants } from 'Duck/user';
import { setSessionPath } from 'Duck/sessions';
import { ModalProvider } from './components/Modal';
diff --git a/frontend/app/components/Signup/Signup.js b/frontend/app/components/Signup/Signup.js
deleted file mode 100644
index c52681c88..000000000
--- a/frontend/app/components/Signup/Signup.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import React from 'react';
-import withPageTitle from 'HOCs/withPageTitle';
-import { Icon } from 'UI';
-
-import { connect } from 'react-redux';
-import cn from 'classnames';
-import SignupForm from './SignupForm';
-import RegisterBg from '../../svg/register.svg';
-import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
-import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
-import { login } from 'App/routes';
-import { withRouter } from 'react-router-dom';
-import { fetchTenants } from 'Duck/user';
-import Copyright from 'Shared/Copyright';
-
-const LOGIN_ROUTE = login();
-const BulletItem = ({ text }) => (
-
-);
-
-const healthStatusCheck_key = '__or__healthStatusCheck_key'
-
-@connect(
- (state, props) => ({
- loading: state.getIn(['user', 'loginRequest', 'loading']),
- authDetails: state.getIn(['user', 'authDetails']),
- }), { fetchTenants }
-)
-@withPageTitle('Signup - OpenReplay')
-@withRouter
-export default class Signup extends React.Component {
- state = {
- healthModalPassed: localStorage.getItem(healthStatusCheck_key === 'true'),
- healthStatusLoading: true,
- healthStatus: null,
- }
-
- static getDerivedStateFromProps(nextProps, prevState) {
- const { authDetails } = nextProps;
- if (Object.keys(authDetails).length === 0) {
- return null;
- }
-
- if (authDetails.tenants) {
- nextProps.history.push(LOGIN_ROUTE);
- }
-
- return null;
- }
-
- getHealth = async () => {
- this.setState({ healthStatusLoading: true });
- const { healthMap } = await getHealthRequest(true);
- this.setState({ healthStatus: healthMap, healthStatusLoading: false });
- }
-
- componentDidMount() {
- if (!this.state.healthModalPassed) void this.getHealth();
-
- const { authDetails } = this.props;
- if (Object.keys(authDetails).length === 0) {
- this.props.fetchTenants();
- }
- }
-
- setHealthModalPassed = () => {
- localStorage.setItem(healthStatusCheck_key, 'true');
- this.setState({ healthModalPassed: true });
- }
-
- render() {
- if (!this.state.healthModalPassed) {
- return (
- null}
- healthResponse={this.state.healthStatus}
- getHealth={this.getHealth}
- isLoading={this.state.healthStatusLoading}
- setPassed={this.setHealthModalPassed}
- />
- )
- }
-
- return (
-
- );
- }
-}
diff --git a/frontend/app/components/Signup/Signup.tsx b/frontend/app/components/Signup/Signup.tsx
new file mode 100644
index 000000000..0165fe4da
--- /dev/null
+++ b/frontend/app/components/Signup/Signup.tsx
@@ -0,0 +1,91 @@
+import React, { useEffect, useState } from 'react';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { connect, ConnectedProps } from 'react-redux';
+import { Icon } from 'UI';
+import SignupForm from './SignupForm';
+import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
+import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
+import { fetchTenants } from 'Duck/user';
+import withPageTitle from 'HOCs/withPageTitle';
+import { login } from 'App/routes';
+import Copyright from 'Shared/Copyright';
+
+const LOGIN_ROUTE = login();
+const BulletItem: React.FC<{ text: string }> = ({ text }) => (
+
+);
+
+const healthStatusCheck_key = '__or__healthStatusCheck_key';
+
+const mapStateToProps = (state: any) => ({
+ loading: state.getIn(['user', 'loginRequest', 'loading']),
+ authDetails: state.getIn(['user', 'authDetails'])
+});
+
+const mapDispatchToProps = {
+ fetchTenants
+};
+
+const connector = connect(mapStateToProps, mapDispatchToProps);
+
+type PropsFromRedux = ConnectedProps;
+
+type SignupProps = PropsFromRedux & RouteComponentProps;
+
+const Signup: React.FC = ({ loading, authDetails, fetchTenants, history }) => {
+ const [healthModalPassed, setHealthModalPassed] = useState(localStorage.getItem(healthStatusCheck_key) === 'true');
+ const [healthStatusLoading, setHealthStatusLoading] = useState(true);
+ const [healthStatus, setHealthStatus] = useState(null);
+
+ const getHealth = async () => {
+ setHealthStatusLoading(true);
+ const { healthMap } = await getHealthRequest(true);
+ setHealthStatus(healthMap);
+ setHealthStatusLoading(false);
+ };
+
+ useEffect(() => {
+ if (!healthModalPassed) void getHealth();
+
+ if (Object.keys(authDetails).length === 0) {
+ fetchTenants();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (Object.keys(authDetails).length === 0) {
+ history.push(LOGIN_ROUTE);
+ }
+ }, [authDetails]);
+
+ if (!healthModalPassed) {
+ return (
+ null}
+ healthResponse={healthStatus}
+ getHealth={getHealth}
+ isLoading={healthStatusLoading}
+ setPassed={() => setHealthModalPassed(true)}
+ />
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default connector(withRouter(withPageTitle('Signup - OpenReplay')(Signup)));
diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js
deleted file mode 100644
index b8242cc84..000000000
--- a/frontend/app/components/Signup/SignupForm/SignupForm.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import React from 'react';
-import { Form, Input, Icon, Button, Link } from 'UI';
-import { login } from 'App/routes';
-import ReCAPTCHA from 'react-google-recaptcha';
-import stl from './signup.module.css';
-import { signup } from 'Duck/user';
-import { connect } from 'react-redux';
-import Select from 'Shared/Select';
-import { SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
-
-const LOGIN_ROUTE = login();
-const recaptchaRef = React.createRef();
-
-@connect(
- (state) => ({
- tenants: state.getIn(['user', 'tenants']),
- errors: state.getIn(['user', 'signupRequest', 'errors']),
- loading: state.getIn(['user', 'signupRequest', 'loading']),
- }),
- { signup }
-)
-export default class SignupForm extends React.Component {
- state = {
- tenantId: '',
- fullname: '',
- password: '',
- email: '',
- projectName: '',
- organizationName: '',
- reload: false,
- CAPTCHA_ENABLED: window.env.CAPTCHA_ENABLED === 'true',
- };
-
- static getDerivedStateFromProps(props, state) {
- if (props.errors && props.errors.size > 0 && state.reload) {
- recaptchaRef.current.reset();
- return {
- reload: false,
- };
- }
- return null;
- }
-
- handleSubmit = (token) => {
- const { tenantId, fullname, password, email, projectName, organizationName, auth } = this.state;
- localStorage.removeItem(SITE_ID_STORAGE_KEY);
- this.props.signup({
- tenantId,
- fullname,
- password,
- email,
- projectName,
- organizationName,
- auth,
- 'g-recaptcha-response': token,
- });
- this.setState({ reload: true });
- };
-
- write = ({ target: { value, name } }) => this.setState({ [name]: value });
- writeOption = ({ name, value }) => this.setState({ [name]: value.value });
-
- onSubmit = (e) => {
- e.preventDefault();
- const { CAPTCHA_ENABLED } = this.state;
- if (CAPTCHA_ENABLED && recaptchaRef.current) {
- recaptchaRef.current.execute();
- } else if (!CAPTCHA_ENABLED) {
- this.handleSubmit();
- }
- };
- render() {
- const { loading, errors, tenants } = this.props;
- const { CAPTCHA_ENABLED } = this.state;
-
- return (
-
-
-

-
-
-
-
- Already having an account?{' '}
-
- Login
-
-
-
- );
- }
-}
diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.tsx b/frontend/app/components/Signup/SignupForm/SignupForm.tsx
new file mode 100644
index 000000000..091400647
--- /dev/null
+++ b/frontend/app/components/Signup/SignupForm/SignupForm.tsx
@@ -0,0 +1,217 @@
+import React, { useState, useRef, ChangeEvent, FormEvent, useEffect } from 'react';
+import { Form, Input, Button, Link } from 'UI';
+import { login } from 'App/routes';
+import ReCAPTCHA from 'react-google-recaptcha';
+import { signup } from 'Duck/user';
+import { connect, ConnectedProps } from 'react-redux';
+import Select from 'Shared/Select';
+import { SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
+import { validatePassword } from 'App/validate';
+import { PASSWORD_POLICY } from 'App/constants';
+import { Alert, Space } from 'antd';
+
+const LOGIN_ROUTE = login();
+
+const mapState = (state: any) => ({
+ tenants: state.getIn(['user', 'tenants']),
+ errors: state.getIn(['user', 'signupRequest', 'errors']),
+ loading: state.getIn(['user', 'signupRequest', 'loading'])
+});
+
+const mapDispatch = {
+ signup
+};
+
+const connector = connect(mapState, mapDispatch);
+
+type PropsFromRedux = ConnectedProps;
+
+type SignupFormProps = PropsFromRedux;
+
+const SignupForm: React.FC = ({ tenants, errors, loading, signup }) => {
+ const [state, setState] = useState({
+ tenantId: '',
+ fullname: '',
+ password: '',
+ email: '',
+ projectName: '',
+ organizationName: '',
+ reload: false,
+ CAPTCHA_ENABLED: window.env.CAPTCHA_ENABLED === 'true'
+ });
+ const recaptchaRef = useRef(null);
+ const [passwordError, setPasswordError] = useState(null);
+
+ const handleSubmit = (token: string) => {
+ const { tenantId, fullname, password, email, projectName, organizationName, auth } = state;
+ if (!validatePassword(password)) return;
+ localStorage.removeItem(SITE_ID_STORAGE_KEY);
+ signup({
+ tenantId,
+ fullname,
+ password,
+ email,
+ projectName,
+ organizationName,
+ auth,
+ 'g-recaptcha-response': token
+ });
+ setState({ ...state, reload: true });
+ };
+
+ const write = ({ target: { value, name } }: ChangeEvent) =>
+ setState({ ...state, [name]: value });
+
+ const writeOption = ({ name, value }: { name: string; value: { value: string } }) =>
+ setState({ ...state, [name]: value.value });
+
+ const onSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ const { CAPTCHA_ENABLED } = state;
+ if (CAPTCHA_ENABLED && recaptchaRef.current) {
+ recaptchaRef.current.execute();
+ } else if (!CAPTCHA_ENABLED) {
+ handleSubmit('');
+ }
+ };
+
+ useEffect(() => {
+ if (state.password && !validatePassword(state.password)) {
+ setPasswordError('Password must be at least 8 characters long');
+ } else {
+ setPasswordError(null);
+ }
+ }, [state.password]);
+
+ return (
+
+
+

+
+
+
+
+ Already having an account?{' '}
+
+ Login
+
+
+
+ );
+};
+
+export default connector(SignupForm);
diff --git a/frontend/app/components/Signup/SignupForm/index.js b/frontend/app/components/Signup/SignupForm/index.ts
similarity index 100%
rename from frontend/app/components/Signup/SignupForm/index.js
rename to frontend/app/components/Signup/SignupForm/index.ts
diff --git a/frontend/app/components/Signup/index.ts b/frontend/app/components/Signup/index.ts
new file mode 100644
index 000000000..6f887af30
--- /dev/null
+++ b/frontend/app/components/Signup/index.ts
@@ -0,0 +1 @@
+export { default } from './Signup'
\ No newline at end of file