Google Sign in added
This commit is contained in:
@@ -5,8 +5,10 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
checkUserExists: (mobileNumber: string) => Promise<{ success: boolean; userExists: boolean; message: string }>;
|
||||
checkEmailExists: (email: string) => Promise<{ exists: boolean }>;
|
||||
login: (mobileNumber: string, pin: string) => Promise<{ success: boolean; message: string; isSuspended?: boolean }>;
|
||||
register: (mobileNumber: string, name: string, pin: string) => Promise<{ success: boolean; message: string }>;
|
||||
loginWithGoogle: (email: string, name: string, mobileNumber?: string) => Promise<{ success: boolean; message: string; isSuspended?: boolean }>;
|
||||
logout: () => void;
|
||||
changePin: (currentPin: string, newPin: string) => Promise<{ success: boolean; message: string }>;
|
||||
updateProfile: (name: string, mobileNumber: string) => Promise<{ success: boolean; message: string }>;
|
||||
@@ -79,6 +81,50 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
};
|
||||
|
||||
const checkEmailExists = async (email: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check-email`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { exists: false };
|
||||
}
|
||||
};
|
||||
|
||||
const loginWithGoogle = async (email: string, name: string, mobileNumber?: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/firebase-login`, { cache: 'no-store',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, name, mobileNumber }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.user) {
|
||||
const mappedUser: User = {
|
||||
id: data.user.id,
|
||||
name: data.user.name,
|
||||
mobileNumber: data.user.mobileNumber,
|
||||
isLoggedIn: data.user.isLoggedIn || data.user.loggedIn || true,
|
||||
isSuspended: data.user.isSuspended || data.user.suspended || false,
|
||||
ritzTokenBalance: data.user.ritzTokenBalance,
|
||||
membership: data.user.membership
|
||||
};
|
||||
setUser(mappedUser);
|
||||
localStorage.setItem('user', JSON.stringify(mappedUser));
|
||||
if (data.token) {
|
||||
localStorage.setItem('token', data.token);
|
||||
}
|
||||
return { success: true, message: data.message };
|
||||
}
|
||||
return { success: false, message: data.message || 'Google Login failed', isSuspended: data.suspended || data.isSuspended };
|
||||
} catch (error) {
|
||||
return { success: false, message: 'Network error. Please try again.' };
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
if (user) {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -136,7 +182,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
mobileNumber: updatedUserDto.mobileNumber,
|
||||
isLoggedIn: updatedUserDto.loggedIn,
|
||||
isSuspended: updatedUserDto.suspended,
|
||||
ritzTokenBalance: updatedUserDto.ritzTokenBalance
|
||||
ritzTokenBalance: updatedUserDto.ritzTokenBalance,
|
||||
membership: updatedUserDto.membership
|
||||
};
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
@@ -169,7 +216,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
mobileNumber: data.mobileNumber,
|
||||
isLoggedIn: data.isLoggedIn || data.loggedIn || true,
|
||||
isSuspended: data.isSuspended || data.suspended,
|
||||
ritzTokenBalance: data.ritzTokenBalance
|
||||
ritzTokenBalance: data.ritzTokenBalance,
|
||||
membership: data.membership
|
||||
};
|
||||
|
||||
// Update state and localStorage
|
||||
@@ -218,7 +266,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, login, register, logout, changePin, updateProfile, refreshUser }}>
|
||||
<AuthContext.Provider value={{ user, isLoading, checkUserExists, checkEmailExists, login, register, loginWithGoogle, logout, changePin, updateProfile, refreshUser }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -429,3 +429,16 @@
|
||||
.feedback-snackbar-action {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.guest-banner-text {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 10px;
|
||||
color: #ef6c37;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
background: rgba(239, 108, 55, 0.08);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(239, 108, 55, 0.15);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -441,4 +441,50 @@
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.google-signin-wrapper {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.google-login-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 24, 40, 0.15);
|
||||
border-radius: 18px;
|
||||
color: #1e293b;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
box-shadow: 0 4px 12px rgba(35, 22, 81, 0.05);
|
||||
}
|
||||
|
||||
.google-login-btn:hover:not(:disabled) {
|
||||
background-color: #f8fafc;
|
||||
border-color: rgba(35, 22, 81, 0.25);
|
||||
box-shadow: 0 10px 20px rgba(35, 22, 81, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.google-login-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(35, 22, 81, 0.05);
|
||||
}
|
||||
|
||||
.google-login-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.google-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -1,87 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Phone, User as UserIcon, Lock, ChevronLeft } from 'lucide-react';
|
||||
import { Lock } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import tilloLogo from '../assets/Tillo.png';
|
||||
import ritLogo from '../assets/RIT_Logo.png';
|
||||
import { auth, googleProvider } from '../firebase';
|
||||
import { signInWithPopup } from 'firebase/auth';
|
||||
import './LoginScreen.css';
|
||||
|
||||
const LoginScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { checkUserExists, login, register } = useAuth();
|
||||
const { loginWithGoogle, checkEmailExists } = useAuth();
|
||||
|
||||
const [step, setStep] = useState<'mobile' | 'pin' | 'register'>('mobile');
|
||||
const [mobileNumber, setMobileNumber] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [pin, setPin] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSuspended, setIsSuspended] = useState(false);
|
||||
|
||||
// First-time registration flow states
|
||||
const [showMobilePrompt, setShowMobilePrompt] = useState(false);
|
||||
const [pendingEmail, setPendingEmail] = useState('');
|
||||
const [pendingName, setPendingName] = useState('');
|
||||
const [mobileNumber, setMobileNumber] = useState('');
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await signInWithPopup(auth, googleProvider);
|
||||
const user = result.user;
|
||||
if (user.email) {
|
||||
// Check if user exists on backend
|
||||
const checkResult = await checkEmailExists(user.email);
|
||||
if (checkResult.exists) {
|
||||
// Already registered, log them in directly
|
||||
const backendResult = await loginWithGoogle(user.email, user.displayName || "Google User");
|
||||
if (backendResult.success) {
|
||||
navigate('/');
|
||||
} else if (backendResult.isSuspended) {
|
||||
setIsSuspended(true);
|
||||
} else {
|
||||
setError(backendResult.message);
|
||||
}
|
||||
} else {
|
||||
// First time sign-in, ask for mobile number
|
||||
setPendingEmail(user.email);
|
||||
setPendingName(user.displayName || "Google User");
|
||||
setShowMobilePrompt(true);
|
||||
}
|
||||
} else {
|
||||
setError('No email found in Google Account.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError(err.message || 'Failed to authenticate with Google');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMobileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (mobileNumber.length !== 10) {
|
||||
if (!/^[0-9]{10}$/.test(mobileNumber)) {
|
||||
setError('Please enter a valid 10-digit mobile number');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await checkUserExists(mobileNumber);
|
||||
setIsLoading(false);
|
||||
|
||||
if (result.success) {
|
||||
if (result.userExists) {
|
||||
setStep('pin');
|
||||
try {
|
||||
const backendResult = await loginWithGoogle(pendingEmail, pendingName, mobileNumber);
|
||||
if (backendResult.success) {
|
||||
navigate('/');
|
||||
} else if (backendResult.isSuspended) {
|
||||
setIsSuspended(true);
|
||||
} else {
|
||||
setStep('register');
|
||||
setError(backendResult.message);
|
||||
}
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (pin.length !== 6) {
|
||||
setError('PIN must be 6 digits');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await login(mobileNumber, pin);
|
||||
setIsLoading(false);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
} else if (result.isSuspended) {
|
||||
setIsSuspended(true);
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (name.length < 2) {
|
||||
setError('Please enter a valid name');
|
||||
return;
|
||||
}
|
||||
if (pin.length !== 6) {
|
||||
setError('PIN must be 6 digits');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await register(mobileNumber, name, pin);
|
||||
setIsLoading(false);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
} else {
|
||||
setError(result.message);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Registration failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,121 +95,84 @@ const LoginScreen: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="login-header">
|
||||
{step !== 'mobile' && (
|
||||
<button
|
||||
className="step-back-btn"
|
||||
onClick={() => setStep('mobile')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="login-title">
|
||||
{step === 'mobile' ? 'Welcome!' : step === 'pin' ? 'Welcome Back' : 'Create Account'}
|
||||
{showMobilePrompt ? 'Almost Done!' : 'Welcome to Tillo'}
|
||||
</h1>
|
||||
<p className="login-subtitle">
|
||||
{step === 'mobile'
|
||||
? 'Enter your mobile number to get started'
|
||||
: step === 'pin'
|
||||
? `Enter PIN for ${mobileNumber}`
|
||||
: 'Enter your details to register'}
|
||||
{showMobilePrompt ? 'Enter your mobile number to complete registration' : 'Sign in to your canteen account using Google Auth'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
{step === 'mobile' && (
|
||||
<form className="login-form" onSubmit={handleMobileSubmit}>
|
||||
{showMobilePrompt ? (
|
||||
<form onSubmit={handleMobileSubmit} className="login-form">
|
||||
<div className="form-group">
|
||||
<label>Mobile Number</label>
|
||||
<div className="input-wrapper">
|
||||
<Phone size={18} className="input-icon" />
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="10-digit mobile number"
|
||||
value={mobileNumber}
|
||||
onChange={(e) => setMobileNumber(e.target.value.replace(/[^0-9]/g, '').slice(0, 10))}
|
||||
disabled={isLoading}
|
||||
style={{ paddingLeft: '16px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="login-button" type="submit" disabled={isLoading || mobileNumber.length !== 10}>
|
||||
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 'pin' && (
|
||||
<form className="login-form" onSubmit={handleLoginSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Security PIN</label>
|
||||
<div className="input-wrapper">
|
||||
<Lock size={18} className="input-icon" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter 6-digit PIN"
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="login-button" type="submit" disabled={isLoading || pin.length !== 6}>
|
||||
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Verify & Login'}
|
||||
</button>
|
||||
<button
|
||||
className="back-to-mobile"
|
||||
type="button"
|
||||
onClick={() => setStep('mobile')}
|
||||
disabled={isLoading}
|
||||
<button
|
||||
type="submit"
|
||||
className="login-button w-full"
|
||||
disabled={isLoading || mobileNumber.length !== 10}
|
||||
>
|
||||
Use different mobile number
|
||||
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Register Account'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 'register' && (
|
||||
<form className="login-form" onSubmit={handleRegisterSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Full Name</label>
|
||||
<div className="input-wrapper">
|
||||
<UserIcon size={18} className="input-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="What should we call you?"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Create PIN</label>
|
||||
<div className="input-wrapper">
|
||||
<Lock size={18} className="input-icon" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Choose a 6-digit PIN"
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value.replace(/[^0-9]/g, '').slice(0, 6))}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="login-button" type="submit" disabled={isLoading || name.length < 2 || pin.length !== 6}>
|
||||
{isLoading ? <div className="loading-dots"><span>.</span><span>.</span><span>.</span></div> : 'Complete Registration'}
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
className="back-to-mobile"
|
||||
type="button"
|
||||
onClick={() => setStep('mobile')}
|
||||
onClick={() => { setShowMobilePrompt(false); setError(null); }}
|
||||
disabled={isLoading}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="google-signin-wrapper">
|
||||
<button
|
||||
className="google-login-btn"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-dots">
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<svg className="google-icon" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Sign in with Google</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -342,3 +342,29 @@
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.profile-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.membership-tag {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 800;
|
||||
padding: 3px 8px;
|
||||
border-radius: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.membership-tag.guest {
|
||||
background: rgba(239, 108, 55, 0.1);
|
||||
color: #ef6c37;
|
||||
}
|
||||
|
||||
.membership-tag.regular {
|
||||
background: rgba(8, 168, 80, 0.1);
|
||||
color: #08a850;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
Phone,
|
||||
ShieldCheck,
|
||||
X,
|
||||
User as UserIcon,
|
||||
Wallet
|
||||
@@ -78,7 +77,6 @@ const ProfileScreen: React.FC = () => {
|
||||
const menuItems = [
|
||||
{ icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' },
|
||||
{ icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' },
|
||||
{ icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' },
|
||||
{ icon: Settings, label: 'Preferences', sub: 'Dark Mode, Language', path: '/preferences' },
|
||||
{ icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' }
|
||||
];
|
||||
@@ -97,7 +95,12 @@ const ProfileScreen: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-info">
|
||||
<h2 className="user-name">{user?.name || 'User Name'}</h2>
|
||||
<div className="profile-name-row">
|
||||
<h2 className="user-name">{user?.name || 'User Name'}</h2>
|
||||
<span className={`membership-tag ${user?.membership === 'Guest Member' ? 'guest' : 'regular'}`}>
|
||||
{user?.membership || 'Member'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="user-contact">
|
||||
<span className="contact-item"><Phone size={14} /> {user?.mobileNumber || '+91 XXXXX XXXXX'}</span>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface User {
|
||||
isLoggedIn: boolean;
|
||||
isSuspended: boolean;
|
||||
ritzTokenBalance: number;
|
||||
membership?: string;
|
||||
}
|
||||
|
||||
export interface Stall {
|
||||
|
||||
Reference in New Issue
Block a user