Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Banner from './components/utils/components/Banner';
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider } from './backend/authContext';
import UnifiedAvailabilityPage from './components/UnifiedAvailabilityPage/UnifiedAvailabilityPage';
import GoogleOneTapPrompt from './components/utils/components/GoogleOneTap/GoogleOneTapPrompt';
import { useState } from 'react';

// legacy support for /groupview/:code
Expand All @@ -36,6 +37,7 @@ function Root() {
<div className="bg-background dark:bg-background-dark h-screen overflow-auto">
{/* <Banner title="2.0 Release is Live" text="Please report bugs" /> */}
<Router>
<GoogleOneTapPrompt />
<NavBar></NavBar>
<Routes>
<Route path="/" element={<HomePage />} />
Expand Down
16 changes: 16 additions & 0 deletions src/backend/authContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
signOut,
GoogleAuthProvider,
onAuthStateChanged,
signInWithCredential,
User,
} from 'firebase/auth';
import { auth, db } from './firebase';
Expand All @@ -21,6 +22,7 @@ interface AuthContextType {
currentUser: User | null;
loading: boolean;
login: () => Promise<User>;
loginWithIdToken: (idToken: string) => Promise<User>;
logout: () => Promise<void>;
}

Expand Down Expand Up @@ -87,6 +89,19 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}
};

// Sign in with Google One Tap (ID token) via Firebase
const loginWithIdToken = async (idToken: string): Promise<User> => {
try {
const credential = GoogleAuthProvider.credential(idToken);
const result = await signInWithCredential(auth, credential);
await ensureUserDocExists(result.user);
return result.user;
} catch (error: any) {
console.error('Error signing in with One Tap:', error);
throw error;
}
};

// Sign out from Firebase
const logout = async (): Promise<void> => {
try {
Expand All @@ -102,6 +117,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
currentUser,
loading,
login,
loginWithIdToken,
logout,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react';
import { useAuth } from '../../../../backend/authContext';

const GSI_SRC = 'https://accounts.google.com/gsi/client';

function loadGsiScript(): Promise<void> {
return new Promise((resolve, reject) => {
const googleAny = window.google as any;
if (googleAny?.accounts?.id) {
resolve();
return;
}

const existing = document.querySelector<HTMLScriptElement>(
`script[src="${GSI_SRC}"]`,
);
if (existing) {
existing.addEventListener('load', () => resolve());
existing.addEventListener('error', () => reject(new Error('GSI failed')));
return;
}

const script = document.createElement('script');
script.src = GSI_SRC;
script.async = true;
script.defer = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('GSI failed'));
document.head.appendChild(script);
});
}

export default function GoogleOneTapPrompt() {
const { currentUser, loading, loginWithIdToken } = useAuth();

React.useEffect(() => {
if (loading) return;
if (currentUser) return;

const clientId = process.env.REACT_APP_CLIENT_ID_GAPI;
if (!clientId) return;

let cancelled = false;

loadGsiScript()
.then(() => {
if (cancelled) return;
const gsi = (window.google as any)?.accounts?.id;
if (!gsi) return;

gsi.initialize({
client_id: clientId,
callback: async (response: { credential?: string }) => {
if (!response?.credential) return;
try {
await loginWithIdToken(response.credential);
} catch {
// If One Tap fails, the user can still use existing sign-in flows.
}
},
auto_select: false,
cancel_on_tap_outside: true,
});

gsi.prompt();
})
.catch(() => {
// Script load failed; ignore silently.
});

return () => {
cancelled = true;
(window.google as any)?.accounts?.id?.cancel?.();
};
}, [currentUser, loading, loginWithIdToken]);

return null;
}

3 changes: 2 additions & 1 deletion src/react-app-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

declare global {
interface Window {
FB: typeof FB
FB: typeof FB;
google?: any;
}
}

Expand Down