feat: otp resend timer and logic added
This commit is contained in:
@@ -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.",
|
"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.",
|
"theVerificationCodeIsIncorrect": "The verification code is incorrect.",
|
||||||
"youHaveSuccessfullyLoggedIn": "You have successfully logged in",
|
"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": {
|
||||||
"completeSignUp": "Complete Sign Up",
|
"completeSignUp": "Complete Sign Up",
|
||||||
|
|||||||
@@ -19,7 +19,10 @@
|
|||||||
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
|
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
|
||||||
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
|
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
|
||||||
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
|
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
|
||||||
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد"
|
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد",
|
||||||
|
"resendCodeIn": "ارسال مجدد کد تا",
|
||||||
|
"moreMinute": "دقیقه دیگر",
|
||||||
|
"resendCode": "ارسال مجدد"
|
||||||
},
|
},
|
||||||
"completeSignUp": {
|
"completeSignUp": {
|
||||||
"completeSignUp": "تکمیل ثبت نام",
|
"completeSignUp": "تکمیل ثبت نام",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 parsePhoneNumberFromString from 'libphonenumber-js';
|
||||||
import React, { useRef, useState, type Dispatch } from 'react';
|
import React, { useRef, useState, type Dispatch } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CountryCodeSelector } from './CountryCodeSelector';
|
import { CountryCodeSelector } from './CountryCodeSelector';
|
||||||
|
import { AuthenticationCard } from './AuthenticationCard';
|
||||||
|
|
||||||
export interface CompleteSignUpProps {
|
export interface CompleteSignUpProps {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -59,7 +60,7 @@ export const CompleteSignUp = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<AuthenticationCard>
|
||||||
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||||
{t('completeSignUp.completeSignUp')}
|
{t('completeSignUp.completeSignUp')}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -101,6 +102,6 @@ export const CompleteSignUp = ({
|
|||||||
<Button onClick={handleCompleteSignUp}>
|
<Button onClick={handleCompleteSignUp}>
|
||||||
{t('verify.confirmAndContinue')}
|
{t('verify.confirmAndContinue')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</AuthenticationCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { useRef, useState, type Dispatch } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CountryCodeSelector } from './CountryCodeSelector';
|
import { CountryCodeSelector } from './CountryCodeSelector';
|
||||||
@@ -7,6 +14,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric';
|
|||||||
import type { AuthMode, AuthType } from '../types/auth-types';
|
import type { AuthMode, AuthType } from '../types/auth-types';
|
||||||
import { isEmail } from '@/utils/regexes/isEmail';
|
import { isEmail } from '@/utils/regexes/isEmail';
|
||||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||||
|
import { AuthenticationCard } from './AuthenticationCard';
|
||||||
|
|
||||||
export interface LoginRegisterFormProps {
|
export interface LoginRegisterFormProps {
|
||||||
loginRegisterValue: string;
|
loginRegisterValue: string;
|
||||||
@@ -95,7 +103,7 @@ export function LoginRegisterForm({
|
|||||||
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<AuthenticationCard>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
@@ -136,6 +144,6 @@ export function LoginRegisterForm({
|
|||||||
{t('loginForm.loginWithGoogle')}
|
{t('loginForm.loginWithGoogle')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</AuthenticationCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
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 { Edit2 } from 'iconsax-reactjs';
|
||||||
import DigitInput from '@/components/components/DigitsInput';
|
import DigitInput from '@/components/components/DigitsInput';
|
||||||
import type { AuthMode, AuthType } from '../types/auth-types';
|
import type { AuthMode, AuthType } from '../types/auth-types';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Toast } from '@/components/Toast';
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { AuthenticationCard } from './AuthenticationCard';
|
||||||
|
|
||||||
interface OtpVerifyFormProps {
|
interface OtpVerifyFormProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -28,6 +29,42 @@ export function OtpVerifyForm({
|
|||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||||
const { t } = useTranslation('authentication');
|
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 handleDigitInputChange = (value: string[]) => {
|
||||||
const formattedValue = value.filter((char) => char !== '').join('');
|
const formattedValue = value.filter((char) => char !== '').join('');
|
||||||
@@ -87,53 +124,76 @@ export function OtpVerifyForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Stack alignItems="center">
|
||||||
<Toast
|
<AuthenticationCard>
|
||||||
open={verifyAlertOpen}
|
<Toast
|
||||||
onClose={() => setVerifyAlertOpen(false)}
|
open={verifyAlertOpen}
|
||||||
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
onClose={() => setVerifyAlertOpen(false)}
|
||||||
>
|
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
||||||
{verifyAlertMessage()}
|
>
|
||||||
</Toast>
|
{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={{
|
sx={{
|
||||||
display: 'flex',
|
justifyContent: 'center',
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
mt: 1.5,
|
||||||
gap: 4,
|
|
||||||
mb: 0.5,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="text"
|
||||||
size="large"
|
loading={resendLoading}
|
||||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
sx={{ width: 'auto' }}
|
||||||
endIcon={<Edit2 />}
|
onClick={canResend ? handleResendOTPCode : undefined}
|
||||||
onClick={onEditValue}
|
|
||||||
>
|
>
|
||||||
{value}
|
{canResend && t('verify.resendCode')}
|
||||||
|
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { FlexBox } from '@/components/components/common/FlexBox';
|
import { FlexBox } from '@/components/components/common/FlexBox';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { Paper } from '@mui/material';
|
import { Paper } from '@mui/material';
|
||||||
import { SmsOtpForm } from '../components/SmsOtpForm';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AuthenticationContainer } from '../components/AuthenticationContainer';
|
import { AuthenticationContainer } from '../components/AuthenticationContainer';
|
||||||
|
|
||||||
@@ -17,16 +16,7 @@ export function AuthenticationPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
<Paper
|
<AuthenticationContainer />
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
borderRadius: 'theme.xl',
|
|
||||||
p: 6,
|
|
||||||
width: '34.5rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AuthenticationContainer />
|
|
||||||
</Paper>
|
|
||||||
</FlexBox>
|
</FlexBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user