From c54749e502ba070a749cfcd8a85b3fb1b974f730 Mon Sep 17 00:00:00 2001 From: huiii421 Date: Thu, 16 Oct 2025 16:35:18 +1100 Subject: [PATCH 1/8] feat: connect google calendar integration to backend API --- src/app/StoreProvider.tsx | 18 ++ src/app/admin/page.tsx | 12 +- .../admin/settings/CalendarIntegrations.tsx | 268 +++++++++++++++++- .../settings/components/CalendarForm.tsx | 4 +- src/app/auth/callback/AuthCallbackContent.tsx | 11 +- 5 files changed, 299 insertions(+), 14 deletions(-) diff --git a/src/app/StoreProvider.tsx b/src/app/StoreProvider.tsx index cb03679..73a3a0a 100644 --- a/src/app/StoreProvider.tsx +++ b/src/app/StoreProvider.tsx @@ -1,8 +1,10 @@ 'use client'; +import { useEffect } from 'react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; +import { useAppSelector } from '@/redux/hooks'; import { persistor, store } from '@/redux/store'; export default function StoreProvider({ @@ -10,9 +12,25 @@ export default function StoreProvider({ }: { children: React.ReactNode; }) { + function SessionSync() { + const user = useAppSelector(s => s.auth.user); + useEffect(() => { + if (!user) return; + try { + if (user._id) sessionStorage.setItem('userId', user._id); + if (user.email) sessionStorage.setItem('userEmail', user.email); + sessionStorage.setItem('user', JSON.stringify(user)); + } catch { + // ignore storage errors + } + }, [user]); + return null; + } + return ( + {children} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 33cdd7e..38521d1 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,15 +1,21 @@ // app/admin/page.tsx 'use client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect } from 'react'; export default function AdminRootRedirect() { const router = useRouter(); + const searchParams = useSearchParams(); useEffect(() => { - router.replace('/admin/overview'); - }, [router]); + const params = searchParams.toString(); + if (params) { + router.replace(`/admin/settings?${params}`); + } else { + router.replace('/admin/overview'); + } + }, [router, searchParams]); return null; } diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index f55711c..a96c7ce 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -8,7 +8,7 @@ import { } from '@mui/material'; import { styled } from '@mui/material/styles'; import Image from 'next/image'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import type { CalendarItem } from '@/app/admin/settings/components/CalendarForm'; import CalendarOptionsList from '@/app/admin/settings/components/CalendarForm'; @@ -16,6 +16,25 @@ import SectionDivider from '@/app/admin/settings/components/SectionDivider'; import SectionHeader from '@/app/admin/settings/components/SectionHeader'; import theme from '@/theme'; +// Backend base URL (e.g., http://localhost:4000 or http://localhost:4000/api) +// Recommended environment variable without /api (e.g., http://localhost:4000). For compatibility, prevent duplicate concatenation here. +const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL ?? '').replace( + /\/+$/, + '', +); + +const buildApiUrl = (path: string): string => { + // Normalize path to start with / + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + if (!API_BASE) return normalizedPath; // Relative path, delegate to same-origin proxy + + // If API_BASE already ends with /api and path also starts with /api, remove one /api to prevent /api/api duplication + if (/\/api\/?$/.test(API_BASE) && normalizedPath.startsWith('/api')) { + return `${API_BASE}${normalizedPath.replace(/^\/api/, '')}`; + } + return `${API_BASE}${normalizedPath}`; +}; + const InfoRow = styled(Box)({ display: 'flex', flexWrap: 'wrap', @@ -90,6 +109,7 @@ const CustomCheckbox = styled(Checkbox)({ export default function IntegrationsSection() { const [isConnected, setIsConnected] = useState(false); const [showGoogleEvents, setShowGoogleEvents] = useState(true); + const [userEmail, setUserEmail] = useState(null); const [calendars, setCalendars] = useState([ { id: 'family', name: 'Family', color: '#d076eb', checked: false }, { id: 'birthdays', name: 'Birthdays', color: '#ae725d', checked: false }, @@ -101,18 +121,148 @@ export default function IntegrationsSection() { }, { id: 'email', - name: 'email51@company.com', + name: + (typeof window !== 'undefined' + ? (sessionStorage.getItem('userEmail') ?? + localStorage.getItem('userEmail') ?? + process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL) + : null) ?? '', color: '#989ffd', checked: true, }, ]); + const getUserId = (): string | null => { + // Prefer reading from sessionStorage/localStorage; replace with your global user context if needed + if (typeof window === 'undefined') return null; + return ( + sessionStorage.getItem('userId') ?? + localStorage.getItem('userId') ?? + process.env.NEXT_PUBLIC_CALENDAR_USER_ID ?? + null + ); + }; + + const getUserEmail = (): string | null => { + if (typeof window === 'undefined') return null; + return ( + sessionStorage.getItem('userEmail') ?? + localStorage.getItem('userEmail') ?? + process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? + null + ); + }; + + const readUserFromStorage = (): { id?: string; email?: string } => { + if (typeof window === 'undefined') return {}; + const tryParse = ( + val: string | null, + ): { _id?: string; id?: string; email?: string } | null => { + if (!val) return null; + try { + return JSON.parse(val) as { _id?: string; id?: string; email?: string }; + } catch { + return null; + } + }; + const candidates = [ + sessionStorage.getItem('user'), + localStorage.getItem('user'), + sessionStorage.getItem('currentUser'), + localStorage.getItem('currentUser'), + sessionStorage.getItem('auth_user'), + localStorage.getItem('auth_user'), + ]; + for (const raw of candidates) { + const obj = tryParse(raw); + if (obj && (obj._id ?? obj.id)) { + return { id: obj._id ?? obj.id, email: obj.email }; + } + } + // Fallback: try global variables (if any) + const anyWindow = window as unknown as Record; + const globalUser = (anyWindow.__APP_USER__ ?? + anyWindow.__CURRENT_USER__ ?? + anyWindow.user) as + | { _id?: string; id?: string; email?: string } + | undefined; + if (globalUser && (globalUser._id ?? globalUser.id)) { + return { id: globalUser._id ?? globalUser.id, email: globalUser.email }; + } + return {}; + }; + const handleConnect = () => { - // TODO: Implement Google Calendar OAuth connection - setIsConnected(true); + const userId = getUserId(); + if (!userId) { + if (typeof window !== 'undefined') { + // Replace with a nicer UI toast if desired + alert( + 'User ID not detected, please login and try again, or set NEXT_PUBLIC_CALENDAR_USER_ID', + ); + } + return; + } + // Validate MongoDB ObjectId (24 hex chars) to avoid backend 500 + const isValidObjectId = /^[a-f\d]{24}$/i.test(userId); + if (!isValidObjectId) { + alert( + 'Invalid user ID format: requires 24-character hexadecimal string (Mongo ObjectId).', + ); + return; + } + // Build state so backend can parse userId and return URL + const from = encodeURIComponent( + typeof window !== 'undefined' + ? window.location.pathname + window.location.search + : '/admin/settings', + ); + const stateObj: Record = { u: userId, from }; + // If backend supports login_hint, include email in state for future use + if (userEmail) { + stateObj.e = userEmail; + } + const state = encodeURIComponent(JSON.stringify(stateObj)); + const userParam = userId ? `userId=${encodeURIComponent(userId)}` : ''; + const joiner = userParam ? '&' : ''; + // Redirect to backend OAuth entry + if (typeof window !== 'undefined') { + window.location.href = `${buildApiUrl('/calendar/oauth/google')}?${userParam}${joiner}state=${state}`; + } }; - const handleRemove = () => { + const handleRemove = async () => { + if (typeof window !== 'undefined') { + const ok = window.confirm( + 'Are you sure you want to remove the connection to Google Calendar?', + ); + if (!ok) return; + } + const userId = getUserId(); + if (!userId) { + // Without userId we cannot delete server token; reset local UI only + setIsConnected(false); + setShowGoogleEvents(true); + setCalendars(prev => + prev.map(cal => ({ + ...cal, + checked: cal.id === 'email', + })), + ); + return; + } + + try { + await fetch( + buildApiUrl(`/calendar-token/user/${encodeURIComponent(userId)}`), + { + method: 'DELETE', + }, + ); + } catch { + // Ignore deletion errors; still reset local UI + } + setIsConnected(false); setShowGoogleEvents(true); setCalendars(prev => @@ -123,6 +273,102 @@ export default function IntegrationsSection() { ); }; + useEffect(() => { + // Handle callback param /settings/calendar?connected=google + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + const connected = params.get('connected'); + if (connected === 'google') { + setIsConnected(true); + } + } + + const userId = getUserId(); + const email = getUserEmail(); + setUserEmail(email); + if (email) { + setCalendars(prev => + prev.map(cal => (cal.id === 'email' ? { ...cal, name: email } : cal)), + ); + } + if (!userId) { + // Fallback: try common storage keys/global variables + const fromStore = readUserFromStorage(); + if (fromStore.id) { + sessionStorage.setItem('userId', fromStore.id); + } + if (fromStore.email) { + sessionStorage.setItem('userEmail', fromStore.email); + setUserEmail(fromStore.email); + setCalendars(prev => + prev.map(cal => + cal.id === 'email' ? { ...cal, name: fromStore.email! } : cal, + ), + ); + } + } + const effectiveUserId = userId ?? readUserFromStorage().id ?? null; + if (!effectiveUserId) return; + + // Check if backend already has a valid token + const checkValid = async () => { + try { + const res = await fetch( + buildApiUrl( + `/calendar-token/user/${encodeURIComponent(effectiveUserId)}/valid`, + ), + ); + if (res.ok) { + // Convention: having a valid token means connected + setIsConnected(true); + } else if (res.status === 404) { + setIsConnected(false); + } + } catch { + // Ignore errors and keep default state + } + }; + + void checkValid(); + }, []); + + useEffect(() => { + // When connected, periodically check if token is expiring; refresh if needed + if (!isConnected) return; + const userId = getUserId(); + if (!userId) return; + + const intervalId = setInterval( + () => { + void (async () => { + try { + const exp = await fetch( + buildApiUrl( + `/calendar-token/user/${encodeURIComponent(userId)}/expiring`, + ), + ); + if (exp.ok) { + const data = (await exp.json()) as { isExpiringSoon?: boolean }; + if (data?.isExpiringSoon) { + await fetch( + buildApiUrl( + `/calendar-token/user/${encodeURIComponent(userId)}/refresh`, + ), + { method: 'POST' }, + ); + } + } + } catch { + // Fail silently; try again in the next interval + } + })(); + }, + 5 * 60 * 1000, + ); // Check every 5 minutes + + return () => clearInterval(intervalId); + }, [isConnected]); + const handleCalendarToggle = (calendarId: string) => { setCalendars(prev => prev.map(cal => @@ -151,7 +397,7 @@ export default function IntegrationsSection() { /> - email51@company.com + {userEmail} Sync your appointments to Google Calendar. Online booking @@ -163,7 +409,9 @@ export default function IntegrationsSection() { {isConnected ? ( - Remove + void handleRemove()}> + Remove + ) : ( Connect )} @@ -175,14 +423,16 @@ export default function IntegrationsSection() { Connected account: - email51@company.com + {userEmail} setShowGoogleEvents(e.target.checked)} + onChange={event => { + setShowGoogleEvents(event.target.checked); + }} size="small" /> } diff --git a/src/app/admin/settings/components/CalendarForm.tsx b/src/app/admin/settings/components/CalendarForm.tsx index 5523577..c003eee 100644 --- a/src/app/admin/settings/components/CalendarForm.tsx +++ b/src/app/admin/settings/components/CalendarForm.tsx @@ -105,7 +105,9 @@ export default function CalendarOptionsList({ onToggle(calendar.id)} + onChange={() => { + onToggle(calendar.id); + }} size="small" /> diff --git a/src/app/auth/callback/AuthCallbackContent.tsx b/src/app/auth/callback/AuthCallbackContent.tsx index 12afc63..ba9413c 100644 --- a/src/app/auth/callback/AuthCallbackContent.tsx +++ b/src/app/auth/callback/AuthCallbackContent.tsx @@ -49,7 +49,7 @@ export default function AuthCallbackContent() { // Clear any persisted auth state to prevent old user ID from being used localStorage.removeItem('persist:root'); - console.log('[AuthCallback] Setting user with ID:', parsedUser._id); + // console.log('[AuthCallback] Setting user with ID:', parsedUser._id); dispatch( setCredentials({ @@ -65,6 +65,15 @@ export default function AuthCallbackContent() { }), ); + // Persist current user to sessionStorage for front-end flows (e.g., OAuth) + try { + sessionStorage.setItem('userId', parsedUser._id); + sessionStorage.setItem('userEmail', parsedUser.email); + sessionStorage.setItem('user', JSON.stringify(parsedUser)); + } catch { + // ignore storage errors + } + router.replace('/admin/overview'); } catch (error) { // eslint-disable-next-line no-console From 815f8e0172a2295f02d0078621187f59c9de8f68 Mon Sep 17 00:00:00 2001 From: huiii421 Date: Sat, 18 Oct 2025 00:24:11 +1100 Subject: [PATCH 2/8] fix bugs: wrong routes for connecting google calendar --- .../admin/settings/CalendarIntegrations.tsx | 26 ++++++------------- src/app/auth/callback/AuthCallbackContent.tsx | 12 ++++++--- src/app/settings/calendar/page.tsx | 16 ++++++++++++ 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 src/app/settings/calendar/page.tsx diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index a96c7ce..1fc0e1a 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -28,10 +28,6 @@ const buildApiUrl = (path: string): string => { const normalizedPath = path.startsWith('/') ? path : `/${path}`; if (!API_BASE) return normalizedPath; // Relative path, delegate to same-origin proxy - // If API_BASE already ends with /api and path also starts with /api, remove one /api to prevent /api/api duplication - if (/\/api\/?$/.test(API_BASE) && normalizedPath.startsWith('/api')) { - return `${API_BASE}${normalizedPath.replace(/^\/api/, '')}`; - } return `${API_BASE}${normalizedPath}`; }; @@ -123,8 +119,7 @@ export default function IntegrationsSection() { id: 'email', name: (typeof window !== 'undefined' - ? (sessionStorage.getItem('userEmail') ?? - localStorage.getItem('userEmail') ?? + ? (localStorage.getItem('userEmail') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL) : null) ?? '', color: '#989ffd', @@ -136,7 +131,6 @@ export default function IntegrationsSection() { // Prefer reading from sessionStorage/localStorage; replace with your global user context if needed if (typeof window === 'undefined') return null; return ( - sessionStorage.getItem('userId') ?? localStorage.getItem('userId') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_ID ?? null @@ -146,7 +140,6 @@ export default function IntegrationsSection() { const getUserEmail = (): string | null => { if (typeof window === 'undefined') return null; return ( - sessionStorage.getItem('userEmail') ?? localStorage.getItem('userEmail') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? null @@ -166,11 +159,8 @@ export default function IntegrationsSection() { } }; const candidates = [ - sessionStorage.getItem('user'), localStorage.getItem('user'), - sessionStorage.getItem('currentUser'), localStorage.getItem('currentUser'), - sessionStorage.getItem('auth_user'), localStorage.getItem('auth_user'), ]; for (const raw of candidates) { @@ -212,11 +202,7 @@ export default function IntegrationsSection() { return; } // Build state so backend can parse userId and return URL - const from = encodeURIComponent( - typeof window !== 'undefined' - ? window.location.pathname + window.location.search - : '/admin/settings', - ); + const from = encodeURIComponent('/admin/settings?connected=google'); const stateObj: Record = { u: userId, from }; // If backend supports login_hint, include email in state for future use if (userEmail) { @@ -279,6 +265,10 @@ export default function IntegrationsSection() { const params = new URLSearchParams(window.location.search); const connected = params.get('connected'); if (connected === 'google') { + if (window.location.pathname !== '/admin/settings') { + window.location.replace('/admin/settings?connected=google'); + return; + } setIsConnected(true); } } @@ -295,10 +285,10 @@ export default function IntegrationsSection() { // Fallback: try common storage keys/global variables const fromStore = readUserFromStorage(); if (fromStore.id) { - sessionStorage.setItem('userId', fromStore.id); + localStorage.setItem('userId', fromStore.id); } if (fromStore.email) { - sessionStorage.setItem('userEmail', fromStore.email); + localStorage.setItem('userEmail', fromStore.email); setUserEmail(fromStore.email); setCalendars(prev => prev.map(cal => diff --git a/src/app/auth/callback/AuthCallbackContent.tsx b/src/app/auth/callback/AuthCallbackContent.tsx index ba9413c..5753fc3 100644 --- a/src/app/auth/callback/AuthCallbackContent.tsx +++ b/src/app/auth/callback/AuthCallbackContent.tsx @@ -67,14 +67,18 @@ export default function AuthCallbackContent() { // Persist current user to sessionStorage for front-end flows (e.g., OAuth) try { - sessionStorage.setItem('userId', parsedUser._id); - sessionStorage.setItem('userEmail', parsedUser.email); - sessionStorage.setItem('user', JSON.stringify(parsedUser)); + // sessionStorage.setItem('userId', parsedUser._id or localStorage.setItem('userId', parsedUser._id); + localStorage.setItem('userId', parsedUser._id); + // sessionStorage.setItem('userEmail', parsedUser.email); + localStorage.setItem('userEmail', parsedUser.email); + // sessionStorage.setItem('user', JSON.stringify(parsedUser)); + localStorage.setItem('user', JSON.stringify(parsedUser)); } catch { // ignore storage errors } - router.replace('/admin/overview'); + const from = searchParams.get('from'); + router.replace(from ?? '/admin/settings?connected=google'); } catch (error) { // eslint-disable-next-line no-console console.error('Auth callback - parsing error:', error); diff --git a/src/app/settings/calendar/page.tsx b/src/app/settings/calendar/page.tsx new file mode 100644 index 0000000..3fd13c2 --- /dev/null +++ b/src/app/settings/calendar/page.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function SettingsCalendarRedirect() { + const router = useRouter(); + const search = useSearchParams(); + + useEffect(() => { + const qs = search.toString(); + router.replace(`/admin/settings${qs ? `?${qs}` : '?connected=google'}`); + }, [router, search]); + + return null; +} From 9736375470ac8778206404444d574f4db38156f2 Mon Sep 17 00:00:00 2001 From: huiii421 Date: Wed, 22 Oct 2025 04:11:49 +1100 Subject: [PATCH 3/8] feat: implement Google Calendar integration with Gmail email detection and connection status management --- .../admin/settings/CalendarIntegrations.tsx | 146 +++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 1fc0e1a..902e56d 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -102,10 +102,26 @@ const CustomCheckbox = styled(Checkbox)({ }, }); +const EmailTypeWarning = styled(Box)({ + backgroundColor: '#fff3cd', + border: '1px solid #ffeaa7', + borderRadius: theme.spacing(1), + padding: theme.spacing(1.5), + marginBottom: theme.spacing(2), + '& .MuiTypography-root': { + fontSize: '0.875rem', + color: '#856404', + }, +}); + export default function IntegrationsSection() { const [isConnected, setIsConnected] = useState(false); const [showGoogleEvents, setShowGoogleEvents] = useState(true); const [userEmail, setUserEmail] = useState(null); + const [loginEmail, setLoginEmail] = useState(null); + const [googleEmail, setGoogleEmail] = useState(null); + const [showEmailTypeWarning, setShowEmailTypeWarning] = useState(false); + const CONNECTED_EMAIL_KEY = 'calendarConnectedEmail'; const [calendars, setCalendars] = useState([ { id: 'family', name: 'Family', color: '#d076eb', checked: false }, { id: 'birthdays', name: 'Birthdays', color: '#ae725d', checked: false }, @@ -139,6 +155,9 @@ export default function IntegrationsSection() { const getUserEmail = (): string | null => { if (typeof window === 'undefined') return null; + // Prioritize connected calendar email; otherwise fall back to login email or default value + const connectedEmail = localStorage.getItem(CONNECTED_EMAIL_KEY); + if (connectedEmail) return connectedEmail; return ( localStorage.getItem('userEmail') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? @@ -146,6 +165,57 @@ export default function IntegrationsSection() { ); }; + // Get current display email (prioritize Google email, otherwise login email) + const getDisplayEmail = (): string | null => { + return googleEmail || loginEmail || userEmail; + }; + + // Check if email is Gmail + const isGmailEmail = (email: string | null): boolean => { + if (!email) return false; + const gmailDomains = ['gmail.com', 'googlemail.com']; + const domain = email.split('@')[1]?.toLowerCase(); + return gmailDomains.includes(domain); + }; + + // Get email type information + const getEmailTypeInfo = (): { isGmail: boolean; message: string } => { + const currentEmail = loginEmail || userEmail; + const isGmail = isGmailEmail(currentEmail); + + if (isGmail) { + return { + isGmail: true, + message: 'Your login email is Gmail, you can directly connect to Google Calendar' + }; + } else { + return { + isGmail: false, + message: 'Your login email is not Gmail, connecting to Google Calendar requires a Gmail account' + }; + } + }; + + // Get Google Calendar user information + const fetchGoogleProfile = async (userId: string): Promise<{ + googleUserId?: string; + userEmail?: string; + userName?: string; + userPicture?: string; + } | null> => { + try { + const response = await fetch( + buildApiUrl(`/calendar-token/user/${encodeURIComponent(userId)}/profile`) + ); + if (response.ok) { + return await response.json(); + } + return null; + } catch { + return null; + } + }; + const readUserFromStorage = (): { id?: string; email?: string } => { if (typeof window === 'undefined') return {}; const tryParse = ( @@ -201,6 +271,17 @@ export default function IntegrationsSection() { ); return; } + + // Check if current login email is Gmail + const emailInfo = getEmailTypeInfo(); + if (!emailInfo.isGmail) { + // If not Gmail, show confirmation dialog + const confirmMessage = `${emailInfo.message}\n\nClick "OK" to redirect to Gmail login page, you need to use a Gmail account for authorization.\n\nNote: After connection, the calendar will use your Gmail account, not the current login email.`; + if (!window.confirm(confirmMessage)) { + return; + } + } + // Build state so backend can parse userId and return URL const from = encodeURIComponent('/admin/settings?connected=google'); const stateObj: Record = { u: userId, from }; @@ -250,11 +331,18 @@ export default function IntegrationsSection() { } setIsConnected(false); + setGoogleEmail(null); + setUserEmail(loginEmail); // Return to login email setShowGoogleEvents(true); + // After disconnection, if login email is not Gmail, show warning again + if (loginEmail && !isGmailEmail(loginEmail)) { + setShowEmailTypeWarning(true); + } setCalendars(prev => prev.map(cal => ({ ...cal, checked: cal.id === 'email', + name: cal.id === 'email' ? (loginEmail || '') : cal.name, })), ); }; @@ -264,17 +352,44 @@ export default function IntegrationsSection() { if (typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); const connected = params.get('connected'); + const gEmail = params.get('gEmail'); if (connected === 'google') { if (window.location.pathname !== '/admin/settings') { window.location.replace('/admin/settings?connected=google'); return; } setIsConnected(true); + setShowEmailTypeWarning(false); // Hide warning after successful connection + // If gEmail is returned, prioritize and persist it + if (gEmail) { + setGoogleEmail(gEmail); + setUserEmail(gEmail); + try { + localStorage.setItem(CONNECTED_EMAIL_KEY, gEmail); + } catch {} + // Update email display in calendar list + setCalendars(prev => + prev.map(cal => (cal.id === 'email' ? { ...cal, name: gEmail } : cal)), + ); + } } } const userId = getUserId(); const email = getUserEmail(); + + // Set login email (from localStorage) + const storedLoginEmail = typeof window !== 'undefined' + ? localStorage.getItem('userEmail') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? null + : null; + setLoginEmail(storedLoginEmail); + + // Check if email type warning should be displayed (only when not connected and not Gmail) + if (!isConnected && storedLoginEmail && !isGmailEmail(storedLoginEmail)) { + setShowEmailTypeWarning(true); + } + + // Set current display email setUserEmail(email); if (email) { setCalendars(prev => @@ -311,6 +426,22 @@ export default function IntegrationsSection() { if (res.ok) { // Convention: having a valid token means connected setIsConnected(true); + setShowEmailTypeWarning(false); // Hide warning after successful connection + // Use new profile API to get Google user information + try { + const profile = await fetchGoogleProfile(effectiveUserId); + if (profile?.userEmail) { + setGoogleEmail(profile.userEmail); + setUserEmail(profile.userEmail); + try { + localStorage.setItem(CONNECTED_EMAIL_KEY, profile.userEmail); + } catch {} + // Update email display in calendar list + setCalendars(prev => + prev.map(cal => (cal.id === 'email' ? { ...cal, name: profile.userEmail! } : cal)), + ); + } + } catch {} } else if (res.status === 404) { setIsConnected(false); } @@ -371,6 +502,17 @@ export default function IntegrationsSection() { <> + + {/* Email type warning */} + {showEmailTypeWarning && !isConnected && ( + + + Note: + {getEmailTypeInfo().message} + + + )} + @@ -387,7 +529,7 @@ export default function IntegrationsSection() { /> - {userEmail} + {getDisplayEmail()} Sync your appointments to Google Calendar. Online booking @@ -413,7 +555,7 @@ export default function IntegrationsSection() { Connected account: - {userEmail} + {googleEmail || userEmail} Date: Wed, 22 Oct 2025 04:16:10 +1100 Subject: [PATCH 4/8] fix bugs for linting --- .../admin/settings/CalendarIntegrations.tsx | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 902e56d..12bc4d8 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -167,7 +167,7 @@ export default function IntegrationsSection() { // Get current display email (prioritize Google email, otherwise login email) const getDisplayEmail = (): string | null => { - return googleEmail || loginEmail || userEmail; + return googleEmail ?? loginEmail ?? userEmail; }; // Check if email is Gmail @@ -180,24 +180,28 @@ export default function IntegrationsSection() { // Get email type information const getEmailTypeInfo = (): { isGmail: boolean; message: string } => { - const currentEmail = loginEmail || userEmail; + const currentEmail = loginEmail ?? userEmail; const isGmail = isGmailEmail(currentEmail); if (isGmail) { return { isGmail: true, - message: 'Your login email is Gmail, you can directly connect to Google Calendar' + message: + 'Your login email is Gmail, you can directly connect to Google Calendar', }; } else { return { isGmail: false, - message: 'Your login email is not Gmail, connecting to Google Calendar requires a Gmail account' + message: + 'Your login email is not Gmail, connecting to Google Calendar requires a Gmail account', }; } }; // Get Google Calendar user information - const fetchGoogleProfile = async (userId: string): Promise<{ + const fetchGoogleProfile = async ( + userId: string, + ): Promise<{ googleUserId?: string; userEmail?: string; userName?: string; @@ -205,10 +209,17 @@ export default function IntegrationsSection() { } | null> => { try { const response = await fetch( - buildApiUrl(`/calendar-token/user/${encodeURIComponent(userId)}/profile`) + buildApiUrl( + `/calendar-token/user/${encodeURIComponent(userId)}/profile`, + ), ); if (response.ok) { - return await response.json(); + return (await response.json()) as { + googleUserId?: string; + userEmail?: string; + userName?: string; + userPicture?: string; + }; } return null; } catch { @@ -342,7 +353,7 @@ export default function IntegrationsSection() { prev.map(cal => ({ ...cal, checked: cal.id === 'email', - name: cal.id === 'email' ? (loginEmail || '') : cal.name, + name: cal.id === 'email' ? (loginEmail ?? '') : cal.name, })), ); }; @@ -366,10 +377,14 @@ export default function IntegrationsSection() { setUserEmail(gEmail); try { localStorage.setItem(CONNECTED_EMAIL_KEY, gEmail); - } catch {} + } catch { + // Ignore storage errors + } // Update email display in calendar list setCalendars(prev => - prev.map(cal => (cal.id === 'email' ? { ...cal, name: gEmail } : cal)), + prev.map(cal => + cal.id === 'email' ? { ...cal, name: gEmail } : cal, + ), ); } } @@ -379,9 +394,12 @@ export default function IntegrationsSection() { const email = getUserEmail(); // Set login email (from localStorage) - const storedLoginEmail = typeof window !== 'undefined' - ? localStorage.getItem('userEmail') ?? process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? null - : null; + const storedLoginEmail = + typeof window !== 'undefined' + ? (localStorage.getItem('userEmail') ?? + process.env.NEXT_PUBLIC_CALENDAR_USER_EMAIL ?? + null) + : null; setLoginEmail(storedLoginEmail); // Check if email type warning should be displayed (only when not connected and not Gmail) @@ -435,13 +453,21 @@ export default function IntegrationsSection() { setUserEmail(profile.userEmail); try { localStorage.setItem(CONNECTED_EMAIL_KEY, profile.userEmail); - } catch {} + } catch { + // Ignore storage errors + } // Update email display in calendar list setCalendars(prev => - prev.map(cal => (cal.id === 'email' ? { ...cal, name: profile.userEmail! } : cal)), + prev.map(cal => + cal.id === 'email' + ? { ...cal, name: profile.userEmail! } + : cal, + ), ); } - } catch {} + } catch { + // Ignore profile fetch errors + } } else if (res.status === 404) { setIsConnected(false); } @@ -488,7 +514,7 @@ export default function IntegrationsSection() { ); // Check every 5 minutes return () => clearInterval(intervalId); - }, [isConnected]); + }, [isConnected, getUserId]); const handleCalendarToggle = (calendarId: string) => { setCalendars(prev => @@ -555,7 +581,7 @@ export default function IntegrationsSection() { Connected account: - {googleEmail || userEmail} + {googleEmail ?? userEmail} Date: Thu, 23 Oct 2025 03:21:21 +1100 Subject: [PATCH 5/8] fix: resolve localStorage data being re-written after calendar deletion --- src/app/StoreProvider.tsx | 16 - .../admin/settings/CalendarIntegrations.tsx | 429 ++++++++++++++++-- src/app/auth/callback/AuthCallbackContent.tsx | 18 +- 3 files changed, 412 insertions(+), 51 deletions(-) diff --git a/src/app/StoreProvider.tsx b/src/app/StoreProvider.tsx index 73a3a0a..d666c13 100644 --- a/src/app/StoreProvider.tsx +++ b/src/app/StoreProvider.tsx @@ -12,25 +12,9 @@ export default function StoreProvider({ }: { children: React.ReactNode; }) { - function SessionSync() { - const user = useAppSelector(s => s.auth.user); - useEffect(() => { - if (!user) return; - try { - if (user._id) sessionStorage.setItem('userId', user._id); - if (user.email) sessionStorage.setItem('userEmail', user.email); - sessionStorage.setItem('user', JSON.stringify(user)); - } catch { - // ignore storage errors - } - }, [user]); - return null; - } - return ( - {children} diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 12bc4d8..737e919 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -144,7 +144,7 @@ export default function IntegrationsSection() { ]); const getUserId = (): string | null => { - // Prefer reading from sessionStorage/localStorage; replace with your global user context if needed + // Prefer reading from localStorage; replace with your global user context if needed if (typeof window === 'undefined') return null; return ( localStorage.getItem('userId') ?? @@ -170,6 +170,44 @@ export default function IntegrationsSection() { return googleEmail ?? loginEmail ?? userEmail; }; + // Get display email with source label for UI/debug + const getDisplayEmailInfo = (): { + email: string | null; + source: 'google' | 'login' | 'fallback' | null; + } => { + if (googleEmail) return { email: googleEmail, source: 'google' }; + if (loginEmail) return { email: loginEmail, source: 'login' }; + return { email: userEmail, source: userEmail ? 'fallback' : null }; + }; + + // Debug function to log current storage state + const logStorageState = () => { + if (typeof window === 'undefined') return; + console.log('=== Calendar Storage Debug ==='); + console.log('sessionStorage userId:', sessionStorage.getItem('userId')); + console.log( + 'sessionStorage userEmail:', + sessionStorage.getItem('userEmail'), + ); + console.log('localStorage userId:', localStorage.getItem('userId')); + console.log('localStorage userEmail:', localStorage.getItem('userEmail')); + console.log( + 'localStorage calendarConnectedEmail:', + localStorage.getItem(CONNECTED_EMAIL_KEY), + ); + console.log( + 'localStorage persist:root exists:', + !!localStorage.getItem('persist:root'), + ); + console.log('State - isConnected:', isConnected); + console.log('State - googleEmail:', googleEmail); + console.log('State - loginEmail:', loginEmail); + console.log('State - userEmail:', userEmail); + console.log('State - showEmailTypeWarning:', showEmailTypeWarning); + console.log('Current URL:', window.location.href); + console.log('=============================='); + }; + // Check if email is Gmail const isGmailEmail = (email: string | null): boolean => { if (!email) return false; @@ -229,6 +267,24 @@ export default function IntegrationsSection() { const readUserFromStorage = (): { id?: string; email?: string } => { if (typeof window === 'undefined') return {}; + // Try Redux Persist root (persist:root) → nested JSON strings + try { + const persistRoot = localStorage.getItem('persist:root'); + if (persistRoot) { + const rootObj = JSON.parse(persistRoot) as { auth?: string }; + if (rootObj?.auth) { + const authObj = JSON.parse(rootObj.auth) as { + user?: { _id?: string; id?: string; email?: string }; + }; + const u = authObj.user; + if (u && (u._id ?? u.id)) { + return { id: u._id ?? u.id, email: u.email }; + } + } + } + } catch { + // ignore parse errors + } const tryParse = ( val: string | null, ): { _id?: string; id?: string; email?: string } | null => { @@ -264,6 +320,16 @@ export default function IntegrationsSection() { }; const handleConnect = () => { + // Clear any deletion flags when connecting + try { + sessionStorage.removeItem('calendar_deleted'); + sessionStorage.removeItem('calendar_deleted_time'); + sessionStorage.removeItem('disable_calendar_backend_check'); + sessionStorage.removeItem('calendar_manually_deleted'); + } catch { + // Ignore storage errors + } + const userId = getUserId(); if (!userId) { if (typeof window !== 'undefined') { @@ -331,31 +397,139 @@ export default function IntegrationsSection() { } try { - await fetch( + // First, get CSRF token + const csrfResponse = await fetch( + buildApiUrl('/calendar-token/csrf-token'), + { + method: 'GET', + credentials: 'include', + }, + ); + + if (!csrfResponse.ok) { + throw new Error('Failed to get CSRF token'); + } + + const csrfData = (await csrfResponse.json()) as { csrfToken?: string }; + const csrfToken = csrfData.csrfToken; + + if (!csrfToken) { + throw new Error('No CSRF token received'); + } + + // Then delete the calendar token + const deleteResponse = await fetch( buildApiUrl(`/calendar-token/user/${encodeURIComponent(userId)}`), { method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken, + }, + credentials: 'include', }, ); - } catch { - // Ignore deletion errors; still reset local UI - } - setIsConnected(false); - setGoogleEmail(null); - setUserEmail(loginEmail); // Return to login email - setShowGoogleEvents(true); - // After disconnection, if login email is not Gmail, show warning again - if (loginEmail && !isGmailEmail(loginEmail)) { - setShowEmailTypeWarning(true); + if (deleteResponse.ok) { + const result = (await deleteResponse.json()) as { message?: string }; + console.log('Calendar token deleted successfully:', result.message); + + // Clean up localStorage immediately after successful deletion + try { + localStorage.removeItem(CONNECTED_EMAIL_KEY); + console.log('localStorage calendarConnectedEmail cleared'); + } catch { + console.error('Failed to clear localStorage calendarConnectedEmail'); + } + + // Reset local state immediately + setIsConnected(false); + setGoogleEmail(null); + setUserEmail(loginEmail); + setShowGoogleEvents(true); + + // Update calendars to show login email + setCalendars(prev => + prev.map(cal => ({ + ...cal, + checked: cal.id === 'email', + name: cal.id === 'email' ? (loginEmail ?? '') : cal.name, + })), + ); + + // Show warning if login email is not Gmail + if (loginEmail && !isGmailEmail(loginEmail)) { + setShowEmailTypeWarning(true); + } + + // Add a flag to prevent re-checking backend after successful deletion + try { + sessionStorage.setItem('calendar_deleted', 'true'); + // Also add a timestamp to make it more specific + sessionStorage.setItem( + 'calendar_deleted_time', + Date.now().toString(), + ); + // Add a flag to completely disable backend checks + sessionStorage.setItem('disable_calendar_backend_check', 'true'); + // Add a permanent flag to prevent any localStorage writes + sessionStorage.setItem('calendar_manually_deleted', 'true'); + } catch { + // Ignore storage errors + } + + // Small delay to ensure state updates, then redirect + setTimeout(() => { + if (typeof window !== 'undefined') { + // Force reload to ensure clean state + window.location.href = '/admin/settings'; + } + }, 200); + } else { + console.error( + 'Failed to delete calendar token:', + deleteResponse.status, + deleteResponse.statusText, + ); + } + } catch (error) { + console.error('Error during calendar token deletion:', error); + // Reset local UI even if server deletion fails + setIsConnected(false); + setGoogleEmail(null); + setUserEmail(loginEmail); + setShowGoogleEvents(true); + + // Clean up localStorage as fallback + try { + localStorage.removeItem(CONNECTED_EMAIL_KEY); + console.log('localStorage calendarConnectedEmail cleared (fallback)'); + } catch { + console.error( + 'Failed to clear localStorage calendarConnectedEmail (fallback)', + ); + } + + // Add flag to prevent re-checking backend after fallback cleanup + try { + sessionStorage.setItem('calendar_deleted', 'true'); + } catch { + // Ignore storage errors + } + + // Update calendars and show warning if needed + setCalendars(prev => + prev.map(cal => ({ + ...cal, + checked: cal.id === 'email', + name: cal.id === 'email' ? (loginEmail ?? '') : cal.name, + })), + ); + + if (loginEmail && !isGmailEmail(loginEmail)) { + setShowEmailTypeWarning(true); + } } - setCalendars(prev => - prev.map(cal => ({ - ...cal, - checked: cal.id === 'email', - name: cal.id === 'email' ? (loginEmail ?? '') : cal.name, - })), - ); }; useEffect(() => { @@ -369,12 +543,15 @@ export default function IntegrationsSection() { window.location.replace('/admin/settings?connected=google'); return; } + // Immediately set connected state to prevent warning from showing setIsConnected(true); - setShowEmailTypeWarning(false); // Hide warning after successful connection + setShowEmailTypeWarning(false); + // If gEmail is returned, prioritize and persist it if (gEmail) { setGoogleEmail(gEmail); setUserEmail(gEmail); + // Always set for OAuth callback (new connection) try { localStorage.setItem(CONNECTED_EMAIL_KEY, gEmail); } catch { @@ -386,7 +563,34 @@ export default function IntegrationsSection() { cal.id === 'email' ? { ...cal, name: gEmail } : cal, ), ); + } else { + // No gEmail in URL, try to fetch profile immediately to resolve googleEmail + const uid = getUserId() ?? readUserFromStorage().id ?? null; + if (uid) { + void (async () => { + const profile = await fetchGoogleProfile(uid); + if (profile?.userEmail) { + setGoogleEmail(profile.userEmail); + setUserEmail(profile.userEmail); + // Always set for OAuth callback (new connection) + try { + localStorage.setItem(CONNECTED_EMAIL_KEY, profile.userEmail); + } catch { + // Ignore storage errors + } + setCalendars(prev => + prev.map(cal => + cal.id === 'email' + ? { ...cal, name: profile.userEmail! } + : cal, + ), + ); + } + })(); + } } + // Early return to prevent further processing + return; } } @@ -414,14 +618,55 @@ export default function IntegrationsSection() { prev.map(cal => (cal.id === 'email' ? { ...cal, name: email } : cal)), ); } + + // Check if localStorage has calendarConnectedEmail, if not, ensure not connected + const hasConnectedEmail = + typeof window !== 'undefined' + ? localStorage.getItem(CONNECTED_EMAIL_KEY) + : null; + if (!hasConnectedEmail) { + setIsConnected(false); + setGoogleEmail(null); + } + + // Check if we're in a callback scenario (OAuth redirect) + const isCallback = + typeof window !== 'undefined' && + (window.location.search.includes('connected=google') || + window.location.search.includes('csrfToken') || + window.location.search.includes('user=')); + + // Check if we just deleted calendar and should skip all backend checks + const justDeletedCalendar = + typeof window !== 'undefined' && + (sessionStorage.getItem('calendar_deleted') === 'true' || + sessionStorage.getItem('disable_calendar_backend_check') === 'true' || + sessionStorage.getItem('calendar_manually_deleted') === 'true'); + + console.log('Is callback scenario:', isCallback); + console.log('Just deleted calendar:', justDeletedCalendar); if (!userId) { // Fallback: try common storage keys/global variables const fromStore = readUserFromStorage(); if (fromStore.id) { - localStorage.setItem('userId', fromStore.id); + // Only set if not already exists AND not in callback scenario AND not just deleted calendar + if ( + !localStorage.getItem('userId') && + !isCallback && + !justDeletedCalendar + ) { + localStorage.setItem('userId', fromStore.id); + } } if (fromStore.email) { - localStorage.setItem('userEmail', fromStore.email); + // Only set if not already exists AND not in callback scenario AND not just deleted calendar + if ( + !localStorage.getItem('userEmail') && + !isCallback && + !justDeletedCalendar + ) { + localStorage.setItem('userEmail', fromStore.email); + } setUserEmail(fromStore.email); setCalendars(prev => prev.map(cal => @@ -435,6 +680,48 @@ export default function IntegrationsSection() { // Check if backend already has a valid token const checkValid = async () => { + // Skip backend check if we just deleted the calendar + const justDeleted = + typeof window !== 'undefined' + ? sessionStorage.getItem('calendar_deleted') + : null; + const deletedTime = + typeof window !== 'undefined' + ? sessionStorage.getItem('calendar_deleted_time') + : null; + const disableBackendCheck = + typeof window !== 'undefined' + ? sessionStorage.getItem('disable_calendar_backend_check') + : null; + + // Check if deletion was recent (within last 10 seconds) + const isRecentDeletion = + deletedTime && Date.now() - parseInt(deletedTime) < 10000; + + if ( + justDeleted === 'true' || + isRecentDeletion || + disableBackendCheck === 'true' + ) { + console.log('Skipping backend check - calendar was just deleted', { + justDeleted, + isRecentDeletion, + disableBackendCheck, + }); + // Clear the flags + try { + sessionStorage.removeItem('calendar_deleted'); + sessionStorage.removeItem('calendar_deleted_time'); + sessionStorage.removeItem('disable_calendar_backend_check'); + } catch { + // Ignore storage errors + } + // Ensure we're in disconnected state + setIsConnected(false); + setGoogleEmail(null); + return; + } + try { const res = await fetch( buildApiUrl( @@ -451,10 +738,42 @@ export default function IntegrationsSection() { if (profile?.userEmail) { setGoogleEmail(profile.userEmail); setUserEmail(profile.userEmail); - try { - localStorage.setItem(CONNECTED_EMAIL_KEY, profile.userEmail); - } catch { - // Ignore storage errors + // Only set if not already exists to avoid overwriting manual deletions + // Also check if we just deleted calendar + const justDeleted = + typeof window !== 'undefined' + ? sessionStorage.getItem('calendar_deleted') + : null; + const disableBackendCheck = + typeof window !== 'undefined' + ? sessionStorage.getItem('disable_calendar_backend_check') + : null; + const manuallyDeleted = + typeof window !== 'undefined' + ? sessionStorage.getItem('calendar_manually_deleted') + : null; + + if ( + !localStorage.getItem(CONNECTED_EMAIL_KEY) && + !justDeleted && + !disableBackendCheck && + !manuallyDeleted + ) { + try { + localStorage.setItem(CONNECTED_EMAIL_KEY, profile.userEmail); + } catch { + // Ignore storage errors + } + } else { + console.log( + 'Skipping localStorage write for calendarConnectedEmail:', + { + hasExisting: !!localStorage.getItem(CONNECTED_EMAIL_KEY), + justDeleted, + disableBackendCheck, + manuallyDeleted, + }, + ); } // Update email display in calendar list setCalendars(prev => @@ -470,13 +789,43 @@ export default function IntegrationsSection() { } } else if (res.status === 404) { setIsConnected(false); + // If no valid token, ensure localStorage is clean + try { + localStorage.removeItem(CONNECTED_EMAIL_KEY); + } catch { + // Ignore storage errors + } } } catch { // Ignore errors and keep default state + // If there's an error, assume not connected and clean localStorage + setIsConnected(false); + try { + localStorage.removeItem(CONNECTED_EMAIL_KEY); + } catch { + // Ignore storage errors + } } }; - void checkValid(); + // Only run backend check if we didn't just delete calendar + if (!justDeletedCalendar) { + void checkValid(); + } else { + console.log('Skipping all backend checks - calendar was just deleted'); + // Clear the temporary flags but keep the permanent one + try { + sessionStorage.removeItem('calendar_deleted'); + sessionStorage.removeItem('calendar_deleted_time'); + sessionStorage.removeItem('disable_calendar_backend_check'); + // Keep calendar_manually_deleted for permanent protection + } catch { + // Ignore storage errors + } + } + + // Debug: log storage state on component mount + logStorageState(); }, []); useEffect(() => { @@ -539,6 +888,20 @@ export default function IntegrationsSection() { )} + {/* Debug button (development only) */} + {process.env.NODE_ENV === 'development' && ( + + + + )} + @@ -555,7 +918,17 @@ export default function IntegrationsSection() { /> - {getDisplayEmail()} + {getDisplayEmailInfo().email} + {getDisplayEmailInfo().source && ( + + ({getDisplayEmailInfo().source}) + + )} Sync your appointments to Google Calendar. Online booking diff --git a/src/app/auth/callback/AuthCallbackContent.tsx b/src/app/auth/callback/AuthCallbackContent.tsx index 5753fc3..62a819b 100644 --- a/src/app/auth/callback/AuthCallbackContent.tsx +++ b/src/app/auth/callback/AuthCallbackContent.tsx @@ -65,14 +65,18 @@ export default function AuthCallbackContent() { }), ); - // Persist current user to sessionStorage for front-end flows (e.g., OAuth) + // Persist current user to localStorage for front-end flows (e.g., OAuth) + // Only set if not already exists to avoid overwriting manual deletions try { - // sessionStorage.setItem('userId', parsedUser._id or localStorage.setItem('userId', parsedUser._id); - localStorage.setItem('userId', parsedUser._id); - // sessionStorage.setItem('userEmail', parsedUser.email); - localStorage.setItem('userEmail', parsedUser.email); - // sessionStorage.setItem('user', JSON.stringify(parsedUser)); - localStorage.setItem('user', JSON.stringify(parsedUser)); + if (!localStorage.getItem('userId')) { + localStorage.setItem('userId', parsedUser._id); + } + if (!localStorage.getItem('userEmail')) { + localStorage.setItem('userEmail', parsedUser.email); + } + if (!localStorage.getItem('user')) { + localStorage.setItem('user', JSON.stringify(parsedUser)); + } } catch { // ignore storage errors } From 5b4bc00e1a906c07b63d4dcaba5120e360fe59fe Mon Sep 17 00:00:00 2001 From: huiii421 Date: Thu, 23 Oct 2025 03:26:13 +1100 Subject: [PATCH 6/8] refactor: remove debug code and console logs from calendar integration --- .../admin/settings/CalendarIntegrations.tsx | 72 +------------------ 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 737e919..3d5af88 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -170,7 +170,7 @@ export default function IntegrationsSection() { return googleEmail ?? loginEmail ?? userEmail; }; - // Get display email with source label for UI/debug + // Get display email with source label for UI const getDisplayEmailInfo = (): { email: string | null; source: 'google' | 'login' | 'fallback' | null; @@ -180,33 +180,6 @@ export default function IntegrationsSection() { return { email: userEmail, source: userEmail ? 'fallback' : null }; }; - // Debug function to log current storage state - const logStorageState = () => { - if (typeof window === 'undefined') return; - console.log('=== Calendar Storage Debug ==='); - console.log('sessionStorage userId:', sessionStorage.getItem('userId')); - console.log( - 'sessionStorage userEmail:', - sessionStorage.getItem('userEmail'), - ); - console.log('localStorage userId:', localStorage.getItem('userId')); - console.log('localStorage userEmail:', localStorage.getItem('userEmail')); - console.log( - 'localStorage calendarConnectedEmail:', - localStorage.getItem(CONNECTED_EMAIL_KEY), - ); - console.log( - 'localStorage persist:root exists:', - !!localStorage.getItem('persist:root'), - ); - console.log('State - isConnected:', isConnected); - console.log('State - googleEmail:', googleEmail); - console.log('State - loginEmail:', loginEmail); - console.log('State - userEmail:', userEmail); - console.log('State - showEmailTypeWarning:', showEmailTypeWarning); - console.log('Current URL:', window.location.href); - console.log('=============================='); - }; // Check if email is Gmail const isGmailEmail = (email: string | null): boolean => { @@ -432,14 +405,11 @@ export default function IntegrationsSection() { if (deleteResponse.ok) { const result = (await deleteResponse.json()) as { message?: string }; - console.log('Calendar token deleted successfully:', result.message); - // Clean up localStorage immediately after successful deletion try { localStorage.removeItem(CONNECTED_EMAIL_KEY); - console.log('localStorage calendarConnectedEmail cleared'); } catch { - console.error('Failed to clear localStorage calendarConnectedEmail'); + // Ignore storage errors } // Reset local state immediately @@ -503,11 +473,8 @@ export default function IntegrationsSection() { // Clean up localStorage as fallback try { localStorage.removeItem(CONNECTED_EMAIL_KEY); - console.log('localStorage calendarConnectedEmail cleared (fallback)'); } catch { - console.error( - 'Failed to clear localStorage calendarConnectedEmail (fallback)', - ); + // Ignore storage errors } // Add flag to prevent re-checking backend after fallback cleanup @@ -643,8 +610,6 @@ export default function IntegrationsSection() { sessionStorage.getItem('disable_calendar_backend_check') === 'true' || sessionStorage.getItem('calendar_manually_deleted') === 'true'); - console.log('Is callback scenario:', isCallback); - console.log('Just deleted calendar:', justDeletedCalendar); if (!userId) { // Fallback: try common storage keys/global variables const fromStore = readUserFromStorage(); @@ -703,11 +668,6 @@ export default function IntegrationsSection() { isRecentDeletion || disableBackendCheck === 'true' ) { - console.log('Skipping backend check - calendar was just deleted', { - justDeleted, - isRecentDeletion, - disableBackendCheck, - }); // Clear the flags try { sessionStorage.removeItem('calendar_deleted'); @@ -764,16 +724,6 @@ export default function IntegrationsSection() { } catch { // Ignore storage errors } - } else { - console.log( - 'Skipping localStorage write for calendarConnectedEmail:', - { - hasExisting: !!localStorage.getItem(CONNECTED_EMAIL_KEY), - justDeleted, - disableBackendCheck, - manuallyDeleted, - }, - ); } // Update email display in calendar list setCalendars(prev => @@ -812,7 +762,6 @@ export default function IntegrationsSection() { if (!justDeletedCalendar) { void checkValid(); } else { - console.log('Skipping all backend checks - calendar was just deleted'); // Clear the temporary flags but keep the permanent one try { sessionStorage.removeItem('calendar_deleted'); @@ -824,8 +773,6 @@ export default function IntegrationsSection() { } } - // Debug: log storage state on component mount - logStorageState(); }, []); useEffect(() => { @@ -888,19 +835,6 @@ export default function IntegrationsSection() { )} - {/* Debug button (development only) */} - {process.env.NODE_ENV === 'development' && ( - - - - )} From 5e7c7582381da4501875a6a864409c8722dd89b6 Mon Sep 17 00:00:00 2001 From: huiii421 Date: Thu, 23 Oct 2025 04:45:38 +1100 Subject: [PATCH 7/8] Modify the location of EmailTypeWarning --- .../admin/settings/CalendarIntegrations.tsx | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 227f15c..819b2a5 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -849,8 +849,31 @@ export default function IntegrationsSection({ return ( <> - - + + + + {/* Pro badge */} + {showProBadge && !editable && ( + + Pro + PRO + + )} + {/* Email type warning */} {showEmailTypeWarning && !isConnected && ( @@ -860,32 +883,10 @@ export default function IntegrationsSection({ )} - - - {/* Pro badge */} - {showProBadge && !editable && ( - - Pro - PRO - - )} + From 283d9e052a2aa8acdda15346b8c8f184d717f109 Mon Sep 17 00:00:00 2001 From: huiii421 Date: Thu, 23 Oct 2025 05:02:34 +1100 Subject: [PATCH 8/8] fix bugs for lint --- src/app/admin/settings/CalendarIntegrations.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/admin/settings/CalendarIntegrations.tsx b/src/app/admin/settings/CalendarIntegrations.tsx index 819b2a5..973ca87 100644 --- a/src/app/admin/settings/CalendarIntegrations.tsx +++ b/src/app/admin/settings/CalendarIntegrations.tsx @@ -195,7 +195,6 @@ export default function IntegrationsSection({ return { email: userEmail, source: userEmail ? 'fallback' : null }; }; - // Check if email is Gmail const isGmailEmail = (email: string | null): boolean => { if (!email) return false; @@ -315,7 +314,6 @@ export default function IntegrationsSection({ }; const handleConnect = () => { - if (!editable) return; // Clear any deletion flags when connecting @@ -797,7 +795,6 @@ export default function IntegrationsSection({ // Ignore storage errors } } - }, []); useEffect(() => { @@ -885,8 +882,6 @@ export default function IntegrationsSection({ )} - - @@ -925,7 +920,10 @@ export default function IntegrationsSection({ {isConnected ? ( - + void handleRemove()} + disabled={!editable} + > Remove ) : editable ? (