chore: all review comments

This commit is contained in:
2025-08-14 00:10:29 +03:30
parent 20da3f980e
commit c15e47b8b0
22 changed files with 137 additions and 583 deletions

View File

@@ -2,7 +2,7 @@ import { CssBaseline } from '@mui/material';
import './App.css';
import { LanguageManager } from './components/LanguageManager';
import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { ForgetPasswordPage } from './features/authorization/routes/ForgetPasswordPage';
function App() {

View File

@@ -11,7 +11,7 @@ import { TextField, Stack } from '@mui/material';
interface DigitInputProps {
error: boolean;
success: boolean;
onChange: Dispatch<SetStateAction<string[]>>;
onChange: Dispatch<SetStateAction<string>>;
}
const DigitInput: React.FC<DigitInputProps> = ({
@@ -26,13 +26,18 @@ const DigitInput: React.FC<DigitInputProps> = ({
inputRefs.current[0]?.focus();
}, []);
const handleDigitInputValueChange = (value: string[]) => {
const formatted = value.filter((char) => char !== '').join('');
onChange(formatted);
};
const handleChange = (value: string, index: number) => {
if (!/^\d$/.test(value) && value !== '') return;
const newCode = [...code];
newCode[index] = value;
setCode(newCode);
onChange(newCode);
handleDigitInputValueChange(newCode);
if (value && index < 4 - 1) {
inputRefs.current[index + 1]?.focus();
@@ -62,7 +67,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
});
setCode(newCode);
onChange(newCode);
handleDigitInputValueChange(newCode);
// Focus the next empty input after the last pasted character
const lastIndex = Math.min(pastedData.length, code.length) - 1;

View File

@@ -1,6 +1,6 @@
import { useState, type JSX } from 'react';
import { LoginRegisterForm } from './LoginRegiserForm';
import type { AuthMode, AuthType } from '../../types/authTypes';
import type { AuthMode, AuthStep, AuthType } from '../../types/authTypes';
import { OtpVerifyForm } from './OtpVerifyForm';
import { isNumeric } from '@/utils/regexes/isNumeric';
import { CompleteSignUp } from './CompleteSignUp';
@@ -8,22 +8,17 @@ import { EnterPasswordForm } from './EnterPasswordForm';
import { UserStatus } from '../../types/userTypes';
import type { CountryCode, GUID } from '@/types/commonTypes';
import { VerifyPhoneNumber } from './VerifyPhoneNumber';
import { useSearchParams } from 'react-router';
import { useNavigate, useSearchParams } from 'react-router-dom';
export const AuthenticationSteps = (): JSX.Element => {
const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/';
const navigate = useNavigate();
const DEFAULT_RETURN_URL = '/profile';
const [searchParams] = useSearchParams();
const authReturnUrl: string =
searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL;
const [authMode, setAuthMode] = useState<AuthMode>('register');
const [authType, setAuthType] = useState<AuthType>('phone');
const [currentStep, setCurrentStep] = useState<
| 'emailOrPhone'
| 'verify'
| 'enterPassword'
| 'addPhoneNumber'
| 'addedPhoneNumberVerify'
>('emailOrPhone');
const [currentStep, setCurrentStep] = useState<AuthStep>('emailOrPhone');
const [loginRegisterValue, setLoginRegisterValue] = useState<string>('');
const [countryCode, setCountryCode] = useState<CountryCode>('+98');
const [addPhoneCountryCode, setAddPhoneCountryCode] =
@@ -54,24 +49,28 @@ export const AuthenticationSteps = (): JSX.Element => {
}
};
const handleOTPVerfied = (userId: GUID) => {
handleUserLoggedIn(userId);
};
const handleUserLoggedIn = (userId: GUID) => {
localStorage.setItem('userID', userId);
location.href = authReturnUrl;
redirectToReturnUrl();
};
const handleConfrimPhoneNumber = (userId: GUID) => {
handleUserLoggedIn(userId);
localStorage.setItem('userID', userId);
setCurrentStep('addPhoneNumber');
};
const handlePhoneNumberVerified = () => {
location.href = authReturnUrl;
redirectToReturnUrl();
};
const redirectToReturnUrl = () => {
if (authReturnUrl === DEFAULT_RETURN_URL) {
navigate(DEFAULT_RETURN_URL);
} else {
location.href = authReturnUrl;
}
};
return (
@@ -95,7 +94,7 @@ export const AuthenticationSteps = (): JSX.Element => {
onVerifyPhoneNumber={handleConfrimPhoneNumber}
authReturnUrl={authReturnUrl}
countryCode={countryCode}
onOTPVerified={handleOTPVerfied}
onOTPVerified={handleUserLoggedIn}
onEditValue={() => setCurrentStep('emailOrPhone')}
authMode={authMode}
authType={authType}

View File

@@ -41,6 +41,10 @@ export const CompleteSignUp = ({
const handleBlur = () => {
setTouched(true);
handleValueError();
};
const handleValueError = () => {
if (!value) {
setError(t('loginForm.thisFieldIsRequired'));
}
@@ -52,15 +56,11 @@ export const CompleteSignUp = ({
};
const handleCompleteSignUp = async () => {
if (!value) {
setError(t('loginForm.thisFieldIsRequired'));
inputRef.current?.focus();
}
if (!isPhoneValid(countryCode, value)) {
setError(t('loginForm.phoneNumberIsInvalid'));
handleValueError();
if (!value || !isPhoneValid(countryCode, value)) {
inputRef.current?.focus();
} else {
setError(undefined);
setSendOtpLoading(true);
await sendSmsOtp({ phoneNumber: countryCode + value });

View File

@@ -1,6 +1,6 @@
import { useRef, useState } from 'react';
import { AuthenticationCard } from '../AuthenticationCard';
import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-reactjs';
import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-react';
import {
Box,
Button,
@@ -11,7 +11,7 @@ import {
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { Toast } from '@/components/Toast';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import type { AuthType } from '../../types/authTypes';
import type { CountryCode, GUID } from '@/types/commonTypes';
import {
@@ -48,7 +48,7 @@ export const EnterPasswordForm = ({
const [showPassword, setShowPassword] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const [loginLoading, setLoginLoading] = useState<boolean>(false);
const [loginStatus, setLoginStatus] = useState<'success' | 'failed'>();
const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState<boolean>();
const [loginAlertOpen, setLoginAlertOpen] = useState<boolean>(false);
const [loginFailedMessage, setLoginFailedMessage] = useState<string>('');
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false);
@@ -74,10 +74,10 @@ export const EnterPasswordForm = ({
const jsonRes = await result.json();
if (jsonRes.success) {
setLoginStatus('success');
setIsLoginStatusSuccess(true);
onLoggedIn(jsonRes.userId);
} else {
setLoginStatus('failed');
setIsLoginStatusSuccess(false);
setLoginFailedMessage(jsonRes.message);
}
setLoginAlertOpen(true);
@@ -103,9 +103,9 @@ export const EnterPasswordForm = ({
<Toast
open={loginAlertOpen}
onClose={() => setLoginAlertOpen(false)}
color={loginStatus === 'failed' ? 'error' : 'success'}
color={!isLoginStatusSuccess ? 'error' : 'success'}
>
{loginStatus === 'failed'
{!isLoginStatusSuccess
? loginFailedMessage
: t('verify.youHaveSuccessfullyLoggedIn')}
</Toast>
@@ -127,7 +127,7 @@ export const EnterPasswordForm = ({
<Button
variant="outlined"
size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }}
sx={{ width: 'auto' }}
endIcon={<Edit2 />}
onClick={onEditValue}
>

View File

@@ -1,10 +1,10 @@
import { Button } from '@mui/material';
import { Google } from 'iconsax-reactjs';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { GoogleCodeClientResponse } from '../../types/userTypes';
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
import type { GUID } from '@/types/commonTypes';
import { Google } from 'iconsax-react';
export interface GoogleAuthenticationProps {
disabled: boolean;
@@ -32,7 +32,7 @@ export const GoogleAuthentication = ({
script.onload = () => {
clientRef.current = google.accounts.oauth2.initCodeClient({
client_id: 'CLIEND_ID',
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
scope: 'openid email profile',
ux_mode: 'popup',
response_type: 'id_token',
@@ -48,13 +48,17 @@ export const GoogleAuthentication = ({
if (jsonRes.success) {
onGoogleAuthenticated(jsonRes.userId);
} else {
handleGoogleLogin();
// Todo: Add useToast to handle error
}
setLoginWithGoogleLoading(false);
},
});
};
return () => {
document.body.removeChild(script);
};
}, []);
const handleGoogleLogin = () => {

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import { isNumeric } from '@/utils/regexes/isNumeric';
import type { AuthType } from '../../types/authTypes';
import { isEmail } from '@/utils/regexes/isEmail';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector';
import type { UserStatus } from '../../types/userTypes';
@@ -12,6 +11,7 @@ import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
import { Toast } from '@/components/Toast';
import type { CountryCode, GUID } from '@/types/commonTypes';
import { GoogleAuthentication } from './GoogleAuthentication';
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
export interface LoginRegisterFormProps {
loginRegisterValue: string;
@@ -62,42 +62,32 @@ export function LoginRegisterForm({
validateInput(loginRegisterValue, authType);
};
const validateInput = (value: string, authType: AuthType) => {
if (!value) {
setError(t('loginForm.thisFieldIsRequired'));
} else if (authType === 'email' && !isEmail(value)) {
setError(t('loginForm.emailIsInvalid'));
} else if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
setError(t('loginForm.phoneNumberIsInvalid'));
} else {
setError(undefined);
}
};
const isPhoneValid = (code: string, phone: string) => {
const phoneNumber = parsePhoneNumberFromString(code + phone);
return phoneNumber && phoneNumber.isValid();
};
const isInputValid = (value: string, authType: AuthType): boolean => {
const validateInput = (
value: string,
authType: AuthType,
setErrors: boolean = true,
): boolean => {
if (!value) {
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
return false;
}
if (authType === 'email' && !isEmail(value)) {
if (setErrors) setError(t('loginForm.emailIsInvalid'));
return false;
}
if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
return false;
}
if (setErrors) setError(undefined);
return true;
};
const handleSubmit = async () => {
if (isInputValid(loginRegisterValue, authType)) {
if (validateInput(loginRegisterValue, authType, false)) {
setCheckStatusLoading(true);
const result = await getUserStatusByPhoneNumberOrEmail({
phoneNumber:

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-reactjs';
import { Edit2 } from 'iconsax-react';
import DigitInput from '@/components/components/DigitsInput';
import type { AuthMode, AuthType } from '../../types/authTypes';
import { useEffect, useState } from 'react';
@@ -80,12 +80,6 @@ export function OtpVerifyForm({
return `${min}:${sec.toString().padStart(2, '0')}`;
};
const handleDigitInputChange = (value: string[]) => {
const formattedValue = value.filter((char) => char !== '').join('');
setOtpCode(formattedValue);
};
const handleVerifyOTP = () => {
if (!otpCode || otpCode.length < 4) {
setOtpDigitInvalid(true);
@@ -146,17 +140,14 @@ export function OtpVerifyForm({
return '';
};
const verifyAlertMessage = (): string => {
if (verifyStatus === 'failed') {
return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
} else if (verifyStatus === 'success' && authMode === 'register') {
return t('verify.youHaveSuccessfullySignedIn');
} else if (verifyStatus === 'success' && authMode === 'login') {
return t('verify.youHaveSuccessfullyLoggedIn');
}
return '';
};
const toastMessage =
verifyStatus === 'failed'
? (errorMessage ?? t('verify.theVerificationCodeIsIncorrect'))
: verifyStatus === 'success' && authMode === 'register'
? t('verify.youHaveSuccessfullySignedIn')
: verifyStatus === 'success' && authMode === 'login'
? t('verify.youHaveSuccessfullyLoggedIn')
: '';
return (
<Stack alignItems="center">
@@ -166,7 +157,7 @@ export function OtpVerifyForm({
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
{toastMessage}
</Toast>
<Box
@@ -199,7 +190,7 @@ export function OtpVerifyForm({
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
onChange={(value) => setOtpCode(value)}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-reactjs';
import { Edit2 } from 'iconsax-react';
import DigitInput from '@/components/components/DigitsInput';
import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast';
@@ -63,12 +63,6 @@ export function VerifyPhoneNumber({
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);
@@ -147,7 +141,7 @@ export function VerifyPhoneNumber({
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
onChange={(value) => setOtpCode(value)}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>

View File

@@ -10,10 +10,10 @@ import {
Typography,
} from '@mui/material';
import { useMemo, useRef, useState, type RefObject } from 'react';
import { ArrowDown2 } from 'iconsax-reactjs';
import { ArrowDown2 } from 'iconsax-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { countries, type Country } from '../data/countries';
import { countries, type Country } from '../../../countries';
import type { CountryCode } from '@/types/commonTypes';
interface CountryCodeSelectorProps {
show: boolean;

View File

@@ -1,6 +1,6 @@
import { useRef, useState } from 'react';
import { AuthenticationCard } from '../AuthenticationCard';
import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-reactjs';
import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-react';
import {
Box,
Button,

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-reactjs';
import { Edit2 } from 'iconsax-react';
import DigitInput from '@/components/components/DigitsInput';
import type { AuthType } from '../../types/authTypes';
import { useEffect, useState } from 'react';
@@ -69,12 +69,6 @@ export function ForgetPasswordOtp({
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);
@@ -158,7 +152,7 @@ export function ForgetPasswordOtp({
<DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'}
success={verifyStatus === 'success'}
onChange={(value) => handleDigitInputChange(value as string[])}
onChange={(value) => setOtpCode(value)}
/>
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>

View File

@@ -4,13 +4,13 @@ import { useTranslation } from 'react-i18next';
import { isNumeric } from '@/utils/regexes/isNumeric';
import type { AuthType } from '../../types/authTypes';
import { isEmail } from '@/utils/regexes/isEmail';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector';
import type { CountryCode } from '@/types/commonTypes';
import { sendForgetPassCode } from '../../api/authorizationAPI';
import type { SendForgetPassCodeRequest } from '../../types/userTypes';
import { Toast } from '@/components/Toast';
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
export interface ForgettedPasswordInfoProps {
forgettedPasswordInfo: string;
@@ -57,42 +57,28 @@ export function ForgettedPasswordInfo({
validateInput(forgettedPasswordInfo, infoType);
};
const validateInput = (value: string, authType: AuthType) => {
const validateInput = (
value: string,
authType: AuthType,
setErrors: boolean = true,
) => {
if (!value) {
setError(t('loginForm.thisFieldIsRequired'));
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
return false;
} else if (authType === 'email' && !isEmail(value)) {
setError(t('loginForm.emailIsInvalid'));
} else if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
setError(t('loginForm.phoneNumberIsInvalid'));
if (setErrors) setError(t('loginForm.emailIsInvalid'));
return false;
} else if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
return false;
} else {
setError(undefined);
if (setErrors) setError(undefined);
return true;
}
};
const isPhoneValid = (code: string, phone: string) => {
const phoneNumber = parsePhoneNumberFromString(code + phone);
return phoneNumber && phoneNumber.isValid();
};
const isInputValid = (value: string, infoType: AuthType): boolean => {
if (!value) {
return false;
}
if (infoType === 'email' && !isEmail(value)) {
return false;
}
if (infoType === 'phone' && !isPhoneValid(countryCode, value)) {
return false;
}
return true;
};
const handleSubmit = async () => {
if (isInputValid(forgettedPasswordInfo, infoType)) {
if (validateInput(forgettedPasswordInfo, infoType, false)) {
setSendCodeLoading(true);
const sendCodeRequest: SendForgetPassCodeRequest = {

View File

@@ -1,3 +1,10 @@
export type AuthType = 'email' | 'phone';
export type AuthMode = 'register' | 'login';
export type AuthStep =
| 'emailOrPhone'
| 'verify'
| 'enterPassword'
| 'addPhoneNumber'
| 'addedPhoneNumberVerify';

View File

@@ -0,0 +1,7 @@
import parsePhoneNumberFromString from 'libphonenumber-js';
export const isPhoneNumber = (code: string, phone: string) => {
const phoneNumber = parsePhoneNumberFromString(code + phone);
return phoneNumber && phoneNumber.isValid();
};