feat: login and register, otp verify api calls added

This commit is contained in:
مهرزاد قدرتی
2025-08-09 15:46:51 +03:30
parent 284e60fab3
commit cd86254ce1
12 changed files with 403 additions and 59 deletions

38
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"react-country-flag": "^3.1.0", "react-country-flag": "^3.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.0", "react-i18next": "^15.6.0",
"react-router": "^7.8.0",
"react-virtuoso": "^4.13.0", "react-virtuoso": "^4.13.0",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1" "stylis-plugin-rtl": "^2.1.1"
@@ -2404,6 +2405,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -3794,6 +3804,28 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -3940,6 +3972,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -24,6 +24,7 @@
"react-country-flag": "^3.1.0", "react-country-flag": "^3.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^15.6.0", "react-i18next": "^15.6.0",
"react-router": "^7.8.0",
"react-virtuoso": "^4.13.0", "react-virtuoso": "^4.13.0",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1" "stylis-plugin-rtl": "^2.1.1"

View File

@@ -2,13 +2,20 @@ import { CssBaseline } from '@mui/material';
import './App.css'; import './App.css';
import { LanguageManager } from './components/LanguageManager'; import { LanguageManager } from './components/LanguageManager';
import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage'; import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router';
function App() { function App() {
return ( return (
<> <>
<CssBaseline /> <CssBaseline />
<LanguageManager /> <LanguageManager />
<AuthenticationPage /> <BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="auth" />} />
<Route path="/auth" element={<AuthenticationPage />} />
</Routes>
</BrowserRouter>
,
</> </>
); );
} }

View File

@@ -1,6 +1,7 @@
import type { ApiResponse } from '@/types/apiResponse'; import type { ApiResponse } from '@/types/apiResponse';
import type { FetchPromise } from '@/types/fetchPromise'; import type { FetchPromise } from '@/types/fetchPromise';
import type { import type {
CompleteUserInformationRequest,
ConfirmEmailOtpRequest, ConfirmEmailOtpRequest,
ConfirmForgetPassCodeRequest, ConfirmForgetPassCodeRequest,
ConfirmOtpResponse, ConfirmOtpResponse,
@@ -18,7 +19,7 @@ import type {
SendSmsOtpRequest, SendSmsOtpRequest,
} from '../types/userTypes'; } from '../types/userTypes';
const API_URL = 'https://account.business-harmony.com/api/'; const API_URL = 'https://account.business-harmony.com/api';
export const fetchRequest = <T = ApiResponse>( export const fetchRequest = <T = ApiResponse>(
url: string, url: string,
@@ -91,6 +92,12 @@ export const loginOrSignUpWithGoogle = async (
); );
}; };
export const completeUserInformation = async (
body: CompleteUserInformationRequest,
) => {
return fetchRequest<ApiResponse>('User/CompleteUserInformation', body);
};
export const logOut = async () => { export const logOut = async () => {
return fetchRequest<ApiResponse>('User/LogOut', {}); return fetchRequest<ApiResponse>('User/LogOut', {});
}; };

View File

@@ -5,61 +5,79 @@ import { OtpVerifyForm } from './OtpVerifyForm';
import { isNumeric } from '@/utils/regexes/isNumeric'; import { isNumeric } from '@/utils/regexes/isNumeric';
import { CompleteSignUp } from './CompleteSignUp'; import { CompleteSignUp } from './CompleteSignUp';
import { EnterPasswordForm } from './EnterPasswordForm'; import { EnterPasswordForm } from './EnterPasswordForm';
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
import { UserStatus } from '../../types/userTypes';
import type { CountryCode } from '@/types/commonTypes';
import { VerifyPhoneNumber } from './VerifyPhoneNumber';
export const AuthenticationSteps = (): JSX.Element => { export const AuthenticationSteps = (): JSX.Element => {
const [authMode, setAuthMode] = useState<AuthMode>('register'); const [authMode, setAuthMode] = useState<AuthMode>('register');
const [authType, setAuthType] = useState<AuthType>('phone'); const [authType, setAuthType] = useState<AuthType>('phone');
const [currentStep, setCurrentStep] = useState< const [currentStep, setCurrentStep] = useState<
| 'emailOrPassword' | 'emailOrPhone'
| 'verify' | 'verify'
| 'enterPassword' | 'enterPassword'
| 'addPhoneNumber' | 'addPhoneNumber'
| 'addedPhoneNumberVerify' | 'addedPhoneNumberVerify'
>('emailOrPassword'); >('emailOrPhone');
const [loginRegisterValue, setLoginRegisterValue] = useState<string>(''); const [loginRegisterValue, setLoginRegisterValue] = useState<string>('');
const [countryCode, setCountryCode] = useState<CountryCode>('+98');
const [addedPhoneNumberValue, setAddedPhoneNumberValue] = const [addedPhoneNumberValue, setAddedPhoneNumberValue] =
useState<string>(''); useState<string>('');
const handleLoginRegister = (value: string) => { const handleLoginRegister = (value: string, userStatus: UserStatus) => {
setLoginRegisterValue(value);
setAuthType(isNumeric(value) ? 'phone' : 'email'); setAuthType(isNumeric(value) ? 'phone' : 'email');
// TODO: after api: send to password if it has account and has password switch (userStatus) {
if (true) { case UserStatus.NotRegistered:
setCurrentStep('enterPassword'); setAuthMode('register');
} else {
setCurrentStep('verify'); setCurrentStep('verify');
break;
case UserStatus.RegisteredWithoutPassword:
setAuthMode('login');
setCurrentStep('verify');
break;
case UserStatus.RegisteredWithPassword:
setAuthMode('login');
setCurrentStep('enterPassword');
break;
} }
}; };
const handleOTPVerfied = (otpCode: string) => { const handleOTPVerfied = (registeredWithoutPhoneNumber: boolean = false) => {
if (authMode === 'register' && authType === 'email') { // if (registeredWithoutPhoneNumber) {
setCurrentStep('addPhoneNumber'); // setCurrentStep('addPhoneNumber');
} // }
}; };
const handleEditValue = () => { const handleEditValue = () => {
setCurrentStep('emailOrPassword'); setCurrentStep('emailOrPhone');
}; };
const handleCompleteSignUp = (countryCode: string, value: string) => { const handleCompleteSignUp = (countryCode: string, value: string) => {
setCurrentStep('addedPhoneNumberVerify'); setCurrentStep('addedPhoneNumberVerify');
}; };
const handleCompleteSignUpOTPVerified = (otpCode: string) => { const handleCompleteSignUpOTPVerified = () => {
console.log(otpCode); console.log('phoneNumberVerified');
}; };
const handleCompleteSignUpEditValue = () => { const handleCompleteSignUpEditValue = () => {
setCurrentStep('emailOrPassword'); setCurrentStep('emailOrPhone');
}; };
const handleLoggedInWithPassowrd = () => {}; const handleLoggedInWithPassowrd = () => {};
return ( return (
<> <>
{currentStep === 'emailOrPassword' && ( {currentStep === 'emailOrPhone' && (
<LoginRegisterForm <LoginRegisterForm
countryCode={countryCode}
setCountryCode={setCountryCode}
loginRegisterValue={loginRegisterValue} loginRegisterValue={loginRegisterValue}
setLoginRegisterValue={setLoginRegisterValue} setLoginRegisterValue={setLoginRegisterValue}
authType={authType} authType={authType}
@@ -70,6 +88,7 @@ export const AuthenticationSteps = (): JSX.Element => {
{currentStep === 'verify' && ( {currentStep === 'verify' && (
<OtpVerifyForm <OtpVerifyForm
countryCode={countryCode}
onOTPVerified={handleOTPVerfied} onOTPVerified={handleOTPVerfied}
onEditValue={handleEditValue} onEditValue={handleEditValue}
authMode={authMode} authMode={authMode}
@@ -97,12 +116,11 @@ export const AuthenticationSteps = (): JSX.Element => {
)} )}
{currentStep === 'addedPhoneNumberVerify' && ( {currentStep === 'addedPhoneNumberVerify' && (
<OtpVerifyForm <VerifyPhoneNumber
onOTPVerified={handleCompleteSignUpOTPVerified} countryCode={countryCode}
onEditValue={handleCompleteSignUpEditValue} onEditValue={handleCompleteSignUpEditValue}
authMode="login"
authType="phone"
value={addedPhoneNumberValue} value={addedPhoneNumberValue}
onPhoneNumberVerified={handleCompleteSignUpOTPVerified}
/> />
)} )}
</> </>

View File

@@ -15,29 +15,38 @@ import { isEmail } from '@/utils/regexes/isEmail';
import parsePhoneNumberFromString from 'libphonenumber-js'; import parsePhoneNumberFromString from 'libphonenumber-js';
import { AuthenticationCard } from '../AuthenticationCard'; import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector'; import { CountryCodeSelector } from '../CountryCodeSelector';
import type { UserStatus } from '../../types/userTypes';
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
import { Toast } from '@/components/Toast';
import type { CountryCode } from '@/types/commonTypes';
export interface LoginRegisterFormProps { export interface LoginRegisterFormProps {
loginRegisterValue: string; loginRegisterValue: string;
setLoginRegisterValue: Dispatch<string>; setLoginRegisterValue: Dispatch<string>;
countryCode: CountryCode;
setCountryCode: Dispatch<CountryCode>;
authType: AuthType; authType: AuthType;
setAuthType: Dispatch<AuthType>; setAuthType: Dispatch<AuthType>;
onLoginRegisterSubmit: (value: string) => void; onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
} }
export function LoginRegisterForm({ export function LoginRegisterForm({
loginRegisterValue, loginRegisterValue,
setLoginRegisterValue, setLoginRegisterValue,
countryCode,
setCountryCode,
authType, authType,
setAuthType, setAuthType,
onLoginRegisterSubmit, onLoginRegisterSubmit,
}: LoginRegisterFormProps) { }: LoginRegisterFormProps) {
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
const { t, i18n } = useTranslation('authentication'); const { t, i18n } = useTranslation('authentication');
const [countryCode, setCountryCode] = useState('+98');
const textFieldRef = useRef<HTMLDivElement>(null); const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const dir = i18n.dir(); const dir = i18n.dir();
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [touched, setTouched] = useState<boolean>(false); const [touched, setTouched] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>();
const inputError: boolean = touched && !!error; const inputError: boolean = touched && !!error;
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -91,9 +100,22 @@ export function LoginRegisterForm({
return true; return true;
}; };
const handleSubmit = () => { const handleSubmit = async () => {
if (isInputValid(loginRegisterValue, authType)) { if (isInputValid(loginRegisterValue, authType)) {
onLoginRegisterSubmit(loginRegisterValue); setCheckStatusLoading(true);
const result = await getUserStatusByPhoneNumberOrEmail({
phoneNumber:
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
email: authType === 'email' ? loginRegisterValue : undefined,
});
const jsonResult = await result.json();
if (jsonResult.success) {
onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus);
} else {
setErrorMessage(jsonResult.message);
}
setCheckStatusLoading(false);
} else { } else {
inputRef.current?.focus(); inputRef.current?.focus();
validateInput(loginRegisterValue, authType); validateInput(loginRegisterValue, authType);
@@ -104,6 +126,14 @@ export function LoginRegisterForm({
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Toast
color="error"
onClose={() => setErrorMessage(undefined)}
open={!!errorMessage}
>
{errorMessage}
</Toast>
<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">
@@ -139,8 +169,14 @@ export function LoginRegisterForm({
/> />
<Stack spacing={2}> <Stack spacing={2}>
<Button onClick={handleSubmit}>{t('loginForm.submitButton')}</Button> <Button loading={checkStatusLoading} onClick={handleSubmit}>
<Button variant="outlined" startIcon={<Google variant="Bold" />}> {t('loginForm.submitButton')}
</Button>
<Button
disabled={checkStatusLoading}
variant="outlined"
startIcon={<Google variant="Bold" />}
>
{t('loginForm.loginWithGoogle')} {t('loginForm.loginWithGoogle')}
</Button> </Button>
</Stack> </Stack>

View File

@@ -6,25 +6,37 @@ import type { AuthMode, AuthType } from '../../types/authTypes';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast'; import { Toast } from '@/components/Toast';
import { AuthenticationCard } from '../AuthenticationCard'; import { AuthenticationCard } from '../AuthenticationCard';
import type { LoginRequest } from '../../types/userTypes';
import { useSearchParams } from 'react-router';
import {
loginOrSignUpWithOtp,
sendEmailOtp,
sendSmsOtp,
} from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes';
interface OtpVerifyFormProps { interface OtpVerifyFormProps {
value: string; value: string;
countryCode: CountryCode;
authType: AuthType; authType: AuthType;
authMode: AuthMode; authMode: AuthMode;
onEditValue: () => void; onEditValue: () => void;
onOTPVerified: (otpCode: string) => void; onOTPVerified: (registeredWithoutPhoneNumber: boolean) => void;
} }
export function OtpVerifyForm({ export function OtpVerifyForm({
value, value,
countryCode,
authType, authType,
authMode, authMode,
onEditValue, onEditValue,
onOTPVerified, onOTPVerified,
}: OtpVerifyFormProps) { }: OtpVerifyFormProps) {
const [searchParams] = useSearchParams();
const [otpCode, setOtpCode] = useState<string>(''); const [otpCode, setOtpCode] = useState<string>('');
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false); const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>(); const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
const [errorMessage, setErrorMessage] = useState<string>();
const [verifyStatusLoading, setVerifyStatusLoading] = const [verifyStatusLoading, setVerifyStatusLoading] =
useState<boolean>(false); useState<boolean>(false);
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false); const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
@@ -46,18 +58,18 @@ export function OtpVerifyForm({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [resendTimer]); }, [resendTimer]);
const handleResendOTPCode = () => { const handleResendOTPCode = async () => {
setResendLoading(true); setResendLoading(true);
// TODO: Call API here instead of settimeout if (authType === 'phone') {
await sendSmsOtp({ phoneNumber: countryCode + value });
setTimeout(() => { } else {
console.log('resended'); await sendEmailOtp({ email: value });
}
setResendTimer(120); setResendTimer(120);
setCanResend(false); setCanResend(false);
setResendLoading(false); setResendLoading(false);
}, 1000);
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@@ -72,7 +84,7 @@ export function OtpVerifyForm({
setOtpCode(formattedValue); setOtpCode(formattedValue);
}; };
const handleVerifyOTP = () => { const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) { if (!otpCode || otpCode.length < 4) {
setOtpDigitInvalid(true); setOtpDigitInvalid(true);
} else { } else {
@@ -80,12 +92,26 @@ export function OtpVerifyForm({
setVerifyStatusLoading(true); setVerifyStatusLoading(true);
// Change setTimeout to api call // Change setTimeout to api call
setTimeout(() => {
setVerifyAlertOpen(true); const loginRequest: LoginRequest = {
otpCode: otpCode,
phoneNumber: authType === 'phone' ? countryCode + value : undefined,
email: authType === 'email' ? value : undefined,
returnUrl: searchParams.get('returnUrl') ?? '/',
};
const result = await loginOrSignUpWithOtp(loginRequest);
const jsonRes = await result.json();
if (jsonRes.success) {
setVerifyStatus('success'); setVerifyStatus('success');
onOTPVerified(otpCode); onOTPVerified(jsonRes.registeredWithOutPhoneNumber);
} else {
setVerifyStatus('failed');
setErrorMessage(jsonRes.message);
}
setVerifyAlertOpen(true);
setVerifyStatusLoading(false); setVerifyStatusLoading(false);
}, 1000);
} }
}; };
@@ -113,7 +139,7 @@ export function OtpVerifyForm({
const verifyAlertMessage = (): string => { const verifyAlertMessage = (): string => {
if (verifyStatus === 'failed') { if (verifyStatus === 'failed') {
return t('verify.theVerificationCodeIsIncorrect'); return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
} else if (verifyStatus === 'success' && authMode === 'register') { } else if (verifyStatus === 'success' && authMode === 'register') {
return t('verify.youHaveSuccessfullySignedIn'); return t('verify.youHaveSuccessfullySignedIn');
} else if (verifyStatus === 'success' && authMode === 'login') { } else if (verifyStatus === 'success' && authMode === 'login') {
@@ -153,7 +179,7 @@ export function OtpVerifyForm({
endIcon={<Edit2 />} endIcon={<Edit2 />}
onClick={onEditValue} onClick={onEditValue}
> >
{value} {authType === 'phone' ? countryCode + value : value}
</Button> </Button>
</Box> </Box>

View File

@@ -0,0 +1,191 @@
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/authTypes';
import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast';
import { AuthenticationCard } from '../AuthenticationCard';
import type { LoginRequest } from '../../types/userTypes';
import { useSearchParams } from 'react-router';
import {
loginOrSignUpWithOtp,
sendEmailOtp,
sendSmsOtp,
} from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes';
interface VerifyPhoneNumberProps {
value: string;
countryCode: CountryCode;
onEditValue: () => void;
onPhoneNumberVerified: () => void;
}
export function VerifyPhoneNumber({
value,
countryCode,
onEditValue,
onPhoneNumberVerified,
}: VerifyPhoneNumberProps) {
const [searchParams] = useSearchParams();
const [otpCode, setOtpCode] = useState<string>('');
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
const [errorMessage, setErrorMessage] = useState<string>();
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 = async () => {
setResendLoading(true);
await sendSmsOtp({ phoneNumber: countryCode + value });
setResendTimer(120);
setCanResend(false);
setResendLoading(false);
};
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 = async () => {
if (!otpCode || otpCode.length < 4) {
setOtpDigitInvalid(true);
} else {
setOtpDigitInvalid(false);
setVerifyStatusLoading(true);
// Change setTimeout to api call
// const loginRequest: LoginRequest = {
// otpCode: otpCode,
// phoneNumber: authType === 'phone' ? countryCode + value : undefined,
// email: authType === 'email' ? value : undefined,
// returnUrl: searchParams.get('returnUrl') ?? '/',
// };
// const result = await loginOrSignUpWithOtp(loginRequest);
// const jsonRes = await result.json();
// if (jsonRes.success) {
// setVerifyStatus('success');
// onOTPVerified(jsonRes.registeredWithOutPhoneNumber);
// } else {
// setVerifyStatus('failed');
// setErrorMessage(jsonRes.message);
// }
setVerifyAlertOpen(true);
setVerifyStatusLoading(false);
}
};
const verifyAlertMessage = (): string => {
if (verifyStatus === 'failed') {
return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
} else {
return t('verify.youHaveSuccessfullyLoggedIn');
}
};
return (
<Stack alignItems="center">
<AuthenticationCard>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
</Toast>
<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}
>
{countryCode + value}
</Button>
</Box>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{t(
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
)}
</Typography>
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
{t('verify.confirmAndLogin')}
</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>
);
}

View File

@@ -15,10 +15,11 @@ import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { countries, type Country } from '../data/countries'; import { countries, type Country } from '../data/countries';
import type { CountryCode } from '@/types/commonTypes';
interface CountryCodeSelectorProps { interface CountryCodeSelectorProps {
show: boolean; show: boolean;
value: string; value: CountryCode;
onChange: (newValue: string) => void; onChange: (newValue: CountryCode) => void;
menuAnchor: HTMLElement | null; menuAnchor: HTMLElement | null;
onCloseFocusRef: RefObject<HTMLInputElement | null>; onCloseFocusRef: RefObject<HTMLInputElement | null>;
} }

View File

@@ -1,13 +1,9 @@
export interface Country { import type { CountryCode } from '@/types/commonTypes';
code: string;
label: string;
phone: string;
}
export interface Country { export interface Country {
code: string; code: string;
label: string; label: string;
phone: string; phone: CountryCode;
} }
export const countries: readonly Country[] = [ export const countries: readonly Country[] = [

View File

@@ -14,9 +14,9 @@ export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse {
export enum UserStatus { export enum UserStatus {
None = 0, None = 0,
Value1 = 1, RegisteredWithPassword = 1,
Value2 = 2, RegisteredWithoutPassword = 2,
Value3 = 3, NotRegistered = 3,
} }
// LoginOrSignUpWithOtp // LoginOrSignUpWithOtp
@@ -104,3 +104,24 @@ export interface LoginOrSignUpWithGoogleResponse extends ApiResponse {
completedUserInformation: boolean; completedUserInformation: boolean;
returnUrl: string; returnUrl: string;
} }
// CompleteUserInformation
export interface CompleteUserInformationRequest {
firstName?: string;
lastName?: string;
gender?: Gender;
nationalCode?: string;
savePassword?: boolean;
password?: string;
saveEmail?: boolean;
email?: string;
birthDate?: string;
countryCode?: string;
userId?: GUID;
}
export enum Gender {
Male = 1,
Female = 2,
}

View File

@@ -1 +1,3 @@
export type GUID = `${string}-${string}-${string}-${string}-${string}`; export type GUID = `${string}-${string}-${string}-${string}-${string}`;
export type CountryCode = `+${number}`;