diff --git a/calendar-integration/google-calendar-utils.js b/calendar-integration/google-calendar-utils.js
index d95d279..59709c5 100644
--- a/calendar-integration/google-calendar-utils.js
+++ b/calendar-integration/google-calendar-utils.js
@@ -1,50 +1,64 @@
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
- const response = await fetch(
- `https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
- {
+ const googleEvents = [];
+ let pageToken = null;
+
+ do {
+ const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
+ url.searchParams.set('singleEvents', 'true');
+ url.searchParams.set('timeMin', startDate);
+ url.searchParams.set('timeMax', endDate);
+ if (pageToken) url.searchParams.set('pageToken', pageToken);
+
+ const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
- },
- );
+ });
- const data = await response.json();
- const googleEvents = [];
+ const data = await response.json();
- data.items?.forEach((item) => {
- let isAllDay = false;
- let startDateTime, endDateTime;
-
- if (item.start) {
- if (item.start.dateTime) {
- startDateTime = new Date(item.start.dateTime);
- } else if (item.start.date) {
- startDateTime = new Date(item.start.date);
- isAllDay = true;
- }
+ if (!response.ok) {
+ throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
}
- if (item.end) {
- if (item.end.dateTime) {
- endDateTime = new Date(item.end.dateTime);
- } else if (item.end.date) {
- endDateTime = new Date(item.end.date);
- isAllDay = true;
+ data.items?.forEach((item) => {
+ let isAllDay = false;
+ let startDateTime, endDateTime;
+
+ if (item.start) {
+ if (item.start.dateTime) {
+ startDateTime = new Date(item.start.dateTime);
+ } else if (item.start.date) {
+ startDateTime = new Date(item.start.date);
+ isAllDay = true;
+ }
}
- }
- const googleEvent = {
- id: item.id,
- title: item.summary || "",
- startDate: startDateTime,
- endDate: endDateTime,
- allDay: isAllDay,
- familyId,
- email,
- };
+ if (item.end) {
+ if (item.end.dateTime) {
+ endDateTime = new Date(item.end.dateTime);
+ } else if (item.end.date) {
+ endDateTime = new Date(item.end.date);
+ isAllDay = true;
+ }
+ }
- googleEvents.push(googleEvent);
- });
+ const googleEvent = {
+ id: item.id,
+ title: item.summary || "",
+ startDate: startDateTime,
+ endDate: endDateTime,
+ allDay: isAllDay,
+ familyId,
+ email,
+ };
- return {googleEvents, success: response.ok};
+ googleEvents.push(googleEvent);
+ });
+
+ // Prepare for the next page if it exists
+ pageToken = data.nextPageToken;
+ } while (pageToken);
+
+ return { googleEvents, success: true };
}
\ No newline at end of file
diff --git a/components/pages/calendar/CalendarHeader.tsx b/components/pages/calendar/CalendarHeader.tsx
index 403e63f..c77de27 100644
--- a/components/pages/calendar/CalendarHeader.tsx
+++ b/components/pages/calendar/CalendarHeader.tsx
@@ -14,6 +14,7 @@ import { useAtom } from "jotai";
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
import { format, isSameDay } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext";
+import {useIsMutating} from "react-query";
export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
diff --git a/components/pages/calendar/CalendarPage.tsx b/components/pages/calendar/CalendarPage.tsx
index eddd7d5..0118f6c 100644
--- a/components/pages/calendar/CalendarPage.tsx
+++ b/components/pages/calendar/CalendarPage.tsx
@@ -11,7 +11,7 @@ export default function CalendarPage() {
paddingT-0
>
diff --git a/components/pages/calendar/EventCalendar.tsx b/components/pages/calendar/EventCalendar.tsx
index 0336d5b..cd7248e 100644
--- a/components/pages/calendar/EventCalendar.tsx
+++ b/components/pages/calendar/EventCalendar.tsx
@@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {Calendar} from "react-native-big-calendar";
-import {ActivityIndicator, StyleSheet, View, ViewStyle} from "react-native";
+import {ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai";
import {
@@ -14,6 +14,9 @@ import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
+import {useCalSync} from "@/hooks/useCalSync";
+import { useIsMutating } from "react-query";
+import {useSyncEvents} from "@/hooks/useSyncOnScroll";
interface EventCalendarProps {
calendarHeight: number;
@@ -37,6 +40,7 @@ export const EventCalendar: React.FC = React.memo(
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
+ const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
const todaysDate = new Date();
@@ -75,7 +79,7 @@ export const EventCalendar: React.FC = React.memo(
);
const memoizedEventCellStyle = useCallback(
- (event: CalendarEvent) => ({backgroundColor: event.eventColor}),
+ (event: CalendarEvent) => ({backgroundColor: event.eventColor, fontSize: 14}),
[]
);
@@ -211,6 +215,7 @@ export const EventCalendar: React.FC = React.memo(
if (isLoading) {
return (
+ {isSyncing && Syncing...}
);
@@ -219,52 +224,65 @@ export const EventCalendar: React.FC = React.memo(
// console.log(enrichedEvents, filteredEvents)
return (
-
+ {isSyncing && (
+
+ {isSyncing && Syncing...}
+
+
+ )}
+
+ }}
+ dayHeaderStyle={dateStyle}
+ dayHeaderHighlightColor={"white"}
+ showAdjacentMonths
+ // headerContainerStyle={mode !== "month" ? {
+ // overflow:"hidden",
+ // height: 12,
+ // } : {}}
+ hourStyle={styles.hourStyle}
+ ampm
+ // renderCustomDateForMonth={renderCustomDateForMonth}
+ />
+ >
+
);
}
);
@@ -291,6 +309,11 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: "center",
alignItems: "center",
+ position: "absolute",
+ width: "100%",
+ height: "100%",
+ zIndex: 100,
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
},
dayHeader: {
backgroundColor: "#4184f2",
diff --git a/firebase/functions/index.js b/firebase/functions/index.js
index 704507b..a524e50 100644
--- a/firebase/functions/index.js
+++ b/firebase/functions/index.js
@@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
}
});
-exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
+exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(async (context) => {
console.log('Running token refresh job...');
const profilesSnapshot = await db.collection('Profiles').get();
diff --git a/hooks/firebase/useGetEvents.ts b/hooks/firebase/useGetEvents.ts
index 3b7254a..65924fd 100644
--- a/hooks/firebase/useGetEvents.ts
+++ b/hooks/firebase/useGetEvents.ts
@@ -20,6 +20,7 @@ export const useGetEvents = () => {
// If family view is active, include family, creator, and attendee events
if (isFamilyView) {
+
const familyQuery = db.collection("Events").where("familyID", "==", familyId);
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
@@ -83,7 +84,7 @@ export const useGetEvents = () => {
const eventColor = profileData?.eventColor || colorMap.pink;
return {
- id: event.id || Math.random().toString(36).substr(2, 9), // Generate temp ID if missing
+ id: event.id || Math.random().toString(36).slice(2, 9), // Generate temp ID if missing
title: event.title,
start: new Date(event.startDate.seconds * 1000),
end: new Date(event.endDate.seconds * 1000),
@@ -96,5 +97,6 @@ export const useGetEvents = () => {
},
staleTime: Infinity,
cacheTime: Infinity,
+ keepPreviousData: true,
});
};
\ No newline at end of file
diff --git a/hooks/useCalSync.ts b/hooks/useCalSync.ts
index 76c2dff..20af736 100644
--- a/hooks/useCalSync.ts
+++ b/hooks/useCalSync.ts
@@ -46,14 +46,9 @@ export const useCalSync = () => {
const {profileData} = useAuthContext();
const {mutateAsync: updateUserData} = useUpdateUserData();
- const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
- useFetchAndSaveGoogleEvents();
- const {
- mutateAsync: fetchAndSaveOutlookEvents,
- isLoading: isSyncingOutlook,
- } = useFetchAndSaveOutlookEvents();
- const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
- useFetchAndSaveAppleEvents();
+ const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
+ const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
+ const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
@@ -74,6 +69,7 @@ export const useCalSync = () => {
}
);
+ console.log(response)
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
@@ -89,6 +85,7 @@ export const useCalSync = () => {
await fetchAndSaveGoogleEvents({
token: accessToken,
+ refreshToken: refreshToken,
email: googleMail,
});
}
@@ -283,6 +280,7 @@ export const useCalSync = () => {
isConnectedToGoogle,
isSyncingOutlook,
isSyncingGoogle,
- isSyncingApple
+ isSyncingApple,
+ isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
}
}
\ No newline at end of file
diff --git a/hooks/useFetchAndSaveAppleEvents.ts b/hooks/useFetchAndSaveAppleEvents.ts
index 85f1cf7..a18276b 100644
--- a/hooks/useFetchAndSaveAppleEvents.ts
+++ b/hooks/useFetchAndSaveAppleEvents.ts
@@ -9,11 +9,12 @@ export const useFetchAndSaveAppleEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({
- mutationKey: ["fetchAndSaveAppleEvents"],
- mutationFn: async ({token, email}: { token?: string, email?: string }) => {
- console.log("CALLL")
- const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
- const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
+ mutationKey: ["fetchAndSaveAppleEvents", "sync"],
+ mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
+ const baseDate = date || new Date();
+ const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
+ const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
+
try {
const response = await fetchiPhoneCalendarEvents(
profileData?.familyId!,
diff --git a/hooks/useFetchAndSaveGoogleEvents.ts b/hooks/useFetchAndSaveGoogleEvents.ts
index 55b0e7a..0b944d6 100644
--- a/hooks/useFetchAndSaveGoogleEvents.ts
+++ b/hooks/useFetchAndSaveGoogleEvents.ts
@@ -1,56 +1,107 @@
-import {useMutation, useQueryClient} from "react-query";
-import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
-import {useAuthContext} from "@/contexts/AuthContext";
-import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
-import {useClearTokens} from "@/hooks/firebase/useClearTokens";
+import { useMutation, useQueryClient } from "react-query";
+import { fetchGoogleCalendarEvents } from "@/calendar-integration/google-calendar-utils";
+import { useAuthContext } from "@/contexts/AuthContext";
+import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent";
+import { useClearTokens } from "@/hooks/firebase/useClearTokens";
+import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
export const useFetchAndSaveGoogleEvents = () => {
- const queryClient = useQueryClient()
- const {profileData} = useAuthContext();
- const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
- const {mutateAsync: clearToken} = useClearTokens();
+ const queryClient = useQueryClient();
+ const { profileData } = useAuthContext();
+ const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider();
+ const { mutateAsync: clearToken } = useClearTokens();
+ const { mutateAsync: updateUserData } = useUpdateUserData();
return useMutation({
- mutationKey: ["fetchAndSaveGoogleEvents"],
- mutationFn: async ({token, email}: { token?: string; email?: string }) => {
- console.log("Fetching Google Calendar events...");
- const timeMin = new Date(new Date().setMonth(new Date().getMonth() - 2));
- const timeMax = new Date(new Date().setMonth(new Date().getMonth() + 2));
+ mutationKey: ["fetchAndSaveGoogleEvents", "sync"],
+ mutationFn: async ({ token, email, date, refreshToken }: { token?: string; refreshToken?: string; email?: string; date?: Date }) => {
+ const baseDate = date || new Date();
+ const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString().slice(0, -5) + "Z";
+ const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString().slice(0, -5) + "Z";
console.log("Token: ", token);
- try {
- const response = await fetchGoogleCalendarEvents(
- token,
- email,
- profileData?.familyId,
- timeMin.toISOString().slice(0, -5) + "Z",
- timeMax.toISOString().slice(0, -5) + "Z"
- );
+ const tryFetchEvents = async (isRetry = false) => {
+ try {
+ const response = await fetchGoogleCalendarEvents(
+ token,
+ email,
+ profileData?.familyId,
+ timeMin,
+ timeMax
+ );
- if(!response.success) {
- await clearToken({email: email!, provider: "google"})
- return
- }
-
- console.log("Google Calendar events fetched:", response);
-
- const items = response?.googleEvents?.map((item) => {
- if (item.allDay) {
- item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
- item.endDate = item.startDate;
+ if (!response.success) {
+ await clearToken({ email: email!, provider: "google" });
+ return; // Stop refetching if clearing the token
}
- return item;
- }) || [];
- await createEventsFromProvider(items);
- } catch (error) {
- console.error("Error fetching Google Calendar events:", error);
- throw error; // Ensure errors are propagated to the mutation
- }
+ console.log("Google Calendar events fetched:", response);
+
+ const items = response?.googleEvents?.map((item) => {
+ if (item.allDay) {
+ item.startDate = new Date(item.startDate.setHours(0, 0, 0, 0));
+ item.endDate = item.startDate;
+ }
+ return item;
+ }) || [];
+
+ await createEventsFromProvider(items);
+ } catch (error) {
+ console.error("Error fetching Google Calendar events:", error);
+
+ if (!isRetry) {
+ const refreshedToken = await handleRefreshToken(email, refreshToken);
+ if (refreshedToken) {
+ await updateUserData({
+ newUserData: {
+ googleAccounts: {
+ ...profileData.googleAccounts,
+ [email!]: { ...profileData.googleAccounts[email!], accessToken: refreshedToken },
+ },
+ },
+ });
+ return tryFetchEvents(true); // Retry once after refreshing
+ } else {
+ await clearToken({ email: email!, provider: "google" });
+ console.error(`Token refresh failed; token cleared for ${email}`);
+ throw error;
+ }
+ } else {
+ console.error(`Retry failed after refreshing token for user ${profileData?.email}:`, error.message);
+ throw error;
+ }
+ }
+ };
+
+ return tryFetchEvents();
},
onSuccess: () => {
- queryClient.invalidateQueries(["events"])
+ queryClient.invalidateQueries(["events"]);
},
});
-};
\ No newline at end of file
+};
+
+async function handleRefreshToken(email: string, refreshToken: string) {
+ if (!refreshToken) return null;
+
+ try {
+ 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",
+ }),
+ });
+
+ const data = await response.json();
+ return data.access_token;
+ } catch (error) {
+ console.error("Error refreshing Google token:", error);
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/hooks/useFetchAndSaveOutlookEvents.ts b/hooks/useFetchAndSaveOutlookEvents.ts
index 5d5f6a3..23dfdbf 100644
--- a/hooks/useFetchAndSaveOutlookEvents.ts
+++ b/hooks/useFetchAndSaveOutlookEvents.ts
@@ -9,10 +9,11 @@ export const useFetchAndSaveOutlookEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({
- mutationKey: ["fetchAndSaveOutlookEvents"],
- mutationFn: async ({token, email}: { token?: string; email?: string }) => {
- const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
- const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));
+ mutationKey: ["fetchAndSaveOutlookEvents", "sync"],
+ mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
+ const baseDate = date || new Date();
+ const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
+ const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
console.log("Token: ", token ?? profileData?.microsoftToken);
diff --git a/hooks/useSyncOnScroll.ts b/hooks/useSyncOnScroll.ts
new file mode 100644
index 0000000..0377197
--- /dev/null
+++ b/hooks/useSyncOnScroll.ts
@@ -0,0 +1,85 @@
+import { useState, useEffect, useCallback } from "react";
+import { useAuthContext } from "@/contexts/AuthContext";
+import { useAtomValue } from "jotai";
+import { useFetchAndSaveGoogleEvents } from "./useFetchAndSaveGoogleEvents";
+import { useFetchAndSaveAppleEvents } from "./useFetchAndSaveAppleEvents";
+import { useFetchAndSaveOutlookEvents } from "./useFetchAndSaveOutlookEvents";
+import { selectedDateAtom } from "@/components/pages/calendar/atoms";
+import { addDays, subDays, isBefore, isAfter, format } from "date-fns";
+
+export const useSyncEvents = () => {
+ const { profileData } = useAuthContext();
+ const selectedDate = useAtomValue(selectedDateAtom);
+
+ const [lastSyncDate, setLastSyncDate] = useState(selectedDate);
+ const [lowerBoundDate, setLowerBoundDate] = useState(subDays(selectedDate, 6 * 30));
+ const [upperBoundDate, setUpperBoundDate] = useState(addDays(selectedDate, 6 * 30));
+ const [isSyncing, setIsSyncing] = useState(false);
+ const [error, setError] = useState(null);
+
+ const syncedRanges = useState>(new Set())[0];
+
+ const { mutateAsync: fetchAndSaveGoogleEvents } = useFetchAndSaveGoogleEvents();
+ const { mutateAsync: fetchAndSaveOutlookEvents } = useFetchAndSaveOutlookEvents();
+ const { mutateAsync: fetchAndSaveAppleEvents } = useFetchAndSaveAppleEvents();
+
+ const generateRangeKey = (startDate: Date, endDate: Date) => {
+ return `${format(startDate, "yyyy-MM-dd")}_${format(endDate, "yyyy-MM-dd")}`;
+ };
+
+ const syncEvents = useCallback(async () => {
+ setIsSyncing(true);
+ setError(null);
+
+ const newLowerBound = subDays(selectedDate, 6 * 30);
+ const newUpperBound = addDays(selectedDate, 6 * 30);
+ const rangeKey = generateRangeKey(newLowerBound, newUpperBound);
+
+ if (syncedRanges.has(rangeKey)) {
+ setIsSyncing(false);
+ return;
+ }
+
+ if (isBefore(selectedDate, lowerBoundDate) || isAfter(selectedDate, upperBoundDate)) {
+ try {
+ const googleEvents = Object.entries(profileData?.googleAccounts || {}).map(([email, { accessToken }]) =>
+ fetchAndSaveGoogleEvents({ token: accessToken, email, date: selectedDate })
+ );
+
+ const outlookEvents = Object.entries(profileData?.microsoftAccounts || {}).map(([email, token]) =>
+ fetchAndSaveOutlookEvents({ token, email, date: selectedDate })
+ );
+
+ const appleEvents = Object.entries(profileData?.appleAccounts || {}).map(([email, token]) =>
+ fetchAndSaveAppleEvents({ token, email, date: selectedDate })
+ );
+
+ await Promise.all([...googleEvents, ...outlookEvents, ...appleEvents]);
+
+ setLastSyncDate(selectedDate);
+ setLowerBoundDate(newLowerBound);
+ setUpperBoundDate(newUpperBound);
+ syncedRanges.add(rangeKey);
+ } catch (err) {
+ console.error("Error syncing events:", err);
+ setError(err);
+ } finally {
+ setIsSyncing(false);
+ }
+ } else {
+ setIsSyncing(false);
+ }
+ }, [selectedDate, lowerBoundDate, upperBoundDate, profileData, fetchAndSaveGoogleEvents, fetchAndSaveOutlookEvents, fetchAndSaveAppleEvents, syncedRanges]);
+
+ useEffect(() => {
+ syncEvents();
+ }, [selectedDate, syncEvents]);
+
+ return {
+ isSyncing,
+ error,
+ lastSyncDate,
+ lowerBoundDate,
+ upperBoundDate,
+ };
+};
\ No newline at end of file
diff --git a/patches/react-native-big-calendar+4.15.1.patch b/patches/react-native-big-calendar+4.15.1.patch
index 9173bde..88c2906 100644
--- a/patches/react-native-big-calendar+4.15.1.patch
+++ b/patches/react-native-big-calendar+4.15.1.patch
@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
-index 848ceba..57fbaed 100644
+index 848ceba..add1b1c 100644
--- a/node_modules/react-native-big-calendar/build/index.js
+++ b/node_modules/react-native-big-calendar/build/index.js
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');