feat: login with google added

This commit is contained in:
مهرزاد قدرتی
2025-08-10 15:37:09 +03:30
parent 945aa379ea
commit a4ee95832f
8 changed files with 163 additions and 93 deletions

View File

@@ -1,29 +1,32 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> --> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<title>Harmony club</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- this script add for preventing initial theme flashing --> <title>Harmony club</title>
<script> <!-- this script add for preventing initial theme flashing -->
(function () { <script>
try { (function () {
const THEME_STORAGE_KEY = 'mui-mode'; try {
const DARK_THEME_CLASS_NAME = 'dark'; const THEME_STORAGE_KEY = 'mui-mode';
const savedMode = localStorage.getItem(THEME_STORAGE_KEY); const DARK_THEME_CLASS_NAME = 'dark';
const prefersDark = window.matchMedia( const savedMode = localStorage.getItem(THEME_STORAGE_KEY);
'(prefers-color-scheme: dark)', const prefersDark = window.matchMedia(
).matches; '(prefers-color-scheme: dark)',
if (savedMode === 'dark' || (!savedMode && prefersDark)) { ).matches;
document.documentElement.classList.add(DARK_THEME_CLASS_NAME); if (savedMode === 'dark' || (!savedMode && prefersDark)) {
} document.documentElement.classList.add(DARK_THEME_CLASS_NAME);
} catch (e) {} }
})(); } catch (e) { }
</script> })();
</head> </script>
<body> </head>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <body>
</body> <div id="root"></div>
</html> <script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -13,8 +13,13 @@ import {
import { UserStatus } from '../../types/userTypes'; import { UserStatus } from '../../types/userTypes';
import type { CountryCode, GUID } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
import { VerifyPhoneNumber } from './VerifyPhoneNumber'; import { VerifyPhoneNumber } from './VerifyPhoneNumber';
import { useSearchParams } from 'react-router';
export const AuthenticationSteps = (): JSX.Element => { export const AuthenticationSteps = (): JSX.Element => {
const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/';
const [searchParams] = useSearchParams();
const authReturnUrl: string =
searchParams.get('returnUrl') ?? DEFAULT_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< const [currentStep, setCurrentStep] = useState<
@@ -55,50 +60,26 @@ export const AuthenticationSteps = (): JSX.Element => {
const handleOTPVerfied = ( const handleOTPVerfied = (
registeredWithoutPhoneNumber: boolean = false, registeredWithoutPhoneNumber: boolean = false,
userId: GUID, userId: GUID,
returnUrl?: string,
) => { ) => {
localStorage.setItem('userID', userId); if (registeredWithoutPhoneNumber) {
// if (registeredWithoutPhoneNumber) { setCurrentStep('addPhoneNumber');
// setCurrentStep('addPhoneNumber');
// }
if (returnUrl) {
location.href = returnUrl;
} }
handleUserLoggedIn(userId);
}; };
const handleEditValue = () => { const handleUserLoggedIn = (userId: GUID) => {
setCurrentStep('emailOrPhone');
};
const handleCompleteSignUp = (countryCode: string, value: string) => {
setCurrentStep('addedPhoneNumberVerify');
};
const handleCompleteSignUpOTPVerified = () => {
console.log('phoneNumberVerified');
};
const handleCompleteSignUpEditValue = () => {
setCurrentStep('emailOrPhone');
};
const handleLoggedInWithPassowrd = (userId: GUID, returnUrl?: string) => {
localStorage.setItem('userID', userId); localStorage.setItem('userID', userId);
if (returnUrl) { location.href = authReturnUrl;
location.href = returnUrl;
}
};
const handleLoginWithOtpInsteadOfPassword = async () => {
setCurrentStep('verify');
}; };
return ( return (
<> <>
{currentStep === 'emailOrPhone' && ( {currentStep === 'emailOrPhone' && (
<LoginRegisterForm <LoginRegisterForm
authReturnUrl={authReturnUrl}
onGoogleAuthenticated={handleUserLoggedIn}
countryCode={countryCode} countryCode={countryCode}
setCountryCode={setCountryCode} setCountryCode={setCountryCode}
loginRegisterValue={loginRegisterValue} loginRegisterValue={loginRegisterValue}
@@ -111,9 +92,10 @@ export const AuthenticationSteps = (): JSX.Element => {
{currentStep === 'verify' && ( {currentStep === 'verify' && (
<OtpVerifyForm <OtpVerifyForm
authReturnUrl={authReturnUrl}
countryCode={countryCode} countryCode={countryCode}
onOTPVerified={handleOTPVerfied} onOTPVerified={handleOTPVerfied}
onEditValue={handleEditValue} onEditValue={() => setCurrentStep('emailOrPhone')}
authMode={authMode} authMode={authMode}
authType={authType} authType={authType}
value={loginRegisterValue} value={loginRegisterValue}
@@ -122,12 +104,13 @@ export const AuthenticationSteps = (): JSX.Element => {
{currentStep === 'enterPassword' && ( {currentStep === 'enterPassword' && (
<EnterPasswordForm <EnterPasswordForm
authReturnUrl={authReturnUrl}
loginRegisterValue={loginRegisterValue} loginRegisterValue={loginRegisterValue}
countryCode={countryCode} countryCode={countryCode}
authType={authType} authType={authType}
onLoggedIn={handleLoggedInWithPassowrd} onLoggedIn={handleUserLoggedIn}
onEditValue={handleEditValue} onEditValue={() => setCurrentStep('emailOrPhone')}
onLoginWithOTP={handleLoginWithOtpInsteadOfPassword} onLoginWithOTP={() => setCurrentStep('verify')}
emailOrPhone={loginRegisterValue} emailOrPhone={loginRegisterValue}
/> />
)} )}
@@ -137,16 +120,16 @@ export const AuthenticationSteps = (): JSX.Element => {
value={addedPhoneNumberValue} value={addedPhoneNumberValue}
setValue={setAddedPhoneNumberValue} setValue={setAddedPhoneNumberValue}
email={loginRegisterValue} email={loginRegisterValue}
onCompleteSignUp={handleCompleteSignUp} onCompleteSignUp={() => setCurrentStep('addedPhoneNumberVerify')}
/> />
)} )}
{currentStep === 'addedPhoneNumberVerify' && ( {currentStep === 'addedPhoneNumberVerify' && (
<VerifyPhoneNumber <VerifyPhoneNumber
countryCode={countryCode} countryCode={countryCode}
onEditValue={handleCompleteSignUpEditValue} onEditValue={() => setCurrentStep('emailOrPhone')}
value={addedPhoneNumberValue} value={addedPhoneNumberValue}
onPhoneNumberVerified={handleCompleteSignUpOTPVerified} onPhoneNumberVerified={handleUserLoggedIn}
/> />
)} )}
</> </>

View File

@@ -24,11 +24,12 @@ import type { LoginRequest, PasswordLoginRequest } from '../../types/userTypes';
export interface EnterPasswordFormProps { export interface EnterPasswordFormProps {
onEditValue: () => void; onEditValue: () => void;
onLoginWithOTP: () => void; onLoginWithOTP: () => void;
onLoggedIn: (userId: GUID, returnUrl?: string) => void; onLoggedIn: (userId: GUID) => void;
emailOrPhone: string; emailOrPhone: string;
authType: AuthType; authType: AuthType;
loginRegisterValue: string; loginRegisterValue: string;
countryCode: CountryCode; countryCode: CountryCode;
authReturnUrl: string;
} }
export const EnterPasswordForm = ({ export const EnterPasswordForm = ({
@@ -39,9 +40,8 @@ export const EnterPasswordForm = ({
authType, authType,
loginRegisterValue, loginRegisterValue,
countryCode, countryCode,
authReturnUrl,
}: EnterPasswordFormProps) => { }: EnterPasswordFormProps) => {
const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/';
const [searchParams] = useSearchParams();
const { t } = useTranslation('authentication'); const { t } = useTranslation('authentication');
const [passValue, setPassValue] = useState<string>(''); const [passValue, setPassValue] = useState<string>('');
const [inputTouched, setInputTouched] = useState<boolean>(false); const [inputTouched, setInputTouched] = useState<boolean>(false);
@@ -68,14 +68,14 @@ export const EnterPasswordForm = ({
authType === 'phone' ? countryCode + loginRegisterValue : undefined, authType === 'phone' ? countryCode + loginRegisterValue : undefined,
email: authType === 'email' ? loginRegisterValue : undefined, email: authType === 'email' ? loginRegisterValue : undefined,
password: passValue, password: passValue,
returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, returnUrl: authReturnUrl,
}; };
const result = await loginWithPassword(apiRequest); const result = await loginWithPassword(apiRequest);
const jsonRes = await result.json(); const jsonRes = await result.json();
if (jsonRes.success) { if (jsonRes.success) {
setLoginStatus('success'); setLoginStatus('success');
onLoggedIn(jsonRes.userId, jsonRes.returnUrl); onLoggedIn(jsonRes.userId);
} else { } else {
setLoginStatus('failed'); setLoginStatus('failed');
setLoginFailedMessage(jsonRes.message); setLoginFailedMessage(jsonRes.message);

View File

@@ -0,0 +1,84 @@
import { Button } from '@mui/material';
import { Google } from 'iconsax-reactjs';
import React, { 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';
declare global {
interface Window {
google: typeof google;
}
const google: any;
}
export interface GoogleAuthenticationProps {
disabled: boolean;
authReturnUrl: string;
onGoogleAuthenticated: (userId: GUID) => void;
}
export const GoogleAuthentication = ({
disabled,
authReturnUrl,
onGoogleAuthenticated,
}: GoogleAuthenticationProps) => {
const { t } = useTranslation('authentication');
const [loginWithGoogleLoading, setLoginWithGoogleLoading] =
useState<boolean>(false);
const clientRef = useRef<any>(null);
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;
document.body.appendChild(script);
script.onload = () => {
clientRef.current = google.accounts.oauth2.initCodeClient({
client_id: 'CLIEND_ID',
scope: 'openid email profile',
ux_mode: 'popup',
response_type: 'id_token',
callback: async (resp: GoogleCodeClientResponse) => {
setLoginWithGoogleLoading(true);
const result = await loginOrSignUpWithGoogle({
idToken: resp.id_token,
returnUrl: authReturnUrl,
});
const jsonRes = await result.json();
if (jsonRes.success) {
onGoogleAuthenticated(jsonRes.userId);
} else {
handleGoogleLogin();
}
setLoginWithGoogleLoading(false);
},
});
};
}, []);
const handleGoogleLogin = () => {
if (clientRef.current) {
clientRef.current.requestCode();
}
};
return (
<Button
onClick={handleGoogleLogin}
disabled={disabled}
loading={loginWithGoogleLoading}
variant="outlined"
startIcon={<Google variant="Bold" />}
>
{t('loginForm.loginWithGoogle')}
</Button>
);
};

View File

@@ -18,7 +18,8 @@ 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 { Toast } from '@/components/Toast';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
import { GoogleAuthentication } from './GoogleAuthentication';
export interface LoginRegisterFormProps { export interface LoginRegisterFormProps {
loginRegisterValue: string; loginRegisterValue: string;
@@ -28,6 +29,8 @@ export interface LoginRegisterFormProps {
authType: AuthType; authType: AuthType;
setAuthType: Dispatch<AuthType>; setAuthType: Dispatch<AuthType>;
onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void; onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
authReturnUrl: string;
onGoogleAuthenticated: (userId: GUID) => void;
} }
export function LoginRegisterForm({ export function LoginRegisterForm({
@@ -38,6 +41,8 @@ export function LoginRegisterForm({
authType, authType,
setAuthType, setAuthType,
onLoginRegisterSubmit, onLoginRegisterSubmit,
authReturnUrl,
onGoogleAuthenticated,
}: LoginRegisterFormProps) { }: LoginRegisterFormProps) {
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false); const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
const { t, i18n } = useTranslation('authentication'); const { t, i18n } = useTranslation('authentication');
@@ -172,13 +177,12 @@ export function LoginRegisterForm({
<Button loading={checkStatusLoading} onClick={handleSubmit}> <Button loading={checkStatusLoading} onClick={handleSubmit}>
{t('loginForm.submitButton')} {t('loginForm.submitButton')}
</Button> </Button>
<Button
<GoogleAuthentication
authReturnUrl={authReturnUrl}
onGoogleAuthenticated={onGoogleAuthenticated}
disabled={checkStatusLoading} disabled={checkStatusLoading}
variant="outlined" />
startIcon={<Google variant="Bold" />}
>
{t('loginForm.loginWithGoogle')}
</Button>
</Stack> </Stack>
</AuthenticationCard> </AuthenticationCard>
); );

View File

@@ -21,11 +21,8 @@ interface OtpVerifyFormProps {
authType: AuthType; authType: AuthType;
authMode: AuthMode; authMode: AuthMode;
onEditValue: () => void; onEditValue: () => void;
onOTPVerified: ( onOTPVerified: (registeredWithoutPhoneNumber: boolean, userID: GUID) => void;
registeredWithoutPhoneNumber: boolean, authReturnUrl: string;
userID: GUID,
returnUrl?: string,
) => void;
} }
export function OtpVerifyForm({ export function OtpVerifyForm({
@@ -35,9 +32,8 @@ export function OtpVerifyForm({
authMode, authMode,
onEditValue, onEditValue,
onOTPVerified, onOTPVerified,
authReturnUrl,
}: OtpVerifyFormProps) { }: OtpVerifyFormProps) {
const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/';
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'>();
@@ -102,18 +98,14 @@ export function OtpVerifyForm({
otpCode: otpCode, otpCode: otpCode,
phoneNumber: authType === 'phone' ? countryCode + value : undefined, phoneNumber: authType === 'phone' ? countryCode + value : undefined,
email: authType === 'email' ? value : undefined, email: authType === 'email' ? value : undefined,
returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, returnUrl: authReturnUrl,
}; };
const result = await loginOrSignUpWithOtp(loginRequest); const result = await loginOrSignUpWithOtp(loginRequest);
const jsonRes = await result.json(); const jsonRes = await result.json();
if (jsonRes.success) { if (jsonRes.success) {
setVerifyStatus('success'); setVerifyStatus('success');
onOTPVerified( onOTPVerified(jsonRes.registeredWithOutPhoneNumber, jsonRes.userId);
jsonRes.registeredWithOutPhoneNumber,
jsonRes.userId,
jsonRes.returnUrl,
);
} else { } else {
setVerifyStatus('failed'); setVerifyStatus('failed');
setErrorMessage(jsonRes.message); setErrorMessage(jsonRes.message);

View File

@@ -13,13 +13,13 @@ import {
sendEmailOtp, sendEmailOtp,
sendSmsOtp, sendSmsOtp,
} from '../../api/authorizationAPI'; } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes'; import type { CountryCode, GUID } from '@/types/commonTypes';
interface VerifyPhoneNumberProps { interface VerifyPhoneNumberProps {
value: string; value: string;
countryCode: CountryCode; countryCode: CountryCode;
onEditValue: () => void; onEditValue: () => void;
onPhoneNumberVerified: () => void; onPhoneNumberVerified: (userId: GUID) => void;
} }
export function VerifyPhoneNumber({ export function VerifyPhoneNumber({

View File

@@ -100,6 +100,10 @@ export interface ConfirmForgetPassCodeRequest {
// LoginOrSignUpWithGoogle // LoginOrSignUpWithGoogle
export interface GoogleCodeClientResponse {
id_token: string;
}
export interface LoginOrSignUpWithGoogleRequest { export interface LoginOrSignUpWithGoogleRequest {
idToken: string; idToken: string;
returnUrl: string; returnUrl: string;