sync update

This commit is contained in:
Milan Paunovic
2024-11-02 22:27:17 +01:00
parent 8a0370933d
commit f1b0bcd32d
6 changed files with 221 additions and 118 deletions

View File

@ -42,6 +42,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const {isSyncing} = useSyncEvents() const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes()); const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useCalSync()
const todaysDate = new Date(); const todaysDate = new Date();

View File

@ -164,19 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
} }
}, [user, ready, redirectOverride]); }, [user, ready, redirectOverride]);
useEffect(() => { // useEffect(() => {
const handleNotification = async (notification: Notifications.Notification) => { // const handleNotification = async (notification: Notifications.Notification) => {
const eventId = notification?.request?.content?.data?.eventId; // const eventId = notification?.request?.content?.data?.eventId;
//
// if (eventId) { // // if (eventId) {
queryClient.invalidateQueries(['events']); // queryClient.invalidateQueries(['events']);
// } // // }
}; // };
//
const sub = Notifications.addNotificationReceivedListener(handleNotification); // const sub = Notifications.addNotificationReceivedListener(handleNotification);
//
return () => sub.remove(); // return () => sub.remove();
}, []); // }, []);
if (!ready) { if (!ready) {
return null; return null;

View File

@ -15,7 +15,7 @@ let eventCount = 0;
let pushTokens = []; let pushTokens = [];
const GOOGLE_CALENDAR_ID = "primary"; 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"; 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 eventData = snapshot.data();
const { familyId, creatorId, email } = eventData; const { familyId, creatorId, email } = eventData;
if (email) { // if (email) {
console.log('Event has an email field. Skipping notification.'); // console.log('Event has an email field. Skipping notification.');
return; // return;
} // }
if (!familyId || !creatorId) { if (!familyId || !creatorId) {
console.error('Missing familyId or creatorId in event data'); 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)) { for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken; const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) { if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken); const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken}; const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}};
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts}); await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
console.log(`Google token updated for user ${profileDoc.id}`); 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; 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) { async function refreshMicrosoftToken(refreshToken) {
try { try {
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
@ -311,51 +296,109 @@ async function removeInvalidPushToken(pushToken) {
// TODO // TODO
} }
const fetch = require("node-fetch");
// Function to refresh Google Token with additional logging
async function refreshGoogleToken(refreshToken) { async function refreshGoogleToken(refreshToken) {
try { try {
const response = await axios.post("https://oauth2.googleapis.com/token", { 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", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", 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) { } catch (error) {
console.error("Error refreshing Google token:", error.message); console.error("Error refreshing Google token:", error.message);
throw error; 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() { async function getGoogleAccessTokens() {
console.log("Fetching Google access tokens for all users...");
const tokens = {}; const tokens = {};
const profilesSnapshot = await db.collection("Profiles").get(); const profilesSnapshot = await db.collection("Profiles").get();
await Promise.all( await Promise.all(
profilesSnapshot.docs.map(async (doc) => { profilesSnapshot.docs.map(async (doc) => {
const profileData = doc.data(); const profileData = doc.data();
const googleAccounts = profileData.googleAccounts || {}; const googleAccounts = profileData?.googleAccounts || {};
for (const googleEmail of Object.keys(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) { if (refreshToken) {
try { 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; tokens[doc.id] = accessToken;
console.log(`Token refreshed successfully for user ${doc.id}`);
} catch (error) { } catch (error) {
tokens[doc.id] = accountInfo?.accessToken;
console.error(`Failed to refresh token for user ${doc.id}:`, error.message); 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; return tokens;
} }
// Function to watch Google Calendar events // Function to watch Google Calendar events with additional logging
const watchCalendarEvents = async (userId, token) => { const watchCalendarEvents = async (userId, token) => {
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`; const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
// 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, { const response = await fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
@ -363,50 +406,95 @@ const watchCalendarEvents = async (userId, token) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
id: `${CHANNEL_ID}-${userId}`, // Unique ID per user id: `${CHANNEL_ID}-${userId}`,
type: "web_hook", type: "web_hook",
address: `${WEBHOOK_URL}?userId=${userId}`, // Pass userId to identify notifications address: `${WEBHOOK_URL}?userId=${userId}`,
params: { params: {
ttl: "80000", // Set to 20 hours in seconds ttl: "80000",
}, },
}), }),
}); });
const responseText = await response.text();
console.log(`Watch response for user ${userId}:`, responseText);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); console.error(`Failed to watch calendar for user ${userId}:`, responseText);
throw new Error(`Failed to watch calendar: ${errorData.error?.message || response.statusText}`); throw new Error(`Failed to watch calendar: ${responseText}`);
} }
return response.json(); 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;
}
}; };
// Schedule function to renew Google Calendar watch every 20 hours for each user // Add this to test webhook connectivity
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 20 hours").onRun(async (context) => { 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 { try {
const tokens = await getGoogleAccessTokens(); const tokens = await getGoogleAccessTokens();
console.log("Tokens: ", tokens);
for (const [userId, token] of Object.entries(tokens)) { for (const [userId, token] of Object.entries(tokens)) {
try { try {
const result = await watchCalendarEvents(userId, token); await watchCalendarEvents(userId, token);
console.log(`Successfully renewed Google Calendar watch for user ${userId}:`, result);
} catch (error) { } catch (error) {
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message); console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
} }
} }
console.log("Google Calendar watch renewal process completed");
} catch (error) { } catch (error) {
console.error("Error in renewGoogleCalendarWatch function:", error.message); 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) => { exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
const userId = req.query.userId; // Extract userId from query params const userId = req.query.userId; // Extract userId from query params
const calendarId = req.body.resourceId; const calendarId = req.body.resourceId;
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
try {
// Fetch push tokens for the specific user // Fetch push tokens for the specific user
const userDoc = await db.collection("Profiles").doc(userId).get(); const userDoc = await db.collection("Profiles").doc(userId).get();
const userData = userDoc.data(); const userData = userDoc.data();
const pushTokens = userData ? userData.pushToken : [];
// Ensure pushTokens is an array
let pushTokens = [];
if (userData && userData.pushToken) {
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
}
if (pushTokens.length === 0) { if (pushTokens.length === 0) {
console.log(`No push tokens found for user ${userId}`); console.log(`No push tokens found for user ${userId}`);
@ -454,5 +542,10 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
} }
} }
console.log(`Sync notification sent for user ${userId}`);
res.status(200).send("Sync notification sent."); 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.");
}
}); });

View File

@ -17,20 +17,23 @@ export const useClearTokens = () => {
if (provider === "google") { if (provider === "google") {
let googleAccounts = profileData?.googleAccounts; let googleAccounts = profileData?.googleAccounts;
if (googleAccounts) { if (googleAccounts) {
googleAccounts[email] = null; const newGoogleAccounts = {...googleAccounts}
newUserData.googleAccounts = googleAccounts; delete newGoogleAccounts[email];
newUserData.googleAccounts = newGoogleAccounts;
} }
} else if (provider === "outlook") { } else if (provider === "outlook") {
let microsoftAccounts = profileData?.microsoftAccounts; let microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) { if (microsoftAccounts) {
microsoftAccounts[email] = null; const newMicrosoftAccounts = {...microsoftAccounts}
newUserData.microsoftAccounts = microsoftAccounts; delete microsoftAccounts[email];
newUserData.microsoftAccounts = newMicrosoftAccounts;
} }
} else if (provider === "apple") { } else if (provider === "apple") {
let appleAccounts = profileData?.appleAccounts; let appleAccounts = profileData?.appleAccounts;
if (appleAccounts) { if (appleAccounts) {
appleAccounts[email] = null; const newAppleAccounts = {...appleAccounts}
newUserData.appleAccounts = appleAccounts; delete newAppleAccounts[email];
newUserData.appleAccounts = newAppleAccounts;
} }
} }
await updateUserData({newUserData}); await updateUserData({newUserData});

View File

@ -82,6 +82,8 @@ export const useCalSync = () => {
[googleMail]: {accessToken, refreshToken}, [googleMail]: {accessToken, refreshToken},
}; };
console.log({refreshToken})
await updateUserData({ await updateUserData({
newUserData: {googleAccounts: updatedGoogleAccounts}, newUserData: {googleAccounts: updatedGoogleAccounts},
}); });
@ -243,8 +245,11 @@ export const useCalSync = () => {
const syncPromises: Promise<void>[] = []; const syncPromises: Promise<void>[] = [];
if (profileData?.googleAccounts) { if (profileData?.googleAccounts) {
for (const [email, { accessToken, refreshToken }] of Object.entries(profileData.googleAccounts)) { console.log(profileData.googleAccounts)
syncPromises.push(fetchAndSaveGoogleEvents({ token: accessToken, refreshToken, email })); 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; const eventId = notification?.request?.content?.data?.eventId;
await resyncAllCalendars(); await resyncAllCalendars();
queryClient.invalidateQueries(["events"]); // queryClient.invalidateQueries(["events"]);
}; };
const sub = Notifications.addNotificationReceivedListener(handleNotification); const sub = Notifications.addNotificationReceivedListener(handleNotification);

View File

@ -153,6 +153,7 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen</string> <string>SplashScreen</string>