mirror of
https://github.com/urosran/cally.git
synced 2025-07-16 10:06:15 +00:00
Fixes
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useRef, useState, useTransition} from 'react';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@ -24,6 +24,7 @@ import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {FlashList} from "@shopify/flash-list";
|
import {FlashList} from "@shopify/flash-list";
|
||||||
import * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
|
import debounce from "debounce";
|
||||||
|
|
||||||
interface CalendarEvent {
|
interface CalendarEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@ -38,7 +39,7 @@ interface CustomMonthCalendarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
const MAX_VISIBLE_EVENTS = 3 ;
|
const MAX_VISIBLE_EVENTS = 3;
|
||||||
const CENTER_MONTH_INDEX = 24;
|
const CENTER_MONTH_INDEX = 24;
|
||||||
|
|
||||||
|
|
||||||
@ -131,8 +132,14 @@ const Day = React.memo(({
|
|||||||
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
|
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
|
||||||
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
|
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
|
||||||
|
|
||||||
|
// Calculate space needed for multi-day events
|
||||||
|
const maxMultiDayPosition = multiDayEvents.length > 0
|
||||||
|
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
|
||||||
|
: 0;
|
||||||
|
const multiDayEventsHeight = maxMultiDayPosition * 16; // Height for multi-day events
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.day, { width: dayWidth }]}>
|
<View style={[styles.day, {width: dayWidth}]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.dayContent}
|
style={styles.dayContent}
|
||||||
onPress={() => onPress(date)}
|
onPress={() => onPress(date)}
|
||||||
@ -150,8 +157,8 @@ const Day = React.memo(({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.eventsContainer}>
|
{/* Multi-day events container */}
|
||||||
{/* Multi-day events first */}
|
<View style={[styles.multiDayContainer, {height: multiDayEventsHeight}]}>
|
||||||
{multiDayEvents.map(event => (
|
{multiDayEvents.map(event => (
|
||||||
<MultiDayEvent
|
<MultiDayEvent
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@ -162,8 +169,10 @@ const Day = React.memo(({
|
|||||||
onPress={() => onPress(event.start)}
|
onPress={() => onPress(event.start)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Single-day events */}
|
{/* Single-day events container */}
|
||||||
|
<View style={[styles.singleDayContainer, {marginTop: multiDayEventsHeight}]}>
|
||||||
{visibleSingleDayEvents.map(event => (
|
{visibleSingleDayEvents.map(event => (
|
||||||
<Event
|
<Event
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@ -171,8 +180,6 @@ const Day = React.memo(({
|
|||||||
onPress={() => onPress(event.start)}
|
onPress={() => onPress(event.start)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Show total number of hidden events */}
|
|
||||||
{totalHiddenEvents > 0 && (
|
{totalHiddenEvents > 0 && (
|
||||||
<Text style={styles.moreEvents}>
|
<Text style={styles.moreEvents}>
|
||||||
{totalHiddenEvents} More
|
{totalHiddenEvents} More
|
||||||
@ -205,31 +212,11 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
|
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
|
||||||
const dayWidth = (screenWidth - 32) / 7;
|
const dayWidth = (screenWidth - 32) / 7;
|
||||||
const centerMonth = useRef(selectedDate);
|
const centerMonth = useRef(selectedDate);
|
||||||
|
const isScrolling = useRef(false);
|
||||||
|
const lastScrollUpdate = useRef<Date>(new Date());
|
||||||
|
|
||||||
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
||||||
|
|
||||||
const events = useMemo(() => {
|
|
||||||
if (!rawEvents?.length) return new Map();
|
|
||||||
if (!selectedDate) return new Map();
|
|
||||||
|
|
||||||
const eventMap = new Map();
|
|
||||||
|
|
||||||
rawEvents.forEach((event) => {
|
|
||||||
if (!event?.start || !event?.end) return;
|
|
||||||
|
|
||||||
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
|
|
||||||
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
|
|
||||||
|
|
||||||
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
|
|
||||||
|
|
||||||
const dateStr = format(startDate, 'yyyy-MM-dd');
|
|
||||||
const existing = eventMap.get(dateStr) || [];
|
|
||||||
eventMap.set(dateStr, [...existing, event]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return eventMap;
|
|
||||||
}, [rawEvents, selectedDate]);
|
|
||||||
|
|
||||||
const onDayPress = useCallback(
|
const onDayPress = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
date && setSelectedDate(date);
|
date && setSelectedDate(date);
|
||||||
@ -305,7 +292,7 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
} else {
|
} else {
|
||||||
const dateStr = format(startDate, 'yyyy-MM-dd');
|
const dateStr = format(startDate, 'yyyy-MM-dd');
|
||||||
const existing = eventMap.get(dateStr) || [];
|
const existing = eventMap.get(dateStr) || [];
|
||||||
eventMap.set(dateStr, [...existing, { ...event, start: startDate, end: endDate }]);
|
eventMap.set(dateStr, [...existing, {...event, start: startDate, end: endDate}]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -316,8 +303,8 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
return durationB - durationA;
|
return durationB - durationA;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { eventMap, multiDayEvents };
|
return {eventMap, multiDayEvents};
|
||||||
}, [rawEvents, selectedDate]);
|
}, [rawEvents]);
|
||||||
|
|
||||||
const getMultiDayEventsForDay = useCallback((date: Date) => {
|
const getMultiDayEventsForDay = useCallback((date: Date) => {
|
||||||
return processedEvents.multiDayEvents.filter(event => {
|
return processedEvents.multiDayEvents.filter(event => {
|
||||||
@ -352,7 +339,14 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
/>
|
/>
|
||||||
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
||||||
|
|
||||||
const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const debouncedSetSelectedDate = useMemo(
|
||||||
|
() => debounce(setSelectedDate, 500),
|
||||||
|
[setSelectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
if (isScrolling.current) return;
|
||||||
|
|
||||||
const currentMonthIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth);
|
const currentMonthIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth);
|
||||||
const currentMonth = monthsToRender[currentMonthIndex];
|
const currentMonth = monthsToRender[currentMonthIndex];
|
||||||
|
|
||||||
@ -362,6 +356,12 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
}
|
}
|
||||||
}, [screenWidth, setSelectedDate, monthsToRender]);
|
}, [screenWidth, setSelectedDate, monthsToRender]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedSetSelectedDate.clear();
|
||||||
|
};
|
||||||
|
}, [debouncedSetSelectedDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -378,7 +378,7 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
pagingEnabled
|
pagingEnabled
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
initialScrollIndex={CENTER_MONTH_INDEX}
|
initialScrollIndex={CENTER_MONTH_INDEX}
|
||||||
onMomentumScrollEnd={onMomentumScrollEnd}
|
onScroll={onScroll}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
estimatedItemSize={screenWidth}
|
estimatedItemSize={screenWidth}
|
||||||
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
||||||
@ -501,7 +501,7 @@ const HEADER_HEIGHT = 40;
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
multiDayContainer: {
|
multiDayContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 24,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
@ -1771,3 +1771,411 @@ exports.updateHouseholdTimestampOnEventUpdate = functions.firestore
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
|
||||||
|
try {
|
||||||
|
console.log('[GOOGLE_SYNC] Starting to sync event to Google Calendar', {
|
||||||
|
eventId: event.id,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
let token = accessToken;
|
||||||
|
|
||||||
|
// Construct the Google Calendar event
|
||||||
|
const googleEvent = {
|
||||||
|
summary: event.title,
|
||||||
|
start: {
|
||||||
|
dateTime: event.allDay ? undefined : event.startDate.toISOString(),
|
||||||
|
date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: event.allDay ? undefined : event.endDate.toISOString(),
|
||||||
|
date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined
|
||||||
|
},
|
||||||
|
visibility: event.private ? 'private' : 'default',
|
||||||
|
id: event.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${event.id}`;
|
||||||
|
|
||||||
|
let response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(googleEvent)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle token refresh if needed
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log('[GOOGLE_SYNC] Token expired, refreshing...');
|
||||||
|
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedGoogleToken;
|
||||||
|
|
||||||
|
// Update the token in Firestore
|
||||||
|
await db.collection("Profiles").doc(creatorId).update({
|
||||||
|
[`googleAccounts.${event.email}.accessToken`]: refreshedGoogleToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry with new token
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${refreshedGoogleToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(googleEvent)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GOOGLE_SYNC] Successfully synced event to Google Calendar', {
|
||||||
|
eventId: event.id,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_SYNC] Error syncing event to Google Calendar:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEventFromGoogle(eventId, accessToken, refreshToken, creatorId, email) {
|
||||||
|
try {
|
||||||
|
console.log('[GOOGLE_DELETE] Starting to delete event from Google Calendar', {
|
||||||
|
eventId,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
let token = accessToken;
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`;
|
||||||
|
|
||||||
|
let response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle token refresh if needed
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log('[GOOGLE_DELETE] Token expired, refreshing...');
|
||||||
|
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedGoogleToken;
|
||||||
|
|
||||||
|
// Update the token in Firestore
|
||||||
|
await db.collection("Profiles").doc(creatorId).update({
|
||||||
|
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry with new token
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${refreshedGoogleToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 404) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Failed to delete event: ${errorData.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', {
|
||||||
|
eventId,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Cloud Function to handle event updates
|
||||||
|
exports.syncEventToGoogleCalendar = functions.firestore
|
||||||
|
.document('Events/{eventId}')
|
||||||
|
.onWrite(async (change, context) => {
|
||||||
|
const eventId = context.params.eventId;
|
||||||
|
const afterData = change.after.exists ? change.after.data() : null;
|
||||||
|
const beforeData = change.before.exists ? change.before.data() : null;
|
||||||
|
|
||||||
|
// Skip if this is a Google-originated event
|
||||||
|
if (afterData?.externalOrigin === 'google' || beforeData?.externalOrigin === 'google') {
|
||||||
|
console.log('[GOOGLE_SYNC] Skipping sync for Google-originated event', { eventId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle deletion
|
||||||
|
if (!afterData && beforeData) {
|
||||||
|
console.log('[GOOGLE_SYNC] Processing event deletion', { eventId });
|
||||||
|
|
||||||
|
// Only proceed if this was previously synced with Google
|
||||||
|
if (!beforeData.email) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorDoc = await db.collection('Profiles').doc(beforeData.creatorId).get();
|
||||||
|
const creatorData = creatorDoc.data();
|
||||||
|
|
||||||
|
if (!creatorData?.googleAccounts?.[beforeData.email]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountData = creatorData.googleAccounts[beforeData.email];
|
||||||
|
|
||||||
|
await deleteEventFromGoogle(
|
||||||
|
eventId,
|
||||||
|
accountData.accessToken,
|
||||||
|
accountData.refreshToken,
|
||||||
|
beforeData.creatorId,
|
||||||
|
beforeData.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle creation or update
|
||||||
|
if (afterData) {
|
||||||
|
// Skip if no creator or email is set
|
||||||
|
if (!afterData.creatorId || !afterData.email) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorDoc = await db.collection('Profiles').doc(afterData.creatorId).get();
|
||||||
|
const creatorData = creatorDoc.data();
|
||||||
|
|
||||||
|
if (!creatorData?.googleAccounts?.[afterData.email]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountData = creatorData.googleAccounts[afterData.email];
|
||||||
|
|
||||||
|
await syncEventToGoogle(
|
||||||
|
afterData,
|
||||||
|
accountData.accessToken,
|
||||||
|
accountData.refreshToken,
|
||||||
|
afterData.creatorId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_SYNC] Error in sync function:', error);
|
||||||
|
|
||||||
|
// Store the error for later retry or monitoring
|
||||||
|
await db.collection('SyncErrors').add({
|
||||||
|
eventId,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
type: 'google'
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}););
|
||||||
|
|
||||||
|
let token = accessToken;
|
||||||
|
|
||||||
|
// Construct the Google Calendar event
|
||||||
|
const googleEvent = {
|
||||||
|
summary: event.title,
|
||||||
|
start: {
|
||||||
|
dateTime: event.allDay ? undefined : event.startDate.toISOString(),
|
||||||
|
date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: event.allDay ? undefined : event.endDate.toISOString(),
|
||||||
|
date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined
|
||||||
|
},
|
||||||
|
visibility: event.private ? 'private' : 'default',
|
||||||
|
id: event.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${event.id}`;
|
||||||
|
|
||||||
|
let response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(googleEvent)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle token refresh if needed
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log('[GOOGLE_SYNC] Token expired, refreshing...');
|
||||||
|
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedGoogleToken;
|
||||||
|
|
||||||
|
// Update the token in Firestore
|
||||||
|
await db.collection("Profiles").doc(creatorId).update({
|
||||||
|
[`googleAccounts.${event.email}.accessToken`]: refreshedGoogleToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry with new token
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${refreshedGoogleToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(googleEvent)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GOOGLE_SYNC] Successfully synced event to Google Calendar', {
|
||||||
|
eventId: event.id,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_SYNC] Error syncing event to Google Calendar:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteEventFromGoogle(eventId, accessToken, refreshToken, creatorId, email) {
|
||||||
|
try {
|
||||||
|
console.log('[GOOGLE_DELETE] Starting to delete event from Google Calendar', {
|
||||||
|
eventId,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
let token = accessToken;
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`;
|
||||||
|
|
||||||
|
let response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle token refresh if needed
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log('[GOOGLE_DELETE] Token expired, refreshing...');
|
||||||
|
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedGoogleToken;
|
||||||
|
|
||||||
|
// Update the token in Firestore
|
||||||
|
await db.collection("Profiles").doc(creatorId).update({
|
||||||
|
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry with new token
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${refreshedGoogleToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 404) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Failed to delete event: ${errorData.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', {
|
||||||
|
eventId,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.handleEventDelete = functions.firestore
|
||||||
|
.document('Events/{eventId}')
|
||||||
|
.onDelete(async (snapshot, context) => {
|
||||||
|
const deletedEvent = snapshot.data();
|
||||||
|
const eventId = context.params.eventId;
|
||||||
|
|
||||||
|
// Skip if this was a Google-originated event to prevent sync loops
|
||||||
|
if (deletedEvent?.externalOrigin === 'google') {
|
||||||
|
console.log('[GOOGLE_DELETE] Skipping delete sync for Google-originated event', { eventId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Only proceed if this was synced with Google (has an email)
|
||||||
|
if (!deletedEvent?.email) {
|
||||||
|
console.log('[GOOGLE_DELETE] Event not synced with Google, skipping', { eventId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorDoc = await admin.firestore()
|
||||||
|
.collection('Profiles')
|
||||||
|
.doc(deletedEvent.creatorId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!creatorDoc.exists) {
|
||||||
|
console.log('[GOOGLE_DELETE] Creator profile not found', {
|
||||||
|
eventId,
|
||||||
|
creatorId: deletedEvent.creatorId
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorData = creatorDoc.data();
|
||||||
|
const googleAccount = creatorData?.googleAccounts?.[deletedEvent.email];
|
||||||
|
|
||||||
|
if (!googleAccount) {
|
||||||
|
console.log('[GOOGLE_DELETE] No Google account found for email', {
|
||||||
|
eventId,
|
||||||
|
email: deletedEvent.email
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteEventFromGoogle(
|
||||||
|
eventId,
|
||||||
|
googleAccount.accessToken,
|
||||||
|
googleAccount.refreshToken,
|
||||||
|
deletedEvent.creatorId,
|
||||||
|
deletedEvent.email
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', {
|
||||||
|
eventId,
|
||||||
|
email: deletedEvent.email
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error);
|
||||||
|
|
||||||
|
// Store the error for monitoring
|
||||||
|
await admin.firestore()
|
||||||
|
.collection('SyncErrors')
|
||||||
|
.add({
|
||||||
|
eventId,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
type: 'google_delete'
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user