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

View File

@ -25,7 +25,7 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
cache: npm cache: yarn
- name: Setup Expo and EAS - name: Setup Expo and EAS
uses: expo/expo-github-action@v8 uses: expo/expo-github-action@v8
@ -34,7 +34,7 @@ jobs:
token: ${{ secrets.EXPO_TOKEN }} token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies - name: Install dependencies
run: npm ci --legacy-peer-deps run: yarn install --immutable
- name: Prebuild, Build and Submit - name: Prebuild, Build and Submit
run: npm run prebuild-build-submit-ios-cicd run: yarn prebuild-build-submit-ios-cicd

View File

@ -13,10 +13,10 @@
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": false,
"bundleIdentifier": "com.cally.app", "bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist", "googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "60", "buildNumber": "74",
"usesAppleSignIn": true "usesAppleSignIn": true
}, },
"android": { "android": {

View File

@ -1,120 +1,108 @@
import React from "react"; import React from "react";
import { Drawer } from "expo-router/drawer"; import {Drawer} from "expo-router/drawer";
import { useSignOut } from "@/hooks/firebase/useSignOut"; import {useSignOut} from "@/hooks/firebase/useSignOut";
import { import {DrawerContentScrollView,} from "@react-navigation/drawer";
DrawerContentScrollView, import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
DrawerItem, import {ImageBackground, StyleSheet} from "react-native";
DrawerItemList,
} from "@react-navigation/drawer";
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
import { Dimensions, ImageBackground, StyleSheet } from "react-native";
import Feather from "@expo/vector-icons/Feather";
import DrawerButton from "@/components/shared/DrawerButton"; import DrawerButton from "@/components/shared/DrawerButton";
import {
AntDesign,
FontAwesome6,
MaterialCommunityIcons,
Octicons,
} from "@expo/vector-icons";
import MenuIcon from "@/assets/svgs/MenuIcon";
import { router } from "expo-router";
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon"; import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon"; import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon"; import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon"; import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon"; import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
import { useAtom, useSetAtom } from "jotai";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
} from "@/components/pages/calendar/atoms";
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon"; import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
import {MaterialIcons} from "@expo/vector-icons";
import {useSetAtom} from "jotai";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
} from "@/components/pages/calendar/atoms";
import Ionicons from "@expo/vector-icons/Ionicons";
export default function TabLayout() { export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut(); const {mutateAsync: signOut} = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom); const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex); const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView); const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex); const setToDosIndex = useSetAtom(toDosPageIndex);
return ( return (
<Drawer <Drawer
initialRouteName={"index"} initialRouteName={"index"}
detachInactiveScreens detachInactiveScreens
screenOptions={{ screenOptions={{
headerShown: true, headerShown: true,
drawerStyle: { drawerStyle: {
width: "90%", width: "90%",
backgroundColor: "#f9f8f7", backgroundColor: "#f9f8f7",
height: "100%", height: "100%",
}, },
}} }}
drawerContent={(props) => { drawerContent={(props) => {
return ( return (
<DrawerContentScrollView {...props} style={{}}> <DrawerContentScrollView {...props} style={{}}>
<View centerV marginH-30 marginT-20 marginB-20 row> <View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground <ImageBackground
source={require("../../assets/images/splash.png")} source={require("../../assets/images/splash.png")}
style={{ style={{
backgroundColor: "transparent", backgroundColor: "transparent",
height: 51.43, height: 51.43,
aspectRatio: 1, aspectRatio: 1,
marginRight: 8, marginRight: 8,
}} }}
/> />
<Text style={styles.title}>Welcome to Cally</Text> <Text style={styles.title}>Welcome to Cally</Text>
</View> </View>
<View <View
style={{ style={{
flexDirection: "row", flexDirection: "row",
paddingHorizontal: 30, paddingHorizontal: 30
}} }}
> >
<View style={{ flex: 1, paddingRight: 5 }}> <View style={{flex: 1, paddingRight: 5}}>
<DrawerButton <DrawerButton
title={"Calendar"} title={"Calendar"}
color="rgb(7, 184, 199)" color="rgb(7, 184, 199)"
bgColor={"rgb(231, 248, 250)"} bgColor={"rgb(231, 248, 250)"}
pressFunc={() => { pressFunc={() => {
props.navigation.navigate("calendar"); props.navigation.navigate("calendar");
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false); setIsFamilyView(false);
}} }}
icon={<NavCalendarIcon />} icon={<NavCalendarIcon/>}
/> />
<DrawerButton <DrawerButton
color="#50be0c" color="#50be0c"
title={"Groceries"} title={"Groceries"}
bgColor={"#eef9e7"} bgColor={"#eef9e7"}
pressFunc={() => { pressFunc={() => {
props.navigation.navigate("grocery"); props.navigation.navigate("grocery");
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false); setIsFamilyView(false);
}} }}
icon={<NavGroceryIcon />} icon={<NavGroceryIcon/>}/>
/> <DrawerButton
<DrawerButton color="#ea156d"
color="#ea156d" title={"Feedback"}
title={"Feedback"} bgColor={"#fdedf4"}
bgColor={"#fdedf4"} pressFunc={() => {
pressFunc={() => { props.navigation.navigate("feedback");
props.navigation.navigate("feedback"); setPageIndex(0);
setPageIndex(0); setToDosIndex(0);
setToDosIndex(0); setUserView(true);
setUserView(true); setIsFamilyView(false);
setIsFamilyView(false); }}
}} icon={<FeedbackNavIcon/>}
icon={<FeedbackNavIcon />} />
/> </View>
</View> <View style={{flex: 1}}>
<View style={{ flex: 1 }}> {/*<DrawerButton
{/*<DrawerButton
color="#fd1775" color="#fd1775"
title={"My Reminders"} title={"My Reminders"}
bgColor={"#ffe8f2"} bgColor={"#ffe8f2"}
@ -127,150 +115,169 @@ export default function TabLayout() {
/> />
} }
/>*/} />*/}
<DrawerButton <DrawerButton
color="#8005eb" color="#8005eb"
title={"To Do's"} title={"To Do's"}
bgColor={"#f3e6fd"} bgColor={"#f3e6fd"}
pressFunc={() => { pressFunc={() => {
props.navigation.navigate("todos"); props.navigation.navigate("todos");
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false); setIsFamilyView(false);
}} }}
icon={<NavToDosIcon />} icon={<NavToDosIcon/>}
/> />
<DrawerButton <DrawerButton
color="#e0ca03" color="#e0ca03"
title={"Brain Dump"} title={"Brain Dump"}
bgColor={"#fffacb"} bgColor={"#fffacb"}
pressFunc={() => { pressFunc={() => {
props.navigation.navigate("brain_dump"); props.navigation.navigate("brain_dump");
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false); setIsFamilyView(false);
}} }}
icon={<NavBrainDumpIcon />} icon={<NavBrainDumpIcon/>}
/> />
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/} <DrawerButton
</View> color="#e0ca03"
</View> title={"Notifications"}
<Button bgColor={"#ffdda1"}
onPress={() => { pressFunc={() => {
props.navigation.navigate("settings"); props.navigation.navigate("notifications");
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false); setIsFamilyView(false);
}} }}
label={"Manage Settings"} icon={<Ionicons name="notifications-outline" size={24} color={"#ffa200"} />}
labelStyle={styles.label} />
iconSource={() => ( </View>
<View </View>
backgroundColor="#ededed" <Button
width={60} onPress={() => {
height={60} props.navigation.navigate("settings");
style={{ borderRadius: 50 }} setPageIndex(0);
marginR-10 setToDosIndex(0);
centerV setUserView(true);
centerH setIsFamilyView(false);
> }}
<NavSettingsIcon /> label={"Manage Settings"}
</View> labelStyle={styles.label}
)} iconSource={() => (
backgroundColor="white" <View
color="#464039" backgroundColor="#ededed"
paddingV-30 width={60}
marginH-30 height={60}
marginB-10 style={{borderRadius: 50}}
borderRadius={18.55} marginR-10
style={{ elevation: 0 }} centerV
/> centerH
>
<NavSettingsIcon/>
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
borderRadius={18.55}
style={{elevation: 0}}
/>
<Button <Button
size={ButtonSize.large} size={ButtonSize.large}
marginH-30 marginH-10
marginT-12 marginT-12
paddingV-15 paddingV-15
style={{ style={{
backgroundColor: "transparent", marginTop: 50,
borderWidth: 1.3, backgroundColor: "transparent",
borderColor: "#fd1775", borderWidth: 1.3,
}} borderColor: "#fd1775",
label="Sign out of Cally" }}
color="#fd1775" label="Sign out of Cally"
labelStyle={styles.signOut} color="#fd1775"
onPress={() => signOut()} labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
}}
>
<Drawer.Screen
name="index"
options={{
drawerLabel: "Calendar",
title: "Calendar",
}}
/> />
</DrawerContentScrollView> <Drawer.Screen
); name="calendar"
}} options={{
> drawerLabel: "Calendar",
<Drawer.Screen title: "Calendar",
name="index" drawerItemStyle: {display: "none"},
options={{ }}
drawerLabel: "Calendar", />
title: "Calendar", <Drawer.Screen
}} name="brain_dump"
/> options={{
<Drawer.Screen drawerLabel: "Brain Dump",
name="calendar" title: "Brain Dump",
options={{ }}
drawerLabel: "Calendar", />
title: "Calendar", <Drawer.Screen
drawerItemStyle: { display: "none" }, name="settings"
}} options={{
/> drawerLabel: "Settings",
<Drawer.Screen title: "Settings",
name="brain_dump" }}
options={{ />
drawerLabel: "Brain Dump", <Drawer.Screen
title: "Brain Dump", name="grocery"
}} options={{
/> drawerLabel: "Grocery",
<Drawer.Screen title: "Grocery",
name="settings" }}
options={{ />
drawerLabel: "Settings", <Drawer.Screen
title: "Settings", name="reminders"
}} options={{
/> drawerLabel: "Reminders",
<Drawer.Screen title: "Reminders",
name="grocery" }}
options={{ />
drawerLabel: "Grocery", <Drawer.Screen
title: "Grocery", name="todos"
}} options={{
/> drawerLabel: "To-Do",
<Drawer.Screen title: "To-Dos",
name="reminders" }}
options={{ />
drawerLabel: "Reminders", <Drawer.Screen
title: "Reminders", name="notifications"
}} options={{
/> drawerLabel: "Notifications",
<Drawer.Screen title: "Notifications",
name="todos" }}
options={{ />
drawerLabel: "To-Do", <Drawer.Screen
title: "To-Dos", name="feedback"
}} options={{drawerLabel: "Feedback", title: "Feedback"}}
/> />
<Drawer.Screen </Drawer>
name="feedback" );
options={{ drawerLabel: "Feedback", title: "Feedback" }}
/>
</Drawer>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 }, signOut: {fontFamily: "Poppins_500Medium", fontSize: 15},
label: { fontFamily: "Poppins_400Medium", fontSize: 15 }, label: {fontFamily: "Poppins_400Medium", fontSize: 15},
title: { title: {
fontSize: 26.13, fontSize: 26.13,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
color: "#262627", color: "#262627",
}, },
}); });

View File

@ -1,14 +1,13 @@
import {BrainDumpProvider} from "@/contexts/DumpContext";
import {View} from "react-native-ui-lib";
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage"; import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
import { BrainDumpProvider } from "@/contexts/DumpContext";
import { ScrollView } from "react-native-gesture-handler";
import { View } from "react-native-ui-lib";
export default function Screen() { export default function Screen() {
return ( return (
<BrainDumpProvider> <BrainDumpProvider>
<View> <View>
<BrainDumpPage /> <BrainDumpPage/>
</View> </View>
</BrainDumpProvider> </BrainDumpProvider>
); );
} }

View File

@ -0,0 +1,5 @@
import {Stack} from "expo-router";
export default function StackLayout () {
return <Stack screenOptions={{headerShown: false}}/>
}

View File

@ -0,0 +1,7 @@
import NotificationsPage from "@/components/pages/notifications/NotificationsPage";
export default function Screen() {
return (
<NotificationsPage/>
);
}

View File

@ -2,7 +2,7 @@ module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: [ presets: [
'babel-preset-expo' 'babel-preset-expo',
] ]
}; };
}; };

View File

@ -1,55 +1,64 @@
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) { export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
const response = await fetch( const googleEvents = [];
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`, let pageToken = null;
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
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 data = await response.json(); const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
const googleEvents = []; const data = await response.json();
data.items?.forEach((item) => {
let isAllDay = false;
const start = item.start;
let startDateTime;
if (start !== undefined) {
if (start.dateTime) {
const stringDate = start.dateTime;
startDateTime = new Date(stringDate);
} else {
const stringDate = start.date;
startDateTime = new Date(stringDate);
isAllDay = true;
}
}
const end = item.end; if (!response.ok) {
let endDateTime; throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
if (end !== undefined) { }
if (end.dateTime) {
const stringDate = end.dateTime;
endDateTime = new Date(stringDate);
} else {
const stringDate = end.date;
endDateTime = new Date(stringDate);
}
}
const googleEvent = { data.items?.forEach((item) => {
id: item.id, let isAllDay = false;
title: item.summary ?? "", let startDateTime, endDateTime;
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
googleEvents.push(googleEvent);
});
return {googleEvents, success: response.ok}; 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 (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;
}
}
const googleEvent = {
id: item.id,
title: item.summary || "",
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email,
};
googleEvents.push(googleEvent);
});
// Prepare for the next page if it exists
pageToken = data.nextPageToken;
} while (pageToken);
return { googleEvents, success: true };
}

View File

@ -14,6 +14,7 @@ import { useAtom } from "jotai";
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms"; import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
import { format, isSameDay } from "date-fns"; import { format, isSameDay } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
import {useIsMutating} from "react-query";
export const CalendarHeader = memo(() => { export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);

View File

@ -11,7 +11,7 @@ export default function CalendarPage() {
paddingT-0 paddingT-0
> >
<HeaderTemplate <HeaderTemplate
message={"Let's get your week started!"} message={"Let's get your week started !"}
isWelcome isWelcome
isCalendar={true} isCalendar={true}
/> />

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Calendar } from "react-native-big-calendar"; 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 { useGetEvents } from "@/hooks/firebase/useGetEvents";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { import {
@ -15,6 +15,8 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { CalendarEvent } from "@/components/pages/calendar/interfaces"; import { CalendarEvent } from "@/components/pages/calendar/interfaces";
import { Text } from "react-native-ui-lib"; import { Text } from "react-native-ui-lib";
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns"; import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
import {useCalSync} from "@/hooks/useCalSync";
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
interface EventCalendarProps { interface EventCalendarProps {
calendarHeight: number; calendarHeight: number;
@ -39,7 +41,9 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const setEventForEdit = useSetAtom(eventForEditAtom); const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom); const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes()); const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useCalSync()
const todaysDate = new Date(); const todaysDate = new Date();
@ -47,15 +51,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
(event: CalendarEvent) => { (event: CalendarEvent) => {
if (mode === "day" || mode === "week") { if (mode === "day" || mode === "week") {
setEditVisible(true); setEditVisible(true);
console.log({ event }); // console.log({event});
setEventForEdit(event); setEventForEdit(event);
} else { } else {
setMode("day"); setMode("day");
setSelectedDate(event.start); setSelectedDate(event.start);
} }
}, },
[setEditVisible, setEventForEdit, mode] [setEditVisible, setEventForEdit, mode]
); );
const handlePressCell = useCallback( const handlePressCell = useCallback(
(date: Date) => { (date: Date) => {
@ -94,7 +98,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
); );
const memoizedEventCellStyle = useCallback( const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => ({ backgroundColor: event.eventColor }), (event: CalendarEvent) => ({ backgroundColor: event.eventColor , fontSize: 14}),
[] []
); );
@ -103,9 +107,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
[profileData] [profileData]
); );
console.log({ // console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek,
memoizedWeekStartsOn,
profileData: profileData?.firstDayOfWeek,
}); });
const isSameDate = useCallback((date1: Date, date2: Date) => { const isSameDate = useCallback((date1: Date, date2: Date) => {
@ -175,11 +177,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
}, {} as Record<string, CalendarEvent[]>); }, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now(); const endTime = Date.now();
console.log( // console.log("memoizedEvents computation time:", endTime - startTime, "ms");
"memoizedEvents computation time:",
endTime - startTime,
"ms"
);
return { enrichedEvents, filteredEvents }; return { enrichedEvents, filteredEvents };
}, [events, selectedDate, mode]); }, [events, selectedDate, mode]);
@ -239,106 +237,123 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
if (isLoading) { if (isLoading) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" /> {isSyncing && <Text>Syncing...</Text>}
</View> <ActivityIndicator size="large" color="#0000ff"/>
); </View>
} );
}
// console.log(enrichedEvents, filteredEvents) // console.log(enrichedEvents, filteredEvents)
return ( return (
<Calendar <>
bodyContainerStyle={styles.calHeader} {isSyncing && (
swipeEnabled <View style={styles.loadingContainer}>
mode={mode} {isSyncing && <Text>Syncing...</Text>}
enableEnrichedEvents={true} <ActivityIndicator size="large" color="#0000ff"/>
sortedMonthView </View>
// enrichedEventsByDate={enrichedEvents} )}
events={filteredEvents} <Calendar
// eventCellStyle={memoizedEventCellStyle} bodyContainerStyle={styles.calHeader}
onPressEvent={handlePressEvent} swipeEnabled
weekStartsOn={memoizedWeekStartsOn} mode={mode}
height={calendarHeight} // enableEnrichedEvents={true}
activeDate={todaysDate} sortedMonthView
date={selectedDate} // enrichedEventsByDate={enrichedEvents}
onPressCell={handlePressCell} events={filteredEvents}
headerContentStyle={memoizedHeaderContentStyle} eventCellStyle={memoizedEventCellStyle}
onSwipeEnd={handleSwipeEnd} onPressEvent={handlePressEvent}
scrollOffsetMinutes={offsetMinutes} weekStartsOn={memoizedWeekStartsOn}
theme={{ height={calendarHeight}
palette: { activeDate={todaysDate}
nowIndicator: profileData?.eventColor || "#fd1575", date={selectedDate}
gray: { onPressCell={handlePressCell}
"100": "#e8eaed", headerContentStyle={memoizedHeaderContentStyle}
"200": "#e8eaed", onSwipeEnd={handleSwipeEnd}
"500": "#b7b7b7", scrollOffsetMinutes={offsetMinutes}
"800": "#919191", theme={{
}, palette: {
}, nowIndicator: profileData?.eventColor || "#fd1575",
typography: { gray: {
fontFamily: "PlusJakartaSans_500Medium", "100": "#e8eaed",
sm: { fontFamily: "Manrope_600SemiBold", fontSize: 15 }, "200": "#e8eaed",
xl: { "500": "#b7b7b7",
fontFamily: "PlusJakartaSans_500Medium", "800": "#919191",
fontSize: 16, },
}, },
moreLabel: {}, typography: {
xs: { fontSize: 10 }, fontFamily: "PlusJakartaSans_500Medium",
}, sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
}} xl: {
dayHeaderStyle={dateStyle} fontFamily: "PlusJakartaSans_500Medium",
dayHeaderHighlightColor={"white"} fontSize: 14,
showAdjacentMonths },
hourStyle={styles.hourStyle} moreLabel: {},
onPressDateHeader={handlePressDayHeader} xs: {fontSize: 10},
ampm },
// renderCustomDateForMonth={renderCustomDateForMonth} }}
/> dayHeaderStyle={dateStyle}
); dayHeaderHighlightColor={"white"}
} showAdjacentMonths
headerContainerStyle={mode !== "month" ? {
overflow:"hidden",
} : {}}
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader} ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
</>
);
}
); );
const styles = StyleSheet.create({ const styles = StyleSheet.create({
segmentslblStyle: { segmentslblStyle: {
fontSize: 12, fontSize: 12,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
}, },
calHeader: { calHeader: {
borderWidth: 0, borderWidth: 0,
}, },
dayModeHeader: { dayModeHeader: {
alignSelf: "flex-start", alignSelf: "flex-start",
justifyContent: "space-between", justifyContent: "space-between",
alignContent: "center", alignContent: "center",
width: 38, width: 38,
right: 42, right: 42,
height: 13, height: 13,
}, },
weekModeHeader: {}, weekModeHeader: {},
monthModeHeader: {}, monthModeHeader: {},
loadingContainer: { loadingContainer: {
flex: 1, flex: 1,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
}, position: "absolute",
dayHeader: { width: "100%",
backgroundColor: "#4184f2", height: "100%",
aspectRatio: 1, zIndex: 100,
borderRadius: 100, backgroundColor: "rgba(255, 255, 255, 0.9)",
alignItems: "center", },
justifyContent: "center", dayHeader: {
}, backgroundColor: "#4184f2",
otherDayHeader: { aspectRatio: 1,
backgroundColor: "transparent", borderRadius: 100,
color: "#919191", alignItems: "center",
aspectRatio: 1, justifyContent: "center",
borderRadius: 100, },
alignItems: "center", otherDayHeader: {
justifyContent: "center", backgroundColor: "transparent",
}, color: "#919191",
hourStyle: { aspectRatio: 1,
color: "#5f6368", borderRadius: 100,
fontSize: 12, alignItems: "center",
fontFamily: "Manrope_500Medium", justifyContent: "center",
}, },
hourStyle: {
color: "#5f6368",
fontSize: 12,
fontFamily: "Manrope_500Medium",
},
}); });

View File

@ -0,0 +1,60 @@
import {FlatList, ScrollView, StyleSheet} from "react-native";
import React from "react";
import {Card, Text, View} from "react-native-ui-lib";
import HeaderTemplate from "@/components/shared/HeaderTemplate";
import {useGetNotifications} from "@/hooks/firebase/useGetNotifications";
import {formatDistanceToNow} from "date-fns";
const NotificationsPage = () => {
const {data: notifications} = useGetNotifications()
console.log(notifications?.[0]?.timestamp)
return (
<View flexG height={"100%"}>
<View flexG>
<ScrollView
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
>
<View marginH-25>
<HeaderTemplate
message={"Welcome to your notifications!"}
isWelcome={false}
children={
<Text
style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
>
See your notifications here.
</Text>
}
/>
<View>
<FlatList data={notifications ?? []} renderItem={({item}) => <Card padding-20 gap-10>
<Text text70>{item.content}</Text>
<View row spread>
<Text text90>{formatDistanceToNow(new Date(item.timestamp), { addSuffix: true })}</Text>
<Text text90>{item.timestamp.toLocaleDateString()}</Text>
</View>
</Card>}/>
</View>
</View>
</ScrollView>
</View>
</View>
);
};
const styles = StyleSheet.create({
searchField: {
borderWidth: 0.7,
borderColor: "#9b9b9b",
borderRadius: 15,
height: 42,
paddingLeft: 10,
marginVertical: 20,
},
});
export default NotificationsPage;

View File

@ -142,7 +142,7 @@ const CalendarSettingsPage = () => {
</View> </View>
</View> </View>
<Text style={styles.subTitle} marginT-30 marginB-25> <Text style={styles.subTitle} marginT-30 marginB-25>
Add Calendar Add Calendars
</Text> </Text>
<Button <Button

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Dialog, Button, Text, View } from "react-native-ui-lib"; import { Dialog, Button, Text, View } from "react-native-ui-lib";
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
interface ConfirmationDialogProps { interface ConfirmationDialogProps {
visible: boolean; visible: boolean;
@ -17,6 +18,7 @@ const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
onConfirm, onConfirm,
}) => { }) => {
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false); const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
return ( return (
<> <>
<Dialog <Dialog

View File

@ -21,6 +21,7 @@ import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePictur
import { colorMap } from "@/constants/colorMap"; import { colorMap } from "@/constants/colorMap";
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs"; import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
import {AntDesign} from "@expo/vector-icons"; import {AntDesign} from "@expo/vector-icons";
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
const MyProfile = () => { const MyProfile = () => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
@ -53,6 +54,7 @@ const MyProfile = () => {
const { mutateAsync: updateUserData } = useUpdateUserData(); const { mutateAsync: updateUserData } = useUpdateUserData();
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture(); const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
const { mutateAsync: deleteAsync } = useDeleteUser()
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const handleUpdateUserData = async () => { const handleUpdateUserData = async () => {
@ -305,9 +307,7 @@ const MyProfile = () => {
}} }}
visible={showDeleteDialog} visible={showDeleteDialog}
onDismiss={handleHideDeleteDialog} onDismiss={handleHideDeleteDialog}
onConfirm={() => { onConfirm={() => deleteAsync({})}
console.log("delete account here");
}}
/> />
</ScrollView> </ScrollView>
); );

View File

@ -164,16 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
} }
}, [user, ready, redirectOverride]); }, [user, ready, redirectOverride]);
useEffect(() => { // useEffect(() => {
const sub = Notifications.addNotificationReceivedListener(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']);
} // // }
}); // };
return () => sub.remove() //
}, []); // const sub = Notifications.addNotificationReceivedListener(handleNotification);
//
// return () => sub.remove();
// }, []);
if (!ready) { if (!ready) {
return null; return null;

View File

@ -1,6 +1,6 @@
const {onRequest} = require("firebase-functions/v2/https"); const {onRequest} = require("firebase-functions/v2/https");
const {getAuth} = require("firebase-admin/auth"); const {getAuth} = require("firebase-admin/auth");
const {getFirestore} = require("firebase-admin/firestore"); const {getFirestore, Timestamp} = require("firebase-admin/firestore");
const logger = require("firebase-functions/logger"); const logger = require("firebase-functions/logger");
const functions = require('firebase-functions'); const functions = require('firebase-functions');
const admin = require('firebase-admin'); const admin = require('firebase-admin');
@ -9,19 +9,23 @@ const {Expo} = require('expo-server-sdk');
admin.initializeApp(); admin.initializeApp();
const db = admin.firestore(); const db = admin.firestore();
let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN }); let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN});
let notificationTimeout = null; let notificationTimeout = null;
let eventCount = 0; let eventCount = 0;
let pushTokens = []; let notificationInProgress = false;
const GOOGLE_CALENDAR_ID = "primary";
const CHANNEL_ID = "cally-family-calendar";
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
exports.sendNotificationOnEventCreation = functions.firestore exports.sendNotificationOnEventCreation = functions.firestore
.document('Events/{eventId}') .document('Events/{eventId}')
.onCreate(async (snapshot, context) => { .onCreate(async (snapshot, context) => {
const eventData = snapshot.data(); const eventData = snapshot.data();
const { familyId, creatorId, email } = eventData; const { familyId, creatorId, email, title } = eventData;
if (email) { if (!!eventData?.externalOrigin) {
console.log('Event has an email field. Skipping notification.'); console.log('Externally synced event, ignoring.')
return; return;
} }
@ -39,60 +43,78 @@ exports.sendNotificationOnEventCreation = functions.firestore
eventCount++; eventCount++;
if (notificationTimeout) { // Only set up the notification timeout if it's not already in progress
clearTimeout(notificationTimeout); if (!notificationInProgress) {
} notificationInProgress = true;
notificationTimeout = setTimeout(async () => { notificationTimeout = setTimeout(async () => {
const eventMessage = eventCount === 1 const eventMessage = eventCount === 1
? `An event "${eventData.title}" has been added. Check it out!` ? `An event "${title}" has been added. Check it out!`
: `${eventCount} new events have been added.`; : `${eventCount} new events have been added.`;
let messages = pushTokens.map(pushToken => { let messages = pushTokens.map(pushToken => {
if (!Expo.isExpoPushToken(pushToken)) { if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`); console.error(`Push token ${pushToken} is not a valid Expo push token`);
return null; return null;
} }
return { return {
to: pushToken, to: pushToken,
sound: 'default', sound: 'default',
title: 'New Events Added!', title: 'New Events Added!',
body: eventMessage, body: eventMessage,
data: { eventId: context.params.eventId }, data: { eventId: context.params.eventId },
}; };
}).filter(Boolean); }).filter(Boolean);
let chunks = expo.chunkPushNotifications(messages); let chunks = expo.chunkPushNotifications(messages);
let tickets = []; let tickets = [];
for (let chunk of chunks) { for (let chunk of chunks) {
try { try {
let ticketChunk = await expo.sendPushNotificationsAsync(chunk); let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk); tickets.push(...ticketChunk);
for (let ticket of ticketChunk) { for (let ticket of ticketChunk) {
if (ticket.status === 'ok') { if (ticket.status === 'ok') {
console.log('Notification successfully sent:', ticket.id); console.log('Notification successfully sent:', ticket.id);
} else if (ticket.status === 'error') { } else if (ticket.status === 'error') {
console.error(`Notification error: ${ticket.message}`); console.error(`Notification error: ${ticket.message}`);
if (ticket.details?.error === 'DeviceNotRegistered') { if (ticket.details?.error === 'DeviceNotRegistered') {
await removeInvalidPushToken(ticket.to); await removeInvalidPushToken(ticket.to);
}
} }
} }
} catch (error) {
console.error('Error sending notification:', error);
} }
} catch (error) {
console.error('Error sending notification:', error);
} }
}
eventCount = 0; // Save the notification in Firestore for record-keeping
pushTokens = []; const notificationData = {
creatorId,
familyId,
content: eventMessage,
eventId: context.params.eventId,
timestamp: Timestamp.now(),
};
}, 5000); try {
await db.collection("Notifications").add(notificationData);
console.log("Notification stored in Firestore:", notificationData);
} catch (error) {
console.error("Error saving notification to Firestore:", error);
}
// Reset state variables after notifications are sent
eventCount = 0;
pushTokens = [];
notificationInProgress = false;
}, 5000);
}
}); });
exports.createSubUser = onRequest(async (request, response) => { exports.createSubUser = onRequest(async (request, response) => {
const authHeader = request.get('Authorization'); const authHeader = request.get('Authorization');
@ -262,7 +284,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...'); console.log('Running token refresh job...');
const profilesSnapshot = await db.collection('Profiles').get(); const profilesSnapshot = await db.collection('Profiles').get();
@ -275,8 +297,11 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
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}`);
} }
@ -292,7 +317,10 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail]; const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
if (microsoftToken) { if (microsoftToken) {
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken); const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
const updatedMicrosoftAccounts = {...profileData.microsoftAccounts, [microsoftEmail]: refreshedMicrosoftToken}; const updatedMicrosoftAccounts = {
...profileData.microsoftAccounts,
[microsoftEmail]: refreshedMicrosoftToken
};
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts}); await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
console.log(`Microsoft token updated for user ${profileDoc.id}`); console.log(`Microsoft token updated for user ${profileDoc.id}`);
} }
@ -320,21 +348,6 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
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', {
@ -385,4 +398,409 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
async function removeInvalidPushToken(pushToken) { async function removeInvalidPushToken(pushToken) {
// TODO // TODO
} }
const fetch = require("node-fetch");
// Function to refresh Google Token with additional logging
async function refreshGoogleToken(refreshToken) {
try {
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",
}),
});
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 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 || {};
for (const googleEmail of Object.keys(googleAccounts)) {
// Check if the googleAccount entry exists and has a refreshToken
const accountInfo = googleAccounts[googleEmail];
const refreshToken = accountInfo?.refreshToken;
if (refreshToken) {
try {
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 with additional logging
const watchCalendarEvents = async (userId, token) => {
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, {
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",
},
}),
});
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;
}
};
// 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 {
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 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;
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
try {
// Fetch user profile data for the specific user
const userDoc = await db.collection("Profiles").doc(userId).get();
const userData = userDoc.data();
// Ensure pushTokens is an array
let pushTokens = [];
if (userData && userData.pushToken) {
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
}
if (pushTokens.length === 0) {
console.log(`No push tokens found for user ${userId}`);
res.status(200).send("No push tokens found for user.");
return;
}
// Call calendarSync with necessary parameters
const {googleAccounts} = userData;
const email = Object.keys(googleAccounts || {})[0]; // Assuming the first account is the primary
const accountData = googleAccounts[email] || {};
const token = accountData.accessToken;
const refreshToken = accountData.refreshToken;
const familyId = userData.familyId;
console.log("Starting calendar sync...");
await calendarSync({userId, email, token, refreshToken, familyId});
console.log("Calendar sync completed.");
// Prepare and send push notifications after sync
// 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;
// }
//
// 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);
// }
// }
//
// 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.");
}
});
async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId }) {
const baseDate = new Date();
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
let events = [];
let pageToken = null;
try {
console.log(`Fetching events for user: ${email}`);
// Fetch all events from Google Calendar within the specified time range
do {
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
url.searchParams.set("singleEvents", "true");
url.searchParams.set("timeMin", timeMin);
url.searchParams.set("timeMax", timeMax);
if (pageToken) url.searchParams.set("pageToken", pageToken);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
if (response.status === 401 && refreshToken) {
console.log(`Token expired for user: ${email}, attempting to refresh`);
const refreshedToken = await refreshGoogleToken(refreshToken);
token = refreshedToken;
if (token) {
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
} else {
console.error(`Failed to refresh token for user: ${email}`);
await clearToken(email);
return;
}
}
if (!response.ok) {
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
}
console.log(`Processing events for user: ${email}`);
data.items?.forEach((item) => {
const googleEvent = {
id: item.id,
title: item.summary || "",
startDate: item.start?.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date),
endDate: item.end?.dateTime ? new Date(item.end.dateTime) : new Date(item.end.date),
allDay: !item.start?.dateTime,
familyId,
email,
creatorId,
externalOrigin: "google",
};
events.push(googleEvent);
console.log(`Processed event: ${JSON.stringify(googleEvent)}`);
});
pageToken = data.nextPageToken;
} while (pageToken);
console.log(`Saving events to Firestore for user: ${email}`);
await saveEventsToFirestore(events);
} catch (error) {
console.error(`Error fetching Google Calendar events for ${email}:`, error);
}
}
async function saveEventsToFirestore(events) {
const batch = db.batch();
events.forEach((event) => {
const eventRef = db.collection("Events").doc(event.id);
batch.set(eventRef, event, { merge: true });
});
await batch.commit();
}
async function calendarSync({ userId, email, token, refreshToken, familyId }) {
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
try {
await fetchAndSaveGoogleEvents({
token,
refreshToken,
email,
familyId,
creatorId: userId,
});
console.log("Calendar events synced successfully.");
} catch (error) {
console.error(`Error syncing calendar for user ${userId}:`, error);
throw error;
}
console.log(`Finished calendar sync for user ${userId}`);
}
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
const userId = req.query.userId;
const calendarId = req.body.resourceId;
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
try {
const userDoc = await db.collection("Profiles").doc(userId).get();
const userData = userDoc.data();
let pushTokens = [];
if (userData && userData.pushToken) {
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
}
if (pushTokens.length === 0) {
console.log(`No push tokens found for user ${userId}`);
res.status(200).send("No push tokens found for user.");
return;
}
const { googleAccounts } = userData;
const email = Object.keys(googleAccounts || {})[0];
const accountData = googleAccounts[email] || {};
const token = accountData.accessToken;
const refreshToken = accountData.refreshToken;
const familyId = userData.familyId;
console.log("Starting calendar sync...");
await calendarSync({ userId, email, token, refreshToken, familyId });
console.log("Calendar sync completed.");
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

@ -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);
}
}
},
});
};

View File

@ -4,6 +4,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms"; import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
import { colorMap } from "@/constants/colorMap"; import { colorMap } from "@/constants/colorMap";
import {uuidv4} from "@firebase/util";
export const useGetEvents = () => { export const useGetEvents = () => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
@ -20,22 +21,22 @@ export const useGetEvents = () => {
// If family view is active, include family, creator, and attendee events // If family view is active, include family, creator, and attendee events
if (isFamilyView) { if (isFamilyView) {
const familyQuery = db.collection("Events").where("familyID", "==", familyId); 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); 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(), familyQuery.get(),
creatorQuery.get(),
attendeeQuery.get(), attendeeQuery.get(),
]); ]);
// Collect all events // Collect all events
const familyEvents = familySnapshot.docs.map(doc => doc.data()); const familyEvents = familySnapshot.docs.map(doc => doc.data());
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
const attendeeEvents = attendeeSnapshot.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 { } else {
// Only include creator and attendee events when family view is off // Only include creator and attendee events when family view is off
const creatorQuery = db.collection("Events").where("creatorId", "==", userId); const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
@ -58,7 +59,7 @@ export const useGetEvents = () => {
if (event.id) { if (event.id) {
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
} else { } 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()); const uniqueEvents = Array.from(uniqueEventsMap.values());
@ -83,7 +84,7 @@ export const useGetEvents = () => {
const eventColor = profileData?.eventColor || colorMap.pink; const eventColor = profileData?.eventColor || colorMap.pink;
return { 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, title: event.title,
start: new Date(event.startDate.seconds * 1000), start: new Date(event.startDate.seconds * 1000),
end: new Date(event.endDate.seconds * 1000), end: new Date(event.endDate.seconds * 1000),
@ -96,5 +97,6 @@ export const useGetEvents = () => {
}, },
staleTime: Infinity, staleTime: Infinity,
cacheTime: Infinity, cacheTime: Infinity,
keepPreviousData: true,
}); });
}; };

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,
};
});
}
})
};

View File

@ -8,6 +8,8 @@ import * as WebBrowser from "expo-web-browser";
import * as Google from "expo-auth-session/providers/google"; import * as Google from "expo-auth-session/providers/google";
import * as AuthSession from "expo-auth-session"; import * as AuthSession from "expo-auth-session";
import * as AppleAuthentication from "expo-apple-authentication"; import * as AppleAuthentication from "expo-apple-authentication";
import * as Notifications from 'expo-notifications';
import {useQueryClient} from "react-query";
const googleConfig = { const googleConfig = {
androidClientId: androidClientId:
@ -44,16 +46,12 @@ const microsoftConfig = {
export const useCalSync = () => { export const useCalSync = () => {
const {profileData} = useAuthContext(); const {profileData} = useAuthContext();
const queryClient = useQueryClient();
const {mutateAsync: updateUserData} = useUpdateUserData(); const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
useFetchAndSaveGoogleEvents(); const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
const { const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
mutateAsync: fetchAndSaveOutlookEvents,
isLoading: isSyncingOutlook,
} = useFetchAndSaveOutlookEvents();
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession(); WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig); const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
@ -74,6 +72,7 @@ export const useCalSync = () => {
} }
); );
console.log(response)
const userInfo = await userInfoResponse.json(); const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email; const googleMail = userInfo.email;
@ -83,12 +82,15 @@ export const useCalSync = () => {
[googleMail]: {accessToken, refreshToken}, [googleMail]: {accessToken, refreshToken},
}; };
console.log({refreshToken})
await updateUserData({ await updateUserData({
newUserData: {googleAccounts: updatedGoogleAccounts}, newUserData: {googleAccounts: updatedGoogleAccounts},
}); });
await fetchAndSaveGoogleEvents({ await fetchAndSaveGoogleEvents({
token: accessToken, token: accessToken,
refreshToken: refreshToken,
email: googleMail, 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; let isConnectedToGoogle = false;
if (profileData?.googleAccounts) { if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => { 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 { return {
handleAppleSignIn, handleAppleSignIn,
handleMicrosoftSignIn, handleMicrosoftSignIn,
@ -283,6 +331,8 @@ export const useCalSync = () => {
isConnectedToGoogle, isConnectedToGoogle,
isSyncingOutlook, isSyncingOutlook,
isSyncingGoogle, isSyncingGoogle,
isSyncingApple isSyncingApple,
resyncAllCalendars,
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
} }
} }

View File

@ -9,11 +9,12 @@ export const useFetchAndSaveAppleEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveAppleEvents"], mutationKey: ["fetchAndSaveAppleEvents", "sync"],
mutationFn: async ({token, email}: { token?: string, email?: string }) => { mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
console.log("CALLL") const baseDate = date || new Date();
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
try { try {
const response = await fetchiPhoneCalendarEvents( const response = await fetchiPhoneCalendarEvents(
profileData?.familyId!, profileData?.familyId!,

View File

@ -1,56 +1,107 @@
import {useMutation, useQueryClient} from "react-query"; import { useMutation, useQueryClient } from "react-query";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils"; import { fetchGoogleCalendarEvents } from "@/calendar-integration/google-calendar-utils";
import {useAuthContext} from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent"; import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent";
import {useClearTokens} from "@/hooks/firebase/useClearTokens"; import { useClearTokens } from "@/hooks/firebase/useClearTokens";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
export const useFetchAndSaveGoogleEvents = () => { export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient();
const {profileData} = useAuthContext(); const { profileData } = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider();
const {mutateAsync: clearToken} = useClearTokens(); const { mutateAsync: clearToken } = useClearTokens();
const { mutateAsync: updateUserData } = useUpdateUserData();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"], mutationKey: ["fetchAndSaveGoogleEvents", "sync"],
mutationFn: async ({token, email}: { token?: string; email?: string }) => { mutationFn: async ({ token, email, date, refreshToken }: { token?: string; refreshToken?: string; email?: string; date?: Date }) => {
console.log("Fetching Google Calendar events..."); const baseDate = date || new Date();
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString().slice(0, -5) + "Z";
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString().slice(0, -5) + "Z";
console.log("Token: ", token); console.log("Token: ", token);
try { const tryFetchEvents = async (isRetry = false) => {
const response = await fetchGoogleCalendarEvents( try {
token, const response = await fetchGoogleCalendarEvents(
email, token,
profileData?.familyId, email,
timeMin.toISOString().slice(0, -5) + "Z", profileData?.familyId,
timeMax.toISOString().slice(0, -5) + "Z" timeMin,
); timeMax
);
if(!response.success) { if (!response.success) {
await clearToken({email: email!, provider: "google"}) await clearToken({ email: email!, provider: "google" });
return return; // Stop refetching if clearing the token
}
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;
} }
return item;
}) || [];
await createEventsFromProvider(items); console.log("Google Calendar events fetched:", response);
} catch (error) {
console.error("Error fetching Google Calendar events:", error); const items = response?.googleEvents?.map((item) => {
throw error; // Ensure errors are propagated to the mutation 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: () => { 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;
}
}

View File

@ -9,10 +9,11 @@ export const useFetchAndSaveOutlookEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveOutlookEvents"], mutationKey: ["fetchAndSaveOutlookEvents", "sync"],
mutationFn: async ({token, email}: { token?: string; email?: string }) => { mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const baseDate = date || new Date();
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3)); 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); console.log("Token: ", token ?? profileData?.microsoftToken);

85
hooks/useSyncOnScroll.ts Normal file
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,
};
};

File diff suppressed because it is too large Load Diff

View File

@ -450,11 +450,11 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = Cally; PRODUCT_NAME = "Cally";
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@ -484,10 +484,10 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = Cally; PRODUCT_NAME = "Cally";
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;

View File

@ -47,7 +47,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>60</string> <string>74</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
@ -136,6 +136,24 @@
<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>
<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>
<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>
<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>

19314
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@
"prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit", "prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit",
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd", "prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
"prebuild-build-submit-cicd": "yarn build-cicd", "prebuild-build-submit-cicd": "yarn build-cicd",
"postinstall": "patch-package" "postinstall": "patch-package",
"functions-deploy": "cd firebase/functions && firebase deploy --only functions"
}, },
"jest": { "jest": {
"preset": "jest-expo" "preset": "jest-expo"
@ -33,19 +34,19 @@
"@expo-google-fonts/poppins": "^0.2.3", "@expo-google-fonts/poppins": "^0.2.3",
"@expo/vector-icons": "^14.0.2", "@expo/vector-icons": "^14.0.2",
"@react-native-community/blur": "^4.4.0", "@react-native-community/blur": "^4.4.0",
"@react-native-community/datetimepicker": "^8.2.0", "@react-native-community/datetimepicker": "8.0.1",
"@react-native-firebase/app": "^20.3.0", "@react-native-firebase/app": "^20.3.0",
"@react-native-firebase/auth": "^20.3.0", "@react-native-firebase/auth": "^20.3.0",
"@react-native-firebase/crashlytics": "^20.3.0", "@react-native-firebase/crashlytics": "^20.3.0",
"@react-native-firebase/firestore": "^20.4.0", "@react-native-firebase/firestore": "^20.4.0",
"@react-native-firebase/functions": "^20.4.0", "@react-native-firebase/functions": "^20.4.0",
"@react-native-firebase/storage": "^21.0.0", "@react-native-firebase/storage": "^20.4.0",
"@react-native-menu/menu": "^1.1.6", "@react-native-menu/menu": "^1.1.6",
"@react-navigation/drawer": "^6.7.2", "@react-navigation/drawer": "^6.7.2",
"@react-navigation/native": "^6.0.2", "@react-navigation/native": "^6.0.2",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"debounce": "^2.1.1", "debounce": "^2.1.1",
"expo": "~51.0.24", "expo": "~51.0.38",
"expo-app-loading": "^2.1.1", "expo-app-loading": "^2.1.1",
"expo-apple-authentication": "~6.4.2", "expo-apple-authentication": "~6.4.2",
"expo-auth-session": "^5.5.2", "expo-auth-session": "^5.5.2",
@ -55,15 +56,15 @@
"expo-calendar": "~13.0.5", "expo-calendar": "~13.0.5",
"expo-camera": "~15.0.16", "expo-camera": "~15.0.16",
"expo-constants": "~16.0.2", "expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.27", "expo-dev-client": "~4.0.28",
"expo-device": "~6.0.2", "expo-device": "~6.0.2",
"expo-font": "~12.0.10", "expo-font": "~12.0.10",
"expo-image-picker": "~15.0.7", "expo-image-picker": "~15.0.7",
"expo-linking": "~6.3.1", "expo-linking": "~6.3.1",
"expo-localization": "~15.0.3", "expo-localization": "~15.0.3",
"expo-notifications": "~0.28.18", "expo-notifications": "~0.28.19",
"expo-router": "~3.5.20", "expo-router": "~3.5.20",
"expo-splash-screen": "~0.27.5", "expo-splash-screen": "~0.27.6",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7", "expo-system-ui": "~3.0.7",
"expo-updates": "~0.25.27", "expo-updates": "~0.25.27",
@ -75,7 +76,7 @@
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-native": "0.74.3", "react-native": "0.74.5",
"react-native-app-auth": "^8.0.0", "react-native-app-auth": "^8.0.0",
"react-native-big-calendar": "^4.15.1", "react-native-big-calendar": "^4.15.1",
"react-native-calendars": "^1.1306.0", "react-native-calendars": "^1.1306.0",
@ -89,7 +90,7 @@
"react-native-reanimated": "~3.10.1", "react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5", "react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1", "react-native-screens": "3.31.1",
"react-native-svg": "^15.7.1", "react-native-svg": "15.2.0",
"react-native-svg-icon": "^0.10.0", "react-native-svg-icon": "^0.10.0",
"react-native-toast-message": "^2.2.1", "react-native-toast-message": "^2.2.1",
"react-native-ui-lib": "^7.27.0", "react-native-ui-lib": "^7.27.0",
@ -98,12 +99,16 @@
"timezonecomplete": "^5.13.1", "timezonecomplete": "^5.13.1",
"tzdata": "^1.0.42" "tzdata": "^1.0.42"
}, },
"resolutions": {
"@react-native/assets-registry": "0.74.83"
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/react": "~18.2.45", "@types/react": "~18.2.45",
"@types/react-native-onboarding-swiper": "^1.1.9", "@types/react-native-onboarding-swiper": "^1.1.9",
"@types/react-test-renderer": "^18.0.7", "@types/react-test-renderer": "^18.0.7",
"babel-plugin-module-resolver": "^5.0.2",
"jest": "^29.2.1", "jest": "^29.2.1",
"jest-expo": "~51.0.3", "jest-expo": "~51.0.3",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",

View File

@ -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 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..f326b8e 100644
--- a/node_modules/react-native-big-calendar/build/index.js --- a/node_modules/react-native-big-calendar/build/index.js
+++ b/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'); @@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
@ -184,3 +184,12 @@ index 848ceba..57fbaed 100644
return finalEvents_1; return finalEvents_1;
} }
}, [events, sortedMonthView]); }, [events, sortedMonthView]);
@@ -1311,7 +1326,7 @@ function _CalendarHeader(_a) {
!stringHasContent(dayHeaderHighlightColor) &&
u['mt-6'],
] }, date.format('D')))),
- showAllDayEventCell ? (React__namespace.createElement(reactNative.View, { style: [
+ showAllDayEventCell ? (React__namespace.createElement(reactNative.ScrollView, { style: [
u['border-l'],
{ borderColor: theme.palette.gray['200'] },
{ height: cellHeight },

5302
yarn.lock

File diff suppressed because it is too large Load Diff