Creating a signup wizard:

// src/components/site/Signup.tsx
import React, { Dispatch, SetStateAction, useState, useEffect, ChangeEvent, FormEvent, useCallback } from 'react';
import Select from 'react-select';
import * as yup from 'yup';
import logger from '../../utils/logger';
import { fetchCsrfToken, CsrfData } from '../../services/csrfService';
import { fetchBusinessCategories, BusinessCategoryData } from '../../services/signupBusinessCategoryService';
import * as signupService from '../../services/signupService';

/**
* Yup validation schemas per stage
* Note: Schema starts at *zero*
*/
const stageSchemas =
[
	//  Stage One / schema[0] - Submit email
	yup.object({ email: yup.string().email('Invalid email').required('Email is required') }),
	
	// Stage "One b" Two / schema[1] - Verify token
	yup.object({ token: yup.string().required('Verification token is required') }),
	
	// Stage three / schema[2] - Credentials
	yup.object().shape({
		fullname: yup.string()
			.notRequired()
			.matches(/^[\p{L} .'-]*$/u, 'Only letters, spaces, and some characters allowed.'),
		password: yup.string().min(8).required('Password is required'),
		passwordRepeat: yup.string()
			.oneOf([yup.ref('password'), null], 'Passwords must match')
			.required('Please confirm your password')
	}),
];

// Type defining your signup form data shape (you can extend)
interface SignupData
{
	email: string;
	token: string;
	fullname: string;
	password: string;
	passwordRepeat: string;
	category: string;
	businessName: string;
	description?: string;
	phone?: string;
	website?: string;
	contactEmail?: string;
	address?: string;
	hours?: string;
	businessCard?: File | null;
	profilePicture?: File | null;
	banner?: File | null;
}

const initialSignupData: SignupData =
{
	email: '',
	token: '',
	fullname: '',
	password: '',
	passwordRepeat: '',
	category: '',
	businessName: '',
	description: '',
	phone: '',
	website: '',
	contactEmail: '',
	address: '',
	hours: '',
	businessCard: null,
	profilePicture: null,
	banner: null,
};

/**
* Yup Errors - Utility
* Note: Using React.Dispatch
*/
function setYupErrors(validationError: any, setFormErrors: Dispatch<any>)
{
	if (validationError.inner)
	{
		const yupErrors: Record<string, string[]> = {};
		validationError.inner.forEach((error: any) =>
		{
			if (!yupErrors[error.path])
			{
				yupErrors[error.path] = [];
			}
			
			yupErrors[error.path].push(error.message);
		});
		
		setFormErrors(yupErrors);
	}
}

/**
* Api Response - Utility
* Note: Using React.Dispatch and React.SetStateAction
*/
function handleApiResponse(
	response: any,
	setError: Dispatch<SetStateAction<string | null>>,
	setSuccess: Dispatch<SetStateAction<string | null>>,
	setFormErrors: Dispatch<SetStateAction<Record<string, any>>>
): boolean
{
	if (response.success)
	{
		setError(null);
		setSuccess(response.message || null);
		setFormErrors({});
		
		return true;
	}
	
	setError(response.message || 'An error occurred.');
	
	if (response.errors && typeof response.errors === 'object')
	{
		setFormErrors(response.errors);
	}
	else 
	{
		setFormErrors({});
	}
	
	return false;
}

const TOTAL_STEPS = stageSchemas.length;

function Signup()
{
	// Signup process
	const [step, setStep] = useState<number>(1);
	const [userId, setUserId] = useState<number | null>(null);
	const [categories, setCategories] = useState<BusinessCategoryData[]>([]);
	const [signupData, setSignupData] = useState<SignupData>(initialSignupData);
	
	// CSRF token and param names
	const [csrf, setCsrf] = useState<CsrfData | null>(null);
	
	const [loading, setLoading] = useState(false);
	
	/**
	* Error Handling and Display Strategy:
	*
	* API error response format expected:
	* 	{
	*		status: 'error',
	*		message: string,// General, user-friendly error summary.
	*		errors?: { [field: string]: string[] }// Optional field-level validation errors.
	*	}
	*
	* UI Display Guidelines:
	* - Show the "message" text in a global alert/banner for overall feedback.
	* - Show the detailed "errors" inline next to corresponding form fields (if any).
	* - Avoid displaying all field errors together in the general alert, as this can overwhelm users.
	*
	* Benefits:
	* - Clear and friendly user guidance via message alert.
	* - Precise, contextual feedback users can act on at fields.
	* - Cleaner and more maintainable UI/UX.
	*
	* Example usage snippet:
	*   setGeneralError(response.message);
	*   setFormErrors(response.errors || {});
	*
	* In JSX:
	*   {generalError && <div className="alert alert-danger">{generalError}</div>}
	*   {formErrors.email && <div className="field-error">{formErrors.email.join(', ')}</div>}
	*/
	const [error, setError] = useState<string | null>(null);// For top-level general errors
	const [formErrors, setFormErrors] = useState<Record<string, string[] | string>>({});
	const[success, setSuccess] = useState<string | null>(null);// For top-level general success messages
	
	useEffect(() =>
	{
		async function init()
		{
			setLoading(true);
			
			try
			{
				const csrfToken = await fetchCsrfToken();
				console.log(csrfToken);
				setCsrf(csrfToken);
			}
			catch (error)
			{
				console.error('CSRF token fetch failed:', error);
				setError('Failed to initialize form. Please refresh.');
			}
			finally
			{
				setLoading(false);
			}
			
			try
			{
				const categories = await fetchBusinessCategories();
				setCategories(categories);
			}
			catch (error)
			{
				console.error('Business categories load failed:', error);
				setError('Failed to load business categories.');
			}
			finally
			{
				setLoading(false);
			}
		}
		init();
		
	}, []);// Empty dependency array ensures it only runs once
	
	/**
	* Update Feild Helper
	* Helper that updates field value and clears the error on change
	*
	* Note: Using React.useCallback
	*/
	const updateField = useCallback(<Key extends keyof SignupData>
	(
		field: Key,
		value: SignupData[Key]
	) =>
	{
		setSignupData(prev => ({ ...prev, [field]: value }));
		
		// Clear error for this field on change
		setFormErrors(prev =>
		{
			if (field in prev)
			{
				const copy = { ...prev };
				delete copy[field];
				return copy;
			}
			
			return prev;
		});
		
	}, []);// Empty dependency array ensures it only runs once
	
	/**
	* Handle file input updates
	*
	* Note: Using React.useCallback
	*/
	const handleFileChange = useCallback(
	(
		error: ChangeEvent<HTMLInputElement>,
		field: keyof SignupData
	) =>
	{
		updateField(field, error.target.files?.[0] || null);
	}, [updateField]);
	
	/**
	* Progress calculation
	*/
	const progressPercent = (step / TOTAL_STEPS) * 100;
	
	/**
	* Centralized submit handler keyed by stage number
	*
	* Note: Using React.useCallback
	*/
	const submitStage = useCallback(async () =>
	{
		// Clear errors and success alarts
		setError(null);
		setFormErrors({});
		setSuccess(null);
		
		if (!csrf)
		{
			setError('CSRF token missing, please refresh.');
			
			return;
		}
		
		setLoading(true);
		
		try
		{
			let response;
			
			switch (step)
			{
				//  Stage One - Submit email
				case 1 :
					/**
					* Yup validation
					* yup.object({ email: yup.string().email('Invalid email').required('Email is required') });
					*/
					await stageSchemas[0].validate({ email: signupData.email }, { abortEarly: false });
				
					try
					{
						response = await signupService.submitSignupStageOneEmail(
						{
							email: signupData.email
						},
							csrf
						);
						
						if (handleApiResponse(response, setError, setSuccess, setFormErrors))
						{
							if (response.userId)
							{
								setUserId(response.userId);
							}
							setStep(2);
						}
					}
					catch (validationError: any)
					{
						setYupErrors(validationError, setFormErrors);
					}
					
					break;
				
				// Stage Two - Verify token
				case 2 :
					if (!userId) 
					{
						// TODO - Handled by logger
						throw new Error('User ID missing');
					}
					
					/**
					* Yup validation
					* yup.object({ token: yup.string().required('Verification token is required') });
					*/
					await stageSchemas[1].validate({ token: signupData.token }, { abortEarly: false });
				
					try
					{
						response = await signupService.submitSignupStageOneVerify(
						{
							userId,
							token: signupData.token
						},
							csrf 
						);
						
						if (handleApiResponse(response, setError, setSuccess, setFormErrors))
						{
							setStep(3);
						}
					}
					catch (validationError: any)
					{
						setYupErrors(validationError, setFormErrors);
					}
					
					break;
				
				// Stage Three - Credentials
				case 3 :
					if (!userId) 
					{
						// TODO - Handled by logger
						throw new Error('User ID missing');
					}
					
					/**
					* Yup validation
					* yup.object().shape({
					* 	fullname: yup.string().required(),
					* 	password: yup.string().min(6).required(),
					*	passwordRepeat: yup.string().oneOf([yup.ref('password')]).required()
					* });
					*/
					await stageSchemas[2].validate(
					{ 
						fullname: signupData.fullname,
						password: signupData.password,
						passwordRepeat: signupData.passwordRepeat
					},{
						abortEarly: false
					});
					
					if (signupData.password !== signupData.passwordRepeat)
					{
						throw new Error('Passwords do not match');
					}
				
					try
					{
						response = await signupService.submitSignupStageTwoCredentials(
							userId,
						{
							fullname: signupData.fullname,
							password: signupData.password,
							passwordRepeat: signupData.passwordRepeat,
						},
							csrf
						);
						
						if (handleApiResponse(response, setError, setSuccess, setFormErrors))
						{
							setStep(4);
						}
					}
					catch (validationError: any)
					{
						setYupErrors(validationError, setFormErrors);
					}
					
					break;
					
				default :
					throw new Error('Invalid step');
			}	
		}
		catch (error: any)
		{
			setError(error.message || 'Unknown error occurred');
		}
		finally
		{
			setLoading(false);
		}
	
	}, [step, signupData, userId, csrf]);
	
	/**
	* Navigation: Back button
	* Back button: only allow going back between editable steps (3+), and no 
	* going back past 3.
	*
	* Note: Using React.useCallback
	*/
	const handleBack = useCallback(() =>
	{
		if (step > 3)
		{
			setError(null);
			setSuccess(null);
			setStep(step - 1);// Go back one step
		}
	}, [step]);
	
	/**
	* Render file inputs helper
	*/
	const renderFileInput = (
		label: string,
		fieldName: keyof SignupData,
		accept?: string
	) => (
		<>
			<label className="form-label">{label}</label>
			<input
				type="file"
				accept={accept}
				disabled={loading}
				onChange={(e) => handleFileChange(e, fieldName)}
				className="form-control mb-3"
			/>
		</>
	);
	
	/**
	* Render fields based on current step
	*/
	const renderStepFields = () =>
	{
		switch (step)
		{
			case 1 :
				return (
					<div className="mb-3">
						
						{csrf && (
							<input type="hidden" name={csrf?.csrfParam} value={csrf?.csrfToken} />
						)}
						
						<label htmlFor="email" className="form-label">Email</label>
						<input
							id="email"
							type="email"
							className={`form-control ${formErrors.email ? 'is-invalid' : ''}`}
							value={signupData.email}
							onChange={e => updateField('email', e.target.value)}
							disabled={loading}
							required
						/>
						
						{formErrors.email && (
							<div className="invalid-feedback">
								{formErrors.email && 
									(Array.isArray(formErrors.email) ? formErrors.email[0] : formErrors.email)
								}
							</div>
						)}
						
					</div>
				);
			
			case 2 :
				return (
					<div className="mb-3">
					
						{csrf && (
							<input type="hidden" name={csrf?.csrfParam} value={csrf?.csrfToken} />
						)}
						
						<label htmlFor="token" className="form-label">Verification Token</label>
						<input
							id="token"
							type="text"
							className={`form-control ${formErrors.token ? 'is-invalid' : ''}`}
							value={signupData.token}
							onChange={e => updateField('token', e.target.value)}
							disabled={loading}
							required
						/>
						
						{formErrors.token && (
							<div className="invalid-feedback">
								{formErrors.token && 
									(Array.isArray(formErrors.token) ? formErrors.token[0] : formErrors.token)
								}
							</div>
						)}
					</div>
				);
				
			case 3 :
				return (
					<div className="mb-3">
						
						{csrf && (
							<input type="hidden" name={csrf?.csrfParam} value={csrf?.csrfToken} />
						)}
						
						<label htmlFor="fullname" className="form-label">Full Name</label>
						<input
							id="fullname"
							type="text"
							className={`form-control ${formErrors.fullname ? 'is-invalid' : ''}`}
							value={signupData.fullname}
							onChange={e => updateField('fullname', e.target.value)}
							disabled={loading}
							//required
						/>
						<div className="form-text mb-3">Optional</div>
						
						{formErrors.fullname && (
							<div className="invalid-feedback">
								{formErrors.fullname && 
									(Array.isArray(formErrors.fullname) ? formErrors.fullname[0] : formErrors.fullname)
								}
							</div>
						)}
						
						<label htmlFor="password" className="form-label">Password</label>
						<input
							id="password"
							type="password"
							className={`form-control ${formErrors.password ? 'is-invalid' : ''}`}
							value={signupData.password}
							onChange={e => updateField('password', e.target.value)}
							disabled={loading}
							required
						/>
						
						{formErrors.password && (
							<div className="invalid-feedback">
								{formErrors.password && 
									(Array.isArray(formErrors.password) ? formErrors.password[0] : formErrors.password)
								}
							</div>
						)}
						
						<label htmlFor="passwordRepeat" className="form-label">Confirm Password</label>
						<input
							id="passwordRepeat"
							type="password"
							className={`form-control ${formErrors.passwordRepeat ? 'is-invalid' : ''}`}
							value={signupData.passwordRepeat}
							onChange={e => updateField('passwordRepeat', e.target.value)}
							disabled={loading}
							required
						/>
						
						{formErrors.passwordRepeat && (
							<div className="invalid-feedback">
								{formErrors.passwordRepeat && 
									(Array.isArray(formErrors.passwordRepeat) ? formErrors.passwordRepeat[0] : formErrors.passwordRepeat)
								}
							</div>
						)}
					</div>
				);
				
			default :
				return (
					<p>Invalid step</p>
				);
		}
	};
	
	return (
		<div id="site-signup-page" className="site-signup-page d-flex justify-content-center align-items-center m-5" style={{ minheight: '80vh' }}>
			<div className="row justify-content-center align-items-center w-100 h-100">
			
				<div className="col-lg-6 col-md-6 col-sm-12">
					
					{step == 1 && (
						<div className="alert alert-info mt-4" role="alert">
							You are currently signing up during our beta phase. Once we are out of beta, there will 
							be a 14-day trial period. Pricing details will be provided then.
						</div>
					)}
					
					{/* Note: Must get past step 1(verify email) & 2(validate token) before allowing to backtrack. */}
					{step > 2 && (
						<>
							<h3 className="text-center mt-5">Sign Up - Step {step - 2} of {TOTAL_STEPS - 2}</h3>
							
							<div className="progress mb-4" style={{ height: '1.5rem' }}>
								<div
									className="progress-bar progress-bar-striped"
									role="progressbar"
									style={{ width: `${((step - 2) / (TOTAL_STEPS - 2)) * 100}%` }}
									aria-valuenow={step - 2}
									aria-valuemin={2}
									aria-valuemax={TOTAL_STEPS - 2}
									>
										Step {step - 2} of {TOTAL_STEPS - 2}
								</div>
							</div>
						</>
					)}
			
					{error && <div className="alert alert-danger">{error}</div>}
					{success && <div className="alert alert-success">{success}</div>}
			
					{/**
					* Wrap current step fields in a form for accessibility 
					* Note: Using React.FormEvent
					*/}
					<form onSubmit={(e: FormEvent) =>
					{
						e.preventDefault();// Prevent default form submission
						if (!loading)
						{
							submitStage();
						}
					}} noValidate>
						
						{renderStepFields()}
						
						{/* Note: Must get past step 1(verify email) & 2(validate token) before allowing to backtrack. */}
						{step >= 3 && (
							<button
								type="button"
								className="btn btn-gray w-100 mb-1"
								onClick={handleBack}
								disabled={loading || step === 3}// disable back on step 3 so it cannot go to step 2 or 1
								//disabled={loading}
							>
								Back
							</button>
						)}
						
						<button
							type="submit"
							className="btn btn-blue w-100"
							// or your real validation for other steps
							disabled={ 
								loading || 
								(step === 1 && !signupData.email) || 
								(step > 1 && !signupData.token) ||
								(step === 3 && !signupData.password && !signupData.passwordRepeat)
							}
						>
							{/*{loading ? 'Please wait...' : step === TOTAL_STEPS ? 'Confirm & Submit' : 'Next'}*/}
							{step === TOTAL_STEPS ? 'Confirm & Submit' : 'Next'}
						</button>
					</form>
					
					<p className="mt-3 text-center">Already have an account? <a href="/login">Log in</a></p>
				</div>
			</div>
		</div>
	);
}

export default Signup;

within the backend yii2 restful api, the user status which needs to be used to determine if the signup process is successful, or not. I
believe needs another step. or I need to change how the steps work, the user enter a email. This begins the signup process:

public function process(): ?User
	{
		if (!$this->validate())
		{
			return false;
		}
	
		$transaction = \Yii::$app->db->beginTransaction();
		try
		{
			//\Yii::info('Starting user process with email: ' . var_export($this->email, true), __METHOD__);
			
			$user = new User();
			$user->email = $this->email;
			$user->status = User::STATUS_INACTIVE;
			$user->subscription = User::SUBSCRIPTION_BASIC;
			$user->generateTemporaryPassword();
			$user->generateAuthKey();
			$user->generateEmailVerificationToken(8, false);
			if (!$user->save(false))
			{
				throw new YiiDbException('Failed to save user data to the database.');
			}
			
			$mailSent = $this->sendEmail($user);
			if (!$mailSent)
			{
				
Setting the user to inactive is fine, since they still need to enter the validaiton code in step one "b"

	public function process(): bool
	{
		if (!$this->validate())
		{
			return false;
		}
		
		try
		{
			$user = User::findOne($this->userId);
			if (!$user)
			{
				throw new YiiDbException('Failed to find user in database.');
			}
			
			$user->status = User::STATUS_ACTIVE;
			$user->removeEmailVerificationToken();
			if (!$user->save())
			{
				throw new YiiDbException('Failed to save user data to the database.');
			}
			
			return true;
		}
		catch (\Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception.
			ExceptionLogger::log($e, 'signup_stageOneB_form_error_');
		}
		
		return false;
	}
	
but this makes the user fully active.. what if they forget to fill in step two, a new password, but now their account is "active". I need a way or suggestions
Do I just add another const? to the user model? 

// User Account Status
const STATUS_DELETED = 0;
const STATUS_INACTIVE = 9;
const STATUS_ACTIVE = 10;

and then in step two:

class SignupStageTwoForm extends Model
{
	public function process():
	{
		// update status here to "active"
	}

suggestions? thoughts?