import PropTypes from "prop-types"; import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Box, Button, Stack, Typography } from "@mui/material"; import { useDispatch } from "react-redux"; import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { register } from "../../../Features/Auth/authSlice"; import { useParams } from "react-router-dom"; import background from "../../../assets/Images/background_pattern_decorative.png"; import Logo from "../../../assets/icons/bwu-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import Check from "../../../Components/Check/Check"; import Field from "../../../Components/Inputs/Field"; import { networkService } from "../../../main"; import "../index.css"; /** * Displays the initial landing page. * * @param {Object} props * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click. * @returns {JSX.Element} */ const LandingPage = ({ isSuperAdmin, onSignup }) => { const theme = useTheme(); return ( <> Sign Up Create your {isSuperAdmin ? "Super admin " : ""}account to get started. By signing up, you agree to our{" "} { window.open( "https://bluewavelabs.ca/terms-of-service-open-source", "_blank", "noreferrer" ); }} sx={{ "&:hover": { color: theme.palette.text.tertiary, }, }} > Terms of Service {" "} and{" "} { window.open( "https://bluewavelabs.ca/privacy-policy-open-source", "_blank", "noreferrer" ); }} sx={{ "&:hover": { color: theme.palette.text.tertiary, }, }} > Privacy Policy. ); }; LandingPage.propTypes = { isSuperAdmin: PropTypes.bool, onSignup: PropTypes.func, }; /** * Renders the first step of the sign up process. * * @param {Object} props * @param {Object} props.form - Form state object. * @param {Object} props.errors - Object containing form validation errors. * @param {Function} props.onSubmit - Callback function to handle form submission. * @param {Function} props.onChange - Callback function to handle form input changes. * @param {Function} props.onBack - Callback function to handle "Back" button click. * @returns {JSX.Element} */ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { const theme = useTheme(); const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, []); return ( <> Sign Up Enter your personal details ); }; StepOne.propTypes = { form: PropTypes.object, errors: PropTypes.object, onSubmit: PropTypes.func, onChange: PropTypes.func, onBack: PropTypes.func, }; /** * Renders the second step of the sign up process. * * @param {Object} props * @param {Object} props.form - Form state object. * @param {Object} props.errors - Object containing form validation errors. * @param {Function} props.onSubmit - Callback function to handle form submission. * @param {Function} props.onChange - Callback function to handle form input changes. * @param {Function} props.onBack - Callback function to handle "Back" button click. * @returns {JSX.Element} */ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { const theme = useTheme(); const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, []); return ( <> Sign Up Enter your email address (e.target.value = e.target.value.toLowerCase())} onChange={onChange} error={errors.email} ref={inputRef} /> ); }; StepTwo.propTypes = { form: PropTypes.object, errors: PropTypes.object, onSubmit: PropTypes.func, onChange: PropTypes.func, onBack: PropTypes.func, }; /** * Renders the third step of the sign up process. * * @param {Object} props * @param {Object} props.form - Form state object. * @param {Object} props.errors - Object containing form validation errors. * @param {Function} props.onSubmit - Callback function to handle form submission. * @param {Function} props.onChange - Callback function to handle form input changes. * @param {Function} props.onBack - Callback function to handle "Back" button click. * @returns {JSX.Element} */ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { const theme = useTheme(); const inputRef = useRef(null); useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, []); return ( <> Sign Up Create your password Must be at least 8 characters long } variant={ errors?.password === "Password is required" ? "error" : form.password === "" ? "info" : form.password.length < 8 ? "error" : "success" } /> Must contain one special character and a number } variant={ errors?.password === "Password is required" ? "error" : form.password === "" ? "info" : !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test( form.password ) ? "error" : "success" } /> Must contain at least {" "} one upper and lower character } variant={ errors?.password === "Password is required" ? "error" : form.password === "" ? "info" : !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password) ? "error" : "success" } /> ); }; StepThree.propTypes = { form: PropTypes.object, errors: PropTypes.object, onSubmit: PropTypes.func, onChange: PropTypes.func, onBack: PropTypes.func, }; const Register = ({ isSuperAdmin }) => { const dispatch = useDispatch(); const navigate = useNavigate(); const { token } = useParams(); const theme = useTheme(); // TODO If possible, change the IDs of these fields to match the backend const idMap = { "register-firstname-input": "firstName", "register-lastname-input": "lastName", "register-email-input": "email", "register-password-input": "password", "register-confirm-input": "confirm", }; const [form, setForm] = useState({ firstName: "", lastName: "", email: "", password: "", confirm: "", role: [], teamId: "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); useEffect(() => { const fetchInvite = async () => { if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); const invite = res.data.data; const { role, email, teamId } = invite; setForm({ ...form, email, role, teamId }); } catch (error) { navigate("/register", { replace: true }); } } }; fetchInvite(); }, []); /** * Validates the form data against the validation schema. * * @param {Object} data - The form data to validate. * @param {Object} [options] - Optional settings for validation. * @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`. */ const validateForm = (data, options = {}) => { const { error } = credentials.validate(data, { abortEarly: false, ...options, }); return error; }; /** * Handles validation errors by setting the state with error messages and displaying a toast notification. * * @param {Object} error - The validation error object returned from the validation schema. */ const handleError = (error) => { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); createToast({ body: error.details[0].message || "Error validating data." }); }; const handleStepOne = async (e) => { e.preventDefault(); let error = validateForm({ firstName: form.firstName, lastName: form.lastName, }); if (error) { handleError(error); return; } setStep(2); }; const handleStepTwo = async (e) => { e.preventDefault(); let error; error = validateForm({ email: form.email }); if (error) { handleError(error); return; } setStep(3); }; // Final step // Attempts account registration const handleStepThree = async (e) => { e.preventDefault(); let registerForm = { ...form, role: isSuperAdmin ? ["superadmin"] : form.role, }; let error = validateForm(registerForm, { context: { password: form.password }, }); if (error) { handleError(error); return; } delete registerForm.confirm; const action = await dispatch(register(registerForm)); if (action.payload.success) { const token = action.payload.data; localStorage.setItem("token", token); navigate("/"); createToast({ body: "Welcome! Your account was created successfully.", }); } else { if (action.payload) { // dispatch errors createToast({ body: action.payload.msg, }); } else { // unknown errors createToast({ body: "Unknown error.", }); } } }; const handleChange = (event) => { const { value, id } = event.target; const name = idMap[id]; setForm((prev) => ({ ...prev, [name]: value, })); const { error } = credentials.validate( { [name]: value }, { abortEarly: false, context: { password: form.password } } ); setErrors((prev) => { const prevErrors = { ...prev }; if (error) prevErrors[name] = error.details[0].message; else delete prevErrors[name]; return prevErrors; }); }; return ( BlueWave Uptime .MuiStack-root": { border: 1, borderRadius: theme.spacing(5), borderColor: theme.palette.border.light, backgroundColor: theme.palette.background.main, padding: { xs: theme.spacing(12), sm: theme.spacing(20), }, }, }} > {step === 0 ? ( setStep(1)} /> ) : step === 1 ? ( setStep(0)} /> ) : step === 2 ? ( setStep(1)} /> ) : step === 3 ? ( setStep(2)} /> ) : ( "" )} Already have an account? — { navigate("/login"); }} sx={{ userSelect: "none", color: theme.palette.primary.main }} > Log In ); }; Register.propTypes = { isSuperAdmin: PropTypes.bool, }; export default Register;