mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 09:45:20 +00:00
Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
6
.github/workflows/ci-cd.yml
vendored
6
.github/workflows/ci-cd.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
cache: yarn
|
||||
|
||||
- name: Setup Expo and EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Prebuild, Build and Submit
|
||||
run: npm run prebuild-build-submit-ios-cicd
|
||||
run: yarn prebuild-build-submit-ios-cicd
|
4
app.json
4
app.json
@ -13,10 +13,10 @@
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "com.cally.app",
|
||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||
"buildNumber": "60",
|
||||
"buildNumber": "74",
|
||||
"usesAppleSignIn": true
|
||||
},
|
||||
"android": {
|
||||
|
@ -1,36 +1,25 @@
|
||||
import React from "react";
|
||||
import {Drawer} from "expo-router/drawer";
|
||||
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||
import {
|
||||
DrawerContentScrollView,
|
||||
DrawerItem,
|
||||
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 {DrawerContentScrollView,} from "@react-navigation/drawer";
|
||||
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||
import {ImageBackground, StyleSheet} from "react-native";
|
||||
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 NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
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 FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
|
||||
export default function TabLayout() {
|
||||
const {mutateAsync: signOut} = useSignOut();
|
||||
@ -69,7 +58,7 @@ export default function TabLayout() {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 30,
|
||||
paddingHorizontal: 30
|
||||
}}
|
||||
>
|
||||
<View style={{flex: 1, paddingRight: 5}}>
|
||||
@ -97,8 +86,7 @@ export default function TabLayout() {
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<NavGroceryIcon />}
|
||||
/>
|
||||
icon={<NavGroceryIcon/>}/>
|
||||
<DrawerButton
|
||||
color="#ea156d"
|
||||
title={"Feedback"}
|
||||
@ -153,7 +141,19 @@ export default function TabLayout() {
|
||||
}}
|
||||
icon={<NavBrainDumpIcon/>}
|
||||
/>
|
||||
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
|
||||
<DrawerButton
|
||||
color="#e0ca03"
|
||||
title={"Notifications"}
|
||||
bgColor={"#ffdda1"}
|
||||
pressFunc={() => {
|
||||
props.navigation.navigate("notifications");
|
||||
setPageIndex(0);
|
||||
setToDosIndex(0);
|
||||
setUserView(true);
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
icon={<Ionicons name="notifications-outline" size={24} color={"#ffa200"} />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
@ -183,17 +183,17 @@ export default function TabLayout() {
|
||||
color="#464039"
|
||||
paddingV-30
|
||||
marginH-30
|
||||
marginB-10
|
||||
borderRadius={18.55}
|
||||
style={{elevation: 0}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size={ButtonSize.large}
|
||||
marginH-30
|
||||
marginH-10
|
||||
marginT-12
|
||||
paddingV-15
|
||||
style={{
|
||||
marginTop: 50,
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1.3,
|
||||
borderColor: "#fd1775",
|
||||
@ -257,6 +257,13 @@ export default function TabLayout() {
|
||||
title: "To-Dos",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="notifications"
|
||||
options={{
|
||||
drawerLabel: "Notifications",
|
||||
title: "Notifications",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="feedback"
|
||||
options={{drawerLabel: "Feedback", title: "Feedback"}}
|
||||
|
@ -1,7 +1,6 @@
|
||||
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";
|
||||
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
|
5
app/(auth)/notifications/_layout.tsx
Normal file
5
app/(auth)/notifications/_layout.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import {Stack} from "expo-router";
|
||||
|
||||
export default function StackLayout () {
|
||||
return <Stack screenOptions={{headerShown: false}}/>
|
||||
}
|
7
app/(auth)/notifications/index.tsx
Normal file
7
app/(auth)/notifications/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NotificationsPage from "@/components/pages/notifications/NotificationsPage";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<NotificationsPage/>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
'babel-preset-expo'
|
||||
'babel-preset-expo',
|
||||
]
|
||||
};
|
||||
};
|
||||
|
@ -1,55 +1,64 @@
|
||||
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
|
||||
{
|
||||
const googleEvents = [];
|
||||
let pageToken = null;
|
||||
|
||||
do {
|
||||
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||
url.searchParams.set('singleEvents', 'true');
|
||||
url.searchParams.set('timeMin', startDate);
|
||||
url.searchParams.set('timeMax', endDate);
|
||||
if (pageToken) url.searchParams.set('pageToken', pageToken);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const googleEvents = [];
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||
}
|
||||
|
||||
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);
|
||||
let startDateTime, endDateTime;
|
||||
|
||||
if (item.start) {
|
||||
if (item.start.dateTime) {
|
||||
startDateTime = new Date(item.start.dateTime);
|
||||
} else if (item.start.date) {
|
||||
startDateTime = new Date(item.start.date);
|
||||
isAllDay = true;
|
||||
}
|
||||
}
|
||||
|
||||
const end = item.end;
|
||||
let endDateTime;
|
||||
if (end !== undefined) {
|
||||
if (end.dateTime) {
|
||||
const stringDate = end.dateTime;
|
||||
endDateTime = new Date(stringDate);
|
||||
} else {
|
||||
const stringDate = end.date;
|
||||
endDateTime = new Date(stringDate);
|
||||
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 ?? "",
|
||||
title: item.summary || "",
|
||||
startDate: startDateTime,
|
||||
endDate: endDateTime,
|
||||
allDay: isAllDay,
|
||||
familyId,
|
||||
email
|
||||
email,
|
||||
};
|
||||
|
||||
googleEvents.push(googleEvent);
|
||||
});
|
||||
|
||||
return {googleEvents, success: response.ok};
|
||||
// Prepare for the next page if it exists
|
||||
pageToken = data.nextPageToken;
|
||||
} while (pageToken);
|
||||
|
||||
return { googleEvents, success: true };
|
||||
}
|
@ -14,6 +14,7 @@ import { useAtom } from "jotai";
|
||||
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
|
||||
import { format, isSameDay } from "date-fns";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import {useIsMutating} from "react-query";
|
||||
|
||||
export const CalendarHeader = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Calendar } from "react-native-big-calendar";
|
||||
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
@ -15,6 +15,8 @@ import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
||||
import { Text } from "react-native-ui-lib";
|
||||
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
@ -39,7 +41,9 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||
|
||||
const {isSyncing} = useSyncEvents()
|
||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||
useCalSync()
|
||||
|
||||
const todaysDate = new Date();
|
||||
|
||||
@ -47,7 +51,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
(event: CalendarEvent) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setEditVisible(true);
|
||||
console.log({ event });
|
||||
// console.log({event});
|
||||
setEventForEdit(event);
|
||||
} else {
|
||||
setMode("day");
|
||||
@ -94,7 +98,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
console.log({
|
||||
memoizedWeekStartsOn,
|
||||
profileData: profileData?.firstDayOfWeek,
|
||||
// console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek,
|
||||
});
|
||||
|
||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||
@ -175,11 +177,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
}, {} as Record<string, CalendarEvent[]>);
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(
|
||||
"memoizedEvents computation time:",
|
||||
endTime - startTime,
|
||||
"ms"
|
||||
);
|
||||
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||
|
||||
return { enrichedEvents, filteredEvents };
|
||||
}, [events, selectedDate, mode]);
|
||||
@ -239,6 +237,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
<ActivityIndicator size="large" color="#0000ff"/>
|
||||
</View>
|
||||
);
|
||||
@ -247,15 +246,22 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
// console.log(enrichedEvents, filteredEvents)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSyncing && (
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
<ActivityIndicator size="large" color="#0000ff"/>
|
||||
</View>
|
||||
)}
|
||||
<Calendar
|
||||
bodyContainerStyle={styles.calHeader}
|
||||
swipeEnabled
|
||||
mode={mode}
|
||||
enableEnrichedEvents={true}
|
||||
// enableEnrichedEvents={true}
|
||||
sortedMonthView
|
||||
// enrichedEventsByDate={enrichedEvents}
|
||||
events={filteredEvents}
|
||||
// eventCellStyle={memoizedEventCellStyle}
|
||||
eventCellStyle={memoizedEventCellStyle}
|
||||
onPressEvent={handlePressEvent}
|
||||
weekStartsOn={memoizedWeekStartsOn}
|
||||
height={calendarHeight}
|
||||
@ -277,10 +283,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
sm: { fontFamily: "Manrope_600SemiBold", fontSize: 15 },
|
||||
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
|
||||
xl: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
},
|
||||
moreLabel: {},
|
||||
xs: {fontSize: 10},
|
||||
@ -289,11 +295,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
dayHeaderStyle={dateStyle}
|
||||
dayHeaderHighlightColor={"white"}
|
||||
showAdjacentMonths
|
||||
headerContainerStyle={mode !== "month" ? {
|
||||
overflow:"hidden",
|
||||
} : {}}
|
||||
hourStyle={styles.hourStyle}
|
||||
onPressDateHeader={handlePressDayHeader}
|
||||
ampm
|
||||
onPressDateHeader={handlePressDayHeader} ampm
|
||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||
/>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -320,6 +330,11 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 100,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||
},
|
||||
dayHeader: {
|
||||
backgroundColor: "#4184f2",
|
||||
|
60
components/pages/notifications/NotificationsPage.tsx
Normal file
60
components/pages/notifications/NotificationsPage.tsx
Normal 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;
|
@ -142,7 +142,7 @@ const CalendarSettingsPage = () => {
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.subTitle} marginT-30 marginB-25>
|
||||
Add Calendar
|
||||
Add Calendars
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
visible: boolean;
|
||||
@ -17,6 +18,7 @@ const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
|
||||
onConfirm,
|
||||
}) => {
|
||||
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
|
@ -21,6 +21,7 @@ import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePictur
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
|
||||
import {AntDesign} from "@expo/vector-icons";
|
||||
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
||||
|
||||
const MyProfile = () => {
|
||||
const { user, profileData } = useAuthContext();
|
||||
@ -53,6 +54,7 @@ const MyProfile = () => {
|
||||
|
||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
||||
const { mutateAsync: deleteAsync } = useDeleteUser()
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
const handleUpdateUserData = async () => {
|
||||
@ -305,9 +307,7 @@ const MyProfile = () => {
|
||||
}}
|
||||
visible={showDeleteDialog}
|
||||
onDismiss={handleHideDeleteDialog}
|
||||
onConfirm={() => {
|
||||
console.log("delete account here");
|
||||
}}
|
||||
onConfirm={() => deleteAsync({})}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
|
@ -164,16 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
||||
}
|
||||
}, [user, ready, redirectOverride]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
||||
const eventId = notification?.request?.content?.data?.eventId;
|
||||
|
||||
if (eventId) {
|
||||
queryClient.invalidateQueries(['events']);
|
||||
}
|
||||
});
|
||||
return () => sub.remove()
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// const handleNotification = async (notification: Notifications.Notification) => {
|
||||
// const eventId = notification?.request?.content?.data?.eventId;
|
||||
//
|
||||
// // if (eventId) {
|
||||
// queryClient.invalidateQueries(['events']);
|
||||
// // }
|
||||
// };
|
||||
//
|
||||
// const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||
//
|
||||
// return () => sub.remove();
|
||||
// }, []);
|
||||
|
||||
if (!ready) {
|
||||
return null;
|
||||
|
@ -1,6 +1,6 @@
|
||||
const {onRequest} = require("firebase-functions/v2/https");
|
||||
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 functions = require('firebase-functions');
|
||||
const admin = require('firebase-admin');
|
||||
@ -12,16 +12,20 @@ const db = admin.firestore();
|
||||
let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN});
|
||||
let notificationTimeout = null;
|
||||
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
|
||||
.document('Events/{eventId}')
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const eventData = snapshot.data();
|
||||
const { familyId, creatorId, email } = eventData;
|
||||
const { familyId, creatorId, email, title } = eventData;
|
||||
|
||||
if (email) {
|
||||
console.log('Event has an email field. Skipping notification.');
|
||||
if (!!eventData?.externalOrigin) {
|
||||
console.log('Externally synced event, ignoring.')
|
||||
return;
|
||||
}
|
||||
|
||||
@ -39,13 +43,13 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
|
||||
eventCount++;
|
||||
|
||||
if (notificationTimeout) {
|
||||
clearTimeout(notificationTimeout);
|
||||
}
|
||||
// Only set up the notification timeout if it's not already in progress
|
||||
if (!notificationInProgress) {
|
||||
notificationInProgress = true;
|
||||
|
||||
notificationTimeout = setTimeout(async () => {
|
||||
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.`;
|
||||
|
||||
let messages = pushTokens.map(pushToken => {
|
||||
@ -86,13 +90,31 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
}
|
||||
}
|
||||
|
||||
// Save the notification in Firestore for record-keeping
|
||||
const notificationData = {
|
||||
creatorId,
|
||||
familyId,
|
||||
content: eventMessage,
|
||||
eventId: context.params.eventId,
|
||||
timestamp: Timestamp.now(),
|
||||
};
|
||||
|
||||
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) => {
|
||||
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...');
|
||||
|
||||
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)) {
|
||||
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||
if (googleToken) {
|
||||
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
||||
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
|
||||
const updatedGoogleAccounts = {
|
||||
...profileData.googleAccounts,
|
||||
[googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}
|
||||
};
|
||||
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
||||
console.log(`Google token updated for user ${profileDoc.id}`);
|
||||
}
|
||||
@ -292,7 +317,10 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
|
||||
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
||||
if (microsoftToken) {
|
||||
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
|
||||
const updatedMicrosoftAccounts = {...profileData.microsoftAccounts, [microsoftEmail]: refreshedMicrosoftToken};
|
||||
const updatedMicrosoftAccounts = {
|
||||
...profileData.microsoftAccounts,
|
||||
[microsoftEmail]: refreshedMicrosoftToken
|
||||
};
|
||||
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
|
||||
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;
|
||||
});
|
||||
|
||||
async function refreshGoogleToken(refreshToken) {
|
||||
try {
|
||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
|
||||
});
|
||||
|
||||
return response.data.access_token; // Return the new access token
|
||||
} catch (error) {
|
||||
console.error("Error refreshing Google token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMicrosoftToken(refreshToken) {
|
||||
try {
|
||||
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
@ -386,3 +399,408 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
||||
async function removeInvalidPushToken(pushToken) {
|
||||
// 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.");
|
||||
}
|
||||
});
|
@ -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
hooks/firebase/useDeleteUser.ts
Normal file
32
hooks/firebase/useDeleteUser.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -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
hooks/firebase/useGetNotifications.ts
Normal file
29
hooks/firebase/useGetNotifications.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
@ -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
|
||||
}
|
||||
}
|
@ -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!,
|
||||
|
@ -3,41 +3,44 @@ import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-
|
||||
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 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);
|
||||
|
||||
const tryFetchEvents = async (isRetry = false) => {
|
||||
try {
|
||||
const response = await fetchGoogleCalendarEvents(
|
||||
token,
|
||||
email,
|
||||
profileData?.familyId,
|
||||
timeMin.toISOString().slice(0, -5) + "Z",
|
||||
timeMax.toISOString().slice(0, -5) + "Z"
|
||||
timeMin,
|
||||
timeMax
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
await clearToken({email: email!, provider: "google"})
|
||||
return
|
||||
await clearToken({ email: email!, provider: "google" });
|
||||
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.startDate = new Date(item.startDate.setHours(0, 0, 0, 0));
|
||||
item.endDate = item.startDate;
|
||||
}
|
||||
return item;
|
||||
@ -46,11 +49,59 @@ export const useFetchAndSaveGoogleEvents = () => {
|
||||
await createEventsFromProvider(items);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Google Calendar events:", error);
|
||||
throw error; // Ensure errors are propagated to the mutation
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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
hooks/useSyncOnScroll.ts
Normal file
85
hooks/useSyncOnScroll.ts
Normal 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,
|
||||
};
|
||||
};
|
550
ios/Podfile.lock
550
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@ -450,11 +450,11 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||
PRODUCT_NAME = Cally;
|
||||
PRODUCT_NAME = "Cally";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
@ -484,10 +484,10 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||
PRODUCT_NAME = Cally;
|
||||
PRODUCT_NAME = "Cally";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
|
@ -47,7 +47,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>60</string>
|
||||
<string>74</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<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>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
|
19314
package-lock.json
generated
19314
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -22,7 +22,8 @@
|
||||
"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-cicd": "yarn build-cicd",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package",
|
||||
"functions-deploy": "cd firebase/functions && firebase deploy --only functions"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
@ -33,19 +34,19 @@
|
||||
"@expo-google-fonts/poppins": "^0.2.3",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@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/auth": "^20.3.0",
|
||||
"@react-native-firebase/crashlytics": "^20.3.0",
|
||||
"@react-native-firebase/firestore": "^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-navigation/drawer": "^6.7.2",
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"debounce": "^2.1.1",
|
||||
"expo": "~51.0.24",
|
||||
"expo": "~51.0.38",
|
||||
"expo-app-loading": "^2.1.1",
|
||||
"expo-apple-authentication": "~6.4.2",
|
||||
"expo-auth-session": "^5.5.2",
|
||||
@ -55,15 +56,15 @@
|
||||
"expo-calendar": "~13.0.5",
|
||||
"expo-camera": "~15.0.16",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-dev-client": "~4.0.27",
|
||||
"expo-dev-client": "~4.0.28",
|
||||
"expo-device": "~6.0.2",
|
||||
"expo-font": "~12.0.10",
|
||||
"expo-image-picker": "~15.0.7",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-localization": "~15.0.3",
|
||||
"expo-notifications": "~0.28.18",
|
||||
"expo-notifications": "~0.28.19",
|
||||
"expo-router": "~3.5.20",
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"expo-splash-screen": "~0.27.6",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-updates": "~0.25.27",
|
||||
@ -75,7 +76,7 @@
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "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-big-calendar": "^4.15.1",
|
||||
"react-native-calendars": "^1.1306.0",
|
||||
@ -89,7 +90,7 @@
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"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-toast-message": "^2.2.1",
|
||||
"react-native-ui-lib": "^7.27.0",
|
||||
@ -98,12 +99,16 @@
|
||||
"timezonecomplete": "^5.13.1",
|
||||
"tzdata": "^1.0.42"
|
||||
},
|
||||
"resolutions": {
|
||||
"@react-native/assets-registry": "0.74.83"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.2.45",
|
||||
"@types/react-native-onboarding-swiper": "^1.1.9",
|
||||
"@types/react-test-renderer": "^18.0.7",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~51.0.3",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
|
@ -1,5 +1,5 @@
|
||||
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
|
||||
index 848ceba..57fbaed 100644
|
||||
index 848ceba..f326b8e 100644
|
||||
--- a/node_modules/react-native-big-calendar/build/index.js
|
||||
+++ b/node_modules/react-native-big-calendar/build/index.js
|
||||
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
|
||||
@ -184,3 +184,12 @@ index 848ceba..57fbaed 100644
|
||||
return finalEvents_1;
|
||||
}
|
||||
}, [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 },
|
||||
|
Reference in New Issue
Block a user