Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
ivic00
2024-11-12 21:42:19 +01:00
33 changed files with 4544 additions and 22626 deletions
+9 -6
View File
@@ -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});
+32
View File
@@ -0,0 +1,32 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import firestore from "@react-native-firebase/firestore";
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";
export const useDeleteUser = () => {
const {user: currentUser} = useAuthContext();
return useMutation({
mutationKey: ["deleteUser"],
mutationFn: async ({customUser}: { customUser?: FirebaseAuthTypes.User }) => {
const user = currentUser ?? customUser;
if (user) {
try {
await firestore()
.collection("Profiles")
.doc(user.uid)
.delete();
await auth().currentUser?.delete();
await auth().signOut();
console.log("User deleted and signed out successfully");
} catch (e) {
console.error("Error deleting user:", e);
}
}
},
});
};
+10 -8
View File
@@ -4,6 +4,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useAtomValue } from "jotai";
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
import { colorMap } from "@/constants/colorMap";
import {uuidv4} from "@firebase/util";
export const useGetEvents = () => {
const { user, profileData } = useAuthContext();
@@ -20,22 +21,22 @@ 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 familyQuery = db.collection("Events").where("familyId", "==", familyId);
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
const [familySnapshot, creatorSnapshot, attendeeSnapshot] = await Promise.all([
const [familySnapshot, attendeeSnapshot] = await Promise.all([
familyQuery.get(),
creatorQuery.get(),
attendeeQuery.get(),
]);
// Collect all events
const familyEvents = familySnapshot.docs.map(doc => doc.data());
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
allEvents = [...familyEvents, ...creatorEvents, ...attendeeEvents];
// console.log("Family events not in creator query: ", familyEvents.filter(event => !creatorEvents.some(creatorEvent => creatorEvent.id === event.id)));
allEvents = [...familyEvents, ...attendeeEvents];
} else {
// Only include creator and attendee events when family view is off
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
@@ -58,7 +59,7 @@ export const useGetEvents = () => {
if (event.id) {
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
} else {
uniqueEventsMap.set(Math.random().toString(36), event); // Generate a temp key for events without ID
uniqueEventsMap.set(uuidv4(), event); // Generate a temp key for events without ID
}
});
const uniqueEvents = Array.from(uniqueEventsMap.values());
@@ -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,
});
};
+29
View File
@@ -0,0 +1,29 @@
import {useQuery} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
export const useGetNotifications = () => {
const { user, profileData } = useAuthContext();
return useQuery({
queryKey: ["notifications", user?.uid],
queryFn: async () => {
const snapshot = await firestore()
.collection("Notifications")
.where("familyId", "==", profileData?.familyId)
.get();
return snapshot.docs.map((doc) => {
const data = doc.data();
return {...data, timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6)} as {
creatorId: string,
familyId: string,
content: string,
eventId: string,
timestamp: Date,
};
});
}
})
};
+59 -9
View File
@@ -8,6 +8,8 @@ import * as WebBrowser from "expo-web-browser";
import * as Google from "expo-auth-session/providers/google";
import * as AuthSession from "expo-auth-session";
import * as AppleAuthentication from "expo-apple-authentication";
import * as Notifications from 'expo-notifications';
import {useQueryClient} from "react-query";
const googleConfig = {
androidClientId:
@@ -44,16 +46,12 @@ const microsoftConfig = {
export const useCalSync = () => {
const {profileData} = useAuthContext();
const queryClient = useQueryClient();
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 +72,7 @@ export const useCalSync = () => {
}
);
console.log(response)
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
@@ -83,12 +82,15 @@ export const useCalSync = () => {
[googleMail]: {accessToken, refreshToken},
};
console.log({refreshToken})
await updateUserData({
newUserData: {googleAccounts: updatedGoogleAccounts},
});
await fetchAndSaveGoogleEvents({
token: accessToken,
refreshToken: refreshToken,
email: googleMail,
});
}
@@ -238,6 +240,38 @@ export const useCalSync = () => {
};
const resyncAllCalendars = async (): Promise<void> => {
try {
const syncPromises: Promise<void>[] = [];
if (profileData?.googleAccounts) {
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 }));
}
}
}
if (profileData?.microsoftAccounts) {
for (const [email, accessToken] of Object.entries(profileData.microsoftAccounts)) {
syncPromises.push(fetchAndSaveOutlookEvents(accessToken, email));
}
}
if (profileData?.appleAccounts) {
for (const [email, token] of Object.entries(profileData.appleAccounts)) {
syncPromises.push(fetchAndSaveAppleEvents({ token, email }));
}
}
await Promise.all(syncPromises);
console.log("All calendars have been resynced.");
} catch (error) {
console.error("Error resyncing calendars:", error);
}
};
let isConnectedToGoogle = false;
if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => {
@@ -270,6 +304,20 @@ export const useCalSync = () => {
}
useEffect(() => {
const handleNotification = async (notification: Notifications.Notification) => {
const eventId = notification?.request?.content?.data?.eventId;
// await resyncAllCalendars();
queryClient.invalidateQueries(["events"]);
};
const sub = Notifications.addNotificationReceivedListener(handleNotification);
return () => sub.remove();
}, []);
return {
handleAppleSignIn,
handleMicrosoftSignIn,
@@ -283,6 +331,8 @@ export const useCalSync = () => {
isConnectedToGoogle,
isSyncingOutlook,
isSyncingGoogle,
isSyncingApple
isSyncingApple,
resyncAllCalendars,
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
}
}
+6 -5
View File
@@ -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!,
+93 -42
View File
@@ -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().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
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"]);
},
});
};
};
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;
}
}
+5 -4
View File
@@ -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);
+85
View File
@@ -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<Date>(selectedDate);
const [lowerBoundDate, setLowerBoundDate] = useState<Date>(subDays(selectedDate, 6 * 30));
const [upperBoundDate, setUpperBoundDate] = useState<Date>(addDays(selectedDate, 6 * 30));
const [isSyncing, setIsSyncing] = useState(false);
const [error, setError] = useState(null);
const syncedRanges = useState<Set<string>>(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,
};
};