feat: OTP verify status and status messages added

This commit is contained in:
مهرزاد قدرتی
2025-07-27 15:05:46 +03:30
parent ea5a679312
commit fc5d441712
7 changed files with 142 additions and 17 deletions

View File

@@ -11,7 +11,13 @@
"verify": {
"verify": "Verify",
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.",
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account."
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.",
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.",
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.",
"theVerificationCodeIsIncorrect": "The verification code is incorrect.",
"youHaveSuccessfullyLoggedIn": "You have successfully logged in",
"youHaveSuccessfullySignedIn": "You have successfully signed in"
}
}
}

View File

@@ -14,6 +14,11 @@
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.",
"confirmAndLogin": "تایید و ورود",
"confirmAndContinue": "تایید و ادامه",
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید."
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید.",
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "کد تایید ۴ رقمی به شماره ایمیل شما ارسال شد. لطفا آن را وارد کنید.",
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد"
}
}

23
src/components/Toast.tsx Normal file
View File

@@ -0,0 +1,23 @@
import { Alert, Snackbar, type AlertColor } from '@mui/material';
import React, { type PropsWithChildren } from 'react';
export interface ToastProps extends PropsWithChildren {
color: AlertColor | undefined;
open: boolean;
onClose: () => void;
}
export const Toast = ({ color, open, onClose, children }: ToastProps) => {
return (
<Snackbar sx={{ minWidth: '396px' }} open={open} onClose={onClose}>
<Alert
onClose={onClose}
severity={color}
variant="filled"
sx={{ width: '100%' }}
>
{children}
</Alert>
</Snackbar>
);
};

View File

@@ -9,10 +9,16 @@ import React, {
import { TextField, Stack } from '@mui/material';
interface DigitInputProps {
error: boolean;
success: boolean;
onChange: Dispatch<SetStateAction<string[]>>;
}
const DigitInput: React.FC<DigitInputProps> = ({ onChange }) => {
const DigitInput: React.FC<DigitInputProps> = ({
onChange,
error,
success,
}) => {
const [code, setCode] = useState<string[]>(['', '', '', '']);
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
@@ -74,6 +80,8 @@ const DigitInput: React.FC<DigitInputProps> = ({ onChange }) => {
>
{code.map((digit, index) => (
<TextField
error={error}
color={success ? 'success' : 'primary'}
key={index}
inputRef={(el) => (inputRefs.current[index] = el)}
value={digit}
@@ -85,6 +93,11 @@ const DigitInput: React.FC<DigitInputProps> = ({ onChange }) => {
maxLength: 1,
sx: {
height: '72px',
color: error
? 'error.main'
: success
? 'success.main'
: 'text.primary',
},
style: {
textAlign: 'center',

View File

@@ -2,21 +2,31 @@ import React, { useState, type JSX } from 'react';
import { LoginRegisterForm } from './LoginRegiserForm';
import type { AuthMode, AuthType } from '../types/auth-types';
import { OtpVerifyForm } from './OtpVerifyForm';
import { isNumeric } from '@/utils/regexes/isNumeric';
export const AuthenticationContainer = (): JSX.Element => {
const [authMode, setAuthMode] = useState<AuthMode>('register');
const [authMode, setAuthMode] = useState<AuthMode>('login');
const [authType, setAuthType] = useState<AuthType>('phone');
const [currentStep, setCurrentStep] = useState<
'emailOrPassword' | 'verify' | 'enterPassword'
>('verify');
const [loginRegisterValue, setLoginRegisterValue] =
useState<string>('9152814093');
'emailOrPassword' | 'verify' | 'enterPassword' | 'addPhoneNumber'
>('emailOrPassword');
const [loginRegisterValue, setLoginRegisterValue] = useState<string>('');
const handleLoginRegister = (value: string) => {
setLoginRegisterValue(value);
setAuthType(isNumeric(value) ? 'phone' : 'email');
setCurrentStep('verify');
};
const handleOTPVerfied = (otpCode: string) => {
console.log(otpCode);
if (authMode === 'register' && authType === 'email') {
setAuthType('phone');
setCurrentStep('addPhoneNumber');
}
};
const handleEditValue = () => {
setCurrentStep('emailOrPassword');
};
@@ -35,6 +45,7 @@ export const AuthenticationContainer = (): JSX.Element => {
{currentStep === 'verify' && (
<OtpVerifyForm
onOTPVerified={handleOTPVerfied}
onEditValue={handleEditValue}
authMode={authMode}
authType={authType}

View File

@@ -1,14 +1,17 @@
import { useTranslation } from 'react-i18next';
import { Box, Button, Typography } from '@mui/material';
import { Alert, Box, Button, Snackbar, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-reactjs';
import DigitInput from '@/components/components/DigitsInput';
import type { AuthMode, AuthType } from '../types/auth-types';
import { useState } from 'react';
import { Toast } from '@/components/Toast';
interface OtpVerifyFormProps {
value: string;
authType: AuthType;
authMode: AuthMode;
onEditValue: () => void;
onOTPVerified: (otpCode: string) => void;
}
export function OtpVerifyForm({
@@ -16,9 +19,38 @@ export function OtpVerifyForm({
authType,
authMode,
onEditValue,
onOTPVerified,
}: OtpVerifyFormProps) {
const [otpCode, setOtpCode] = useState<string>('');
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
const [verifyStatus, setVerifyStatus] = useState<
'loading' | 'success' | 'failed'
>();
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
const { t } = useTranslation('authentication');
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);
setVerifyStatus('loading');
// Change setTimeout to api call
setTimeout(() => {
setVerifyAlertOpen(true);
setVerifyStatus('success');
onOTPVerified(otpCode);
}, 1000);
}
};
const otpMessage = (): string => {
if (authType === 'phone' && authMode === 'login') {
return t(
@@ -28,6 +60,26 @@ export function OtpVerifyForm({
return t(
'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount',
);
} else if (authType === 'email' && authMode === 'login') {
return t(
'verify.a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt',
);
} else if (authType === 'email' && authMode === 'register') {
return t(
'verify.thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount',
);
}
return '';
};
const verifyAlertMessage = (): string => {
if (verifyStatus === 'failed') {
return t('verify.theVerificationCodeIsIncorrect');
} else if (verifyStatus === 'success' && authMode === 'register') {
return t('verify.youHaveSuccessfullySignedIn');
} else if (verifyStatus === 'success' && authMode === 'login') {
return t('verify.youHaveSuccessfullyLoggedIn');
}
return '';
@@ -35,6 +87,14 @@ export function OtpVerifyForm({
return (
<Box sx={{ width: '100%' }}>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
</Toast>
<Box
sx={{
display: 'flex',
@@ -62,8 +122,13 @@ export function OtpVerifyForm({
{otpMessage()}
</Typography>
<DigitInput onChange={(value) => console.log(value)} />
<Button>
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatus === 'loading'}>
{authMode === 'register'
? t('verify.confirmAndContinue')
: t('verify.confirmAndLogin')}

View File

@@ -5,6 +5,8 @@ export const PALETTE: Palette = {
primary: {
light: {
main: blue.A400,
dark: blue.A700,
light: blue.A100,
contrastText: '#FFFFFF',
},
// TODO
@@ -49,9 +51,9 @@ export const PALETTE: Palette = {
},
error: {
light: {
main: '#E53935',
dark: '#C62828',
light: '#EF5350',
main: '#d32f2f',
dark: '#c62828',
light: '#ef5350',
contrastText: '#FFFFFF',
},
// TODO
@@ -94,9 +96,9 @@ export const PALETTE: Palette = {
},
success: {
light: {
main: '#43A047',
dark: '#1B5E20',
light: '#81C784',
main: '#2e7d32',
dark: '#1b5e20',
light: '#4caf50',
contrastText: '#FFFFFF',
},
// TODO