252 lines
7.7 KiB
TypeScript
252 lines
7.7 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { AuthenticationCard } from '../AuthenticationCard';
|
|
import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-react';
|
|
import {
|
|
Box,
|
|
Button,
|
|
IconButton,
|
|
Stack,
|
|
TextField,
|
|
Typography,
|
|
} from '@mui/material';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { containsNumber } from '@/utils/regexes/containsNumber';
|
|
import { containsSymbol } from '@/utils/regexes/containsSymbol';
|
|
import { least8Chars } from '@/utils/regexes/least8Chars';
|
|
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
|
|
import type { ResetPasswordRequest } from '../../types/userTypes';
|
|
import type { AuthType } from '../../types/authTypes';
|
|
import type { CountryCode } from '@/types/commonTypes';
|
|
import { resetPassword } from '../../api/authorizationAPI';
|
|
import { Icon, useToast } from '@rkheftan/harmony-ui';
|
|
import { useApi } from '@/hooks/useApi';
|
|
|
|
export interface ChangePasswordProps {
|
|
onEditInfo: () => void;
|
|
onPasswordChanged: () => void;
|
|
forgetPasswordInfo: string;
|
|
infoType: AuthType;
|
|
countryCode: CountryCode;
|
|
}
|
|
|
|
export const ChangePassword = ({
|
|
onEditInfo,
|
|
onPasswordChanged,
|
|
forgetPasswordInfo,
|
|
infoType,
|
|
countryCode,
|
|
}: ChangePasswordProps) => {
|
|
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 toast = useToast();
|
|
const { loading: resetPasswordLoading, execute: resetPasswordCall } =
|
|
useApi(resetPassword);
|
|
|
|
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 = async () => {
|
|
if (!passValue || !isValidPassword(passValue)) {
|
|
setInputTouched(true);
|
|
inputRef.current?.focus();
|
|
} else if (passValue !== confirmPassValue) {
|
|
setConfirmInputTouched(true);
|
|
confirmInputRef.current?.focus();
|
|
} else {
|
|
const apiRequest: ResetPasswordRequest = {
|
|
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
|
phoneNumber:
|
|
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
|
newPassword: passValue,
|
|
confirmNewPassword: confirmPassValue,
|
|
};
|
|
|
|
const res = await resetPasswordCall(apiRequest);
|
|
|
|
if (!res) return;
|
|
|
|
if (res.success) {
|
|
onPasswordChanged();
|
|
toast({
|
|
message: t('forgetPassword.passwordChangedSuccessfully'),
|
|
severity: 'success',
|
|
});
|
|
} else {
|
|
toast({
|
|
message: res.message,
|
|
severity: 'error',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const isValidPassword = (value: string) => {
|
|
return (
|
|
containsNumber(value) &&
|
|
containsSymbol(value) &&
|
|
least8Chars(value) &&
|
|
hasUpperAndLowerLetter(value)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<AuthenticationCard>
|
|
<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={<Icon Component={Edit2} />}
|
|
onClick={onEditInfo}
|
|
>
|
|
{forgetPasswordInfo}
|
|
</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 && (
|
|
<Icon
|
|
Component={TickCircle}
|
|
variant="Bold"
|
|
color="success.main"
|
|
/>
|
|
),
|
|
endAdornment: passValue ? (
|
|
<IconButton
|
|
color="primary"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
>
|
|
{showPassword ? (
|
|
<Icon Component={Eye} />
|
|
) : (
|
|
<Icon Component={EyeSlash} />
|
|
)}
|
|
</IconButton>
|
|
) : (
|
|
''
|
|
),
|
|
},
|
|
}}
|
|
sx={{ mt: 4 }}
|
|
/>
|
|
|
|
{!isValidPassword(passValue) && (
|
|
<Stack spacing={1} sx={{ mt: 2 }}>
|
|
{passwordValidationRules.map((rule) => (
|
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
|
<Icon
|
|
Component={TickCircle}
|
|
variant={rule.validator(passValue) ? 'Bold' : 'Linear'}
|
|
color={
|
|
rule.validator(passValue) ? 'success.main' : '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 && (
|
|
<Icon
|
|
Component={TickCircle}
|
|
variant="Bold"
|
|
color="success.main"
|
|
/>
|
|
),
|
|
endAdornment: confirmPassValue ? (
|
|
<IconButton
|
|
color="primary"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<Icon Component={Eye} />
|
|
) : (
|
|
<Icon Component={EyeSlash} />
|
|
)}
|
|
</IconButton>
|
|
) : (
|
|
''
|
|
),
|
|
},
|
|
}}
|
|
sx={{ my: 4 }}
|
|
/>
|
|
|
|
<Stack spacing={1}>
|
|
<Button loading={resetPasswordLoading} onClick={handleSubmit}>
|
|
{t('forgetPassword.confirm')}
|
|
</Button>
|
|
</Stack>
|
|
</AuthenticationCard>
|
|
);
|
|
};
|