diff --git a/src/Root.tsx b/src/Root.tsx
index d82bfda..c56ac05 100644
--- a/src/Root.tsx
+++ b/src/Root.tsx
@@ -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
@@ -36,6 +37,7 @@ function Root() {
{/*
*/}
+
} />
diff --git a/src/backend/authContext.tsx b/src/backend/authContext.tsx
index ec4c9e6..6b891ad 100644
--- a/src/backend/authContext.tsx
+++ b/src/backend/authContext.tsx
@@ -10,6 +10,7 @@ import {
signOut,
GoogleAuthProvider,
onAuthStateChanged,
+ signInWithCredential,
User,
} from 'firebase/auth';
import { auth, db } from './firebase';
@@ -21,6 +22,7 @@ interface AuthContextType {
currentUser: User | null;
loading: boolean;
login: () => Promise;
+ loginWithIdToken: (idToken: string) => Promise;
logout: () => Promise;
}
@@ -87,6 +89,19 @@ export const AuthProvider: React.FC = ({ children }) => {
}
};
+ // Sign in with Google One Tap (ID token) via Firebase
+ const loginWithIdToken = async (idToken: string): Promise => {
+ 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 => {
try {
@@ -102,6 +117,7 @@ export const AuthProvider: React.FC = ({ children }) => {
currentUser,
loading,
login,
+ loginWithIdToken,
logout,
};
diff --git a/src/components/utils/components/GoogleOneTap/GoogleOneTapPrompt.tsx b/src/components/utils/components/GoogleOneTap/GoogleOneTapPrompt.tsx
new file mode 100644
index 0000000..f678974
--- /dev/null
+++ b/src/components/utils/components/GoogleOneTap/GoogleOneTapPrompt.tsx
@@ -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 {
+ return new Promise((resolve, reject) => {
+ const googleAny = window.google as any;
+ if (googleAny?.accounts?.id) {
+ resolve();
+ return;
+ }
+
+ const existing = document.querySelector(
+ `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;
+}
+
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
index 40e1f70..f5de38d 100644
--- a/src/react-app-env.d.ts
+++ b/src/react-app-env.d.ts
@@ -3,7 +3,8 @@
declare global {
interface Window {
- FB: typeof FB
+ FB: typeof FB;
+ google?: any;
}
}