From 77b7ba107785f8969ed0bcbe376fb47c149ede04 Mon Sep 17 00:00:00 2001 From: ising2025 Date: Fri, 10 Apr 2026 14:52:24 -0400 Subject: [PATCH 1/2] Handle refresh token for google calendar api --- functions/package.json | 7 +- functions/src/index.ts | 129 +++++++++ src/backend/firebase.ts | 2 +- src/backend/functions.ts | 10 + src/backend/googleCalService.ts | 246 ++++++++---------- src/backend/useGoogleCalService.ts | 6 + .../SideBySideView/SharedSidebar.tsx | 100 ++++--- .../SideBySideView/SideBySideView.tsx | 150 +++++------ src/index.tsx | 7 + 9 files changed, 384 insertions(+), 273 deletions(-) create mode 100644 src/backend/functions.ts diff --git a/functions/package.json b/functions/package.json index fef8388..37583ec 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,9 +15,10 @@ }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^12.6.0", - "firebase-functions": "^6.0.1", - "resend": "^6.9.1" + "firebase-admin": "^13.7.0", + "firebase-functions": "^7.2.3", + "googleapis": "^171.4.0", + "resend": "^6.10.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.12.0", diff --git a/functions/src/index.ts b/functions/src/index.ts index b5355ea..3d2e841 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,6 +5,8 @@ import { } from "firebase-functions/v2/https"; import { defineSecret } from "firebase-functions/params"; import { Resend } from "resend"; +import { google } from "googleapis"; +import * as admin from "firebase-admin"; // Define the secret const resendApiKey = defineSecret("RESEND_API_KEY"); @@ -63,3 +65,130 @@ export const sendEmail = onCall( } }, ); + +const googleClientSecret = defineSecret("GOOGLE_CLIENT_SECRET"); +const googleClientId = "83459975838-fqj9tuogpa17urv03jsaos2nbjfde9ne.apps.googleusercontent.com"; + +if (!admin.apps.length) { + admin.initializeApp(); +} + +export const connectCalendar = onCall({ + secrets: [googleClientSecret], + cors: ["http://localhost:3000", "https://studybuddy-392c7.web.app", "https://studybuddy-392c7.firebaseapp.com"], +}, async (request) => { + if (!request.auth) { + throw new HttpsError("unauthenticated", "User must be logged in."); + } + + const { code } = request.data; + const uid = request.auth.uid; + + const oauth2Client = new google.auth.OAuth2( + googleClientId, + googleClientSecret.value(), + "postmessage" + ); + + try { + const { tokens } = await oauth2Client.getToken(code); + + if (tokens.refresh_token) { + await admin.firestore().collection("users").doc(uid).set({ + calendarConnected: true, + googleRefreshToken: tokens.refresh_token, + updatedAt: new Date().toISOString(), // <-- added () + }, { merge: true }); + } + + return { success: true }; + } catch (error) { + console.error("Google Token Exchange Error:", error); + throw new HttpsError("internal", "Failed to connect Google Calendar"); + } +}); + +export const fetchCalendar = onCall({ + secrets: [googleClientSecret], + cors: ["http://localhost:3000", "https://studybuddy-392c7.web.app", "https://studybuddy-392c7.firebaseapp.com"], +}, async (request) => { + if (!request.auth) { + throw new HttpsError("unauthenticated", "User must be logged in."); + } + + const uid = request.auth.uid; + const db = admin.firestore(); + + try { + const userDoc = await db.collection("users").doc(uid).get(); + const refreshToken = userDoc.data()?.googleRefreshToken; + + if (!refreshToken) { + throw new HttpsError("not-found", "No calendar token found. Please reconnect."); + } + + const oauth2Client = new google.auth.OAuth2( + googleClientId, + googleClientSecret.value(), + "postmessage" + ); + + oauth2Client.setCredentials({ refresh_token: refreshToken }); + const calendar = google.calendar({ version: "v3", auth: oauth2Client }); + const response = await calendar.calendarList.list(); + + return { success: true, calendars: response.data.items || [] }; + } catch (error: any) { + console.error("Error fetching calendar:", error); + + if (error.message?.includes("invalid_grant")) { + await db.collection("users").doc(uid).update({ + googleRefreshToken: admin.firestore.FieldValue.delete(), + calendarConnected: false, + }); + throw new HttpsError("unauthenticated", "Calendar access revoked. Please reconnect."); + } + + if (error instanceof HttpsError) throw error; + throw new HttpsError("internal", "Failed to fetch calendars"); + } +}); + +export const fetchEvents = onCall({ + secrets: [googleClientSecret], +}, async (request) => { + if (!request.auth) { + throw new HttpsError("unauthenticated", "User must be logged in."); + } + + const uid = request.auth.uid; + const { calendarId, timeMin, timeMax, timezone } = request.data; + const db = admin.firestore(); + + const userDoc = await db.collection("users").doc(uid).get(); + const refreshToken = userDoc.data()?.googleRefreshToken; + + if (!refreshToken) { + throw new HttpsError("not-found", "No calendar token found."); + } + + const oauth2Client = new google.auth.OAuth2( + googleClientId, + googleClientSecret.value(), + "postmessage" + ); + + oauth2Client.setCredentials({ refresh_token: refreshToken }); + const calendar = google.calendar({ version: "v3", auth: oauth2Client }); + + const response = await calendar.events.list({ + calendarId, + timeMin, + timeMax, + singleEvents: true, + orderBy: "startTime", + timeZone: timezone, + }); + + return { success: true, events: response.data.items || [] }; +}); \ No newline at end of file diff --git a/src/backend/firebase.ts b/src/backend/firebase.ts index cf175f8..4b23768 100644 --- a/src/backend/firebase.ts +++ b/src/backend/firebase.ts @@ -40,5 +40,5 @@ const db = getFirestore(app); export const SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'; const googleProvider = new GoogleAuthProvider(); -export { auth, db, analytics, googleProvider }; +export { auth, db, analytics, googleProvider, app }; diff --git a/src/backend/functions.ts b/src/backend/functions.ts new file mode 100644 index 0000000..d51bca8 --- /dev/null +++ b/src/backend/functions.ts @@ -0,0 +1,10 @@ +import { getFunctions, connectFunctionsEmulator } from 'firebase/functions'; +import { app } from './firebase'; + +const functions = getFunctions(app); + +if (window.location.hostname === 'localhost') { + connectFunctionsEmulator(functions, 'localhost', 5001); +} + +export { functions }; \ No newline at end of file diff --git a/src/backend/googleCalService.ts b/src/backend/googleCalService.ts index 4188e91..4a2427d 100644 --- a/src/backend/googleCalService.ts +++ b/src/backend/googleCalService.ts @@ -3,6 +3,7 @@ import { time } from "console"; import { getUserTimezone } from "../components/utils/functions/timzoneConversions"; +import { getAccountId } from '../backend/events'; // Why we feel this is secure: https://stackoverflow.com/questions/18280827/localstorage-vs-cookies-for-oauth2-in-html5-web-app @@ -39,6 +40,7 @@ class GoogleCalendarService { private initPromise: Promise | null = null; private accessChangeListeners: AccessChangeListener[] = []; private tokenClient: google.accounts.oauth2.TokenClient | null = null; + private codeClient: any; constructor() { // Check if access was previously granted @@ -244,55 +246,72 @@ class GoogleCalendarService { } private async _initializeClientImpl(): Promise { - if (!this.apiLoaded) { - const loaded = await this.loadScripts(); - if (!loaded) { - console.error('Failed to load necessary scripts before initializing'); - return false; - } + if (!this.apiLoaded) { + const loaded = await this.loadScripts(); + if (!loaded) { + console.error('Failed to load necessary scripts'); + return false; } + } - try { - // Initialize GAPI client with API key and discovery docs - await window.gapi.client.init({ - apiKey: GOOGLE_API_KEY, - discoveryDocs: DISCOVERY_DOCS, - }); - - // Initialize the token client - this.tokenClient = window.google.accounts.oauth2.initTokenClient({ - client_id: GOOGLE_CLIENT_ID || '', - scope: CALENDAR_SCOPE, - prompt: '', // Empty prompt means don't show UI automatically - callback: (tokenResponse: any) => { - // This callback gets triggered when you get a token - if (tokenResponse && !tokenResponse.error) { - // We have a token, so we have access - this.hasAccess = true; - - // Store token expiry for better tracking - if (tokenResponse.expires_in) { - const expiresAt = Date.now() + (tokenResponse.expires_in * 1000); - localStorage.setItem(TOKEN_EXPIRY_KEY, expiresAt.toString()); - } - } else if (tokenResponse?.error && - tokenResponse.error !== 'popup_closed_by_user' && - tokenResponse.error !== 'access_denied') { - console.error('Token error:', tokenResponse?.error); - this.hasAccess = false; - } - }, - }); - - this.initialized = true; + try { + + await window.gapi.client.init({ + apiKey: GOOGLE_API_KEY, + discoveryDocs: DISCOVERY_DOCS, + }); + + + this.codeClient = (window.google.accounts.oauth2 as any).initCodeClient({ + client_id: GOOGLE_CLIENT_ID || '', + scope: CALENDAR_SCOPE, + ux_mode: 'popup', + select_account: true, + access_type: 'offline', + prompt: 'consent', + callback: async (response: any) => { + if (response.code) { + console.log('Authorization code received:', response.code); + + // 3. Send the code to your Node.js server + await this.sendCodeToBackend(response.code); + } else if (response.error) { + console.error('Code Client Error:', response.error); + } + }, + }); + + this.initialized = true; + return true; + } catch (error) { + console.error('Error initializing client:', error); + this.initialized = false; + return false; + } +} + +// Helper method +// Add userId as a second parameter +private async sendCodeToBackend(code: string): Promise { + try { + const { httpsCallable } = await import('firebase/functions'); + const { functions } = await import('./functions'); + const connectCalendarFn = httpsCallable(functions, 'connectCalendar'); + + const result = await connectCalendarFn({ code }); + const data = result.data as any; + console.log('connectCalendar result:', data); + + if (data.success) { + this.hasAccess = true; return true; - } catch (error) { - console.error('Error initializing client:', error); - this.initialized = false; - this.hasAccess = false; - return false; } + return false; + } catch (err) { + console.error('Failed to exchange code with backend:', err); + return false; } +} // Check if token is expired or about to expire private isTokenExpired(): boolean { @@ -358,110 +377,49 @@ class GoogleCalendarService { // Request Calendar access with better error handling public async requestCalendarAccess(): Promise { - if (!this.initialized) { - const initialized = await this.initializeClient(); - if (!initialized) { - throw new Error('Failed to initialize Google API client'); - } - } - - // If we already have access and token is not expired, just return true - if (this._hasAccess && !this.isTokenExpired()) { - console.log('Calendar access already granted with valid token'); - return true; - } - - // If token is expired, try silent refresh first - if (this._hasAccess && this.isTokenExpired()) { - console.log('Token expired, attempting silent refresh...'); - const refreshed = await this.attemptSilentRefresh(); - if (refreshed) { - console.log('Token refreshed successfully'); - return true; - } - } - - try { - if (!this.tokenClient) { - throw new Error('Token client not initialized'); - } - - // Request an access token - this will show the popup - return new Promise((resolve) => { - this.tokenClient!.callback = (tokenResponse) => { - if (tokenResponse && !tokenResponse.error) { - this.hasAccess = true; - - // Store token expiry - if (tokenResponse.expires_in) { - const expiresAt = Date.now() + (tokenResponse.expires_in * 1000); - localStorage.setItem(TOKEN_EXPIRY_KEY, expiresAt.toString()); - } - - resolve(true); - } else { - console.error('Error getting token:', tokenResponse?.error); - this.hasAccess = false; - resolve(false); - } - }; - - // Explicitly prompt the user for consent - this.tokenClient!.requestAccessToken({ prompt: 'consent' }); - }); - } catch (error) { - console.error('Error in requestCalendarAccess:', error); - this.hasAccess = false; - return false; + try { + // 1. Ensure the client is initialized + if (!this.codeClient) { + const initialized = await this._initializeClientImpl(); + if (!initialized) return false; } - } - // Ensure we have valid access, with fallback strategies - private async ensureAccess(): Promise { - // If we think we have access, check if token is still valid - if (this._hasAccess) { - // Check if token is expired - if (this.isTokenExpired()) { - console.log('Token is expired, attempting refresh...'); - - // Try silent refresh - const refreshed = await this.attemptSilentRefresh(); - if (refreshed) { - console.log('Token refreshed successfully'); - return true; + // 2. Return a Promise that resolves when the backend handshake is done + return new Promise((resolve) => { + // We override the callback to resolve this specific promise + const originalCallback = this.codeClient.callback; + + this.codeClient.callback = async (response: any) => { + if (response.code) { + // Send to Node.js backend + const success = await this.sendCodeToBackend(response.code); + this.hasAccess = success; + resolve(success); } else { - console.log('Token refresh failed, user interaction required'); + console.error('Code request failed or was closed'); this.hasAccess = false; - return false; - } - } - - // Token is not expired, check if it's actually set - if (window.gapi?.client) { - const token = window.gapi.client.getToken(); - if (token && token.access_token) { - return true; + resolve(false); } - // Try to restore from localStorage if not set - const storedToken = localStorage.getItem(TOKEN_KEY); - if (storedToken) { - try { - const parsedToken: StoredToken = JSON.parse(storedToken); - window.gapi.client.setToken(parsedToken); - return true; - } catch (error) { - console.error('Error restoring token:', error); - this.clearStoredTokenData(); - this.hasAccess = false; - } - } - } - } + // Restore the original callback if necessary + this.codeClient.callback = originalCallback; + }; - // If we don't have access or restoration failed + // 3. Trigger the popup + // 'prompt: consent' ensures you get a refresh_token every time during testing + this.codeClient.requestCode(); + }); + } catch (error) { + console.error('Error in requestCalendarAccess:', error); + this.hasAccess = false; return false; } +} + + // Ensure we have valid access, with fallback strategies + private async ensureAccess(): Promise { + return this._hasAccess; + } // Fetch list of calendars with better error handling public async fetchCalendars(): Promise { @@ -475,6 +433,12 @@ class GoogleCalendarService { throw new Error('Calendar access not granted. Please request access first.'); } + const token = window.gapi.client.getToken(); + if (!token) { + + throw new Error("Calendar access not granted. Please request access first."); + } + try { const response = await window.gapi.client.calendar.calendarList.list(); return response.result.items || []; @@ -491,6 +455,10 @@ class GoogleCalendarService { } } + public setHasAccess(value: boolean) { + this.hasAccess = value; // this already calls notifyAccessChange() via the setter + } + private convertEventsToTimezone(events: any, targetTimezone: string) { console.log("converting"); return events.map((event: any) => { diff --git a/src/backend/useGoogleCalService.ts b/src/backend/useGoogleCalService.ts index 8c596ae..ed4c3d1 100644 --- a/src/backend/useGoogleCalService.ts +++ b/src/backend/useGoogleCalService.ts @@ -206,6 +206,11 @@ export function useGoogleCalendar() { } }, [syncAuthState]); + const markAsConnected = useCallback(() => { + calendarService.setHasAccess(true); + setHasAccess(true); + }, []); + return { hasAccess, loading, @@ -215,5 +220,6 @@ export function useGoogleCalendar() { getCalendars, getEvents, disconnect, + markAsConnected, }; } \ No newline at end of file diff --git a/src/components/SideBySideView/SharedSidebar.tsx b/src/components/SideBySideView/SharedSidebar.tsx index 71d826a..98c7751 100644 --- a/src/components/SideBySideView/SharedSidebar.tsx +++ b/src/components/SideBySideView/SharedSidebar.tsx @@ -23,6 +23,8 @@ import GCAL_LOGO from './gcal-logo.png' import LoginPopup from '../utils/components/LoginPopup'; import ButtonSmall from '../utils/components/ButtonSmall'; import InviteButton from '../utils/components/InviteButton'; +import { functions } from '../../backend/functions'; +import { httpsCallable } from 'firebase/functions'; interface Calendar { id: string; @@ -104,43 +106,48 @@ export default function SharedSidebar({ const [isCalendarsExpanded, setIsCalendarsExpanded] = useState(true); const [showLoginPopup, setShowLoginPopup] = useState(false); - const { hasAccess, requestAccess, getCalendars } = useGoogleCalendar(); + const { hasAccess, requestAccess, getCalendars, markAsConnected } = useGoogleCalendar(); const { login, currentUser } = useAuth(); + const fetchCalendarFn = httpsCallable(functions, 'fetchCalendar'); + const connectCalendarFn = httpsCallable(functions, 'connectCalendar'); + // Load saved calendar preferences on mount useEffect(() => { - const loadSavedCalendarPreferences = async () => { - const uid = getAccountId(); - if (!uid || !hasAccess) return; - - try { - const savedCalendarIds = await getSelectedCalendarIDsByUserID(uid); - if (savedCalendarIds.length > 0 && setSelectedCalendarIds) { - setSelectedCalendarIds(savedCalendarIds); - } - } catch (error) { - console.error('Error loading saved calendar preferences:', error); - } - }; + const checkExistingAccess = async () => { + // Wait until we know auth state — null means still loading + if (!currentUser) return; - loadSavedCalendarPreferences(); - }, [hasAccess, setSelectedCalendarIds]); - - const fetchUserCalendars = async () => { try { - if (!hasAccess) { - return []; - } - const calendars = await getCalendars(); - if (setGoogleCalendars) { - setGoogleCalendars(calendars); + const result = await fetchCalendarFn({}); + const calendars = (result.data as any).calendars; + if (Array.isArray(calendars)) { + markAsConnected(); + if (setGoogleCalendars) setGoogleCalendars(calendars); } - return calendars; - } catch (error) { - console.error('Error fetching Google Calendars:', error); - return []; + } catch (err: any) { + // not-found is expected for new users — not a real error + if (err?.code === 'functions/not-found') return; + console.error('Silent calendar restore failed:', err); } }; + checkExistingAccess(); +}, [currentUser]); + +const fetchUserCalendars = async () => { + try { + const result = await fetchCalendarFn({}); + const calendars = (result.data as any).calendars; + if (Array.isArray(calendars)) { + markAsConnected(); + if (setGoogleCalendars) setGoogleCalendars(calendars); + } + return calendars; + } catch (error) { + console.error('Error fetching Google Calendars:', error); + return []; + } +}; const handleCalendarToggle = (calId: string) => { if (setSelectedCalendarIds) { @@ -160,21 +167,30 @@ export default function SharedSidebar({ } }; - const handleSignIn = async () => { - if (currentUser && !hasAccess) { - await requestAccess(); - await fetchUserCalendars(); - } else { - const user = await login(); - if (user) { - updateAnonymousUserToAuthUser(getAccountName()); - const scopeGranted = await requestAccess(); - if (scopeGranted) { - await fetchUserCalendars(); - } - } +const handleSignIn = async () => { + if (!currentUser) { + const user = await login(); + if (!user) return; + updateAnonymousUserToAuthUser(getAccountName()); + } + + try { + const result = await fetchCalendarFn({}); + const calendars = (result.data as any).calendars; + if (Array.isArray(calendars)) { + markAsConnected(); + if (setGoogleCalendars) setGoogleCalendars(calendars); + return; } - }; + } catch (err) { + console.log('Silent fetch failed, proceeding to OAuth popup...'); + } + + const scopeGranted = await requestAccess(); + if (scopeGranted) { + await fetchUserCalendars(); + } +}; const filteredChartedUsers = { ...chartedUsers, diff --git a/src/components/SideBySideView/SideBySideView.tsx b/src/components/SideBySideView/SideBySideView.tsx index c653491..377e8df 100644 --- a/src/components/SideBySideView/SideBySideView.tsx +++ b/src/components/SideBySideView/SideBySideView.tsx @@ -25,9 +25,10 @@ import TimezoneChanger from '../utils/components/TimezoneChanger'; import { getUserTimezone } from '../utils/functions/timzoneConversions'; import ButtonSmall from '../utils/components/ButtonSmall'; import { IconArrowsMaximize, IconArrowsMinimize } from '@tabler/icons-react'; -import { useGoogleCalendar } from '../../backend/useGoogleCalService'; import { useAuth } from '../../backend/authContext'; import { generateTimeBlocks } from '../utils/functions/generateTimeBlocks'; +import { functions } from '../../backend/functions'; +import { httpsCallable } from 'firebase/functions'; interface Calendar { id: string; @@ -131,6 +132,7 @@ export default function SideBySideView({ const [participantToggleClicked, setParticipantToggleClicked] = useState(true); const [calendarHeight, setCalendarHeight] = useState(null); + const fetchCalendarFn = httpsCallable(functions, 'fetchCalendar'); // Track which calendar is expanded (null = side-by-side, 'left' = edit expanded, 'right' = group expanded) const [expandedCalendar, setExpandedCalendar] = useState< @@ -138,8 +140,7 @@ export default function SideBySideView({ >(null); // Google Calendar hook for fetching events - const { hasAccess, requestAccess, getEvents, getCalendars } = - useGoogleCalendar(); + // const { hasAccess, requestAccess, getEvents, getCalendars } = useGoogleCalendar(); const { login, currentUser } = useAuth(); // Helper functions for autofill @@ -204,91 +205,65 @@ export default function SideBySideView({ return newAvailability; }; - // Fetch Google Calendar events when selected calendars change - const fetchGoogleCalEvents = useCallback( - async (calIds: string[]) => { - if (!hasAccess || calIds.length === 0) { - setGoogleCalendarEvents([]); - return []; - } + const fetchEventsFn = httpsCallable(functions, 'fetchEvents'); - const dates = calendarFramework.dates.flat(); - if (dates.length === 0) return []; - - const dateTimestamps = dates.map((d) => (d.date as Date).getTime()); - const startDate = new Date(Math.min(...dateTimestamps)); - startDate.setHours(0, 0, 0, 0); - const timeMin = startDate.toISOString(); - - const endDate = new Date(Math.max(...dateTimestamps)); - endDate.setHours(23, 59, 59, 999); - const timeMax = endDate.toISOString(); - - const allEvents: calendar_v3.Schema$Event[] = []; - - for (const calId of calIds) { - try { - const events = await getEvents( - calId, - timeMin, - timeMax, - calendarFramework.timezone - ); - - // Filter out multi-day events - const singleDayEvents = events.filter((event) => { - if (!event.start?.dateTime || !event.end?.dateTime) return false; - const start = new Date(event.start.dateTime); - const end = new Date(event.end.dateTime); - return start.getDay() === end.getDay(); - }); - - allEvents.push(...singleDayEvents); - } catch (error) { - console.error('Error fetching events for calendar:', calId, error); - } - } +const fetchGoogleCalEvents = useCallback( + async (calIds: string[]) => { + if (calIds.length === 0) { + setGoogleCalendarEvents([]); + return []; + } - setGoogleCalendarEvents(allEvents); - return allEvents; - }, - [ - hasAccess, - calendarFramework.dates, - calendarFramework.timezone, - getEvents, - setGoogleCalendarEvents, - ] - ); + const dates = calendarFramework.dates.flat(); + if (dates.length === 0) return []; - // Fetch calendars and events when access is available - useEffect(() => { - const initializeCalendars = async () => { - if (hasAccess && googleCalendars.length === 0) { - try { - const calendars = await getCalendars(); - setGoogleCalendars(calendars); - } catch (error) { - console.error('Error fetching calendars:', error); - } + const dateTimestamps = dates.map((d) => (d.date as Date).getTime()); + const startDate = new Date(Math.min(...dateTimestamps)); + startDate.setHours(0, 0, 0, 0); + + const endDate = new Date(Math.max(...dateTimestamps)); + endDate.setHours(23, 59, 59, 999); + + const allEvents: calendar_v3.Schema$Event[] = []; + + for (const calId of calIds) { + try { + const result = await fetchEventsFn({ + calendarId: calId, + timeMin: startDate.toISOString(), + timeMax: endDate.toISOString(), + timezone: calendarFramework.timezone, + }); + + const events = (result.data as any).events || []; + + const singleDayEvents = events.filter((event: any) => { + if (!event.start?.dateTime || !event.end?.dateTime) return false; + const start = new Date(event.start.dateTime); + const end = new Date(event.end.dateTime); + return start.getDay() === end.getDay(); + }); + + allEvents.push(...singleDayEvents); + } catch (error) { + console.error('Error fetching events for calendar:', calId, error); } - }; - initializeCalendars(); - }, [hasAccess, googleCalendars.length, getCalendars, setGoogleCalendars]); + } + + setGoogleCalendarEvents(allEvents); + return allEvents; + }, + [calendarFramework.dates, calendarFramework.timezone, setGoogleCalendarEvents] +); // Fetch events when selected calendar IDs change useEffect(() => { - if (hasAccess && selectedCalendarIds.length > 0) { - fetchGoogleCalEvents(selectedCalendarIds); - } else if (selectedCalendarIds.length === 0) { - setGoogleCalendarEvents([]); - } - }, [ - hasAccess, - selectedCalendarIds, - fetchGoogleCalEvents, - setGoogleCalendarEvents, - ]); + if (selectedCalendarIds.length > 0) { + fetchGoogleCalEvents(selectedCalendarIds); + } else { + setGoogleCalendarEvents([]); + } +}, [selectedCalendarIds, fetchGoogleCalEvents, setGoogleCalendarEvents]); // Get current user index const getCurrentUserIndex = useCallback(() => { @@ -331,17 +306,16 @@ export default function SideBySideView({ updateAnonymousUserToAuthUser(getAccountName()); } - if (!hasAccess) { - const scopeGranted = await requestAccess(); - if (!scopeGranted) return; - } + // Remove the hasAccess / requestAccess block entirely const calIds = selectedCalendarIds.length > 0 ? selectedCalendarIds : []; if (calIds.length === 0) { - // If no calendars selected, try to fetch them first - const calendars = await getCalendars(); - setGoogleCalendars(calendars); + const result = await fetchCalendarFn({}); + const calendars = (result.data as any).calendars; + if (Array.isArray(calendars)) { + setGoogleCalendars(calendars); + } return; } diff --git a/src/index.tsx b/src/index.tsx index ae62afc..a21c488 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,13 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import Root from './Root'; import { Favi } from './components/navbar/CalendarIcon'; +import { getFunctions, connectFunctionsEmulator } from 'firebase/functions'; +import { app } from './backend/firebase'; + +const functions = getFunctions(app); +if (window.location.hostname === 'localhost') { + connectFunctionsEmulator(functions, 'localhost', 5001); +} // test From bdb4c6b249e348f6895ec85c55c5f04d8e648e4f Mon Sep 17 00:00:00 2001 From: ising2025 Date: Tue, 14 Apr 2026 17:11:00 -0400 Subject: [PATCH 2/2] Delete firestore-debug.log --- firestore-debug.log | 69 --------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 firestore-debug.log diff --git a/firestore-debug.log b/firestore-debug.log deleted file mode 100644 index 835b2e2..0000000 --- a/firestore-debug.log +++ /dev/null @@ -1,69 +0,0 @@ -Oct 04, 2024 6:08:03 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start -INFO: Started WebSocket server on ws://127.0.0.1:9150 -API endpoint: http://127.0.0.1:8080 -If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: - - export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 - -Dev App Server is now running. - -Oct 04, 2024 6:08:17 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler initChannel -INFO: Connected to new websocket client -Oct 04, 2024 6:09:30 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler channelClosed -INFO: Websocket client disconnected -Oct 04, 2024 6:09:30 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler initChannel -INFO: Connected to new websocket client -Oct 04, 2024 6:10:21 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 6:10:21 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Oct 04, 2024 6:11:45 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 6:12:15 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 6:12:54 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 6:30:22 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler channelClosed -INFO: Websocket client disconnected -Oct 04, 2024 6:31:17 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler initChannel -INFO: Connected to new websocket client -Oct 04, 2024 6:34:21 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler channelClosed -INFO: Websocket client disconnected -Oct 04, 2024 6:35:36 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler initChannel -INFO: Connected to new websocket client -Oct 04, 2024 7:08:40 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:08:40 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Oct 04, 2024 7:08:45 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler channelClosed -INFO: Websocket client disconnected -Oct 04, 2024 7:08:45 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler initChannel -INFO: Connected to new websocket client -Oct 04, 2024 7:08:49 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:09:36 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:09:42 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:09:56 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:10:18 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:56:42 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:56:52 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:02 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:12 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:22 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:32 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:38 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected non-HTTP/2 connection. -Oct 04, 2024 7:57:39 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketChannelHandler channelClosed -INFO: Websocket client disconnected -*** shutting down gRPC server since JVM is shutting down -*** server shut down