diff --git a/public/locales/en/completionForm.json b/public/locales/en/completionForm.json
index 77b3ae3..ba8f78d 100644
--- a/public/locales/en/completionForm.json
+++ b/public/locales/en/completionForm.json
@@ -4,7 +4,7 @@
"description": "Enter your business information",
"name": "Name",
"familyName": "Family Name",
- "gender": "Gender(Optional)",
+ "gender": "Gender",
"optionalNationalCode": "National Code(Optional)",
"determinePassword": "Determine Password",
"password": "Password",
@@ -19,7 +19,7 @@
"woman": "female",
"hasNumber": "includes number",
"hasMinLength": "at least 8 characters",
- "hasUpperAndLower": "includes a lowercase and uppercase letter",
+ "hasUpperAndLower": "includes a lowercase and uppercase English letter",
"hasSpecialChar": "includes sign (!@#$%^&*)",
"notCompatibility": "does not match",
"emailCorrectForm": "Enter the correct email form.",
@@ -28,6 +28,7 @@
"agreementPart2": ".",
"sent": "sent",
"country": "country",
+ "noOption": "No option",
"dateOfBirth": "Date of birth(optional)",
"submitSuccess": "Information successfully registered",
"submitError": "Error in registering information",
@@ -46,6 +47,8 @@
"confirmPasswordRequired": "Please confirm your password.",
"passwordsDoNotMatch": "Passwords do not match.",
"verificationCodeRequired": "Verification code is required.",
- "mustVerifyCode": "Please click the verify button to confirm the code."
+ "mustVerifyCode": "Please click the verify button to confirm the code.",
+ "genderRequired": "Selecting a gender is required.",
+ "verificationCodeInvalid": "Verification code must be 4 digits."
}
}
diff --git a/public/locales/fa/completionForm.json b/public/locales/fa/completionForm.json
index c8f12c9..bb609a8 100644
--- a/public/locales/fa/completionForm.json
+++ b/public/locales/fa/completionForm.json
@@ -4,7 +4,7 @@
"description": "اطلاعات کسب و کار خود را وارد کنید",
"name": "نام",
"familyName": "نام خانوادگی",
- "gender": "جنسیت(اختیاری)",
+ "gender": "جنسیت",
"optionalNationalCode": "کدملی(اختیاری)",
"determinePassword": "تعیین رمز عبور",
"password": "رمز عبور",
@@ -19,7 +19,7 @@
"woman": "زن",
"hasNumber": "شامل عدد",
"hasMinLength": "حداقل 8 کاراکتر",
- "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ",
+ "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ انگلیسی",
"hasSpecialChar": "شامل علامت (!@#$%^&*)",
"notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد",
"emailCorrectForm": "ساختار ایمیل صحیح نیست",
@@ -28,6 +28,7 @@
"agreementPart2": " می باشد.",
"sent": "ارسال شد!",
"country": "کشور",
+ "noOption": "گزینه ای یافت نشد",
"dateOfBirth": "تاریخ تولد(اختیاری)",
"invalidCountry": "کشور انتخاب شده صحیح نیست",
"rules": "قوانین و مقررات",
@@ -54,6 +55,8 @@
"confirmPasswordRequired": "لطفاً رمز عبور خود را تکرار کنید.",
"passwordsDoNotMatch": "رمزهای عبور با یکدیگر مطابقت ندارند.",
"verificationCodeRequired": "وارد کردن کد تایید الزامی است.",
- "mustVerifyCode": "لطفاً برای تایید کد، روی دکمه بررسی کلیک کنید."
+ "mustVerifyCode": "لطفاً برای تایید کد، روی دکمه بررسی کلیک کنید.",
+ "genderRequired": "انتخاب جنسیت الزامی است.",
+ "verificationCodeInvalid": "کد تایید باید ۴ رقم باشد."
}
}
diff --git a/src/features/authorization/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx
index 444b83e..7265387 100644
--- a/src/features/authorization/components/AuthenticationCard.tsx
+++ b/src/features/authorization/components/AuthenticationCard.tsx
@@ -18,11 +18,12 @@ export const AuthenticationCard = ({
sx={{
borderRadius: 2,
p: {
- xs: 4,
+ xs: 3,
md: 6,
},
marginInline: 2,
- width: (t) => `calc(100% - ${t.spacing(2)})`,
+ boxSizing: 'border-box',
+ width: '100%',
maxWidth: maxWidth ?? '552px',
...sx,
diff --git a/src/features/authorization/components/UserCompletionForm/DateOfBirth.tsx b/src/features/authorization/components/UserCompletionForm/DateOfBirth.tsx
index ecf6d99..bb803ac 100644
--- a/src/features/authorization/components/UserCompletionForm/DateOfBirth.tsx
+++ b/src/features/authorization/components/UserCompletionForm/DateOfBirth.tsx
@@ -15,6 +15,8 @@ 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';
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
const { t, i18n } = useTranslation('completionForm');
@@ -49,6 +51,10 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
);
};
+ const CustomCalendarIcon = () => (
+
+ );
+
return (
,
+ ) => {
+ const value = e.target.value;
+ // Allow only digits and enforce the max length
+ if (isNumeric(value) && value.length <= 4) {
+ setVerificationCode(value);
+ }
+ };
+
const formatTimerValue = () => {
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
const s = String(countdown % 60).padStart(2, '0');
@@ -71,7 +82,6 @@ export function EmailSection({
display: 'flex',
flexDirection: 'column',
gap: 2,
- px: { xs: 2, sm: 0 },
}}
>
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' }}
@@ -97,7 +108,7 @@ export function EmailSection({
input: {
startAdornment:
!isVerifyingCode && emailVerified ? (
-
+
) : null,
- endAdornment:
- buttonState === 'counting' ? (
-
-
-
-
-
- ) : null,
- sx: {
- paddingLeft: buttonState === 'counting' ? 0 : undefined,
- },
+ endAdornment: codeSent ? (
+
+
+
+
+
+ ) : null,
},
}}
/>
@@ -164,9 +171,11 @@ export function EmailSection({
>
setVerificationCode(e.target.value)}
+ onChange={handleVerificationCodeChange}
sx={{ flex: '1 1 260px' }}
disabled={isVerifyingCode}
onBlur={() => handleBlur('verificationCode')}
diff --git a/src/features/authorization/components/UserCompletionForm/PasswordSection.tsx b/src/features/authorization/components/UserCompletionForm/PasswordSection.tsx
index 9254980..c3b4518 100644
--- a/src/features/authorization/components/UserCompletionForm/PasswordSection.tsx
+++ b/src/features/authorization/components/UserCompletionForm/PasswordSection.tsx
@@ -7,6 +7,7 @@ import {
FormGroup,
Typography,
InputAdornment,
+ Grid,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
@@ -27,7 +28,6 @@ export function PasswordSection({
hasUpperAndLower,
hasSpecialChar,
validPassword,
- showValidations,
errors,
touched,
handleBlur,
@@ -36,6 +36,7 @@ export function PasswordSection({
const [showPasswordText, setShowPasswordText] = useState(false);
const [showPasswordRepetitionText, setShowPasswordRepetitionText] =
useState(false);
+ const [showValidations, setShowValidation] = useState(false);
const handleTogglePasswordSection = (
e: React.ChangeEvent,
@@ -47,6 +48,11 @@ export function PasswordSection({
const handleTogglePasswordRepetitionEye = () =>
setShowPasswordRepetitionText((prev) => !prev);
+ const handleBlurPassword = () => {
+ handleBlur('password');
+ setShowValidation(false);
+ };
+
return (
<>
@@ -72,29 +78,18 @@ export function PasswordSection({
{showPasswordSection && (
-
-
+
+
setPassword(e.target.value)}
variant="outlined"
type={showPasswordText ? 'text' : 'password'}
autoFocus
- onBlur={() => handleBlur('password')}
+ onBlur={handleBlurPassword}
+ onFocus={() => setShowValidation(true)}
error={touched.password && !!errors.password}
helperText={touched.password && errors.password}
sx={{
@@ -103,18 +98,10 @@ export function PasswordSection({
pr: 8, // Increased padding to accommodate both icons
},
}}
- // FIXME: deprecated
- InputProps={{
- endAdornment: (
-
-
+ slotProps={{
+ input: {
+ endAdornment: (
+
)}
- {validPassword && (
-
- )}
-
-
- ),
+
+ ),
+ startAdornment: validPassword && (
+
+
+
+ ),
+ },
}}
/>
-
+
+
-
+ slotProps={{
+ input: {
+ endAdornment: (
+
)}
- {confirmPassword.length > 0 && (
-
- )}
-
-
- ),
+
+ ),
+ startAdornment: confirmPassword.length > 0 && (
+
+
+
+ ),
+ },
}}
/>
-
+
- {password && showValidations && (
-
-
+ {showValidations && (
+ <>
+
+
+
+
-
-
+
+
+
+
+
+
-
-
+
+ >
)}
-
+
)}
>
);
diff --git a/src/features/authorization/components/UserCompletionForm/PersonalInfoFields.tsx b/src/features/authorization/components/UserCompletionForm/PersonalInfoFields.tsx
index 0fc74b1..555ad56 100644
--- a/src/features/authorization/components/UserCompletionForm/PersonalInfoFields.tsx
+++ b/src/features/authorization/components/UserCompletionForm/PersonalInfoFields.tsx
@@ -6,7 +6,9 @@ import {
Select,
Box,
Autocomplete,
+ Grid,
type SelectChangeEvent,
+ FormHelperText,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { Woman, Man } from 'iconsax-react';
@@ -63,134 +65,115 @@ export function PersonalInfoFields({
];
return (
-
-
-
- setFirstName(e.target.value)}
- sx={{ flex: '1 1 260px' }}
- onBlur={() => handleBlur('firstName')}
- error={touched.firstName && !!errors.firstName}
- helperText={touched.firstName && errors.firstName}
- />
- setLastName(e.target.value)}
- sx={{ flex: '1 1 260px' }}
- onBlur={() => handleBlur('lastName')}
- error={touched.lastName && !!errors.lastName}
- helperText={touched.lastName && errors.lastName}
- />
-
+
+
+ setFirstName(e.target.value)}
+ onBlur={() => handleBlur('firstName')}
+ error={touched.firstName && !!errors.firstName}
+ helperText={touched.firstName && errors.firstName}
+ />
+
-
-
- {t('completion.gender')}
-
-
+
+ setLastName(e.target.value)}
+ onBlur={() => handleBlur('lastName')}
+ error={touched.lastName && !!errors.lastName}
+ helperText={touched.lastName && errors.lastName}
+ />
+
- setNationalId(e.target.value)}
- variant="outlined"
- sx={{ flex: '1 1 260px' }}
- onBlur={() => handleBlur('nationalId')}
- error={touched.nationalId && !!errors.nationalId}
- helperText={touched.nationalId && errors.nationalId}
- />
-
+
+
+ {t('completion.gender')}
+
-
- option.label}
- value={currentCountry}
- onChange={(_, newValue) => setCountry(newValue?.code || '')}
- onBlur={() => handleBlur('country')}
- renderOption={(props, option) => (
-
-
- {option.label}
-
- )}
- renderInput={(params) => (
- {errors.sex}
+ )}
+
+
+
+
+ option.label}
+ noOptionsText={t('completion.noOption')}
+ value={currentCountry}
+ onChange={(_, newValue) => setCountry(newValue?.code || '')}
+ onBlur={() => handleBlur('country')}
+ renderOption={(props, option) => (
+
+
- )}
- clearOnEscape
- />
+ {option.label}
+
+ )}
+ renderInput={(params) => (
+
+ )}
+ clearOnEscape
+ />
+
-
-
-
-
-
-
+
+ setNationalId(e.target.value)}
+ variant="outlined"
+ onBlur={() => handleBlur('nationalId')}
+ error={touched.nationalId && !!errors.nationalId}
+ helperText={touched.nationalId && errors.nationalId}
+ />
+
+
+
+
+
+
);
}
diff --git a/src/features/authorization/components/UserCompletionForm/SubmitSection.tsx b/src/features/authorization/components/UserCompletionForm/SubmitSection.tsx
index 982a40a..0e97f19 100644
--- a/src/features/authorization/components/UserCompletionForm/SubmitSection.tsx
+++ b/src/features/authorization/components/UserCompletionForm/SubmitSection.tsx
@@ -28,9 +28,8 @@ export function SubmitSection({ onSubmit, loading }: SubmitProps) {
display: 'flex',
flexWrap: 'wrap',
gap: 2,
- px: { xs: 2, sm: 0 },
mb: 2,
- justifyContent: { xs: 'center', sm: 'flex-start' },
+ justifyContent: 'flex-start',
}}
>
({});
const [touched, setTouched] = useState<{ [key: string]: boolean }>({});
@@ -73,10 +72,6 @@ export function UserCompletionPage() {
completeUserInformationApi,
);
- useEffect(() => {
- setShowPasswordValidations(password ? !validPassword : false);
- }, [password, validPassword]);
-
useEffect(() => {
let timer: NodeJS.Timeout;
if (buttonState === 'counting' && countdown > 0) {
@@ -101,6 +96,17 @@ export function UserCompletionPage() {
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) {
@@ -122,14 +128,23 @@ export function UserCompletionPage() {
};
const handleVerifyCode = async () => {
- if (!verificationCode.trim()) {
- // Manually trigger the validation error and stop
+ const trimmedCode = verificationCode.trim();
+
+ // Manually trigger the validation error and stop
+ 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 });
@@ -161,6 +176,7 @@ export function UserCompletionPage() {
verificationCode: showEmail,
password: showPasswordSection,
confirmPassword: showPasswordSection,
+ sex: true,
});
const isValid = validateForm();
@@ -207,10 +223,22 @@ export function UserCompletionPage() {
setCodeSent(false);
setEmailVerified(false);
setVerificationCode('');
+ // We clear both touched and errors
+ 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 = () => {
- // TODO: check if need separate validation or same common validation
const newErrors: { [key: string]: string } = {};
// Rule 1: First Name is required
@@ -235,11 +263,16 @@ export function UserCompletionPage() {
// Rule 6: If verification code sent and email section is active
if (showEmail && codeSent && !emailVerified) {
- if (!verificationCode.trim()) {
+ 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 2: The user has typed a code but hasn't clicked "Verify" yet.
+ // Case 3: The user has typed a code but hasn't clicked "Verify" yet.
newErrors.verificationCode = t('validation.mustVerifyCode');
}
}
@@ -263,6 +296,10 @@ export function UserCompletionPage() {
}
}
+ if (sex === null) {
+ newErrors.sex = t('validation.genderRequired');
+ }
+
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Returns true if form is valid
};
@@ -271,6 +308,43 @@ export function UserCompletionPage() {
setTouched((prev) => ({ ...prev, [field]: true }));
};
+ useEffect(() => {
+ if (!showPasswordSection) {
+ // We clear both touched and errors to prevent lingering validation messages
+ 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]);
+
+ // 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;
+ delete newTouched.verificationCode;
+ return newTouched;
+ });
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ delete newErrors.email;
+ delete newErrors.verificationCode;
+ return newErrors;
+ });
+ }
+ }, [showEmail]);
+
// re-validate whenever a field the user has touched changes value
useEffect(() => {
// Only run validation if at least one field has been touched
@@ -289,6 +363,7 @@ export function UserCompletionPage() {
password,
confirmPassword,
showPasswordSection,
+ sex,
touched,
]);
@@ -300,17 +375,16 @@ export function UserCompletionPage() {
sx={{
minHeight: '100vh',
gap: 3,
- // TODO: check if this padding needed for mobile view
- py: { sm: 0, xs: 2 },
+ py: { md: 0, xs: 4 },
}}
>
@@ -361,7 +435,6 @@ export function UserCompletionPage() {
hasUpperAndLower={hasUpperAndLower}
hasSpecialChar={hasSpecialChar}
validPassword={validPassword}
- showValidations={showPasswordValidations}
errors={errors}
touched={touched}
handleBlur={handleBlur}
diff --git a/src/features/authorization/types/settingForm.ts b/src/features/authorization/types/settingForm.ts
index 5fa7bc8..68d7030 100644
--- a/src/features/authorization/types/settingForm.ts
+++ b/src/features/authorization/types/settingForm.ts
@@ -49,7 +49,6 @@ export interface PasswordSectionProps extends ValidationProps {
hasUpperAndLower: boolean;
hasSpecialChar: boolean;
validPassword: boolean;
- showValidations: boolean;
}
export interface ValidationItemProps {