Merge branch 'develop' into feat/google-auth
This commit is contained in:
@@ -99,7 +99,7 @@ export const CompleteSignUp = ({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -54,6 +54,7 @@ export function LoginRegisterForm({
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = event.target.value;
|
||||
newValue = replacePersianWithRealNumbers(newValue);
|
||||
if (newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
}
|
||||
@@ -152,7 +153,7 @@ export function LoginRegisterForm({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -56,6 +56,12 @@ export function OtpVerifyForm({
|
||||
const { loading: loginSignUpLoading, execute: loginSignUpCall } =
|
||||
useApi(loginOrSignUpWithOtp);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -35,6 +35,12 @@ export function VerifyPhoneNumber({
|
||||
const { loading: confirmSmsOtpLoading, execute: confirmSmsOtpCall } =
|
||||
useApi(confirmSmsOtp);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { countries, type Country } from '../../../data/countries';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { LTRBox } from '@/components/common/LTRBox';
|
||||
interface CountryCodeSelectorProps {
|
||||
show: boolean;
|
||||
value: CountryCode;
|
||||
@@ -187,7 +188,9 @@ export function CountryCodeSelector({
|
||||
{filteredCountries.length === 0 ? (
|
||||
<MenuItem disabled>
|
||||
<ListItem>
|
||||
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
|
||||
<ListItemText>
|
||||
{t('messages.noResualtFound', { ns: 'common' })}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
) : (
|
||||
@@ -209,7 +212,9 @@ export function CountryCodeSelector({
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(country.label, { ns: 'country' })} />
|
||||
<Typography color="text.secondary">
|
||||
{country.phone}
|
||||
<Typography>
|
||||
<LTRBox>{country.phone}</LTRBox>
|
||||
</Typography>
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
|
||||
@@ -48,6 +48,12 @@ export function ForgetPasswordOtp({
|
||||
execute: confirmForgetPassCodeCall,
|
||||
} = useApi(confirmForgetPassCode);
|
||||
|
||||
useEffect(() => {
|
||||
if (otpCode.length === 4) {
|
||||
handleVerifyOTP();
|
||||
}
|
||||
}, [otpCode]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { SendForgetPassCodeRequest } from '../../types/userTypes';
|
||||
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||
|
||||
export interface ForgettedPasswordInfoProps {
|
||||
forgettedPasswordInfo: string;
|
||||
@@ -46,6 +47,7 @@ export function ForgettedPasswordInfo({
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = event.target.value;
|
||||
newValue = replacePersianWithRealNumbers(newValue);
|
||||
if (newValue.startsWith('09')) {
|
||||
newValue = newValue.substring(1);
|
||||
}
|
||||
@@ -138,7 +140,7 @@ export function ForgettedPasswordInfo({
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
DatePicker,
|
||||
PickersDay,
|
||||
@@ -13,7 +13,6 @@ import { getDay } from 'date-fns-jalali';
|
||||
import { format as formatJalali } from 'date-fns-jalali';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { type DateOfBirthProps } from '../../types/settingForm';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Calendar } from 'iconsax-react';
|
||||
@@ -21,6 +20,7 @@ import { Calendar } from 'iconsax-react';
|
||||
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const isFarsi = i18n.language === 'fa' || i18n.language === 'fa-IR';
|
||||
const [openView, setOpenView] = useState<'year' | 'month' | 'day'>('year');
|
||||
|
||||
const { Adapter, locale, formatString, dayOfWeekFormatter } = useMemo(() => {
|
||||
if (isFarsi) {
|
||||
@@ -44,11 +44,7 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const dayNumber = isFarsi
|
||||
? formatJalali(props.day, 'dd')
|
||||
: format(props.day, 'dd');
|
||||
return (
|
||||
<PickersDay {...props}>
|
||||
{toLocaleDigits(dayNumber, i18n.language)}
|
||||
</PickersDay>
|
||||
);
|
||||
return <PickersDay {...props}>{dayNumber}</PickersDay>;
|
||||
};
|
||||
|
||||
const CustomCalendarIcon = () => (
|
||||
@@ -58,23 +54,23 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={Adapter} adapterLocale={locale}>
|
||||
<DatePicker
|
||||
label={toLocaleDigits(t('completion.dateOfBirth'), i18n.language)}
|
||||
label={t('completion.dateOfBirth')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
format={formatString}
|
||||
dayOfWeekFormatter={dayOfWeekFormatter}
|
||||
slots={{ day: CustomDay, openPickerIcon: CustomCalendarIcon }}
|
||||
disableFuture
|
||||
openTo="year"
|
||||
views={['year', 'month', 'day']}
|
||||
view={openView}
|
||||
onViewChange={(newView) => setOpenView(newView)}
|
||||
onYearChange={() => setOpenView('month')}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
},
|
||||
textField: { fullWidth: true },
|
||||
popper: {
|
||||
sx: {
|
||||
'& .MuiDateCalendar-root': {
|
||||
// TODO: fix this to use textfield width instead of defining hardcode
|
||||
width: '309px',
|
||||
},
|
||||
'& .MuiDateCalendar-root': { width: '309px' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Box,
|
||||
@@ -13,30 +13,35 @@ import { useTranslation } from 'react-i18next';
|
||||
import { TickCircle, Edit2 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type EmailSectionProps } from '../../types/settingForm';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function EmailSection({
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
}: EmailSectionProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
const ADORN_W = 24;
|
||||
const INPUT_H = 56;
|
||||
|
||||
export function EmailSection(props: EmailSectionProps) {
|
||||
const {
|
||||
showEmail,
|
||||
setShowEmail,
|
||||
email,
|
||||
setEmail,
|
||||
codeSent,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
buttonState,
|
||||
handleSendCode,
|
||||
handleVerifyCode,
|
||||
emailVerified,
|
||||
isVerifyingCode,
|
||||
isSendingCode,
|
||||
handleEditEmail,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
countdown,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('completionForm');
|
||||
|
||||
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowEmail(e.target.checked);
|
||||
@@ -45,29 +50,31 @@ export function EmailSection({
|
||||
const handleVerificationCodeChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce the max length
|
||||
if (isNumeric(value) && value.length <= 4) {
|
||||
setVerificationCode(value);
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
if (isNumeric(normalized) && normalized.length <= 4) {
|
||||
setVerificationCode(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimerValue = () => {
|
||||
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||
const s = String(countdown % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${m}:${s}`, i18n.language);
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonState !== 'counting') {
|
||||
setVerificationCode('');
|
||||
}
|
||||
}, [buttonState, setVerificationCode]);
|
||||
|
||||
const showCodeSection =
|
||||
showEmail && codeSent && !emailVerified && buttonState === 'counting';
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Switch checked={showEmail} onChange={handleToggleEmail} />
|
||||
<Typography
|
||||
sx={{ color: showEmail ? 'primary.main' : 'text.primary' }}
|
||||
@@ -76,14 +83,9 @@ export function EmailSection({
|
||||
</Typography>
|
||||
</Box>
|
||||
</FormGroup>
|
||||
|
||||
{showEmail && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -97,40 +99,75 @@ export function EmailSection({
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={email}
|
||||
autoFocus
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onBlur={() => handleBlur('email')}
|
||||
disabled={isSendingCode || codeSent}
|
||||
error={touched.email && !!errors.email}
|
||||
helperText={touched.email && errors.email}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment:
|
||||
!isVerifyingCode && emailVerified ? (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
endAdornment: codeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleEditEmail}>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
visibility:
|
||||
!isVerifyingCode && emailVerified
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
color="success.main"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ width: ADORN_W }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: ADORN_W,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ visibility: codeSent ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleEditEmail}
|
||||
disabled={!codeSent}
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isVerifyingCode &&
|
||||
!emailVerified &&
|
||||
(buttonState === 'counting' ? (
|
||||
@@ -140,6 +177,8 @@ export function EmailSection({
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
textAlign: 'center',
|
||||
lineHeight: `${INPUT_H}px`,
|
||||
height: INPUT_H,
|
||||
}}
|
||||
color="primary"
|
||||
variant="body1"
|
||||
@@ -154,41 +193,53 @@ export function EmailSection({
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.vericationCodeButton')}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
{!emailVerified && codeSent && (
|
||||
|
||||
{showCodeSection && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('completion.verificationCode')}
|
||||
autoFocus
|
||||
variant="outlined"
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
'& .MuiOutlinedInput-root': { height: INPUT_H },
|
||||
}}
|
||||
disabled={isVerifyingCode}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
error={touched.verificationCode && !!errors.verificationCode}
|
||||
helperText={touched.verificationCode && errors.verificationCode}
|
||||
inputProps={{
|
||||
inputMode: 'numeric',
|
||||
pattern: '[0-9]*',
|
||||
maxLength: 4,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleVerifyCode}
|
||||
loading={isVerifyingCode}
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '156px' },
|
||||
alignSelf: 'center',
|
||||
alignSelf: 'stretch',
|
||||
height: INPUT_H,
|
||||
}}
|
||||
>
|
||||
{t('completion.checkCodeButton')}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PersonalInfoFieldsProps } from '../../types/settingForm';
|
||||
import { Gender } from '../../types/settingForm';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { sanitizeLocalNumber } from '@/utils/regexes/sanitizeNumber';
|
||||
|
||||
export function PersonalInfoFields({
|
||||
firstName,
|
||||
@@ -129,6 +130,7 @@ export function PersonalInfoFields({
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) => setCountry(newValue?.code || '')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
autoFocus
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props} key={option.code}>
|
||||
<ReactCountryFlag
|
||||
@@ -163,7 +165,13 @@ export function PersonalInfoFields({
|
||||
label={t('completion.optionalNationalCode')}
|
||||
placeholder={t('completion.optionalNationalCode')}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const normalized = sanitizeLocalNumber(e.target.value);
|
||||
|
||||
if (normalized.length <= 10) {
|
||||
setNationalId(normalized);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
onBlur={() => handleBlur('nationalId')}
|
||||
error={touched.nationalId && !!errors.nationalId}
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type SubmitProps } from '../../types/settingForm';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
export function SubmitSection({ loading }: SubmitProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
@@ -21,6 +24,7 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
};
|
||||
|
||||
const agreementText = t('completion.agreement');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@@ -72,7 +76,16 @@ export function SubmitSection({ loading }: SubmitProps) {
|
||||
maxWidth="md"
|
||||
dir={i18n.language.startsWith('fa') ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => setOpenDialog(false)}>
|
||||
<Icon Component={CloseCircle} size="medium" color="primary.main" />
|
||||
</IconButton>
|
||||
{t('completion.rules') || t('completion.rules')}
|
||||
</DialogTitle>
|
||||
|
||||
|
||||
@@ -64,10 +64,8 @@ export function UserCompletionPage() {
|
||||
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,
|
||||
);
|
||||
@@ -89,13 +87,30 @@ export function UserCompletionPage() {
|
||||
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;
|
||||
@@ -106,13 +121,11 @@ export function UserCompletionPage() {
|
||||
delete newErrors.verificationCode;
|
||||
return newErrors;
|
||||
});
|
||||
|
||||
const res = await sendCode({ email });
|
||||
|
||||
if (res) {
|
||||
if (res.success) {
|
||||
showToast({
|
||||
message: res.message || t('completion.successfulCodeSent'),
|
||||
message: t('completion.successfulCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setCodeSent(true);
|
||||
@@ -129,8 +142,6 @@ export function UserCompletionPage() {
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
// Manually trigger the validation error and stop
|
||||
if (!trimmedCode) {
|
||||
setTouched((prev) => ({ ...prev, verificationCode: true }));
|
||||
setErrors((prev) => ({
|
||||
@@ -146,9 +157,7 @@ export function UserCompletionPage() {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await verifyCode({ email, otpCode: verificationCode });
|
||||
|
||||
if (res) {
|
||||
if (res.success) {
|
||||
setEmailVerified(true);
|
||||
@@ -178,12 +187,10 @@ export function UserCompletionPage() {
|
||||
confirmPassword: showPasswordSection,
|
||||
sex: true,
|
||||
});
|
||||
|
||||
const isValid = validateForm();
|
||||
if (!isValid) {
|
||||
return; // Stop the submission
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await submitForm({
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -196,16 +203,13 @@ export function UserCompletionPage() {
|
||||
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',
|
||||
);
|
||||
@@ -223,7 +227,6 @@ export function UserCompletionPage() {
|
||||
setCodeSent(false);
|
||||
setEmailVerified(false);
|
||||
setVerificationCode('');
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
@@ -240,77 +243,49 @@ export function UserCompletionPage() {
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
// Rule 1: First Name is required
|
||||
if (!firstName.trim())
|
||||
newErrors.firstName = t('validation.firstNameRequired');
|
||||
|
||||
// Rule 2: Last Name is required
|
||||
if (!lastName.trim()) newErrors.lastName = t('validation.lastNameRequired');
|
||||
|
||||
// Rule 3: Country is required
|
||||
if (!country) newErrors.country = t('validation.countryRequired');
|
||||
|
||||
// Rule 4: Email is required and must be valid IF the section is shown
|
||||
if (showEmail && !isEmail(email)) {
|
||||
newErrors.email = t('validation.emailInvalid');
|
||||
}
|
||||
|
||||
// Rule 5: National ID must be 10 digits IF it's not empty
|
||||
if (nationalId && !nationalIdRegex.test(nationalId)) {
|
||||
newErrors.nationalId = t('validation.nationalIdInvalid');
|
||||
}
|
||||
|
||||
// Rule 6: If verification code sent and email section is active
|
||||
if (showEmail && codeSent && !emailVerified) {
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
if (!trimmedCode) {
|
||||
// Case 1: The code is required but the field is empty.
|
||||
newErrors.verificationCode = t('validation.verificationCodeRequired');
|
||||
} else if (trimmedCode.length < 4) {
|
||||
// Case 2 : The code is entered but is less than 4 digits.
|
||||
newErrors.verificationCode = t('validation.verificationCodeInvalid');
|
||||
} else {
|
||||
// Case 3: The user has typed a code but hasn't clicked "Verify" yet.
|
||||
newErrors.verificationCode = t('validation.mustVerifyCode');
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 7: Password validation
|
||||
if (showPasswordSection) {
|
||||
// Rule 1: Check if the main password is valid
|
||||
if (!password.trim()) {
|
||||
newErrors.password = t('validation.passwordRequired');
|
||||
} else if (!validPassword) {
|
||||
// 'validPassword' is the boolean you already calculate
|
||||
newErrors.password = t('validation.passwordInvalid');
|
||||
}
|
||||
|
||||
// Rule 2: Check if the confirmation password matches
|
||||
if (!confirmPassword.trim()) {
|
||||
newErrors.confirmPassword = t('validation.confirmPasswordRequired');
|
||||
} else if (!matchPassword) {
|
||||
// 'matchPassword' is the boolean you already calculate
|
||||
newErrors.confirmPassword = t('validation.passwordsDoNotMatch');
|
||||
}
|
||||
}
|
||||
|
||||
if (sex === null) {
|
||||
newErrors.sex = t('validation.genderRequired');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0; // Returns true if form is valid
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleBlur = (field: string) => {
|
||||
setTouched((prev) => ({ ...prev, [field]: true }));
|
||||
};
|
||||
const handleBlur = (_field: string) => {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPasswordSection) {
|
||||
// We clear both touched and errors to prevent lingering validation messages
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.password;
|
||||
@@ -326,10 +301,8 @@ export function UserCompletionPage() {
|
||||
}
|
||||
}, [showPasswordSection]);
|
||||
|
||||
// This effect resets email fields when the section is hidden
|
||||
useEffect(() => {
|
||||
if (!showEmail) {
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
@@ -345,9 +318,7 @@ export function UserCompletionPage() {
|
||||
}
|
||||
}, [showEmail]);
|
||||
|
||||
// re-validate whenever a field the user has touched changes value
|
||||
useEffect(() => {
|
||||
// Only run validation if at least one field has been touched
|
||||
if (Object.keys(touched).length > 0) {
|
||||
validateForm();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user