feat: otp resend timer and logic added

This commit is contained in:
مهرزاد قدرتی
2025-07-28 13:02:24 +03:30
parent 45371337b7
commit 3f7242742e
8 changed files with 143 additions and 104 deletions

View File

@@ -16,7 +16,10 @@
"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"
"youHaveSuccessfullySignedIn": "You have successfully signed in",
"resendCodeIn": "Resend code in",
"moreMinute": "minute",
"resendCode": "Resend code"
},
"completeSignUp": {
"completeSignUp": "Complete Sign Up",

View File

@@ -19,7 +19,10 @@
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد"
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد",
"resendCodeIn": "ارسال مجدد کد تا",
"moreMinute": "دقیقه دیگر",
"resendCode": "ارسال مجدد"
},
"completeSignUp": {
"completeSignUp": "تکمیل ثبت نام",

View File

@@ -0,0 +1,18 @@
import { Paper } from '@mui/material';
import React, { type PropsWithChildren } from 'react';
// Beacuse in the otp verify there is a element outside of the authentication card
export const AuthenticationCard = ({ children }: PropsWithChildren) => {
return (
<Paper
elevation={0}
sx={{
bor: 2,
p: 6,
width: '34.5rem',
}}
>
{children}
</Paper>
);
};

View File

@@ -1,8 +1,9 @@
import { Box, Button, TextField, Typography } from '@mui/material';
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';
export interface CompleteSignUpProps {
email: string;
@@ -59,7 +60,7 @@ export const CompleteSignUp = ({
};
return (
<Box sx={{ width: '100%' }}>
<AuthenticationCard>
<Typography variant="h5" sx={{ mb: 0.5 }}>
{t('completeSignUp.completeSignUp')}
</Typography>
@@ -101,6 +102,6 @@ export const CompleteSignUp = ({
<Button onClick={handleCompleteSignUp}>
{t('verify.confirmAndContinue')}
</Button>
</Box>
</AuthenticationCard>
);
};

View File

@@ -1,4 +1,11 @@
import { Box, Button, Stack, TextField, Typography } from '@mui/material';
import {
Box,
Button,
Paper,
Stack,
TextField,
Typography,
} from '@mui/material';
import { useRef, useState, type Dispatch } from 'react';
import { useTranslation } from 'react-i18next';
import { CountryCodeSelector } from './CountryCodeSelector';
@@ -7,6 +14,7 @@ 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';
export interface LoginRegisterFormProps {
loginRegisterValue: string;
@@ -95,7 +103,7 @@ export function LoginRegisterForm({
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
return (
<Box sx={{ width: '100%' }}>
<AuthenticationCard>
<Stack spacing={1}>
<Typography variant="h5">{t('loginForm.title')}</Typography>
<Typography variant="body2" color="text.secondary">
@@ -136,6 +144,6 @@ export function LoginRegisterForm({
{t('loginForm.loginWithGoogle')}
</Button>
</Stack>
</Box>
</AuthenticationCard>
);
}

View File

@@ -1,10 +1,11 @@
import { useTranslation } from 'react-i18next';
import { Alert, Box, Button, Snackbar, Typography } from '@mui/material';
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 { useState } from 'react';
import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast';
import { AuthenticationCard } from './AuthenticationCard';
interface OtpVerifyFormProps {
value: string;
@@ -28,6 +29,42 @@ export function OtpVerifyForm({
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('');
@@ -87,53 +124,76 @@ export function OtpVerifyForm({
};
return (
<Box sx={{ width: '100%' }}>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
</Toast>
<Stack alignItems="center">
<AuthenticationCard>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
</Toast>
<Box
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 4,
mb: 0.5,
}}
>
<Typography variant="h5">{t('verify.verify')}</Typography>
<Button
variant="outlined"
size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />}
onClick={onEditValue}
>
{value}
</Button>
</Box>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{otpMessage()}
</Typography>
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
{authMode === 'register'
? t('verify.confirmAndContinue')
: t('verify.confirmAndLogin')}
</Button>
</AuthenticationCard>
<Stack
direction="row"
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
justifyContent: 'space-between',
gap: 4,
mb: 0.5,
mt: 1.5,
}}
>
<Typography variant="h5">{t('verify.verify')}</Typography>
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
<Button
variant="outlined"
size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />}
onClick={onEditValue}
variant="text"
loading={resendLoading}
sx={{ width: 'auto' }}
onClick={canResend ? handleResendOTPCode : undefined}
>
{value}
{canResend && t('verify.resendCode')}
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
</Button>
</Box>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{otpMessage()}
</Typography>
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
{authMode === 'register'
? t('verify.confirmAndContinue')
: t('verify.confirmAndLogin')}
</Button>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,44 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Box, Button, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-reactjs';
import DigitInput from '@/components/components/DigitsInput';
interface SmsOtpProps {
value: string;
type: 'phone' | 'email';
}
export function SmsOtpForm({ value, type }: SmsOtpProps) {
const { t } = useTranslation('authentication');
return (
<Box sx={{ width: '100%' }}>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 4,
mb: 0.5,
}}
>
<Typography variant="h5">اعتبارسنجی</Typography>
<Button
variant="outlined"
size="large"
sx={{ direction: 'auto', textTransform: 'lowercase' }}
endIcon={<Edit2 />}
>
{value}
</Button>
</Box>
<Typography variant="body2" color="textSecondary">
کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.
</Typography>
<DigitInput onChange={(value) => console.log(value)} />
<Button>{t('smsOtp.confirmAndLogin')}</Button>
</Box>
);
}

View File

@@ -1,7 +1,6 @@
import { FlexBox } from '@/components/components/common/FlexBox';
import Logo from '@/components/Logo';
import { Paper } from '@mui/material';
import { SmsOtpForm } from '../components/SmsOtpForm';
import { useState } from 'react';
import { AuthenticationContainer } from '../components/AuthenticationContainer';
@@ -17,16 +16,7 @@ export function AuthenticationPage() {
}}
>
<Logo />
<Paper
elevation={0}
sx={{
borderRadius: 'theme.xl',
p: 6,
width: '34.5rem',
}}
>
<AuthenticationContainer />
</Paper>
<AuthenticationContainer />
</FlexBox>
);
}