mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 08:24:55 +00:00
sync update
This commit is contained in:
@ -165,14 +165,17 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}, [user, ready, redirectOverride]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
const eventId = notification?.request?.content?.data?.eventId;
|
const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
|
||||||
if (eventId) {
|
// if (eventId) {
|
||||||
queryClient.invalidateQueries(['events']);
|
queryClient.invalidateQueries(['events']);
|
||||||
}
|
// }
|
||||||
});
|
};
|
||||||
return () => sub.remove()
|
|
||||||
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
|
||||||
|
return () => sub.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
|
|||||||
@ -14,6 +14,11 @@ let notificationTimeout = null;
|
|||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
let pushTokens = [];
|
let pushTokens = [];
|
||||||
|
|
||||||
|
const GOOGLE_CALENDAR_ID = "primary";
|
||||||
|
const CHANNEL_ID = "unique-channel-id";
|
||||||
|
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||||
|
|
||||||
|
|
||||||
exports.sendNotificationOnEventCreation = functions.firestore
|
exports.sendNotificationOnEventCreation = functions.firestore
|
||||||
.document('Events/{eventId}')
|
.document('Events/{eventId}')
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
@ -305,3 +310,149 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
|||||||
async function removeInvalidPushToken(pushToken) {
|
async function removeInvalidPushToken(pushToken) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
return response.data.access_token;
|
||||||
|
} 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
|
||||||
|
async function getGoogleAccessTokens() {
|
||||||
|
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)) {
|
||||||
|
const { refreshToken } = googleAccounts[googleEmail];
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const accessToken = await refreshGoogleToken(refreshToken);
|
||||||
|
tokens[doc.id] = accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to refresh token for user ${doc.id}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to watch Google Calendar events
|
||||||
|
const watchCalendarEvents = async (userId, token) => {
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: `${CHANNEL_ID}-${userId}`, // Unique ID per user
|
||||||
|
type: "web_hook",
|
||||||
|
address: `${WEBHOOK_URL}?userId=${userId}`, // Pass userId to identify notifications
|
||||||
|
params: {
|
||||||
|
ttl: "80000", // Set to 20 hours in seconds
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(`Failed to watch calendar: ${errorData.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schedule function to renew Google Calendar watch every 20 hours for each user
|
||||||
|
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 20 hours").onRun(async (context) => {
|
||||||
|
try {
|
||||||
|
const tokens = await getGoogleAccessTokens();
|
||||||
|
|
||||||
|
for (const [userId, token] of Object.entries(tokens)) {
|
||||||
|
try {
|
||||||
|
const result = await watchCalendarEvents(userId, token);
|
||||||
|
console.log(`Successfully renewed Google Calendar watch for user ${userId}:`, result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in renewGoogleCalendarWatch function:", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to handle notifications from Google Calendar for a specific user
|
||||||
|
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||||
|
const userId = req.query.userId; // Extract userId from query params
|
||||||
|
const calendarId = req.body.resourceId;
|
||||||
|
|
||||||
|
// Fetch push tokens for the specific user
|
||||||
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
const pushTokens = userData ? 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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send("Sync notification sent.");
|
||||||
|
});
|
||||||
@ -8,6 +8,8 @@ import * as WebBrowser from "expo-web-browser";
|
|||||||
import * as Google from "expo-auth-session/providers/google";
|
import * as Google from "expo-auth-session/providers/google";
|
||||||
import * as AuthSession from "expo-auth-session";
|
import * as AuthSession from "expo-auth-session";
|
||||||
import * as AppleAuthentication from "expo-apple-authentication";
|
import * as AppleAuthentication from "expo-apple-authentication";
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import {useQueryClient} from "react-query";
|
||||||
|
|
||||||
const googleConfig = {
|
const googleConfig = {
|
||||||
androidClientId:
|
androidClientId:
|
||||||
@ -44,6 +46,7 @@ const microsoftConfig = {
|
|||||||
|
|
||||||
export const useCalSync = () => {
|
export const useCalSync = () => {
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
|
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
|
||||||
@ -235,6 +238,35 @@ export const useCalSync = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const resyncAllCalendars = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const syncPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (profileData?.googleAccounts) {
|
||||||
|
for (const [email, { accessToken, refreshToken }] of Object.entries(profileData.googleAccounts)) {
|
||||||
|
syncPromises.push(fetchAndSaveGoogleEvents({ token: accessToken, refreshToken, email }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileData?.microsoftAccounts) {
|
||||||
|
for (const [email, accessToken] of Object.entries(profileData.microsoftAccounts)) {
|
||||||
|
syncPromises.push(fetchAndSaveOutlookEvents(accessToken, email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileData?.appleAccounts) {
|
||||||
|
for (const [email, token] of Object.entries(profileData.appleAccounts)) {
|
||||||
|
syncPromises.push(fetchAndSaveAppleEvents({ token, email }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
console.log("All calendars have been resynced.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resyncing calendars:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let isConnectedToGoogle = false;
|
let isConnectedToGoogle = false;
|
||||||
if (profileData?.googleAccounts) {
|
if (profileData?.googleAccounts) {
|
||||||
Object.values(profileData?.googleAccounts).forEach((item) => {
|
Object.values(profileData?.googleAccounts).forEach((item) => {
|
||||||
@ -267,6 +299,20 @@ export const useCalSync = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
|
const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
|
||||||
|
await resyncAllCalendars();
|
||||||
|
queryClient.invalidateQueries(["events"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleAppleSignIn,
|
handleAppleSignIn,
|
||||||
handleMicrosoftSignIn,
|
handleMicrosoftSignIn,
|
||||||
@ -281,6 +327,7 @@ export const useCalSync = () => {
|
|||||||
isSyncingOutlook,
|
isSyncingOutlook,
|
||||||
isSyncingGoogle,
|
isSyncingGoogle,
|
||||||
isSyncingApple,
|
isSyncingApple,
|
||||||
|
resyncAllCalendars,
|
||||||
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
|
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,6 +152,7 @@
|
|||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user