feat: forget password steps and pages added
This commit is contained in:
@@ -33,5 +33,21 @@
|
||||
"loginPassword": "Login password",
|
||||
"loginWithOneTimeCode": "Login with one-time code",
|
||||
"iForgotMyPassword": "I forgot my password."
|
||||
},
|
||||
"forgetPassword": {
|
||||
"forgetPassword": "Forget password",
|
||||
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "Please enter your mobile number/email to recover your password.",
|
||||
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "An email containing a recovery code has been sent to this email address.",
|
||||
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "An recovery code has been sent to this phone number.",
|
||||
"confirm": "Confirm",
|
||||
"changePassword": "Change password",
|
||||
"createANewPassword": "Create a new password",
|
||||
"newPassword": "New password",
|
||||
"includingANumber": "Including a number",
|
||||
"atLeast8Characters": "At least 8 characters",
|
||||
"containsAnUppercaseAndLowercaseLetter": "Contains an uppercase and lowercase letter",
|
||||
"ContainsASymbol": "Contains the symbol (!@#$%&*^)",
|
||||
"confirmPassword": "Confirm password",
|
||||
"passwordChangedSuccessfully": "Password changed successfully"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,5 +35,21 @@
|
||||
"loginPassword": "رمز ورود",
|
||||
"loginWithOneTimeCode": "ورود با کد یکبار مصرف",
|
||||
"iForgotMyPassword": "رمز ورودم را فراموش کردم"
|
||||
},
|
||||
"forgetPassword": {
|
||||
"forgetPassword": "فراموشی رمز",
|
||||
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "لطفا برای بازیابی رمز عبور شماره موبایل/ایمیل خود را وارد کنید.",
|
||||
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "یک ایمیل حاوی کد بازیابی به این ایمیل ارسال شد",
|
||||
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "یک کد بازیابی به این شماره ارسال شد",
|
||||
"confirm": "تایید",
|
||||
"changePassword": "تغییر رمز عبور",
|
||||
"createANewPassword": "یک رمز عبور جدید ایجاد کنید",
|
||||
"newPassword": "رمز عبور جدید",
|
||||
"includingANumber": "شامل عدد",
|
||||
"atLeast8Characters": "حداقل ۸ حرف",
|
||||
"containsAnUppercaseAndLowercaseLetter": "شامل یک حرف بزرگ و کوچک",
|
||||
"ContainsASymbol": "شامل علامت (!@#$%&*^)",
|
||||
"confirmPassword": "تکرار رمز عبور",
|
||||
"passwordChangedSuccessfully": "رمز عبور با موفقیت تغییر یافت"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Box, Button, Paper, TextField, Typography } from '@mui/material';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import React, { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CountryCodeSelector } from './CountryCodeSelector';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
|
||||
export interface CompleteSignUpProps {
|
||||
email: string;
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
} from '@mui/material';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CountryCodeSelector } from './CountryCodeSelector';
|
||||
import { Google } from 'iconsax-reactjs';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import type { AuthMode, AuthType } from '../../types/auth-types';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
|
||||
export interface LoginRegisterFormProps {
|
||||
loginRegisterValue: string;
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useMemo, useRef, useState, type RefObject } from 'react';
|
||||
import { countries, type Country } from '../../data/countries';
|
||||
import { ArrowDown2 } from 'iconsax-reactjs';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { countries, type Country } from '../data/countries';
|
||||
interface CountryCodeSelectorProps {
|
||||
show: boolean;
|
||||
value: string;
|
||||
@@ -0,0 +1,239 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit2,
|
||||
Eye,
|
||||
EyeSlash,
|
||||
MaskLeft,
|
||||
TickCircle,
|
||||
} from 'iconsax-reactjs';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { containsNumber } from '@/utils/regexes/containsNumber';
|
||||
import { containsSymbol } from '@/utils/regexes/containsSymbol';
|
||||
import { least8Chars } from '@/utils/regexes/least8Chars';
|
||||
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
|
||||
|
||||
export interface ChangePasswordProps {
|
||||
onEditInfo: () => void;
|
||||
onPasswordChanged: () => void;
|
||||
forgettedPasswordInfo: string;
|
||||
}
|
||||
|
||||
export const ChangePassword = ({
|
||||
onEditInfo,
|
||||
onPasswordChanged,
|
||||
forgettedPasswordInfo,
|
||||
}: ChangePasswordProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('authentication');
|
||||
const [passValue, setPassValue] = useState<string>('');
|
||||
const [confirmPassValue, setConfirmPassValue] = useState<string>('');
|
||||
const [inputTouched, setInputTouched] = useState<boolean>(false);
|
||||
const [confirmInputTouched, setConfirmInputTouched] =
|
||||
useState<boolean>(false);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] =
|
||||
useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const confirmInputRef = useRef<HTMLInputElement>(null);
|
||||
const [changePasswordLoading, setChangePasswordLoading] =
|
||||
useState<boolean>(false);
|
||||
const [changePasswordStatus, setChangePasswordStatus] = useState<
|
||||
'success' | 'failed'
|
||||
>();
|
||||
const [changePassAlertOpen, setChangePassAlertOpen] =
|
||||
useState<boolean>(false);
|
||||
const [changePassFailedMessage, setChangePassFailedMessage] =
|
||||
useState<string>('');
|
||||
|
||||
const passwordValidationRules = [
|
||||
{ title: t('forgetPassword.includingANumber'), validator: containsNumber },
|
||||
{ title: t('forgetPassword.atLeast8Characters'), validator: least8Chars },
|
||||
{
|
||||
title: t('forgetPassword.containsAnUppercaseAndLowercaseLetter'),
|
||||
validator: hasUpperAndLowerLetter,
|
||||
},
|
||||
{ title: t('forgetPassword.ContainsASymbol'), validator: containsSymbol },
|
||||
];
|
||||
|
||||
const handleBlur = () => {
|
||||
setInputTouched(true);
|
||||
};
|
||||
|
||||
const handleConfirmPassBlur = () => {
|
||||
setConfirmInputTouched(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!passValue || !isValidPassword(passValue)) {
|
||||
setInputTouched(true);
|
||||
inputRef.current?.focus();
|
||||
} else if (passValue !== confirmPassValue) {
|
||||
setConfirmInputTouched(true);
|
||||
confirmInputRef.current?.focus();
|
||||
} else {
|
||||
setChangePasswordLoading(true);
|
||||
|
||||
// Change setTimeout to api call
|
||||
setTimeout(() => {
|
||||
setChangePassAlertOpen(true);
|
||||
// setLoginStatus('success');
|
||||
// setLoginStatus('failed');
|
||||
// setLoginFailedMessage('رمز عبور اشتباه میباشد');
|
||||
onPasswordChanged();
|
||||
setChangePasswordLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidPassword = (value: string) => {
|
||||
return (
|
||||
containsNumber(value) &&
|
||||
containsSymbol(value) &&
|
||||
least8Chars(value) &&
|
||||
hasUpperAndLowerLetter(value)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={changePassAlertOpen}
|
||||
onClose={() => setChangePassAlertOpen(false)}
|
||||
color={changePasswordStatus === 'failed' ? 'error' : 'success'}
|
||||
>
|
||||
{changePasswordStatus === 'failed'
|
||||
? changePassFailedMessage
|
||||
: t('forgetPassword.passwordChangedSuccessfully')}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.changePassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{forgettedPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t('forgetPassword.createANewPassword')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={t('forgetPassword.newPassword')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={passValue}
|
||||
inputRef={inputRef}
|
||||
onChange={(e) => setPassValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={inputTouched && !isValidPassword(passValue)}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||
input: {
|
||||
startAdornment: confirmPassValue &&
|
||||
isValidPassword(passValue) &&
|
||||
passValue === confirmPassValue && (
|
||||
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||
),
|
||||
endAdornment: passValue ? (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <Eye /> : <EyeSlash />}
|
||||
</IconButton>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ mt: 4 }}
|
||||
/>
|
||||
|
||||
{!isValidPassword(passValue) && (
|
||||
<Stack spacing={1} sx={{ mt: 2 }}>
|
||||
{passwordValidationRules.map((rule) => (
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<TickCircle
|
||||
variant={rule.validator(passValue) ? 'Bold' : 'Linear'}
|
||||
color={
|
||||
rule.validator(passValue)
|
||||
? theme.palette.success.main
|
||||
: theme.palette.primary.light
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography variant="body2">{rule.title}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('forgetPassword.confirmPassword')}
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassValue}
|
||||
inputRef={confirmInputRef}
|
||||
onChange={(e) => setConfirmPassValue(e.target.value)}
|
||||
onBlur={handleConfirmPassBlur}
|
||||
error={confirmInputTouched && confirmPassValue !== passValue}
|
||||
slotProps={{
|
||||
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||
input: {
|
||||
startAdornment: confirmPassValue &&
|
||||
isValidPassword(passValue) &&
|
||||
passValue === confirmPassValue && (
|
||||
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||
),
|
||||
endAdornment: confirmPassValue ? (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showPassword ? <Eye /> : <EyeSlash />}
|
||||
</IconButton>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Button loading={changePasswordLoading} onClick={handleSubmit}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { AuthType } from '../../types/auth-types';
|
||||
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
|
||||
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
|
||||
export const ForgetPasswordContainer = () => {
|
||||
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
||||
'enterInfo' | 'verifyOtp' | 'setPassword'
|
||||
>('enterInfo');
|
||||
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
|
||||
useState<string>('');
|
||||
const [infoType, setInfoType] = useState<AuthType>('email');
|
||||
|
||||
const handleSendForgetPassOtp = (value: string) => {
|
||||
console.log(value);
|
||||
setForgetPassCurrentStep('verifyOtp');
|
||||
};
|
||||
|
||||
const handleEditInfo = () => {
|
||||
setForgetPassCurrentStep('enterInfo');
|
||||
};
|
||||
|
||||
const handleOtpVerified = () => {
|
||||
setForgetPassCurrentStep('setPassword');
|
||||
};
|
||||
|
||||
const handlePasswordChanged = () => {
|
||||
console.log('changingPasswordTo');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{forgetPassCurrentStep === 'enterInfo' && (
|
||||
<ForgettedPasswordInfo
|
||||
infoType={infoType}
|
||||
setInfoType={setInfoType}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
setForgettedPasswordInfo={setForgettedPasswordInfo}
|
||||
onSendOtp={handleSendForgetPassOtp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{forgetPassCurrentStep === 'verifyOtp' && (
|
||||
<ForgetPasswordOtp
|
||||
infoType={infoType}
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
onOTPVerified={handleOtpVerified}
|
||||
/>
|
||||
)}
|
||||
|
||||
{forgetPassCurrentStep === 'setPassword' && (
|
||||
<ChangePassword
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
onPasswordChanged={handlePasswordChanged}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material';
|
||||
import { Edit2 } from 'iconsax-reactjs';
|
||||
import DigitInput from '@/components/components/DigitsInput';
|
||||
import type { AuthMode, AuthType } from '../../types/auth-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
|
||||
interface ForgetPasswordOtpProps {
|
||||
forgettedPasswordInfo: string;
|
||||
infoType: AuthType;
|
||||
onEditInfo: () => void;
|
||||
onOTPVerified: (otpCode: string) => void;
|
||||
}
|
||||
|
||||
export function ForgetPasswordOtp({
|
||||
forgettedPasswordInfo,
|
||||
infoType,
|
||||
onEditInfo,
|
||||
onOTPVerified,
|
||||
}: ForgetPasswordOtpProps) {
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||
const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>();
|
||||
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||
useState<boolean>(false);
|
||||
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||
const { t } = useTranslation('authentication');
|
||||
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [resendTimer]);
|
||||
|
||||
const handleResendOTPCode = () => {
|
||||
setResendLoading(true);
|
||||
|
||||
// TODO: Call API here instead of settimeout
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('resended');
|
||||
|
||||
setResendTimer(120);
|
||||
setCanResend(false);
|
||||
setResendLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const handleDigitInputChange = (value: string[]) => {
|
||||
const formattedValue = value.filter((char) => char !== '').join('');
|
||||
|
||||
setOtpCode(formattedValue);
|
||||
};
|
||||
|
||||
const handleVerifyOTP = () => {
|
||||
if (!otpCode || otpCode.length < 4) {
|
||||
setOtpDigitInvalid(true);
|
||||
} else {
|
||||
setOtpDigitInvalid(false);
|
||||
setVerifyStatusLoading(true);
|
||||
|
||||
// Change setTimeout to api call
|
||||
setTimeout(() => {
|
||||
setVerifyAlertOpen(false);
|
||||
onOTPVerified(otpCode);
|
||||
setVerifyStatusLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={verifyAlertOpen}
|
||||
onClose={() => setVerifyAlertOpen(false)}
|
||||
color={'error'}
|
||||
>
|
||||
{t('verify.theVerificationCodeIsIncorrect')}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{forgettedPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{infoType === 'email'
|
||||
? t(
|
||||
'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
|
||||
)
|
||||
: t(
|
||||
'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||
success={verifyStatus === 'success'}
|
||||
onChange={(value) => handleDigitInputChange(value as string[])}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
loading={resendLoading}
|
||||
sx={{ width: 'auto' }}
|
||||
onClick={canResend ? handleResendOTPCode : undefined}
|
||||
>
|
||||
{canResend && t('verify.resendCode')}
|
||||
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Google } from 'iconsax-reactjs';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import type { AuthMode, AuthType } from '../../types/auth-types';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
|
||||
export interface ForgettedPasswordInfoProps {
|
||||
forgettedPasswordInfo: string;
|
||||
setForgettedPasswordInfo: Dispatch<string>;
|
||||
infoType: AuthType;
|
||||
setInfoType: Dispatch<AuthType>;
|
||||
onSendOtp: (value: string) => void;
|
||||
}
|
||||
|
||||
export function ForgettedPasswordInfo({
|
||||
forgettedPasswordInfo,
|
||||
setForgettedPasswordInfo,
|
||||
infoType,
|
||||
setInfoType,
|
||||
onSendOtp,
|
||||
}: ForgettedPasswordInfoProps) {
|
||||
const { t, i18n } = useTranslation('authentication');
|
||||
const [countryCode, setCountryCode] = useState('+98');
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const dir = i18n.dir();
|
||||
const [error, setError] = useState<string>();
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const inputError: boolean = touched && !!error;
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
setForgettedPasswordInfo(newValue);
|
||||
|
||||
// If the new value contains only digits (or is empty), it's a phone number
|
||||
if (isNumeric(newValue)) {
|
||||
setInfoType('phone');
|
||||
} else {
|
||||
setInfoType('email');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
};
|
||||
|
||||
const validateInput = (value: string, authType: AuthType) => {
|
||||
if (!value) {
|
||||
setError(t('loginForm.thisFieldIsRequired'));
|
||||
} else if (authType === 'email' && !isEmail(value)) {
|
||||
setError(t('loginForm.emailIsInvalid'));
|
||||
} else if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
|
||||
setError(t('loginForm.phoneNumberIsInvalid'));
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const isPhoneValid = (code: string, phone: string) => {
|
||||
const phoneNumber = parsePhoneNumberFromString(code + phone);
|
||||
|
||||
return phoneNumber && phoneNumber.isValid();
|
||||
};
|
||||
|
||||
const isInputValid = (value: string, authType: AuthType): boolean => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authType === 'email' && !isEmail(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (isInputValid(forgettedPasswordInfo, infoType)) {
|
||||
onSendOtp(forgettedPasswordInfo);
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
}
|
||||
};
|
||||
|
||||
const showAdornment =
|
||||
infoType === 'phone' && forgettedPasswordInfo.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t(
|
||||
'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword',
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={forgettedPasswordInfo}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4, mb: 8 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button onClick={handleSubmit}>{t('forgetPassword.confirm')}</Button>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Logo from '@/components/Logo';
|
||||
import { Paper } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps';
|
||||
import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer';
|
||||
|
||||
export function AuthenticationPage() {
|
||||
return (
|
||||
@@ -16,7 +17,7 @@ export function AuthenticationPage() {
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<AuthenticationSteps />
|
||||
<ForgetPasswordContainer />
|
||||
</FlexBox>
|
||||
);
|
||||
}
|
||||
|
||||
1
src/utils/regexes/containsNumber.tsx
Normal file
1
src/utils/regexes/containsNumber.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const containsNumber = (value: string) => /\d/.test(value);
|
||||
1
src/utils/regexes/containsSymbol.tsx
Normal file
1
src/utils/regexes/containsSymbol.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value);
|
||||
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export const hasUpperAndLowerLetter = (value: string) => {
|
||||
const hasUpper = /[A-Z]/.test(value);
|
||||
const hasLower = /[a-z]/.test(value);
|
||||
return hasUpper && hasLower;
|
||||
};
|
||||
1
src/utils/regexes/least8Chars.tsx
Normal file
1
src/utils/regexes/least8Chars.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const least8Chars = (value: string) => value.length >= 8;
|
||||
Reference in New Issue
Block a user