import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Button, Stack, Typography } from "@mui/material"; import { StepOne } from "./StepOne"; import { StepTwo } from "./StepTwo"; import { StepThree } from "./StepThree"; import { networkService } from "../../../main"; import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { register } from "../../../Features/Auth/authSlice"; import Background from "../../../assets/Images/background-grid.svg?react"; import Logo from "../../../assets/icons/bwu-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; 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, }; 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(); const { password, confirm } = e.target.elements; let registerForm = { ...form, password: password.value, confirm: confirm.value, role: isSuperAdmin ? ["superadmin"] : form.role, inviteToken: token ? token : "", // Add the token to the request for verification }; let error = validateForm(registerForm, { context: { password: registerForm.password }, }); if (error) { handleError(error); return; } delete registerForm.confirm; const action = await dispatch(register(registerForm)); if (action.payload.success) { const authToken = action.payload.data; localStorage.setItem("token", authToken); navigate("/uptime"); createToast({ body: "Welcome! Your account was created successfully.", }); } else { if (action.payload) { createToast({ body: action.payload.msg, }); } else { 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;