Merge pull request #15 from rkheftan/feat/auth

Feat/auth
This commit is contained in:
SajadMRjl
2025-08-16 02:45:33 +03:30
committed by GitHub
29 changed files with 765 additions and 1040 deletions

5
.env
View File

@@ -1 +1,6 @@
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/ VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
VITE_API_URL=https://accounts.business-harmony.com/api/
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
VITE_IDENTITY_CLIENT_ID=harmony_identity
VITE_IDENTITY_SCOPE=openid profile offline_access harmony_identity

1049
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"loginWithGoogle": "Login with google", "loginWithGoogle": "Login with google",
"emailIsInvalid": "Email is invalid", "emailIsInvalid": "Email is invalid",
"phoneNumberIsInvalid": "Phone number is invalid", "phoneNumberIsInvalid": "Phone number is invalid",
"thisFieldIsRequired": "This field is requried" "thisFieldIsRequired": "This field is requried",
"googleAuthenticationFailed": "Login with google failed"
}, },
"verify": { "verify": {
"verify": "Verify", "verify": "Verify",

View File

@@ -246,5 +246,9 @@
"yemen": "Yemen", "yemen": "Yemen",
"zambia": "Zambia", "zambia": "Zambia",
"zimbabwe": "Zimbabwe" "zimbabwe": "Zimbabwe"
},
"messages": {
"noResualtFound": "No result found.",
"serverError": "Internal server error"
} }
} }

View File

@@ -7,7 +7,8 @@
"loginWithGoogle": "ورود با گوگل", "loginWithGoogle": "ورود با گوگل",
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد", "emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد", "phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
"thisFieldIsRequired": "این فیلد الزامی است" "thisFieldIsRequired": "این فیلد الزامی است",
"googleAuthenticationFailed": "ورود با گوگل با خطا مواجه شد"
}, },
"verify": { "verify": {
"verify": "اعتبارسنجی", "verify": "اعتبارسنجی",

View File

@@ -185,7 +185,8 @@
"zimbabwe": "زیمبابوه" "zimbabwe": "زیمبابوه"
}, },
"messages": { "messages": {
"noResualtFound": "نتیجه ای یافت نشد." "noResualtFound": "نتیجه ای یافت نشد.",
"serverError": "خطای سمت سرور"
}, },
"side": { "side": {
"account": "حساب کاربری", "account": "حساب کاربری",

View File

@@ -0,0 +1,13 @@
import { getAccessToken } from '@/lib/apiClient';
import { type PropsWithChildren } from 'react';
import { Navigate } from 'react-router-dom';
export const ProtectedRoute = ({ children }: PropsWithChildren) => {
if (!getAccessToken()) {
// If no token, redirect to login page
return <Navigate to="/login" replace />;
}
// If token exists, render the children components
return children;
};

View File

@@ -1,7 +1,5 @@
import type { ApiResponse } from '@/types/apiResponse'; import type { ApiResponse } from '@/types/apiResponse';
import type { FetchPromise } from '@/types/fetchPromise';
import type { import type {
CompleteUserInformationRequest,
ConfirmEmailOtpRequest, ConfirmEmailOtpRequest,
ConfirmForgetPassCodeRequest, ConfirmForgetPassCodeRequest,
ConfirmOtpResponse, ConfirmOtpResponse,
@@ -19,86 +17,66 @@ import type {
SendForgetPassCodeRequest, SendForgetPassCodeRequest,
SendSmsOtpRequest, SendSmsOtpRequest,
} from '../types/userTypes'; } from '../types/userTypes';
import apiClient from '@/lib/apiClient';
const API_URL = 'https://accounts.business-harmony.com/api';
export const fetchRequest = <T = ApiResponse>(
url: string,
body: Object | null,
): FetchPromise<T> => {
return fetch(`${API_URL}/${url}`, {
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
};
// GetUserStatusByPhoneNumberOrEmail // GetUserStatusByPhoneNumberOrEmail
export const getUserStatusByPhoneNumberOrEmail = async ( export const getUserStatusByPhoneNumberOrEmail = async (
body: GetUserStatusByPhoneNumberOrEmailRequest, body: GetUserStatusByPhoneNumberOrEmailRequest,
) => { ) => {
return fetchRequest<GetUserStatusByPhoneNumberOrEmailResponse>( return apiClient.post<GetUserStatusByPhoneNumberOrEmailResponse>(
'User/GetUserStatusByPhoneNumberOrEmail', 'User/GetUserStatusByPhoneNumberOrEmail',
body, body,
); );
}; };
export const loginOrSignUpWithOtp = async (body: LoginRequest) => { export const loginOrSignUpWithOtp = async (body: LoginRequest) => {
return fetchRequest<LoginResponse>('User/LoginOrSignUpWithOtp', body); return apiClient.post<LoginResponse>('User/LoginOrSignUpWithOtp', body);
}; };
export const loginWithPassword = async (body: PasswordLoginRequest) => { export const loginWithPassword = async (body: PasswordLoginRequest) => {
return fetchRequest<LoginResponse>('User/LoginWithPassword', body); return apiClient.post<LoginResponse>('User/LoginWithPassword', body);
}; };
export const sendSmsOtp = async (body: SendSmsOtpRequest) => { export const sendSmsOtp = async (body: SendSmsOtpRequest) => {
return fetchRequest<ApiResponse>('User/SendSmsOtp', body); return apiClient.post<ApiResponse>('User/SendSmsOtp', body);
}; };
export const sendEmailOtp = async (body: SendEmailOtpRequest) => { export const sendEmailOtp = async (body: SendEmailOtpRequest) => {
return fetchRequest<ApiResponse>('User/SendEmailOtp', body); return apiClient.post<ApiResponse>('User/SendEmailOtp', body);
}; };
export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => { export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => {
return fetchRequest<ConfirmOtpResponse>('User/ConfirmSmsOtp', body); return apiClient.post<ConfirmOtpResponse>('User/ConfirmSmsOtp', body);
}; };
export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => { export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => {
return fetchRequest<ConfirmOtpResponse>('User/ConfirmEmailOtp', body); return apiClient.post<ConfirmOtpResponse>('User/ConfirmEmailOtp', body);
}; };
export const resetPassword = async (body: ResetPasswordRequest) => { export const resetPassword = async (body: ResetPasswordRequest) => {
return fetchRequest<ResetPasswordResponse>('User/ResetPassword', body); return apiClient.post<ResetPasswordResponse>('User/ResetPassword', body);
}; };
export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => { export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => {
return fetchRequest<ApiResponse>('User/SendForgetPassCode', body); return apiClient.post<ApiResponse>('User/SendForgetPassCode', body);
}; };
export const confirmForgetPassCode = async ( export const confirmForgetPassCode = async (
body: ConfirmForgetPassCodeRequest, body: ConfirmForgetPassCodeRequest,
) => { ) => {
return fetchRequest<ConfirmOtpResponse>('User/ConfirmForgetPassCode', body); return apiClient.post<ConfirmOtpResponse>('User/ConfirmForgetPassCode', body);
}; };
export const loginOrSignUpWithGoogle = async ( export const loginOrSignUpWithGoogle = async (
body: LoginOrSignUpWithGoogleRequest, body: LoginOrSignUpWithGoogleRequest,
) => { ) => {
return fetchRequest<LoginOrSignUpWithGoogleResponse>( return apiClient.post<LoginOrSignUpWithGoogleResponse>(
'User/LoginOrSignUpWithGoogle', 'User/LoginOrSignUpWithGoogle',
body, body,
); );
}; };
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 apiClient.post<ApiResponse>('User/LogOut', {});
}; };

View File

@@ -12,10 +12,10 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
export const AuthenticationSteps = (): JSX.Element => { export const AuthenticationSteps = (): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
const DEFAULT_RETURN_URL = '/profile';
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const authReturnUrl: string = const authReturnUrl: string | null = searchParams.get('returnUrl');
searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL; const authReturnUrlOrDefault: string =
authReturnUrl ?? import.meta.env.VITE_DEFUALT_AUTH_RETURN_URL;
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<AuthStep>('emailOrPhone'); const [currentStep, setCurrentStep] = useState<AuthStep>('emailOrPhone');
@@ -66,8 +66,8 @@ export const AuthenticationSteps = (): JSX.Element => {
}; };
const redirectToReturnUrl = () => { const redirectToReturnUrl = () => {
if (authReturnUrl === DEFAULT_RETURN_URL) { if (!authReturnUrl) {
navigate(DEFAULT_RETURN_URL); navigate(import.meta.env.VITE_DEFUALT_AUTH_RETURN_URL);
} else { } else {
location.href = authReturnUrl; location.href = authReturnUrl;
} }
@@ -77,7 +77,7 @@ export const AuthenticationSteps = (): JSX.Element => {
<> <>
{currentStep === 'emailOrPhone' && ( {currentStep === 'emailOrPhone' && (
<LoginRegisterForm <LoginRegisterForm
authReturnUrl={authReturnUrl} authReturnUrl={authReturnUrlOrDefault}
onGoogleAuthenticated={handleUserLoggedIn} onGoogleAuthenticated={handleUserLoggedIn}
countryCode={countryCode} countryCode={countryCode}
setCountryCode={setCountryCode} setCountryCode={setCountryCode}
@@ -92,7 +92,7 @@ export const AuthenticationSteps = (): JSX.Element => {
{currentStep === 'verify' && ( {currentStep === 'verify' && (
<OtpVerifyForm <OtpVerifyForm
onVerifyPhoneNumber={handleConfrimPhoneNumber} onVerifyPhoneNumber={handleConfrimPhoneNumber}
authReturnUrl={authReturnUrl} authReturnUrl={authReturnUrlOrDefault}
countryCode={countryCode} countryCode={countryCode}
onOTPVerified={handleUserLoggedIn} onOTPVerified={handleUserLoggedIn}
onEditValue={() => setCurrentStep('emailOrPhone')} onEditValue={() => setCurrentStep('emailOrPhone')}
@@ -104,7 +104,7 @@ export const AuthenticationSteps = (): JSX.Element => {
{currentStep === 'enterPassword' && ( {currentStep === 'enterPassword' && (
<EnterPasswordForm <EnterPasswordForm
authReturnUrl={authReturnUrl} authReturnUrl={authReturnUrlOrDefault}
loginRegisterValue={loginRegisterValue} loginRegisterValue={loginRegisterValue}
countryCode={countryCode} countryCode={countryCode}
authType={authType} authType={authType}

View File

@@ -6,6 +6,7 @@ import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector'; import { CountryCodeSelector } from '../CountryCodeSelector';
import { sendSmsOtp } from '../../api/authorizationAPI'; import { sendSmsOtp } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { useApi } from '@/hooks/useApi';
export interface CompleteSignUpProps { export interface CompleteSignUpProps {
email: string; email: string;
@@ -30,7 +31,7 @@ export const CompleteSignUp = ({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [touched, setTouched] = useState<boolean>(false); const [touched, setTouched] = useState<boolean>(false);
const inputError: boolean = touched && !!error; const inputError: boolean = touched && !!error;
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false); const { loading: sendSmsLoading, execute: sendSmsCall } = useApi(sendSmsOtp);
const isPhoneValid = (code: string, phone: string) => { const isPhoneValid = (code: string, phone: string) => {
const phoneNumber = parsePhoneNumberFromString(code + phone); const phoneNumber = parsePhoneNumberFromString(code + phone);
@@ -61,12 +62,8 @@ export const CompleteSignUp = ({
if (!value || !isPhoneValid(countryCode, value)) { if (!value || !isPhoneValid(countryCode, value)) {
inputRef.current?.focus(); inputRef.current?.focus();
} else { } else {
setSendOtpLoading(true); await sendSmsCall({ phoneNumber: countryCode + value });
await sendSmsOtp({ phoneNumber: countryCode + value });
onCompleteSignUp(countryCode, value); onCompleteSignUp(countryCode, value);
setSendOtpLoading(false);
} }
}; };
@@ -110,7 +107,7 @@ export const CompleteSignUp = ({
sx={{ my: 4 }} sx={{ my: 4 }}
/> />
<Button loading={sendOtpLoading} onClick={handleCompleteSignUp}> <Button loading={sendSmsLoading} onClick={handleCompleteSignUp}>
{t('verify.confirmAndContinue')} {t('verify.confirmAndContinue')}
</Button> </Button>
</AuthenticationCard> </AuthenticationCard>

View File

@@ -10,7 +10,6 @@ import {
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Toast } from '@/components/Toast';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import type { AuthType } from '../../types/authTypes'; import type { AuthType } from '../../types/authTypes';
import type { CountryCode, GUID } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
@@ -20,6 +19,8 @@ import {
sendSmsOtp, sendSmsOtp,
} from '../../api/authorizationAPI'; } from '../../api/authorizationAPI';
import type { PasswordLoginRequest } from '../../types/userTypes'; import type { PasswordLoginRequest } from '../../types/userTypes';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
export interface EnterPasswordFormProps { export interface EnterPasswordFormProps {
onEditValue: () => void; onEditValue: () => void;
@@ -47,11 +48,16 @@ export const EnterPasswordForm = ({
const [inputTouched, setInputTouched] = useState<boolean>(false); const [inputTouched, setInputTouched] = useState<boolean>(false);
const [showPassword, setShowPassword] = useState<boolean>(false); const [showPassword, setShowPassword] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [loginLoading, setLoginLoading] = useState<boolean>(false); const toast = useToast();
const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState<boolean>(); const { loading: smsResendLoading, execute: smsResendCall } =
const [loginAlertOpen, setLoginAlertOpen] = useState<boolean>(false); useApi(sendSmsOtp);
const [loginFailedMessage, setLoginFailedMessage] = useState<string>(''); const { loading: emailResendLoading, execute: emailResendCall } =
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false); useApi(sendEmailOtp);
const {
data: loginWithPassResult,
loading: loginWithPassLoading,
execute: loginWithPassCall,
} = useApi(loginWithPassword);
const handleBlur = () => { const handleBlur = () => {
setInputTouched(true); setInputTouched(true);
@@ -61,8 +67,6 @@ export const EnterPasswordForm = ({
if (!passValue) { if (!passValue) {
inputRef.current?.focus(); inputRef.current?.focus();
} else { } else {
setLoginLoading(true);
const apiRequest: PasswordLoginRequest = { const apiRequest: PasswordLoginRequest = {
phoneNumber: phoneNumber:
authType === 'phone' ? countryCode + loginRegisterValue : undefined, authType === 'phone' ? countryCode + loginRegisterValue : undefined,
@@ -70,46 +74,38 @@ export const EnterPasswordForm = ({
password: passValue, password: passValue,
returnUrl: authReturnUrl, returnUrl: authReturnUrl,
}; };
const result = await loginWithPassword(apiRequest); await loginWithPassCall(apiRequest);
const jsonRes = await result.json();
if (jsonRes.success) { if (!loginWithPassResult) return;
setIsLoginStatusSuccess(true);
onLoggedIn(jsonRes.userId); if (loginWithPassResult.success) {
onLoggedIn(loginWithPassResult.userId);
toast({
message: t('verify.youHaveSuccessfullyLoggedIn'),
severity: 'success',
});
} else { } else {
setIsLoginStatusSuccess(false); toast({
setLoginFailedMessage(jsonRes.message); message: loginWithPassResult.message,
severity: 'error',
});
} }
setLoginAlertOpen(true);
setLoginLoading(false);
} }
}; };
const handleLoginWithOtp = async () => { const handleLoginWithOtp = async () => {
setSendOtpLoading(true);
if (authType === 'phone') { if (authType === 'phone') {
await sendSmsOtp({ phoneNumber: countryCode + loginRegisterValue }); await smsResendCall({ phoneNumber: countryCode + loginRegisterValue });
} else { } else {
await sendEmailOtp({ email: loginRegisterValue }); await emailResendCall({ email: loginRegisterValue });
} }
setSendOtpLoading(false);
onLoginWithOTP(); onLoginWithOTP();
}; };
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Toast
open={loginAlertOpen}
onClose={() => setLoginAlertOpen(false)}
color={!isLoginStatusSuccess ? 'error' : 'success'}
>
{!isLoginStatusSuccess
? loginFailedMessage
: t('verify.youHaveSuccessfullyLoggedIn')}
</Toast>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -128,7 +124,7 @@ export const EnterPasswordForm = ({
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ width: 'auto' }} sx={{ width: 'auto' }}
endIcon={<Edit2 />} endIcon={<Icon Component={Edit2} />}
onClick={onEditValue} onClick={onEditValue}
> >
{emailOrPhone} {emailOrPhone}
@@ -159,7 +155,11 @@ export const EnterPasswordForm = ({
color="primary" color="primary"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? <Eye /> : <EyeSlash />} {showPassword ? (
<Icon Component={Eye} />
) : (
<Icon Component={EyeSlash} />
)}
</IconButton> </IconButton>
), ),
}, },
@@ -171,14 +171,14 @@ export const EnterPasswordForm = ({
onClick={handleLoginWithOtp} onClick={handleLoginWithOtp}
sx={{ width: 'auto', mb: 2 }} sx={{ width: 'auto', mb: 2 }}
variant="text" variant="text"
loading={sendOtpLoading} loading={emailResendLoading || smsResendLoading}
endIcon={<ArrowLeft />} endIcon={<Icon Component={ArrowLeft} />}
> >
{t('enterPassword.loginWithOneTimeCode')} {t('enterPassword.loginWithOneTimeCode')}
</Button> </Button>
<Stack spacing={1}> <Stack spacing={1}>
<Button loading={loginLoading} onClick={handleSubmit}> <Button loading={loginWithPassLoading} onClick={handleSubmit}>
{t('verify.confirmAndLogin')} {t('verify.confirmAndLogin')}
</Button> </Button>
<Link to="/forget-password"> <Link to="/forget-password">

View File

@@ -1,10 +1,12 @@
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { GoogleCodeClientResponse } from '../../types/userTypes'; import type { GoogleCodeClientResponse } from '../../types/userTypes';
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI'; import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
import type { GUID } from '@/types/commonTypes'; import type { GUID } from '@/types/commonTypes';
import { Google } from 'iconsax-react'; import { Google } from 'iconsax-react';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
export interface GoogleAuthenticationProps { export interface GoogleAuthenticationProps {
disabled: boolean; disabled: boolean;
@@ -18,9 +20,12 @@ export const GoogleAuthentication = ({
onGoogleAuthenticated, onGoogleAuthenticated,
}: GoogleAuthenticationProps) => { }: GoogleAuthenticationProps) => {
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const [loginWithGoogleLoading, setLoginWithGoogleLoading] = const {
useState<boolean>(false); data: loginWithGoogleResult,
loading: loginWithGoogleLoading,
execute: loginWithGoogleCall,
} = useApi(loginOrSignUpWithGoogle);
const toast = useToast();
const clientRef = useRef<any>(null); const clientRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
@@ -37,21 +42,21 @@ export const GoogleAuthentication = ({
ux_mode: 'popup', ux_mode: 'popup',
response_type: 'id_token', response_type: 'id_token',
callback: async (resp: GoogleCodeClientResponse) => { callback: async (resp: GoogleCodeClientResponse) => {
setLoginWithGoogleLoading(true); await loginWithGoogleCall({
const result = await loginOrSignUpWithGoogle({
idToken: resp.id_token, idToken: resp.id_token,
returnUrl: authReturnUrl, returnUrl: authReturnUrl,
}); });
const jsonRes = await result.json();
if (jsonRes.success) { if (!loginWithGoogleResult) return;
onGoogleAuthenticated(jsonRes.userId);
if (loginWithGoogleResult.success) {
onGoogleAuthenticated(loginWithGoogleResult.userId);
} else { } else {
// Todo: Add useToast to handle error toast({
message: t('loginForm.googleAuthenticationFailed'),
severity: 'error',
});
} }
setLoginWithGoogleLoading(false);
}, },
}); });
}; };
@@ -73,7 +78,7 @@ export const GoogleAuthentication = ({
disabled={disabled} disabled={disabled}
loading={loginWithGoogleLoading} loading={loginWithGoogleLoading}
variant="outlined" variant="outlined"
startIcon={<Google variant="Bold" />} startIcon={<Icon Component={Google} variant="Bold" />}
> >
{t('loginForm.loginWithGoogle')} {t('loginForm.loginWithGoogle')}
</Button> </Button>

View File

@@ -8,10 +8,11 @@ import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector'; import { CountryCodeSelector } from '../CountryCodeSelector';
import type { UserStatus } from '../../types/userTypes'; import type { UserStatus } from '../../types/userTypes';
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
import { Toast } from '@/components/Toast';
import type { CountryCode, GUID } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
import { GoogleAuthentication } from './GoogleAuthentication'; import { GoogleAuthentication } from './GoogleAuthentication';
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber'; import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
import { useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
export interface LoginRegisterFormProps { export interface LoginRegisterFormProps {
loginRegisterValue: string; loginRegisterValue: string;
@@ -36,14 +37,18 @@ export function LoginRegisterForm({
authReturnUrl, authReturnUrl,
onGoogleAuthenticated, onGoogleAuthenticated,
}: LoginRegisterFormProps) { }: LoginRegisterFormProps) {
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const textFieldRef = useRef<HTMLDivElement>(null); const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
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 toast = useToast();
const {
data: userStatus,
loading: userStatusLoading,
execute: execUserStatus,
} = useApi(getUserStatusByPhoneNumberOrEmail);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value; const newValue = event.target.value;
@@ -88,20 +93,21 @@ export function LoginRegisterForm({
const handleSubmit = async () => { const handleSubmit = async () => {
if (validateInput(loginRegisterValue, authType, false)) { if (validateInput(loginRegisterValue, authType, false)) {
setCheckStatusLoading(true); await execUserStatus({
const result = await getUserStatusByPhoneNumberOrEmail({
phoneNumber: phoneNumber:
authType === 'phone' ? countryCode + loginRegisterValue : undefined, authType === 'phone' ? countryCode + loginRegisterValue : undefined,
email: authType === 'email' ? loginRegisterValue : undefined, email: authType === 'email' ? loginRegisterValue : undefined,
}); });
const jsonResult = await result.json();
if (jsonResult.success) { if (!userStatus) {
onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus); return;
} else { }
setErrorMessage(jsonResult.message);
if (userStatus.success) {
onLoginRegisterSubmit(loginRegisterValue, userStatus.userStatus);
} else {
toast({ message: userStatus.message, severity: 'error' });
} }
setCheckStatusLoading(false);
} else { } else {
inputRef.current?.focus(); inputRef.current?.focus();
validateInput(loginRegisterValue, authType); validateInput(loginRegisterValue, authType);
@@ -112,14 +118,6 @@ 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">
@@ -155,14 +153,14 @@ export function LoginRegisterForm({
/> />
<Stack spacing={2}> <Stack spacing={2}>
<Button loading={checkStatusLoading} onClick={handleSubmit}> <Button loading={userStatusLoading} onClick={handleSubmit}>
{t('loginForm.submitButton')} {t('loginForm.submitButton')}
</Button> </Button>
<GoogleAuthentication <GoogleAuthentication
authReturnUrl={authReturnUrl} authReturnUrl={authReturnUrl}
onGoogleAuthenticated={onGoogleAuthenticated} onGoogleAuthenticated={onGoogleAuthenticated}
disabled={checkStatusLoading} disabled={userStatusLoading}
/> />
</Stack> </Stack>
</AuthenticationCard> </AuthenticationCard>

View File

@@ -4,7 +4,6 @@ import { Edit2 } from 'iconsax-react';
import DigitInput from '@/components/components/DigitsInput'; import DigitInput from '@/components/components/DigitsInput';
import type { AuthMode, AuthType } from '../../types/authTypes'; import type { AuthMode, AuthType } from '../../types/authTypes';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast';
import { AuthenticationCard } from '../AuthenticationCard'; import { AuthenticationCard } from '../AuthenticationCard';
import type { LoginRequest } from '../../types/userTypes'; import type { LoginRequest } from '../../types/userTypes';
import { import {
@@ -13,6 +12,8 @@ import {
sendSmsOtp, sendSmsOtp,
} from '../../api/authorizationAPI'; } from '../../api/authorizationAPI';
import type { CountryCode, GUID } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
interface OtpVerifyFormProps { interface OtpVerifyFormProps {
value: string; value: string;
@@ -37,15 +38,20 @@ export function OtpVerifyForm({
}: OtpVerifyFormProps) { }: OtpVerifyFormProps) {
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 [isStatusSuccess, setIsStatusSuccess] = useState<boolean>();
const [errorMessage, setErrorMessage] = useState<string>();
const [verifyStatusLoading, setVerifyStatusLoading] =
useState<boolean>(false);
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const [resendTimer, setResendTimer] = useState<number>(120); const [resendTimer, setResendTimer] = useState<number>(120);
const [canResend, setCanResend] = useState(false); const [canResend, setCanResend] = useState(false);
const [resendLoading, setResendLoading] = useState<boolean>(false); const toast = useToast();
const { loading: smsResendLoading, execute: smsResendCall } =
useApi(sendSmsOtp);
const { loading: emailResendLoading, execute: emailResendCall } =
useApi(sendEmailOtp);
const {
data: loginSignUpResult,
loading: loginSignUpLoading,
execute: loginSignUpCall,
} = useApi(loginOrSignUpWithOtp);
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
@@ -61,17 +67,14 @@ export function OtpVerifyForm({
}, [resendTimer]); }, [resendTimer]);
const handleResendOTPCode = async () => { const handleResendOTPCode = async () => {
setResendLoading(true);
if (authType === 'phone') { if (authType === 'phone') {
await sendSmsOtp({ phoneNumber: countryCode + value }); await smsResendCall({ phoneNumber: countryCode + value });
} else { } else {
await sendEmailOtp({ email: value }); await emailResendCall({ email: value });
} }
setResendTimer(120); setResendTimer(120);
setCanResend(false); setCanResend(false);
setResendLoading(false);
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@@ -90,7 +93,6 @@ export function OtpVerifyForm({
const handleLoginOrSignUp = async () => { const handleLoginOrSignUp = async () => {
setOtpDigitInvalid(false); setOtpDigitInvalid(false);
setVerifyStatusLoading(true);
const loginRequest: LoginRequest = { const loginRequest: LoginRequest = {
otpCode: otpCode, otpCode: otpCode,
@@ -98,27 +100,39 @@ export function OtpVerifyForm({
email: authType === 'email' ? value : undefined, email: authType === 'email' ? value : undefined,
returnUrl: authReturnUrl, returnUrl: authReturnUrl,
}; };
const result = await loginOrSignUpWithOtp(loginRequest); await loginSignUpCall(loginRequest);
const jsonRes = await result.json();
if (jsonRes.success) { if (!loginSignUpResult) {
setVerifyStatus('success'); return;
if (jsonRes.registeredWithOutPhoneNumber) {
onVerifyPhoneNumber(jsonRes.userId);
} else {
onOTPVerified(jsonRes.userId);
}
} else {
setVerifyStatus('failed');
setErrorMessage(jsonRes.message);
} }
setVerifyAlertOpen(true); if (loginSignUpResult && loginSignUpResult.success) {
setVerifyStatusLoading(false); setIsStatusSuccess(true);
if (loginSignUpResult.registeredWithOutPhoneNumber) {
onVerifyPhoneNumber(loginSignUpResult.userId);
} else {
onOTPVerified(loginSignUpResult.userId);
}
toast({
message:
authMode === 'login'
? t('verify.youHaveSuccessfullyLoggedIn')
: t('verify.youHaveSuccessfullySignedIn'),
severity: 'success',
});
} else {
setIsStatusSuccess(false);
toast({
message: loginSignUpResult.message,
severity: 'error',
});
}
}; };
const otpMessage = (): string => { const otpMessageFn = (): string => {
if (authType === 'phone' && authMode === 'login') { if (authType === 'phone' && authMode === 'login') {
return t( return t(
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt', 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
@@ -140,26 +154,11 @@ export function OtpVerifyForm({
return ''; return '';
}; };
const toastMessage = const otpMessage = otpMessageFn();
verifyStatus === 'failed'
? (errorMessage ?? t('verify.theVerificationCodeIsIncorrect'))
: verifyStatus === 'success' && authMode === 'register'
? t('verify.youHaveSuccessfullySignedIn')
: verifyStatus === 'success' && authMode === 'login'
? t('verify.youHaveSuccessfullyLoggedIn')
: '';
return ( return (
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{toastMessage}
</Toast>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -176,7 +175,7 @@ export function OtpVerifyForm({
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }} sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />} endIcon={<Icon Component={Edit2} />}
onClick={onEditValue} onClick={onEditValue}
> >
{authType === 'phone' ? countryCode + value : value} {authType === 'phone' ? countryCode + value : value}
@@ -184,16 +183,16 @@ export function OtpVerifyForm({
</Box> </Box>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}> <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{otpMessage()} {otpMessage}
</Typography> </Typography>
<DigitInput <DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'} error={otpDigitInvalid || isStatusSuccess === false}
success={verifyStatus === 'success'} success={isStatusSuccess === true}
onChange={(value) => setOtpCode(value)} onChange={(value) => setOtpCode(value)}
/> />
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}> <Button onClick={handleVerifyOTP} loading={loginSignUpLoading}>
{authMode === 'register' {authMode === 'register'
? t('verify.confirmAndContinue') ? t('verify.confirmAndContinue')
: t('verify.confirmAndLogin')} : t('verify.confirmAndLogin')}
@@ -212,7 +211,7 @@ export function OtpVerifyForm({
<Button <Button
variant="text" variant="text"
loading={resendLoading} loading={smsResendLoading || emailResendLoading}
sx={{ width: 'auto' }} sx={{ width: 'auto' }}
onClick={canResend ? handleResendOTPCode : undefined} onClick={canResend ? handleResendOTPCode : undefined}
> >

View File

@@ -3,11 +3,12 @@ import { Box, Button, Stack, Typography } from '@mui/material';
import { Edit2 } from 'iconsax-react'; import { Edit2 } from 'iconsax-react';
import DigitInput from '@/components/components/DigitsInput'; import DigitInput from '@/components/components/DigitsInput';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Toast } from '@/components/Toast';
import { AuthenticationCard } from '../AuthenticationCard'; import { AuthenticationCard } from '../AuthenticationCard';
import type { ConfirmSmsOtpRequest } from '../../types/userTypes'; import type { ConfirmSmsOtpRequest } from '../../types/userTypes';
import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI'; import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
interface VerifyPhoneNumberProps { interface VerifyPhoneNumberProps {
value: string; value: string;
@@ -24,15 +25,18 @@ export function VerifyPhoneNumber({
}: VerifyPhoneNumberProps) { }: VerifyPhoneNumberProps) {
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 [isStatusSuccess, setIsStatusSuccess] = useState<boolean>();
const [errorMessage, setErrorMessage] = useState<string>();
const [verifyStatusLoading, setVerifyStatusLoading] =
useState<boolean>(false);
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const [resendTimer, setResendTimer] = useState<number>(120); const [resendTimer, setResendTimer] = useState<number>(120);
const [canResend, setCanResend] = useState(false); const [canResend, setCanResend] = useState(false);
const [resendLoading, setResendLoading] = useState<boolean>(false); const toast = useToast();
const { loading: smsResendLoading, execute: smsResendCall } =
useApi(sendSmsOtp);
const {
data: confirmSmsOtpResult,
loading: confirmSmsOtpLoading,
execute: confirmSmsOtpCall,
} = useApi(confirmSmsOtp);
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
@@ -48,13 +52,10 @@ export function VerifyPhoneNumber({
}, [resendTimer]); }, [resendTimer]);
const handleResendOTPCode = async () => { const handleResendOTPCode = async () => {
setResendLoading(true); await smsResendCall({ phoneNumber: countryCode + value });
await sendSmsOtp({ phoneNumber: countryCode + value });
setResendTimer(120); setResendTimer(120);
setCanResend(false); setCanResend(false);
setResendLoading(false);
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@@ -68,47 +69,37 @@ export function VerifyPhoneNumber({
setOtpDigitInvalid(true); setOtpDigitInvalid(true);
} else { } else {
setOtpDigitInvalid(false); setOtpDigitInvalid(false);
setVerifyStatusLoading(true);
const confirmSmsOtpRequest: ConfirmSmsOtpRequest = { const confirmSmsOtpRequest: ConfirmSmsOtpRequest = {
otpCode: otpCode, otpCode: otpCode,
phoneNumber: countryCode + value, phoneNumber: countryCode + value,
}; };
const result = await confirmSmsOtp(confirmSmsOtpRequest); await confirmSmsOtpCall(confirmSmsOtpRequest);
const jsonRes = await result.json();
if (jsonRes.success) { if (!confirmSmsOtpResult) return;
setVerifyStatus('success');
if (confirmSmsOtpResult.success) {
setIsStatusSuccess(true);
toast({
message: t('verify.youHaveSuccessfullyLoggedIn'),
severity: 'success',
});
onPhoneNumberVerified(); onPhoneNumberVerified();
} else { } else {
setVerifyStatus('failed'); setIsStatusSuccess(false);
setErrorMessage(jsonRes.message); toast({
message:
confirmSmsOtpResult.message ??
t('verify.theVerificationCodeIsIncorrect'),
severity: 'error',
});
} }
setVerifyAlertOpen(true);
setVerifyStatusLoading(false);
}
};
const verifyAlertMessage = (): string => {
if (verifyStatus === 'failed') {
return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
} else {
return t('verify.youHaveSuccessfullyLoggedIn');
} }
}; };
return ( return (
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Toast
open={verifyAlertOpen}
onClose={() => setVerifyAlertOpen(false)}
color={verifyStatus === 'failed' ? 'error' : 'success'}
>
{verifyAlertMessage()}
</Toast>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -125,7 +116,7 @@ export function VerifyPhoneNumber({
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }} sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />} endIcon={<Icon Component={Edit2} />}
onClick={onEditValue} onClick={onEditValue}
> >
{countryCode + value} {countryCode + value}
@@ -139,12 +130,12 @@ export function VerifyPhoneNumber({
</Typography> </Typography>
<DigitInput <DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'} error={otpDigitInvalid || isStatusSuccess === false}
success={verifyStatus === 'success'} success={isStatusSuccess === true}
onChange={(value) => setOtpCode(value)} onChange={(value) => setOtpCode(value)}
/> />
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}> <Button onClick={handleVerifyOTP} loading={confirmSmsOtpLoading}>
{t('verify.confirmAndLogin')} {t('verify.confirmAndLogin')}
</Button> </Button>
</AuthenticationCard> </AuthenticationCard>
@@ -161,7 +152,7 @@ export function VerifyPhoneNumber({
<Button <Button
variant="text" variant="text"
loading={resendLoading} loading={smsResendLoading}
sx={{ width: 'auto' }} sx={{ width: 'auto' }}
onClick={canResend ? handleResendOTPCode : undefined} onClick={canResend ? handleResendOTPCode : undefined}
> >

View File

@@ -15,6 +15,7 @@ import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { countries, type Country } from '../../../countries'; import { countries, type Country } from '../../../countries';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { Icon } from '@rkheftan/harmony-ui';
interface CountryCodeSelectorProps { interface CountryCodeSelectorProps {
show: boolean; show: boolean;
value: CountryCode; value: CountryCode;
@@ -114,7 +115,7 @@ export function CountryCodeSelector({
}} }}
> >
{/* This inner Box prevents the content from being squeezed during the transition */} {/* This inner Box prevents the content from being squeezed during the transition */}
<ArrowDown2 size="24" variant="Bold" /> <Icon Component={ArrowDown2} size="medium" variant="Bold" />
<Typography <Typography
variant="body1" variant="body1"

View File

@@ -20,6 +20,8 @@ import type { ResetPasswordRequest } from '../../types/userTypes';
import type { AuthType } from '../../types/authTypes'; import type { AuthType } from '../../types/authTypes';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { resetPassword } from '../../api/authorizationAPI'; import { resetPassword } from '../../api/authorizationAPI';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
export interface ChangePasswordProps { export interface ChangePasswordProps {
onEditInfo: () => void; onEditInfo: () => void;
@@ -48,15 +50,12 @@ export const ChangePassword = ({
useState<boolean>(false); useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const confirmInputRef = useRef<HTMLInputElement>(null); const confirmInputRef = useRef<HTMLInputElement>(null);
const [changePasswordLoading, setChangePasswordLoading] = const toast = useToast();
useState<boolean>(false); const {
const [changePasswordStatus, setChangePasswordStatus] = useState< data: resetPasswordData,
'success' | 'failed' loading: resetPasswordLoading,
>(); execute: resetPasswordCall,
const [changePassAlertOpen, setChangePassAlertOpen] = } = useApi(resetPassword);
useState<boolean>(false);
const [changePassFailedMessage, setChangePassFailedMessage] =
useState<string>('');
const passwordValidationRules = [ const passwordValidationRules = [
{ title: t('forgetPassword.includingANumber'), validator: containsNumber }, { title: t('forgetPassword.includingANumber'), validator: containsNumber },
@@ -84,8 +83,6 @@ export const ChangePassword = ({
setConfirmInputTouched(true); setConfirmInputTouched(true);
confirmInputRef.current?.focus(); confirmInputRef.current?.focus();
} else { } else {
setChangePasswordLoading(true);
const apiRequest: ResetPasswordRequest = { const apiRequest: ResetPasswordRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined, email: infoType === 'email' ? forgettedPasswordInfo : undefined,
phoneNumber: phoneNumber:
@@ -96,19 +93,22 @@ export const ChangePassword = ({
confirmNewPassword: confirmPassValue, confirmNewPassword: confirmPassValue,
}; };
const result = await resetPassword(apiRequest); await resetPasswordCall(apiRequest);
const jsonRes = await result.json();
if (jsonRes.success) { if (!resetPasswordData) return;
setChangePasswordStatus('success');
if (resetPasswordData.success) {
onPasswordChanged(); onPasswordChanged();
toast({
message: t('forgetPassword.passwordChangedSuccessfully'),
severity: 'success',
});
} else { } else {
setChangePasswordStatus('failed'); toast({
setChangePassFailedMessage(jsonRes.message); message: resetPasswordData.message,
severity: 'error',
});
} }
setChangePassAlertOpen(true);
setChangePasswordLoading(false);
} }
}; };
@@ -123,16 +123,6 @@ export const ChangePassword = ({
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Toast
open={changePassAlertOpen}
onClose={() => setChangePassAlertOpen(false)}
color={changePasswordStatus === 'failed' ? 'error' : 'success'}
>
{changePasswordStatus === 'failed'
? changePassFailedMessage
: t('forgetPassword.passwordChangedSuccessfully')}
</Toast>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -151,7 +141,7 @@ export const ChangePassword = ({
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }} sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />} endIcon={<Icon Component={Edit2} />}
onClick={onEditInfo} onClick={onEditInfo}
> >
{forgettedPasswordInfo} {forgettedPasswordInfo}
@@ -177,14 +167,22 @@ export const ChangePassword = ({
startAdornment: confirmPassValue && startAdornment: confirmPassValue &&
isValidPassword(passValue) && isValidPassword(passValue) &&
passValue === confirmPassValue && ( passValue === confirmPassValue && (
<TickCircle variant="Bold" color={theme.palette.success.main} /> <Icon
Component={TickCircle}
variant="Bold"
color="success.main"
/>
), ),
endAdornment: passValue ? ( endAdornment: passValue ? (
<IconButton <IconButton
color="primary" color="primary"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? <Eye /> : <EyeSlash />} {showPassword ? (
<Icon Component={Eye} />
) : (
<Icon Component={EyeSlash} />
)}
</IconButton> </IconButton>
) : ( ) : (
'' ''
@@ -198,12 +196,11 @@ export const ChangePassword = ({
<Stack spacing={1} sx={{ mt: 2 }}> <Stack spacing={1} sx={{ mt: 2 }}>
{passwordValidationRules.map((rule) => ( {passwordValidationRules.map((rule) => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<TickCircle <Icon
Component={TickCircle}
variant={rule.validator(passValue) ? 'Bold' : 'Linear'} variant={rule.validator(passValue) ? 'Bold' : 'Linear'}
color={ color={
rule.validator(passValue) rule.validator(passValue) ? 'success.main' : 'primary.light'
? theme.palette.success.main
: theme.palette.primary.light
} }
/> />
@@ -227,14 +224,22 @@ export const ChangePassword = ({
startAdornment: confirmPassValue && startAdornment: confirmPassValue &&
isValidPassword(passValue) && isValidPassword(passValue) &&
passValue === confirmPassValue && ( passValue === confirmPassValue && (
<TickCircle variant="Bold" color={theme.palette.success.main} /> <Icon
Component={TickCircle}
variant="Bold"
color="success.main"
/>
), ),
endAdornment: confirmPassValue ? ( endAdornment: confirmPassValue ? (
<IconButton <IconButton
color="primary" color="primary"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
> >
{showConfirmPassword ? <Eye /> : <EyeSlash />} {showConfirmPassword ? (
<Icon Component={Eye} />
) : (
<Icon Component={EyeSlash} />
)}
</IconButton> </IconButton>
) : ( ) : (
'' ''
@@ -245,7 +250,7 @@ export const ChangePassword = ({
/> />
<Stack spacing={1}> <Stack spacing={1}>
<Button loading={changePasswordLoading} onClick={handleSubmit}> <Button loading={resetPasswordLoading} onClick={handleSubmit}>
{t('forgetPassword.confirm')} {t('forgetPassword.confirm')}
</Button> </Button>
</Stack> </Stack>

View File

@@ -4,8 +4,10 @@ import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
import { ForgetPasswordOtp } from './ForgetPasswordOtp'; import { ForgetPasswordOtp } from './ForgetPasswordOtp';
import { ChangePassword } from './ChangePassword'; import { ChangePassword } from './ChangePassword';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { useNavigate } from 'react-router-dom';
export const ForgetPasswordContainer = () => { export const ForgetPasswordContainer = () => {
const navigate = useNavigate();
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState< const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
'enterInfo' | 'verifyOtp' | 'setPassword' 'enterInfo' | 'verifyOtp' | 'setPassword'
>('enterInfo'); >('enterInfo');
@@ -27,7 +29,7 @@ export const ForgetPasswordContainer = () => {
}; };
const handlePasswordChanged = () => { const handlePasswordChanged = () => {
console.log('changingPasswordTo'); navigate('/login');
}; };
return ( return (

View File

@@ -6,9 +6,17 @@ import type { 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 { ConfirmForgetPassCodeRequest } from '../../types/userTypes'; import type {
ConfirmForgetPassCodeRequest,
SendForgetPassCodeRequest,
} from '../../types/userTypes';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode } from '@/types/commonTypes';
import { confirmForgetPassCode } from '../../api/authorizationAPI'; import {
confirmForgetPassCode,
sendForgetPassCode,
} from '../../api/authorizationAPI';
import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
interface ForgetPasswordOtpProps { interface ForgetPasswordOtpProps {
forgettedPasswordInfo: string; forgettedPasswordInfo: string;
@@ -27,14 +35,21 @@ export function ForgetPasswordOtp({
}: ForgetPasswordOtpProps) { }: ForgetPasswordOtpProps) {
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<'failed' | 'success'>(); const [isStatusSuccess, setIsStatusSuccess] = useState<boolean>();
const [verifyStatusLoading, setVerifyStatusLoading] =
useState<boolean>(false);
const [verifyAlertMessage, setVerifyAlertMessage] = useState<string>();
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const [resendTimer, setResendTimer] = useState<number>(120); const [resendTimer, setResendTimer] = useState<number>(120);
const [canResend, setCanResend] = useState(false); const [canResend, setCanResend] = useState(false);
const [resendLoading, setResendLoading] = useState<boolean>(false); const toast = useToast();
const {
data: sendForgetPassCodeData,
loading: sendForgetPassCodeLoading,
execute: sendForgetPassCodeCall,
} = useApi(sendForgetPassCode);
const {
data: confirmForgetPassCodeData,
loading: confirmForgetPassCodeLoading,
execute: confirmForgetPassCodeCall,
} = useApi(confirmForgetPassCode);
useEffect(() => { useEffect(() => {
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
@@ -49,18 +64,16 @@ export function ForgetPasswordOtp({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [resendTimer]); }, [resendTimer]);
const handleResendOTPCode = () => { const handleResendOTPCode = async () => {
setResendLoading(true); const sendCodeRequest: SendForgetPassCodeRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
// TODO: Call API here instead of settimeout phoneNumber:
infoType === 'phone' ? countryCode + forgettedPasswordInfo : undefined,
setTimeout(() => { };
console.log('resended'); await sendForgetPassCodeCall(sendCodeRequest);
setResendTimer(120); setResendTimer(120);
setCanResend(false); setCanResend(false);
setResendLoading(false);
}, 1000);
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
@@ -74,7 +87,6 @@ export function ForgetPasswordOtp({
setOtpDigitInvalid(true); setOtpDigitInvalid(true);
} else { } else {
setOtpDigitInvalid(false); setOtpDigitInvalid(false);
setVerifyStatusLoading(true);
// Change setTimeout to api call // Change setTimeout to api call
const apiRequest: ConfirmForgetPassCodeRequest = { const apiRequest: ConfirmForgetPassCodeRequest = {
@@ -86,32 +98,26 @@ export function ForgetPasswordOtp({
code: otpCode, code: otpCode,
}; };
const result = await confirmForgetPassCode(apiRequest); confirmForgetPassCodeCall(apiRequest);
const jsonRes = await result.json();
if (jsonRes.success) { if (!confirmForgetPassCodeData) return;
setVerifyStatus('success');
if (confirmForgetPassCodeData.success) {
setIsStatusSuccess(true);
onOTPVerified(otpCode); onOTPVerified(otpCode);
} else { } else {
setVerifyStatus('failed'); setIsStatusSuccess(false);
setVerifyAlertMessage(jsonRes.message); toast({
message: confirmForgetPassCodeData.message,
severity: 'error',
});
} }
setVerifyStatusLoading(false);
} }
}; };
return ( return (
<Stack alignItems="center"> <Stack alignItems="center">
<AuthenticationCard> <AuthenticationCard>
<Toast
open={!!verifyAlertMessage}
onClose={() => setVerifyAlertMessage(undefined)}
color={'error'}
>
{verifyAlertMessage}
</Toast>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -130,7 +136,7 @@ export function ForgetPasswordOtp({
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ textTransform: 'lowercase', width: 'auto' }} sx={{ textTransform: 'lowercase', width: 'auto' }}
endIcon={<Edit2 />} endIcon={<Icon Component={Edit2} />}
onClick={onEditInfo} onClick={onEditInfo}
> >
{infoType === 'phone' {infoType === 'phone'
@@ -150,12 +156,15 @@ export function ForgetPasswordOtp({
</Typography> </Typography>
<DigitInput <DigitInput
error={otpDigitInvalid || verifyStatus === 'failed'} error={otpDigitInvalid || isStatusSuccess === false}
success={verifyStatus === 'success'} success={isStatusSuccess === true}
onChange={(value) => setOtpCode(value)} onChange={(value) => setOtpCode(value)}
/> />
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}> <Button
onClick={handleVerifyOTP}
loading={confirmForgetPassCodeLoading}
>
{t('forgetPassword.confirm')} {t('forgetPassword.confirm')}
</Button> </Button>
</AuthenticationCard> </AuthenticationCard>
@@ -172,7 +181,7 @@ export function ForgetPasswordOtp({
<Button <Button
variant="text" variant="text"
loading={resendLoading} loading={sendForgetPassCodeLoading}
sx={{ width: 'auto' }} sx={{ width: 'auto' }}
onClick={canResend ? handleResendOTPCode : undefined} onClick={canResend ? handleResendOTPCode : undefined}
> >

View File

@@ -11,6 +11,8 @@ import { sendForgetPassCode } from '../../api/authorizationAPI';
import type { SendForgetPassCodeRequest } from '../../types/userTypes'; import type { SendForgetPassCodeRequest } from '../../types/userTypes';
import { Toast } from '@/components/Toast'; import { Toast } from '@/components/Toast';
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber'; import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
import { useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
export interface ForgettedPasswordInfoProps { export interface ForgettedPasswordInfoProps {
forgettedPasswordInfo: string; forgettedPasswordInfo: string;
@@ -36,9 +38,13 @@ export function ForgettedPasswordInfo({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
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 [sendCodeLoading, setSendCodeLoading] = useState<boolean>(false);
const inputError: boolean = touched && !!error; const inputError: boolean = touched && !!error;
const toast = useToast();
const {
data: sendForgetPassCodeData,
loading: sendForgetPassCodeLoading,
execute: sendForgetPassCodeCall,
} = useApi(sendForgetPassCode);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value; const newValue = event.target.value;
@@ -79,8 +85,6 @@ export function ForgettedPasswordInfo({
const handleSubmit = async () => { const handleSubmit = async () => {
if (validateInput(forgettedPasswordInfo, infoType, false)) { if (validateInput(forgettedPasswordInfo, infoType, false)) {
setSendCodeLoading(true);
const sendCodeRequest: SendForgetPassCodeRequest = { const sendCodeRequest: SendForgetPassCodeRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined, email: infoType === 'email' ? forgettedPasswordInfo : undefined,
phoneNumber: phoneNumber:
@@ -88,14 +92,17 @@ export function ForgettedPasswordInfo({
? countryCode + forgettedPasswordInfo ? countryCode + forgettedPasswordInfo
: undefined, : undefined,
}; };
const result = await sendForgetPassCode(sendCodeRequest); sendForgetPassCodeCall(sendCodeRequest);
const jsonRes = await result.json();
if (!jsonRes.success) { if (!sendForgetPassCodeData) return;
setErrorMessage(jsonRes.message);
if (!sendForgetPassCodeData.success) {
toast({
message: sendForgetPassCodeData.message,
severity: 'error',
});
} }
setSendCodeLoading(false);
onVerifyOtp(forgettedPasswordInfo); onVerifyOtp(forgettedPasswordInfo);
} else { } else {
inputRef.current?.focus(); inputRef.current?.focus();
@@ -108,14 +115,6 @@ export function ForgettedPasswordInfo({
return ( return (
<AuthenticationCard> <AuthenticationCard>
<Toast
color="error"
onClose={() => setErrorMessage(undefined)}
open={!!errorMessage}
>
{errorMessage}
</Toast>
<Stack spacing={1}> <Stack spacing={1}>
<Typography variant="h5"> <Typography variant="h5">
{t('forgetPassword.forgetPassword')} {t('forgetPassword.forgetPassword')}
@@ -155,7 +154,7 @@ export function ForgettedPasswordInfo({
/> />
<Stack spacing={2}> <Stack spacing={2}>
<Button loading={sendCodeLoading} onClick={handleSubmit}> <Button loading={sendForgetPassCodeLoading} onClick={handleSubmit}>
{t('forgetPassword.confirm')} {t('forgetPassword.confirm')}
</Button> </Button>
</Stack> </Stack>

View File

@@ -0,0 +1,2 @@
export * from './routes/AuthenticationPage';
export * from './routes/ForgetPasswordPage';

View File

@@ -115,24 +115,3 @@ 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,4 +1,10 @@
// FIXME: all the responses should extend this interface
// import type { ApiResponse } from '@/types/apiResponse';
import { useToast } from '@rkheftan/harmony-ui';
import type { AxiosError } from 'axios';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
// Define options for the hook // Define options for the hook
interface UseApiOptions { interface UseApiOptions {
// If true, the API call will be executed immediately on mount // If true, the API call will be executed immediately on mount
@@ -9,9 +15,13 @@ export function useApi<T, P extends any[]>(
apiFunction: (...args: P) => Promise<{ data: T }>, apiFunction: (...args: P) => Promise<{ data: T }>,
options: UseApiOptions = {}, options: UseApiOptions = {},
) { ) {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const [data, setData] = useState<T | null>(null); const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown>(null); const [error, setError] = useState<unknown>(null);
const execute = useCallback( const execute = useCallback(
async (...args: P) => { async (...args: P) => {
setLoading(true); setLoading(true);
@@ -21,15 +31,28 @@ export function useApi<T, P extends any[]>(
const response = await apiFunction(...args); const response = await apiFunction(...args);
setData(response.data); setData(response.data);
} catch (err) { } catch (err: unknown) {
// TODO: can handle some common errors here, 400 and 500 errors const axisoError: AxiosError = err as AxiosError;
if (axisoError.response?.status === 401) {
navigate('/login');
}
if (axisoError.response?.status === 500) {
toast({
message: t('messages.serverError'),
severity: 'error',
});
}
setError(err); setError(err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, },
[apiFunction], [apiFunction, navigate, t, toast],
); )
// If the 'immediate' option is true, execute the function on mount // If the 'immediate' option is true, execute the function on mount
useEffect(() => { useEffect(() => {

View File

@@ -1,11 +1,12 @@
import axios from 'axios'; import axios from 'axios';
// Function to get the token from local storage or state management // Function to get the token from local storage or state management
const getToken = () => localStorage.getItem('authToken'); export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
export const getAccessToken = () => sessionStorage.getItem(ACCESS_TOKEN_KEY);
const apiClient = axios.create({ const apiClient = axios.create({
// Define the base URL for all API requests // Define the base URL for all API requests
baseURL: 'https://accounts.business-harmony.com/api/', baseURL: import.meta.env.VITE_API_URL,
// Set a timeout for requests (e.g., 10 seconds) // Set a timeout for requests (e.g., 10 seconds)
timeout: 10000, timeout: 10000,
@@ -21,7 +22,7 @@ const apiClient = axios.create({
// This runs BEFORE each request is sent // This runs BEFORE each request is sent
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { (config) => {
const token = getToken(); const token = getAccessToken();
if (token) { if (token) {
// Add the authorization token to the headers // Add the authorization token to the headers
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;

14
src/lib/identityClient.ts Normal file
View File

@@ -0,0 +1,14 @@
import axios from 'axios';
const identityClient = axios.create({
// Define the base URL for all API requests
baseURL: import.meta.env.VITE_IDENTITY_URL,
// Set a timeout for requests (e.g., 10 seconds)
timeout: 10000,
// Set default headers
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});

View File

@@ -1,4 +1,8 @@
import { Layout } from '@/components/Layout/Layout'; import { Layout } from '@/components/Layout/Layout';
import {
AuthenticationPage,
ForgetPasswordPage,
} from '@/features/authorization';
import { import {
Calendar, Calendar,
Devices, Devices,
@@ -18,6 +22,7 @@ import { Navigate } from 'react-router-dom';
export interface RouteConfig { export interface RouteConfig {
path: string; path: string;
element?: ReactNode; element?: ReactNode;
authorize?: boolean;
navConfig?: { navConfig?: {
title: string; // Translation key title: string; // Translation key
icon?: Icon; icon?: Icon;
@@ -27,6 +32,20 @@ export interface RouteConfig {
// can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));) // can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));)
export const appRoutes: RouteConfig[] = [ export const appRoutes: RouteConfig[] = [
{
path: '/',
element: <Navigate to="/setting/profile" replace />,
},
{
path: '/login',
element: <AuthenticationPage />,
authorize: l,
},
{
path: '/forget-password',
element: <ForgetPasswordPage />,
authorize: false,
},
{ {
path: '/', path: '/',
element: <Navigate to="/setting/profile" replace />, element: <Navigate to="/setting/profile" replace />,

View File

@@ -1,6 +1,7 @@
import { Suspense, type ReactNode } from 'react'; import { Suspense, type ReactNode } from 'react';
import { createBrowserRouter, type RouteObject } from 'react-router-dom'; import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import { appRoutes, type RouteConfig } from './config'; import { appRoutes, type RouteConfig } from './config';
import { ProtectedRoute } from '@/components/ProtectedRoute';
/** /**
* A recursive function to map our custom route config to the format * A recursive function to map our custom route config to the format
@@ -18,10 +19,9 @@ function mapRoutes(routes: RouteConfig[]): RouteObject[] {
// element = <route.layout>{element}</route.layout>; // element = <route.layout>{element}</route.layout>;
// } // }
// Conditionally wrap the element in the authentication guard if (route.authorize) {
// if (route.authRequired) { element = <ProtectedRoute>{element}</ProtectedRoute>;
// element = <ProtectedRoute>{element}</ProtectedRoute>; }
// }
return { return {
path: route.path, path: route.path,

View File

@@ -4,7 +4,7 @@ import type { Palette } from './color.type';
export const PALETTE: Palette = { export const PALETTE: Palette = {
primary: { primary: {
light: { light: {
main: blue[400], main: blue.A400,
dark: blue[700], dark: blue[700],
light: blue[100], light: blue[100],
contrastText: '#FFFFFF', contrastText: '#FFFFFF',

View File

@@ -1,5 +0,0 @@
export type FetchPromise<T = ''> = Promise<FetchResponse<T>>;
export interface FetchResponse<T> extends Response {
json(): Promise<T>;
}