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

View File

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

View File

@@ -24,11 +24,12 @@ import type { LoginRequest, PasswordLoginRequest } from '../../types/userTypes';
export interface EnterPasswordFormProps {
onEditValue: () => void;
onLoginWithOTP: () => void;
onLoggedIn: (userId: GUID, returnUrl?: string) => void;
onLoggedIn: (userId: GUID) => void;
emailOrPhone: string;
authType: AuthType;
loginRegisterValue: string;
countryCode: CountryCode;
authReturnUrl: string;
}
export const EnterPasswordForm = ({
@@ -39,9 +40,8 @@ export const EnterPasswordForm = ({
authType,
loginRegisterValue,
countryCode,
authReturnUrl,
}: EnterPasswordFormProps) => {
const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/';
const [searchParams] = useSearchParams();
const { t } = useTranslation('authentication');
const [passValue, setPassValue] = useState<string>('');
const [inputTouched, setInputTouched] = useState<boolean>(false);
@@ -68,14 +68,14 @@ export const EnterPasswordForm = ({
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
email: authType === 'email' ? loginRegisterValue : undefined,
password: passValue,
returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL,
returnUrl: authReturnUrl,
};
const result = await loginWithPassword(apiRequest);
const jsonRes = await result.json();
if (jsonRes.success) {
setLoginStatus('success');
onLoggedIn(jsonRes.userId, jsonRes.returnUrl);
onLoggedIn(jsonRes.userId);
} else {
setLoginStatus('failed');
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 { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
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 {
loginRegisterValue: string;
@@ -28,6 +29,8 @@ export interface LoginRegisterFormProps {
authType: AuthType;
setAuthType: Dispatch<AuthType>;
onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
authReturnUrl: string;
onGoogleAuthenticated: (userId: GUID) => void;
}
export function LoginRegisterForm({
@@ -38,6 +41,8 @@ export function LoginRegisterForm({
authType,
setAuthType,
onLoginRegisterSubmit,
authReturnUrl,
onGoogleAuthenticated,
}: LoginRegisterFormProps) {
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
const { t, i18n } = useTranslation('authentication');
@@ -172,13 +177,12 @@ export function LoginRegisterForm({
<Button loading={checkStatusLoading} onClick={handleSubmit}>
{t('loginForm.submitButton')}
</Button>
<Button
<GoogleAuthentication
authReturnUrl={authReturnUrl}
onGoogleAuthenticated={onGoogleAuthenticated}
disabled={checkStatusLoading}
variant="outlined"
startIcon={<Google variant="Bold" />}
>
{t('loginForm.loginWithGoogle')}
</Button>
/>
</Stack>
</AuthenticationCard>
);

View File

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

View File

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

View File

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