feat: login and register, otp verify api calls added
This commit is contained in:
38
package-lock.json
generated
38
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
,
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
export type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
|
||||||
|
export type CountryCode = `+${number}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user