453 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|