Files
Account/src/features/authentication/routes/UserCompletionPage.tsx
2025-09-22 12:04:54 +03:30

453 lines
14 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Box, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
import Logo from '@/components/Logo';
import { PersonalInfoFields } from '../components/UserCompletionForm/PersonalInfoFields';
import { PasswordSection } from '../components/UserCompletionForm/PasswordSection';
import { EmailSection } from '../components/UserCompletionForm/EmailSection';
import { SubmitSection } from '../components/UserCompletionForm/SubmitSection';
import { useToast } from '@rkheftan/harmony-ui';
import { Gender } from '../types/settingForm';
import { useApi } from '@/hooks/useApi';
import {
sendEmailOtpApi,
confirmEmailOtpApi,
completeUserInformationApi,
} from '../api/userCompletion';
import { containsSymbol } from '@/utils/regexes/containsSymbol';
import { least8Chars } from '@/utils/regexes/least8Chars';
import { containsNumber } from '@/utils/regexes/containsNumber';
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
import { isEmail } from '@/utils/regexes/isEmail';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { FlexBox } from '@/components/common/FlexBox';
import { AuthenticationCard } from '../components/AuthenticationCard';
import { nationalIdRegex } from '@/utils/regexes/nationalId';
export function UserCompletionPage() {
const [params] = useSearchParams();
const { t } = useTranslation('completionForm');
const showToast = useToast();
const navigate = useNavigate();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [nationalId, setNationalId] = useState('');
const [birthDate, setBirthDate] = useState<Date | null>(null);
const [sex, setSex] = useState<Gender | null>(null);
const [country, setCountry] = useState('');
const [showPasswordSection, setShowPasswordSection] = useState(false);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showEmail, setShowEmail] = useState(false);
const [email, setEmail] = useState('');
const [verificationCode, setVerificationCode] = useState('');
const [codeSent, setCodeSent] = useState(false);
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
'default',
);
const [countdown, setCountdown] = useState(0);
const [emailVerified, setEmailVerified] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [touched, setTouched] = useState<{ [key: string]: boolean }>({});
const hasNumber = containsNumber(password);
const hasMinLength = least8Chars(password);
const hasUpperAndLower = hasUpperAndLowerLetter(password);
const hasSpecialChar = containsSymbol(password);
const validPassword =
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
const matchPassword = password === confirmPassword;
const correctEmail = isEmail(email);
const { execute: sendCode, loading: isSendingCode } = useApi(sendEmailOtpApi);
const { execute: verifyCode, loading: isVerifyingCode } =
useApi(confirmEmailOtpApi);
const { execute: submitForm, loading: isSubmitting } = useApi(
completeUserInformationApi,
);
useEffect(() => {
let timer: NodeJS.Timeout;
if (buttonState === 'counting' && countdown > 0) {
timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
setButtonState('default');
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [buttonState, countdown]);
useEffect(() => {
if (buttonState === 'default') {
setCodeSent(false);
setVerificationCode('');
// setEmailVerified(false);
setTouched((prev) => {
const n = { ...prev };
delete n.verificationCode;
return n;
});
setErrors((prev) => {
const n = { ...prev };
delete n.verificationCode;
return n;
});
}
}, [buttonState]);
const handleSendCode = async () => {
if (!isEmail(email)) {
setTouched((prev) => ({ ...prev, email: true }));
setErrors((prev) => ({ ...prev, email: t('validation.emailInvalid') }));
return;
}
setTouched((prev) => {
const newTouched = { ...prev };
delete newTouched.verificationCode;
return newTouched;
});
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors.verificationCode;
return newErrors;
});
const res = await sendCode({ email });
if (res) {
if (res.success) {
showToast({
message: t('completion.successfulCodeSent'),
severity: 'success',
});
setCodeSent(true);
setButtonState('counting');
setCountdown(120);
} else {
showToast({
message: res.message || t('completion.problem'),
severity: 'error',
});
}
}
};
const handleVerifyCode = async () => {
const trimmedCode = verificationCode.trim();
if (!trimmedCode) {
setTouched((prev) => ({ ...prev, verificationCode: true }));
setErrors((prev) => ({
...prev,
verificationCode: t('validation.verificationCodeRequired'),
}));
return;
} else if (trimmedCode.length < 4) {
setTouched((prev) => ({ ...prev, verificationCode: true }));
setErrors((prev) => ({
...prev,
verificationCode: t('validation.verificationCodeInvalid'),
}));
return;
}
const res = await verifyCode({ email, otpCode: verificationCode });
if (res) {
if (res.success) {
setEmailVerified(true);
showToast({
message: res.message || t('completion.codeVerified'),
severity: 'success',
});
} else {
showToast({
message: res.message || t('completion.invalidCode'),
severity: 'error',
});
setEmailVerified(false);
}
}
};
const handleSubmit = async () => {
setTouched({
firstName: true,
lastName: true,
country: true,
email: showEmail,
nationalId: true,
verificationCode: showEmail,
password: showPasswordSection,
confirmPassword: showPasswordSection,
sex: true,
});
const isValid = validateForm();
if (!isValid) {
return;
}
const res = await submitForm({
firstName,
lastName,
gender: sex || 0,
nationalCode: nationalId,
savePassword: showPasswordSection,
password: showPasswordSection ? password : undefined,
saveEmail: showEmail,
email: showEmail ? email : undefined,
birthDate,
countryCode: country,
});
if (res) {
if (res.success) {
showToast({
message: res.message || t('completion.submitSuccess'),
severity: 'success',
});
const returnUrl = params.get('returnUrl');
navigate(
returnUrl ? `/account-created?returnUrl=${returnUrl}` : '/setting',
);
} else {
showToast({
message: res.message || t('completion.submitError'),
severity: 'error',
});
}
}
};
const handleEditEmail = () => {
setButtonState('default');
setCodeSent(false);
setEmailVerified(false);
setVerificationCode('');
setTouched((prev) => {
const newTouched = { ...prev };
delete newTouched.email;
delete newTouched.verificationCode;
return newTouched;
});
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors.email;
delete newErrors.verificationCode;
return newErrors;
});
};
const validateForm = () => {
const newErrors: { [key: string]: string } = {};
if (!firstName.trim())
newErrors.firstName = t('validation.firstNameRequired');
if (!lastName.trim()) newErrors.lastName = t('validation.lastNameRequired');
if (!country) newErrors.country = t('validation.countryRequired');
if (showEmail && !isEmail(email)) {
newErrors.email = t('validation.emailInvalid');
}
if (nationalId && !nationalIdRegex.test(nationalId)) {
newErrors.nationalId = t('validation.nationalIdInvalid');
}
if (showEmail && codeSent && !emailVerified) {
const trimmedCode = verificationCode.trim();
if (!trimmedCode) {
newErrors.verificationCode = t('validation.verificationCodeRequired');
} else if (trimmedCode.length < 4) {
newErrors.verificationCode = t('validation.verificationCodeInvalid');
} else {
newErrors.verificationCode = t('validation.mustVerifyCode');
}
}
if (showPasswordSection) {
if (!password.trim()) {
newErrors.password = t('validation.passwordRequired');
} else if (!validPassword) {
newErrors.password = t('validation.passwordInvalid');
}
if (!confirmPassword.trim()) {
newErrors.confirmPassword = t('validation.confirmPasswordRequired');
} else if (!matchPassword) {
newErrors.confirmPassword = t('validation.passwordsDoNotMatch');
}
}
if (sex === null) {
newErrors.sex = t('validation.genderRequired');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleBlur = (_field: string) => {};
useEffect(() => {
if (!showPasswordSection) {
setTouched((prev) => {
const newTouched = { ...prev };
delete newTouched.password;
delete newTouched.confirmPassword;
return newTouched;
});
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors.password;
delete newErrors.confirmPassword;
return newErrors;
});
}
}, [showPasswordSection]);
useEffect(() => {
if (!showEmail) {
setTouched((prev) => {
const newTouched = { ...prev };
delete newTouched.email;
delete newTouched.verificationCode;
return newTouched;
});
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors.email;
delete newErrors.verificationCode;
return newErrors;
});
}
}, [showEmail]);
useEffect(() => {
if (Object.keys(touched).length > 0) {
validateForm();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
firstName,
lastName,
country,
email,
nationalId,
verificationCode,
codeSent,
emailVerified,
password,
confirmPassword,
showPasswordSection,
sex,
touched,
]);
return (
<FlexBox
direction="column"
align="center"
justify="center"
sx={{
minHeight: '100vh',
gap: 3,
py: { md: 0, xs: 4 },
}}
>
<Logo />
<AuthenticationCard
sx={{
maxHeight: { sm: '80vh', xs: 'unset' },
flex: { sm: 'unset', xs: 1 },
overflowY: 'auto',
scrollbarGutter: 'stable both-edges',
}}
maxWidth="730px"
>
<Box
component="form"
id="completionForm"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void handleSubmit();
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="h5" color="text.primary">
{t('completion.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('completion.description')}
</Typography>
</Box>
<PersonalInfoFields
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
nationalId={nationalId}
setNationalId={setNationalId}
birthDate={birthDate}
setBirthDate={setBirthDate}
sex={sex}
setSex={setSex}
country={country}
setCountry={setCountry}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<PasswordSection
showPasswordSection={showPasswordSection}
setShowPasswordSection={setShowPasswordSection}
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
matchPassword={matchPassword}
hasNumber={hasNumber}
hasMinLength={hasMinLength}
hasUpperAndLower={hasUpperAndLower}
hasSpecialChar={hasSpecialChar}
validPassword={validPassword}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<EmailSection
showEmail={showEmail}
setShowEmail={setShowEmail}
email={email}
setEmail={setEmail}
correctEmail={correctEmail}
codeSent={codeSent}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
buttonState={buttonState}
countdown={countdown}
handleSendCode={handleSendCode}
handleVerifyCode={handleVerifyCode}
emailVerified={emailVerified}
isVerifyingCode={isVerifyingCode}
isSendingCode={isSendingCode}
handleEditEmail={handleEditEmail}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<SubmitSection loading={isSubmitting} />
</Box>
</Box>
</AuthenticationCard>
</FlexBox>
);
}