diff --git a/components/pages/calendar/EventCalendar.tsx b/components/pages/calendar/EventCalendar.tsx index 28e2133..d893796 100644 --- a/components/pages/calendar/EventCalendar.tsx +++ b/components/pages/calendar/EventCalendar.tsx @@ -42,6 +42,7 @@ export const EventCalendar: React.FC = React.memo( const {isSyncing} = useSyncEvents() const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes()); + useCalSync() const todaysDate = new Date(); diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index 1ab0a61..cd60b22 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -164,19 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) => } }, [user, ready, redirectOverride]); - useEffect(() => { - const handleNotification = async (notification: Notifications.Notification) => { - const eventId = notification?.request?.content?.data?.eventId; - - // if (eventId) { - queryClient.invalidateQueries(['events']); - // } - }; - - const sub = Notifications.addNotificationReceivedListener(handleNotification); - - return () => sub.remove(); - }, []); + // useEffect(() => { + // const handleNotification = async (notification: Notifications.Notification) => { + // const eventId = notification?.request?.content?.data?.eventId; + // + // // if (eventId) { + // queryClient.invalidateQueries(['events']); + // // } + // }; + // + // const sub = Notifications.addNotificationReceivedListener(handleNotification); + // + // return () => sub.remove(); + // }, []); if (!ready) { return null; diff --git a/firebase/functions/index.js b/firebase/functions/index.js index 24ab442..8ca6724 100644 --- a/firebase/functions/index.js +++ b/firebase/functions/index.js @@ -15,7 +15,7 @@ let eventCount = 0; let pushTokens = []; const GOOGLE_CALENDAR_ID = "primary"; -const CHANNEL_ID = "unique-channel-id"; +const CHANNEL_ID = "cally-family-calendar"; const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification"; @@ -25,10 +25,10 @@ exports.sendNotificationOnEventCreation = functions.firestore const eventData = snapshot.data(); const { familyId, creatorId, email } = eventData; - if (email) { - console.log('Event has an email field. Skipping notification.'); - return; - } + // if (email) { + // console.log('Event has an email field. Skipping notification.'); + // return; + // } if (!familyId || !creatorId) { console.error('Missing familyId or creatorId in event data'); @@ -199,8 +199,8 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn for (const googleEmail of Object.keys(profileData?.googleAccounts)) { const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken; if (googleToken) { - const refreshedGoogleToken = await refreshGoogleToken(googleToken); - const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken}; + const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken); + const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}}; await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts}); console.log(`Google token updated for user ${profileDoc.id}`); } @@ -244,21 +244,6 @@ exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(asyn return null; }); -async function refreshGoogleToken(refreshToken) { - try { - const response = await axios.post('https://oauth2.googleapis.com/token', { - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig - }); - - return response.data.access_token; // Return the new access token - } catch (error) { - console.error("Error refreshing Google token:", error); - throw error; - } -} - async function refreshMicrosoftToken(refreshToken) { try { const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { @@ -311,148 +296,256 @@ async function removeInvalidPushToken(pushToken) { // TODO } +const fetch = require("node-fetch"); + +// Function to refresh Google Token with additional logging async function refreshGoogleToken(refreshToken) { try { - const response = await axios.post("https://oauth2.googleapis.com/token", { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", + console.log("Refreshing Google token..."); + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", + }), }); - return response.data.access_token; + + if (!response.ok) { + const errorData = await response.json(); + console.error("Error refreshing Google token:", errorData); + throw new Error(`Failed to refresh Google token: ${errorData.error || response.statusText}`); + } + + const data = await response.json(); + console.log("Google token refreshed successfully"); + + // Return both the access token and refresh token (if a new one is provided) + return { + refreshedGoogleToken: data.access_token, + refreshedRefreshToken: data.refresh_token || refreshToken, // Return the existing refresh token if a new one is not provided + }; } catch (error) { console.error("Error refreshing Google token:", error.message); throw error; } } -// Helper function to get Google access tokens for all users and refresh them if needed +// Helper function to get Google access tokens for all users and refresh them if needed with logging async function getGoogleAccessTokens() { + console.log("Fetching Google access tokens for all users..."); const tokens = {}; const profilesSnapshot = await db.collection("Profiles").get(); await Promise.all( profilesSnapshot.docs.map(async (doc) => { const profileData = doc.data(); - const googleAccounts = profileData.googleAccounts || {}; + const googleAccounts = profileData?.googleAccounts || {}; for (const googleEmail of Object.keys(googleAccounts)) { - const { refreshToken } = googleAccounts[googleEmail]; + // Check if the googleAccount entry exists and has a refreshToken + const accountInfo = googleAccounts[googleEmail]; + const refreshToken = accountInfo?.refreshToken; + if (refreshToken) { try { - const accessToken = await refreshGoogleToken(refreshToken); + console.log(`Refreshing token for user ${doc.id} (email: ${googleEmail})`); + const {refreshedGoogleToken} = await refreshGoogleToken(refreshToken); tokens[doc.id] = accessToken; + console.log(`Token refreshed successfully for user ${doc.id}`); } catch (error) { + tokens[doc.id] = accountInfo?.accessToken; console.error(`Failed to refresh token for user ${doc.id}:`, error.message); } + } else { + console.log(`No refresh token available for user ${doc.id} (email: ${googleEmail})`); } } }) ); + console.log("Access tokens fetched and refreshed as needed"); return tokens; } -// Function to watch Google Calendar events +// Function to watch Google Calendar events with additional logging const watchCalendarEvents = async (userId, token) => { const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`; - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - id: `${CHANNEL_ID}-${userId}`, // Unique ID per user - type: "web_hook", - address: `${WEBHOOK_URL}?userId=${userId}`, // Pass userId to identify notifications - params: { - ttl: "80000", // Set to 20 hours in seconds + // Verify the token is valid + console.log(`Attempting to watch calendar for user ${userId}`); + console.log(`Token being used: ${token ? 'present' : 'missing'}`); + console.log(`Calendar ID: ${GOOGLE_CALENDAR_ID}`); + console.log(`Webhook URL: ${WEBHOOK_URL}?userId=${userId}`); + + try { + // Test the token first + const testResponse = await fetch( + `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events?maxResults=1`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + if (!testResponse.ok) { + console.error(`Token validation failed for user ${userId}:`, await testResponse.text()); + throw new Error('Token validation failed'); + } + + console.log(`Token validated successfully for user ${userId}`); + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - }), - }); + body: JSON.stringify({ + id: `${CHANNEL_ID}-${userId}`, + type: "web_hook", + address: `${WEBHOOK_URL}?userId=${userId}`, + params: { + ttl: "80000", + }, + }), + }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(`Failed to watch calendar: ${errorData.error?.message || response.statusText}`); + const responseText = await response.text(); + console.log(`Watch response for user ${userId}:`, responseText); + + if (!response.ok) { + console.error(`Failed to watch calendar for user ${userId}:`, responseText); + throw new Error(`Failed to watch calendar: ${responseText}`); + } + + const result = JSON.parse(responseText); + console.log(`Successfully set up Google Calendar watch for user ${userId}`, result); + + // Store the watch details in Firestore for monitoring + await db.collection('CalendarWatches').doc(userId).set({ + watchId: result.id, + resourceId: result.resourceId, + expiration: result.expiration, + createdAt: admin.firestore.FieldValue.serverTimestamp(), + }); + + return result; + } catch (error) { + console.error(`Error in watchCalendarEvents for user ${userId}:`, error); + // Store the error in Firestore for monitoring + await db.collection('CalendarWatchErrors').add({ + userId, + error: error.message, + timestamp: admin.firestore.FieldValue.serverTimestamp(), + }); + throw error; } - - return response.json(); }; -// Schedule function to renew Google Calendar watch every 20 hours for each user -exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 20 hours").onRun(async (context) => { +// Add this to test webhook connectivity +exports.testWebhook = functions.https.onRequest(async (req, res) => { + console.log('Test webhook received'); + console.log('Headers:', req.headers); + console.log('Body:', req.body); + console.log('Query:', req.query); + + res.status(200).send('Test webhook received successfully'); +}); + +// Schedule function to renew Google Calendar watch every 20 hours for each user with logging +exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 10 minutes").onRun(async (context) => { + console.log("Starting Google Calendar watch renewal process..."); try { const tokens = await getGoogleAccessTokens(); + console.log("Tokens: ", tokens); for (const [userId, token] of Object.entries(tokens)) { try { - const result = await watchCalendarEvents(userId, token); - console.log(`Successfully renewed Google Calendar watch for user ${userId}:`, result); + await watchCalendarEvents(userId, token); } catch (error) { console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message); } } + + console.log("Google Calendar watch renewal process completed"); } catch (error) { console.error("Error in renewGoogleCalendarWatch function:", error.message); } }); -// Function to handle notifications from Google Calendar for a specific user +// Function to handle notifications from Google Calendar with additional logging exports.sendSyncNotification = functions.https.onRequest(async (req, res) => { const userId = req.query.userId; // Extract userId from query params const calendarId = req.body.resourceId; - // Fetch push tokens for the specific user - const userDoc = await db.collection("Profiles").doc(userId).get(); - const userData = userDoc.data(); - const pushTokens = userData ? userData.pushToken : []; + console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`); - if (pushTokens.length === 0) { - console.log(`No push tokens found for user ${userId}`); - res.status(200).send("No push tokens found for user."); - return; - } + try { + // Fetch push tokens for the specific user + const userDoc = await db.collection("Profiles").doc(userId).get(); + const userData = userDoc.data(); - const syncMessage = "New events have been synced."; - - let messages = pushTokens.map(pushToken => { - if (!Expo.isExpoPushToken(pushToken)) { - console.error(`Push token ${pushToken} is not a valid Expo push token`); - return null; + // Ensure pushTokens is an array + let pushTokens = []; + if (userData && userData.pushToken) { + pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken]; } - return { - to: pushToken, - sound: "default", - title: "Event Sync", - body: syncMessage, - data: { userId, calendarId }, - }; - }).filter(Boolean); + if (pushTokens.length === 0) { + console.log(`No push tokens found for user ${userId}`); + res.status(200).send("No push tokens found for user."); + return; + } - let chunks = expo.chunkPushNotifications(messages); - let tickets = []; + const syncMessage = "New events have been synced."; - for (let chunk of chunks) { - try { - let ticketChunk = await expo.sendPushNotificationsAsync(chunk); - tickets.push(...ticketChunk); + let messages = pushTokens.map(pushToken => { + if (!Expo.isExpoPushToken(pushToken)) { + console.error(`Push token ${pushToken} is not a valid Expo push token`); + return null; + } - for (let ticket of ticketChunk) { - if (ticket.status === "ok") { - console.log("Notification successfully sent:", ticket.id); - } else if (ticket.status === "error") { - console.error(`Notification error: ${ticket.message}`); - if (ticket.details?.error === "DeviceNotRegistered") { - await removeInvalidPushToken(ticket.to); + return { + to: pushToken, + sound: "default", + title: "Event Sync", + body: syncMessage, + data: { userId, calendarId }, + }; + }).filter(Boolean); + + let chunks = expo.chunkPushNotifications(messages); + let tickets = []; + + for (let chunk of chunks) { + try { + let ticketChunk = await expo.sendPushNotificationsAsync(chunk); + tickets.push(...ticketChunk); + + for (let ticket of ticketChunk) { + if (ticket.status === "ok") { + console.log("Notification successfully sent:", ticket.id); + } else if (ticket.status === "error") { + console.error(`Notification error: ${ticket.message}`); + if (ticket.details?.error === "DeviceNotRegistered") { + await removeInvalidPushToken(ticket.to); + } } } + } catch (error) { + console.error("Error sending notification:", error.message); } - } catch (error) { - console.error("Error sending notification:", error.message); } - } - res.status(200).send("Sync notification sent."); + console.log(`Sync notification sent for user ${userId}`); + res.status(200).send("Sync notification sent."); + } catch (error) { + console.error(`Error in sendSyncNotification for user ${userId}:`, error.message); + res.status(500).send("Failed to send sync notification."); + } }); \ No newline at end of file diff --git a/hooks/firebase/useClearTokens.ts b/hooks/firebase/useClearTokens.ts index 35fd305..8d8fa12 100644 --- a/hooks/firebase/useClearTokens.ts +++ b/hooks/firebase/useClearTokens.ts @@ -17,20 +17,23 @@ export const useClearTokens = () => { if (provider === "google") { let googleAccounts = profileData?.googleAccounts; if (googleAccounts) { - googleAccounts[email] = null; - newUserData.googleAccounts = googleAccounts; + const newGoogleAccounts = {...googleAccounts} + delete newGoogleAccounts[email]; + newUserData.googleAccounts = newGoogleAccounts; } } else if (provider === "outlook") { let microsoftAccounts = profileData?.microsoftAccounts; if (microsoftAccounts) { - microsoftAccounts[email] = null; - newUserData.microsoftAccounts = microsoftAccounts; + const newMicrosoftAccounts = {...microsoftAccounts} + delete microsoftAccounts[email]; + newUserData.microsoftAccounts = newMicrosoftAccounts; } } else if (provider === "apple") { let appleAccounts = profileData?.appleAccounts; if (appleAccounts) { - appleAccounts[email] = null; - newUserData.appleAccounts = appleAccounts; + const newAppleAccounts = {...appleAccounts} + delete newAppleAccounts[email]; + newUserData.appleAccounts = newAppleAccounts; } } await updateUserData({newUserData}); diff --git a/hooks/useCalSync.ts b/hooks/useCalSync.ts index 42620e1..4f670a2 100644 --- a/hooks/useCalSync.ts +++ b/hooks/useCalSync.ts @@ -82,6 +82,8 @@ export const useCalSync = () => { [googleMail]: {accessToken, refreshToken}, }; + console.log({refreshToken}) + await updateUserData({ newUserData: {googleAccounts: updatedGoogleAccounts}, }); @@ -243,8 +245,11 @@ export const useCalSync = () => { const syncPromises: Promise[] = []; if (profileData?.googleAccounts) { - for (const [email, { accessToken, refreshToken }] of Object.entries(profileData.googleAccounts)) { - syncPromises.push(fetchAndSaveGoogleEvents({ token: accessToken, refreshToken, email })); + console.log(profileData.googleAccounts) + for (const [email, emailAcc] of Object.entries(profileData.googleAccounts)) { + if(emailAcc?.accessToken) { + syncPromises.push(fetchAndSaveGoogleEvents({ token: emailAcc?.accessToken, refreshToken: emailAcc?.refreshToken, email })); + } } } @@ -304,7 +309,7 @@ export const useCalSync = () => { const eventId = notification?.request?.content?.data?.eventId; await resyncAllCalendars(); - queryClient.invalidateQueries(["events"]); + // queryClient.invalidateQueries(["events"]); }; const sub = Notifications.addNotificationReceivedListener(handleNotification); diff --git a/ios/cally/Info.plist b/ios/cally/Info.plist index 4c9788a..35d74d5 100644 --- a/ios/cally/Info.plist +++ b/ios/cally/Info.plist @@ -153,6 +153,7 @@ $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route UILaunchStoryboardName SplashScreen