mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 01:35:22 +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
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: npm
|
cache: yarn
|
||||||
|
|
||||||
- name: Setup Expo and EAS
|
- name: Setup Expo and EAS
|
||||||
uses: expo/expo-github-action@v8
|
uses: expo/expo-github-action@v8
|
||||||
@ -34,7 +34,7 @@ jobs:
|
|||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --legacy-peer-deps
|
run: yarn install --immutable
|
||||||
|
|
||||||
- name: Prebuild, Build and Submit
|
- name: Prebuild, Build and Submit
|
||||||
run: npm run prebuild-build-submit-ios-cicd
|
run: yarn prebuild-build-submit-ios-cicd
|
4
app.json
4
app.json
@ -13,10 +13,10 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.cally.app",
|
"bundleIdentifier": "com.cally.app",
|
||||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||||
"buildNumber": "60",
|
"buildNumber": "74",
|
||||||
"usesAppleSignIn": true
|
"usesAppleSignIn": true
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
@ -1,120 +1,108 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Drawer } from "expo-router/drawer";
|
import {Drawer} from "expo-router/drawer";
|
||||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||||
import {
|
import {DrawerContentScrollView,} from "@react-navigation/drawer";
|
||||||
DrawerContentScrollView,
|
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||||
DrawerItem,
|
import {ImageBackground, StyleSheet} from "react-native";
|
||||||
DrawerItemList,
|
|
||||||
} from "@react-navigation/drawer";
|
|
||||||
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
|
|
||||||
import { Dimensions, ImageBackground, StyleSheet } from "react-native";
|
|
||||||
import Feather from "@expo/vector-icons/Feather";
|
|
||||||
import DrawerButton from "@/components/shared/DrawerButton";
|
import DrawerButton from "@/components/shared/DrawerButton";
|
||||||
import {
|
|
||||||
AntDesign,
|
|
||||||
FontAwesome6,
|
|
||||||
MaterialCommunityIcons,
|
|
||||||
Octicons,
|
|
||||||
} from "@expo/vector-icons";
|
|
||||||
import MenuIcon from "@/assets/svgs/MenuIcon";
|
|
||||||
import { router } from "expo-router";
|
|
||||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||||
import { useAtom, useSetAtom } from "jotai";
|
|
||||||
import {
|
|
||||||
isFamilyViewAtom,
|
|
||||||
settingsPageIndex,
|
|
||||||
toDosPageIndex,
|
|
||||||
userSettingsView,
|
|
||||||
} from "@/components/pages/calendar/atoms";
|
|
||||||
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||||
|
import {MaterialIcons} from "@expo/vector-icons";
|
||||||
|
import {useSetAtom} from "jotai";
|
||||||
|
import {
|
||||||
|
isFamilyViewAtom,
|
||||||
|
settingsPageIndex,
|
||||||
|
toDosPageIndex,
|
||||||
|
userSettingsView,
|
||||||
|
} from "@/components/pages/calendar/atoms";
|
||||||
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { mutateAsync: signOut } = useSignOut();
|
const {mutateAsync: signOut} = useSignOut();
|
||||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
const setUserView = useSetAtom(userSettingsView);
|
const setUserView = useSetAtom(userSettingsView);
|
||||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
initialRouteName={"index"}
|
initialRouteName={"index"}
|
||||||
detachInactiveScreens
|
detachInactiveScreens
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
drawerStyle: {
|
drawerStyle: {
|
||||||
width: "90%",
|
width: "90%",
|
||||||
backgroundColor: "#f9f8f7",
|
backgroundColor: "#f9f8f7",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
drawerContent={(props) => {
|
drawerContent={(props) => {
|
||||||
return (
|
return (
|
||||||
<DrawerContentScrollView {...props} style={{}}>
|
<DrawerContentScrollView {...props} style={{}}>
|
||||||
<View centerV marginH-30 marginT-20 marginB-20 row>
|
<View centerV marginH-30 marginT-20 marginB-20 row>
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
source={require("../../assets/images/splash.png")}
|
source={require("../../assets/images/splash.png")}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
height: 51.43,
|
height: 51.43,
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.title}>Welcome to Cally</Text>
|
<Text style={styles.title}>Welcome to Cally</Text>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: 30,
|
paddingHorizontal: 30
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, paddingRight: 5 }}>
|
<View style={{flex: 1, paddingRight: 5}}>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
title={"Calendar"}
|
title={"Calendar"}
|
||||||
color="rgb(7, 184, 199)"
|
color="rgb(7, 184, 199)"
|
||||||
bgColor={"rgb(231, 248, 250)"}
|
bgColor={"rgb(231, 248, 250)"}
|
||||||
pressFunc={() => {
|
pressFunc={() => {
|
||||||
props.navigation.navigate("calendar");
|
props.navigation.navigate("calendar");
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavCalendarIcon />}
|
icon={<NavCalendarIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#50be0c"
|
color="#50be0c"
|
||||||
title={"Groceries"}
|
title={"Groceries"}
|
||||||
bgColor={"#eef9e7"}
|
bgColor={"#eef9e7"}
|
||||||
pressFunc={() => {
|
pressFunc={() => {
|
||||||
props.navigation.navigate("grocery");
|
props.navigation.navigate("grocery");
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavGroceryIcon />}
|
icon={<NavGroceryIcon/>}/>
|
||||||
/>
|
<DrawerButton
|
||||||
<DrawerButton
|
color="#ea156d"
|
||||||
color="#ea156d"
|
title={"Feedback"}
|
||||||
title={"Feedback"}
|
bgColor={"#fdedf4"}
|
||||||
bgColor={"#fdedf4"}
|
pressFunc={() => {
|
||||||
pressFunc={() => {
|
props.navigation.navigate("feedback");
|
||||||
props.navigation.navigate("feedback");
|
setPageIndex(0);
|
||||||
setPageIndex(0);
|
setToDosIndex(0);
|
||||||
setToDosIndex(0);
|
setUserView(true);
|
||||||
setUserView(true);
|
setIsFamilyView(false);
|
||||||
setIsFamilyView(false);
|
}}
|
||||||
}}
|
icon={<FeedbackNavIcon/>}
|
||||||
icon={<FeedbackNavIcon />}
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
<View style={{flex: 1}}>
|
||||||
<View style={{ flex: 1 }}>
|
{/*<DrawerButton
|
||||||
{/*<DrawerButton
|
|
||||||
color="#fd1775"
|
color="#fd1775"
|
||||||
title={"My Reminders"}
|
title={"My Reminders"}
|
||||||
bgColor={"#ffe8f2"}
|
bgColor={"#ffe8f2"}
|
||||||
@ -127,150 +115,169 @@ export default function TabLayout() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>*/}
|
/>*/}
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#8005eb"
|
color="#8005eb"
|
||||||
title={"To Do's"}
|
title={"To Do's"}
|
||||||
bgColor={"#f3e6fd"}
|
bgColor={"#f3e6fd"}
|
||||||
pressFunc={() => {
|
pressFunc={() => {
|
||||||
props.navigation.navigate("todos");
|
props.navigation.navigate("todos");
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavToDosIcon />}
|
icon={<NavToDosIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#e0ca03"
|
color="#e0ca03"
|
||||||
title={"Brain Dump"}
|
title={"Brain Dump"}
|
||||||
bgColor={"#fffacb"}
|
bgColor={"#fffacb"}
|
||||||
pressFunc={() => {
|
pressFunc={() => {
|
||||||
props.navigation.navigate("brain_dump");
|
props.navigation.navigate("brain_dump");
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavBrainDumpIcon />}
|
icon={<NavBrainDumpIcon/>}
|
||||||
/>
|
/>
|
||||||
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
|
<DrawerButton
|
||||||
</View>
|
color="#e0ca03"
|
||||||
</View>
|
title={"Notifications"}
|
||||||
<Button
|
bgColor={"#ffdda1"}
|
||||||
onPress={() => {
|
pressFunc={() => {
|
||||||
props.navigation.navigate("settings");
|
props.navigation.navigate("notifications");
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
label={"Manage Settings"}
|
icon={<Ionicons name="notifications-outline" size={24} color={"#ffa200"} />}
|
||||||
labelStyle={styles.label}
|
/>
|
||||||
iconSource={() => (
|
</View>
|
||||||
<View
|
</View>
|
||||||
backgroundColor="#ededed"
|
<Button
|
||||||
width={60}
|
onPress={() => {
|
||||||
height={60}
|
props.navigation.navigate("settings");
|
||||||
style={{ borderRadius: 50 }}
|
setPageIndex(0);
|
||||||
marginR-10
|
setToDosIndex(0);
|
||||||
centerV
|
setUserView(true);
|
||||||
centerH
|
setIsFamilyView(false);
|
||||||
>
|
}}
|
||||||
<NavSettingsIcon />
|
label={"Manage Settings"}
|
||||||
</View>
|
labelStyle={styles.label}
|
||||||
)}
|
iconSource={() => (
|
||||||
backgroundColor="white"
|
<View
|
||||||
color="#464039"
|
backgroundColor="#ededed"
|
||||||
paddingV-30
|
width={60}
|
||||||
marginH-30
|
height={60}
|
||||||
marginB-10
|
style={{borderRadius: 50}}
|
||||||
borderRadius={18.55}
|
marginR-10
|
||||||
style={{ elevation: 0 }}
|
centerV
|
||||||
/>
|
centerH
|
||||||
|
>
|
||||||
|
<NavSettingsIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
backgroundColor="white"
|
||||||
|
color="#464039"
|
||||||
|
paddingV-30
|
||||||
|
marginH-30
|
||||||
|
borderRadius={18.55}
|
||||||
|
style={{elevation: 0}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size={ButtonSize.large}
|
size={ButtonSize.large}
|
||||||
marginH-30
|
marginH-10
|
||||||
marginT-12
|
marginT-12
|
||||||
paddingV-15
|
paddingV-15
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "transparent",
|
marginTop: 50,
|
||||||
borderWidth: 1.3,
|
backgroundColor: "transparent",
|
||||||
borderColor: "#fd1775",
|
borderWidth: 1.3,
|
||||||
}}
|
borderColor: "#fd1775",
|
||||||
label="Sign out of Cally"
|
}}
|
||||||
color="#fd1775"
|
label="Sign out of Cally"
|
||||||
labelStyle={styles.signOut}
|
color="#fd1775"
|
||||||
onPress={() => signOut()}
|
labelStyle={styles.signOut}
|
||||||
|
onPress={() => signOut()}
|
||||||
|
/>
|
||||||
|
</DrawerContentScrollView>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
drawerLabel: "Calendar",
|
||||||
|
title: "Calendar",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</DrawerContentScrollView>
|
<Drawer.Screen
|
||||||
);
|
name="calendar"
|
||||||
}}
|
options={{
|
||||||
>
|
drawerLabel: "Calendar",
|
||||||
<Drawer.Screen
|
title: "Calendar",
|
||||||
name="index"
|
drawerItemStyle: {display: "none"},
|
||||||
options={{
|
}}
|
||||||
drawerLabel: "Calendar",
|
/>
|
||||||
title: "Calendar",
|
<Drawer.Screen
|
||||||
}}
|
name="brain_dump"
|
||||||
/>
|
options={{
|
||||||
<Drawer.Screen
|
drawerLabel: "Brain Dump",
|
||||||
name="calendar"
|
title: "Brain Dump",
|
||||||
options={{
|
}}
|
||||||
drawerLabel: "Calendar",
|
/>
|
||||||
title: "Calendar",
|
<Drawer.Screen
|
||||||
drawerItemStyle: { display: "none" },
|
name="settings"
|
||||||
}}
|
options={{
|
||||||
/>
|
drawerLabel: "Settings",
|
||||||
<Drawer.Screen
|
title: "Settings",
|
||||||
name="brain_dump"
|
}}
|
||||||
options={{
|
/>
|
||||||
drawerLabel: "Brain Dump",
|
<Drawer.Screen
|
||||||
title: "Brain Dump",
|
name="grocery"
|
||||||
}}
|
options={{
|
||||||
/>
|
drawerLabel: "Grocery",
|
||||||
<Drawer.Screen
|
title: "Grocery",
|
||||||
name="settings"
|
}}
|
||||||
options={{
|
/>
|
||||||
drawerLabel: "Settings",
|
<Drawer.Screen
|
||||||
title: "Settings",
|
name="reminders"
|
||||||
}}
|
options={{
|
||||||
/>
|
drawerLabel: "Reminders",
|
||||||
<Drawer.Screen
|
title: "Reminders",
|
||||||
name="grocery"
|
}}
|
||||||
options={{
|
/>
|
||||||
drawerLabel: "Grocery",
|
<Drawer.Screen
|
||||||
title: "Grocery",
|
name="todos"
|
||||||
}}
|
options={{
|
||||||
/>
|
drawerLabel: "To-Do",
|
||||||
<Drawer.Screen
|
title: "To-Dos",
|
||||||
name="reminders"
|
}}
|
||||||
options={{
|
/>
|
||||||
drawerLabel: "Reminders",
|
<Drawer.Screen
|
||||||
title: "Reminders",
|
name="notifications"
|
||||||
}}
|
options={{
|
||||||
/>
|
drawerLabel: "Notifications",
|
||||||
<Drawer.Screen
|
title: "Notifications",
|
||||||
name="todos"
|
}}
|
||||||
options={{
|
/>
|
||||||
drawerLabel: "To-Do",
|
<Drawer.Screen
|
||||||
title: "To-Dos",
|
name="feedback"
|
||||||
}}
|
options={{drawerLabel: "Feedback", title: "Feedback"}}
|
||||||
/>
|
/>
|
||||||
<Drawer.Screen
|
</Drawer>
|
||||||
name="feedback"
|
);
|
||||||
options={{ drawerLabel: "Feedback", title: "Feedback" }}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 },
|
signOut: {fontFamily: "Poppins_500Medium", fontSize: 15},
|
||||||
label: { fontFamily: "Poppins_400Medium", fontSize: 15 },
|
label: {fontFamily: "Poppins_400Medium", fontSize: 15},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 26.13,
|
fontSize: 26.13,
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
color: "#262627",
|
color: "#262627",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
|
import {BrainDumpProvider} from "@/contexts/DumpContext";
|
||||||
|
import {View} from "react-native-ui-lib";
|
||||||
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
|
import BrainDumpPage from "@/components/pages/brain_dump/BrainDumpPage";
|
||||||
import { BrainDumpProvider } from "@/contexts/DumpContext";
|
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
|
||||||
import { View } from "react-native-ui-lib";
|
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
return (
|
return (
|
||||||
<BrainDumpProvider>
|
<BrainDumpProvider>
|
||||||
<View>
|
<View>
|
||||||
<BrainDumpPage />
|
<BrainDumpPage/>
|
||||||
</View>
|
</View>
|
||||||
</BrainDumpProvider>
|
</BrainDumpProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: [
|
presets: [
|
||||||
'babel-preset-expo'
|
'babel-preset-expo',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,55 +1,64 @@
|
|||||||
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
|
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
|
||||||
const response = await fetch(
|
const googleEvents = [];
|
||||||
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
|
let pageToken = null;
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
do {
|
||||||
|
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||||
|
url.searchParams.set('singleEvents', 'true');
|
||||||
|
url.searchParams.set('timeMin', startDate);
|
||||||
|
url.searchParams.set('timeMax', endDate);
|
||||||
|
if (pageToken) url.searchParams.set('pageToken', pageToken);
|
||||||
|
|
||||||
const data = await response.json();
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const googleEvents = [];
|
const data = await response.json();
|
||||||
data.items?.forEach((item) => {
|
|
||||||
let isAllDay = false;
|
|
||||||
const start = item.start;
|
|
||||||
let startDateTime;
|
|
||||||
if (start !== undefined) {
|
|
||||||
if (start.dateTime) {
|
|
||||||
const stringDate = start.dateTime;
|
|
||||||
startDateTime = new Date(stringDate);
|
|
||||||
} else {
|
|
||||||
const stringDate = start.date;
|
|
||||||
startDateTime = new Date(stringDate);
|
|
||||||
isAllDay = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = item.end;
|
if (!response.ok) {
|
||||||
let endDateTime;
|
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||||
if (end !== undefined) {
|
}
|
||||||
if (end.dateTime) {
|
|
||||||
const stringDate = end.dateTime;
|
|
||||||
endDateTime = new Date(stringDate);
|
|
||||||
} else {
|
|
||||||
const stringDate = end.date;
|
|
||||||
endDateTime = new Date(stringDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const googleEvent = {
|
data.items?.forEach((item) => {
|
||||||
id: item.id,
|
let isAllDay = false;
|
||||||
title: item.summary ?? "",
|
let startDateTime, endDateTime;
|
||||||
startDate: startDateTime,
|
|
||||||
endDate: endDateTime,
|
|
||||||
allDay: isAllDay,
|
|
||||||
familyId,
|
|
||||||
email
|
|
||||||
};
|
|
||||||
googleEvents.push(googleEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {googleEvents, success: response.ok};
|
if (item.start) {
|
||||||
}
|
if (item.start.dateTime) {
|
||||||
|
startDateTime = new Date(item.start.dateTime);
|
||||||
|
} else if (item.start.date) {
|
||||||
|
startDateTime = new Date(item.start.date);
|
||||||
|
isAllDay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.end) {
|
||||||
|
if (item.end.dateTime) {
|
||||||
|
endDateTime = new Date(item.end.dateTime);
|
||||||
|
} else if (item.end.date) {
|
||||||
|
endDateTime = new Date(item.end.date);
|
||||||
|
isAllDay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const googleEvent = {
|
||||||
|
id: item.id,
|
||||||
|
title: item.summary || "",
|
||||||
|
startDate: startDateTime,
|
||||||
|
endDate: endDateTime,
|
||||||
|
allDay: isAllDay,
|
||||||
|
familyId,
|
||||||
|
email,
|
||||||
|
};
|
||||||
|
|
||||||
|
googleEvents.push(googleEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare for the next page if it exists
|
||||||
|
pageToken = data.nextPageToken;
|
||||||
|
} while (pageToken);
|
||||||
|
|
||||||
|
return { googleEvents, success: true };
|
||||||
|
}
|
@ -14,6 +14,7 @@ import { useAtom } from "jotai";
|
|||||||
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
|
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
|
||||||
import { format, isSameDay } from "date-fns";
|
import { format, isSameDay } from "date-fns";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import {useIsMutating} from "react-query";
|
||||||
|
|
||||||
export const CalendarHeader = memo(() => {
|
export const CalendarHeader = memo(() => {
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
|
@ -11,7 +11,7 @@ export default function CalendarPage() {
|
|||||||
paddingT-0
|
paddingT-0
|
||||||
>
|
>
|
||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Let's get your week started!"}
|
message={"Let's get your week started !"}
|
||||||
isWelcome
|
isWelcome
|
||||||
isCalendar={true}
|
isCalendar={true}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Calendar } from "react-native-big-calendar";
|
import { Calendar } from "react-native-big-calendar";
|
||||||
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
|
import { ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle } from "react-native";
|
||||||
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
||||||
import { useAtom, useSetAtom } from "jotai";
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
@ -15,6 +15,8 @@ import { useAuthContext } from "@/contexts/AuthContext";
|
|||||||
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
||||||
import { Text } from "react-native-ui-lib";
|
import { Text } from "react-native-ui-lib";
|
||||||
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
|
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
|
||||||
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
|
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
||||||
|
|
||||||
interface EventCalendarProps {
|
interface EventCalendarProps {
|
||||||
calendarHeight: number;
|
calendarHeight: number;
|
||||||
@ -39,7 +41,9 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
const {isSyncing} = useSyncEvents()
|
||||||
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
|
useCalSync()
|
||||||
|
|
||||||
const todaysDate = new Date();
|
const todaysDate = new Date();
|
||||||
|
|
||||||
@ -47,15 +51,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
(event: CalendarEvent) => {
|
(event: CalendarEvent) => {
|
||||||
if (mode === "day" || mode === "week") {
|
if (mode === "day" || mode === "week") {
|
||||||
setEditVisible(true);
|
setEditVisible(true);
|
||||||
console.log({ event });
|
// console.log({event});
|
||||||
setEventForEdit(event);
|
setEventForEdit(event);
|
||||||
} else {
|
} else {
|
||||||
setMode("day");
|
setMode("day");
|
||||||
setSelectedDate(event.start);
|
setSelectedDate(event.start);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setEditVisible, setEventForEdit, mode]
|
[setEditVisible, setEventForEdit, mode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePressCell = useCallback(
|
const handlePressCell = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
@ -94,7 +98,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const memoizedEventCellStyle = useCallback(
|
const memoizedEventCellStyle = useCallback(
|
||||||
(event: CalendarEvent) => ({ backgroundColor: event.eventColor }),
|
(event: CalendarEvent) => ({ backgroundColor: event.eventColor , fontSize: 14}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -103,9 +107,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
[profileData]
|
[profileData]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log({
|
// console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek,
|
||||||
memoizedWeekStartsOn,
|
|
||||||
profileData: profileData?.firstDayOfWeek,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||||
@ -175,11 +177,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
}, {} as Record<string, CalendarEvent[]>);
|
}, {} as Record<string, CalendarEvent[]>);
|
||||||
|
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
console.log(
|
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
"memoizedEvents computation time:",
|
|
||||||
endTime - startTime,
|
|
||||||
"ms"
|
|
||||||
);
|
|
||||||
|
|
||||||
return { enrichedEvents, filteredEvents };
|
return { enrichedEvents, filteredEvents };
|
||||||
}, [events, selectedDate, mode]);
|
}, [events, selectedDate, mode]);
|
||||||
@ -239,106 +237,123 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color="#0000ff" />
|
{isSyncing && <Text>Syncing...</Text>}
|
||||||
</View>
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
);
|
</View>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// console.log(enrichedEvents, filteredEvents)
|
// console.log(enrichedEvents, filteredEvents)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<>
|
||||||
bodyContainerStyle={styles.calHeader}
|
{isSyncing && (
|
||||||
swipeEnabled
|
<View style={styles.loadingContainer}>
|
||||||
mode={mode}
|
{isSyncing && <Text>Syncing...</Text>}
|
||||||
enableEnrichedEvents={true}
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
sortedMonthView
|
</View>
|
||||||
// enrichedEventsByDate={enrichedEvents}
|
)}
|
||||||
events={filteredEvents}
|
<Calendar
|
||||||
// eventCellStyle={memoizedEventCellStyle}
|
bodyContainerStyle={styles.calHeader}
|
||||||
onPressEvent={handlePressEvent}
|
swipeEnabled
|
||||||
weekStartsOn={memoizedWeekStartsOn}
|
mode={mode}
|
||||||
height={calendarHeight}
|
// enableEnrichedEvents={true}
|
||||||
activeDate={todaysDate}
|
sortedMonthView
|
||||||
date={selectedDate}
|
// enrichedEventsByDate={enrichedEvents}
|
||||||
onPressCell={handlePressCell}
|
events={filteredEvents}
|
||||||
headerContentStyle={memoizedHeaderContentStyle}
|
eventCellStyle={memoizedEventCellStyle}
|
||||||
onSwipeEnd={handleSwipeEnd}
|
onPressEvent={handlePressEvent}
|
||||||
scrollOffsetMinutes={offsetMinutes}
|
weekStartsOn={memoizedWeekStartsOn}
|
||||||
theme={{
|
height={calendarHeight}
|
||||||
palette: {
|
activeDate={todaysDate}
|
||||||
nowIndicator: profileData?.eventColor || "#fd1575",
|
date={selectedDate}
|
||||||
gray: {
|
onPressCell={handlePressCell}
|
||||||
"100": "#e8eaed",
|
headerContentStyle={memoizedHeaderContentStyle}
|
||||||
"200": "#e8eaed",
|
onSwipeEnd={handleSwipeEnd}
|
||||||
"500": "#b7b7b7",
|
scrollOffsetMinutes={offsetMinutes}
|
||||||
"800": "#919191",
|
theme={{
|
||||||
},
|
palette: {
|
||||||
},
|
nowIndicator: profileData?.eventColor || "#fd1575",
|
||||||
typography: {
|
gray: {
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
"100": "#e8eaed",
|
||||||
sm: { fontFamily: "Manrope_600SemiBold", fontSize: 15 },
|
"200": "#e8eaed",
|
||||||
xl: {
|
"500": "#b7b7b7",
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
"800": "#919191",
|
||||||
fontSize: 16,
|
},
|
||||||
},
|
},
|
||||||
moreLabel: {},
|
typography: {
|
||||||
xs: { fontSize: 10 },
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
},
|
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
|
||||||
}}
|
xl: {
|
||||||
dayHeaderStyle={dateStyle}
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
dayHeaderHighlightColor={"white"}
|
fontSize: 14,
|
||||||
showAdjacentMonths
|
},
|
||||||
hourStyle={styles.hourStyle}
|
moreLabel: {},
|
||||||
onPressDateHeader={handlePressDayHeader}
|
xs: {fontSize: 10},
|
||||||
ampm
|
},
|
||||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
}}
|
||||||
/>
|
dayHeaderStyle={dateStyle}
|
||||||
);
|
dayHeaderHighlightColor={"white"}
|
||||||
}
|
showAdjacentMonths
|
||||||
|
headerContainerStyle={mode !== "month" ? {
|
||||||
|
overflow:"hidden",
|
||||||
|
} : {}}
|
||||||
|
hourStyle={styles.hourStyle}
|
||||||
|
onPressDateHeader={handlePressDayHeader} ampm
|
||||||
|
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
segmentslblStyle: {
|
segmentslblStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
},
|
},
|
||||||
calHeader: {
|
calHeader: {
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
},
|
},
|
||||||
dayModeHeader: {
|
dayModeHeader: {
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
width: 38,
|
width: 38,
|
||||||
right: 42,
|
right: 42,
|
||||||
height: 13,
|
height: 13,
|
||||||
},
|
},
|
||||||
weekModeHeader: {},
|
weekModeHeader: {},
|
||||||
monthModeHeader: {},
|
monthModeHeader: {},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
position: "absolute",
|
||||||
dayHeader: {
|
width: "100%",
|
||||||
backgroundColor: "#4184f2",
|
height: "100%",
|
||||||
aspectRatio: 1,
|
zIndex: 100,
|
||||||
borderRadius: 100,
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||||
alignItems: "center",
|
},
|
||||||
justifyContent: "center",
|
dayHeader: {
|
||||||
},
|
backgroundColor: "#4184f2",
|
||||||
otherDayHeader: {
|
aspectRatio: 1,
|
||||||
backgroundColor: "transparent",
|
borderRadius: 100,
|
||||||
color: "#919191",
|
alignItems: "center",
|
||||||
aspectRatio: 1,
|
justifyContent: "center",
|
||||||
borderRadius: 100,
|
},
|
||||||
alignItems: "center",
|
otherDayHeader: {
|
||||||
justifyContent: "center",
|
backgroundColor: "transparent",
|
||||||
},
|
color: "#919191",
|
||||||
hourStyle: {
|
aspectRatio: 1,
|
||||||
color: "#5f6368",
|
borderRadius: 100,
|
||||||
fontSize: 12,
|
alignItems: "center",
|
||||||
fontFamily: "Manrope_500Medium",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
hourStyle: {
|
||||||
|
color: "#5f6368",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
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>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.subTitle} marginT-30 marginB-25>
|
<Text style={styles.subTitle} marginT-30 marginB-25>
|
||||||
Add Calendar
|
Add Calendars
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
||||||
|
|
||||||
interface ConfirmationDialogProps {
|
interface ConfirmationDialogProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -17,6 +18,7 @@ const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
|
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -21,6 +21,7 @@ import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePictur
|
|||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
|
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
|
||||||
import {AntDesign} from "@expo/vector-icons";
|
import {AntDesign} from "@expo/vector-icons";
|
||||||
|
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
||||||
|
|
||||||
const MyProfile = () => {
|
const MyProfile = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
@ -53,6 +54,7 @@ const MyProfile = () => {
|
|||||||
|
|
||||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||||
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
||||||
|
const { mutateAsync: deleteAsync } = useDeleteUser()
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
const handleUpdateUserData = async () => {
|
const handleUpdateUserData = async () => {
|
||||||
@ -305,9 +307,7 @@ const MyProfile = () => {
|
|||||||
}}
|
}}
|
||||||
visible={showDeleteDialog}
|
visible={showDeleteDialog}
|
||||||
onDismiss={handleHideDeleteDialog}
|
onDismiss={handleHideDeleteDialog}
|
||||||
onConfirm={() => {
|
onConfirm={() => deleteAsync({})}
|
||||||
console.log("delete account here");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
@ -164,16 +164,19 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
}, [user, ready, redirectOverride]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
// const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
const eventId = notification?.request?.content?.data?.eventId;
|
// const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
//
|
||||||
if (eventId) {
|
// // if (eventId) {
|
||||||
queryClient.invalidateQueries(['events']);
|
// queryClient.invalidateQueries(['events']);
|
||||||
}
|
// // }
|
||||||
});
|
// };
|
||||||
return () => sub.remove()
|
//
|
||||||
}, []);
|
// const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
//
|
||||||
|
// return () => sub.remove();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const {onRequest} = require("firebase-functions/v2/https");
|
const {onRequest} = require("firebase-functions/v2/https");
|
||||||
const {getAuth} = require("firebase-admin/auth");
|
const {getAuth} = require("firebase-admin/auth");
|
||||||
const {getFirestore} = require("firebase-admin/firestore");
|
const {getFirestore, Timestamp} = require("firebase-admin/firestore");
|
||||||
const logger = require("firebase-functions/logger");
|
const logger = require("firebase-functions/logger");
|
||||||
const functions = require('firebase-functions');
|
const functions = require('firebase-functions');
|
||||||
const admin = require('firebase-admin');
|
const admin = require('firebase-admin');
|
||||||
@ -9,19 +9,23 @@ const {Expo} = require('expo-server-sdk');
|
|||||||
admin.initializeApp();
|
admin.initializeApp();
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
|
|
||||||
let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
|
let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN});
|
||||||
let notificationTimeout = null;
|
let notificationTimeout = null;
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
let pushTokens = [];
|
let notificationInProgress = false;
|
||||||
|
|
||||||
|
const GOOGLE_CALENDAR_ID = "primary";
|
||||||
|
const CHANNEL_ID = "cally-family-calendar";
|
||||||
|
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||||
|
|
||||||
exports.sendNotificationOnEventCreation = functions.firestore
|
exports.sendNotificationOnEventCreation = functions.firestore
|
||||||
.document('Events/{eventId}')
|
.document('Events/{eventId}')
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
const eventData = snapshot.data();
|
const eventData = snapshot.data();
|
||||||
const { familyId, creatorId, email } = eventData;
|
const { familyId, creatorId, email, title } = eventData;
|
||||||
|
|
||||||
if (email) {
|
if (!!eventData?.externalOrigin) {
|
||||||
console.log('Event has an email field. Skipping notification.');
|
console.log('Externally synced event, ignoring.')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,60 +43,78 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
|
|
||||||
eventCount++;
|
eventCount++;
|
||||||
|
|
||||||
if (notificationTimeout) {
|
// Only set up the notification timeout if it's not already in progress
|
||||||
clearTimeout(notificationTimeout);
|
if (!notificationInProgress) {
|
||||||
}
|
notificationInProgress = true;
|
||||||
|
|
||||||
notificationTimeout = setTimeout(async () => {
|
notificationTimeout = setTimeout(async () => {
|
||||||
const eventMessage = eventCount === 1
|
const eventMessage = eventCount === 1
|
||||||
? `An event "${eventData.title}" has been added. Check it out!`
|
? `An event "${title}" has been added. Check it out!`
|
||||||
: `${eventCount} new events have been added.`;
|
: `${eventCount} new events have been added.`;
|
||||||
|
|
||||||
let messages = pushTokens.map(pushToken => {
|
let messages = pushTokens.map(pushToken => {
|
||||||
if (!Expo.isExpoPushToken(pushToken)) {
|
if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to: pushToken,
|
to: pushToken,
|
||||||
sound: 'default',
|
sound: 'default',
|
||||||
title: 'New Events Added!',
|
title: 'New Events Added!',
|
||||||
body: eventMessage,
|
body: eventMessage,
|
||||||
data: { eventId: context.params.eventId },
|
data: { eventId: context.params.eventId },
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
let chunks = expo.chunkPushNotifications(messages);
|
let chunks = expo.chunkPushNotifications(messages);
|
||||||
let tickets = [];
|
let tickets = [];
|
||||||
|
|
||||||
for (let chunk of chunks) {
|
for (let chunk of chunks) {
|
||||||
try {
|
try {
|
||||||
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
||||||
tickets.push(...ticketChunk);
|
tickets.push(...ticketChunk);
|
||||||
|
|
||||||
for (let ticket of ticketChunk) {
|
for (let ticket of ticketChunk) {
|
||||||
if (ticket.status === 'ok') {
|
if (ticket.status === 'ok') {
|
||||||
console.log('Notification successfully sent:', ticket.id);
|
console.log('Notification successfully sent:', ticket.id);
|
||||||
} else if (ticket.status === 'error') {
|
} else if (ticket.status === 'error') {
|
||||||
console.error(`Notification error: ${ticket.message}`);
|
console.error(`Notification error: ${ticket.message}`);
|
||||||
if (ticket.details?.error === 'DeviceNotRegistered') {
|
if (ticket.details?.error === 'DeviceNotRegistered') {
|
||||||
await removeInvalidPushToken(ticket.to);
|
await removeInvalidPushToken(ticket.to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending notification:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending notification:', error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
eventCount = 0;
|
// Save the notification in Firestore for record-keeping
|
||||||
pushTokens = [];
|
const notificationData = {
|
||||||
|
creatorId,
|
||||||
|
familyId,
|
||||||
|
content: eventMessage,
|
||||||
|
eventId: context.params.eventId,
|
||||||
|
timestamp: Timestamp.now(),
|
||||||
|
};
|
||||||
|
|
||||||
}, 5000);
|
try {
|
||||||
|
await db.collection("Notifications").add(notificationData);
|
||||||
|
console.log("Notification stored in Firestore:", notificationData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving notification to Firestore:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state variables after notifications are sent
|
||||||
|
eventCount = 0;
|
||||||
|
pushTokens = [];
|
||||||
|
notificationInProgress = false;
|
||||||
|
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
exports.createSubUser = onRequest(async (request, response) => {
|
exports.createSubUser = onRequest(async (request, response) => {
|
||||||
const authHeader = request.get('Authorization');
|
const authHeader = request.get('Authorization');
|
||||||
|
|
||||||
@ -262,7 +284,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
|
exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(async (context) => {
|
||||||
console.log('Running token refresh job...');
|
console.log('Running token refresh job...');
|
||||||
|
|
||||||
const profilesSnapshot = await db.collection('Profiles').get();
|
const profilesSnapshot = await db.collection('Profiles').get();
|
||||||
@ -275,8 +297,11 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
|
|||||||
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
||||||
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||||
if (googleToken) {
|
if (googleToken) {
|
||||||
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
const {refreshedGoogleToken, refreshedRefreshToken} = await refreshGoogleToken(googleToken);
|
||||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
const updatedGoogleAccounts = {
|
||||||
|
...profileData.googleAccounts,
|
||||||
|
[googleEmail]: {accessToken: refreshedGoogleToken, refreshToken: refreshedRefreshToken}
|
||||||
|
};
|
||||||
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
await profileDoc.ref.update({googleAccounts: updatedGoogleAccounts});
|
||||||
console.log(`Google token updated for user ${profileDoc.id}`);
|
console.log(`Google token updated for user ${profileDoc.id}`);
|
||||||
}
|
}
|
||||||
@ -292,7 +317,10 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
|
|||||||
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
||||||
if (microsoftToken) {
|
if (microsoftToken) {
|
||||||
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
|
const refreshedMicrosoftToken = await refreshMicrosoftToken(microsoftToken);
|
||||||
const updatedMicrosoftAccounts = {...profileData.microsoftAccounts, [microsoftEmail]: refreshedMicrosoftToken};
|
const updatedMicrosoftAccounts = {
|
||||||
|
...profileData.microsoftAccounts,
|
||||||
|
[microsoftEmail]: refreshedMicrosoftToken
|
||||||
|
};
|
||||||
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
|
await profileDoc.ref.update({microsoftAccounts: updatedMicrosoftAccounts});
|
||||||
console.log(`Microsoft token updated for user ${profileDoc.id}`);
|
console.log(`Microsoft token updated for user ${profileDoc.id}`);
|
||||||
}
|
}
|
||||||
@ -320,21 +348,6 @@ exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function refreshGoogleToken(refreshToken) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data.access_token; // Return the new access token
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error refreshing Google token:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshMicrosoftToken(refreshToken) {
|
async function refreshMicrosoftToken(refreshToken) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
@ -385,4 +398,409 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
|||||||
|
|
||||||
async function removeInvalidPushToken(pushToken) {
|
async function removeInvalidPushToken(pushToken) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
// Function to refresh Google Token with additional logging
|
||||||
|
async function refreshGoogleToken(refreshToken) {
|
||||||
|
try {
|
||||||
|
console.log("Refreshing Google token...");
|
||||||
|
const response = await fetch("https://oauth2.googleapis.com/token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Error refreshing Google token:", errorData);
|
||||||
|
throw new Error(`Failed to refresh Google token: ${errorData.error || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Google token refreshed successfully");
|
||||||
|
|
||||||
|
// Return both the access token and refresh token (if a new one is provided)
|
||||||
|
return {
|
||||||
|
refreshedGoogleToken: data.access_token,
|
||||||
|
refreshedRefreshToken: data.refresh_token || refreshToken, // Return the existing refresh token if a new one is not provided
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Google token:", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get Google access tokens for all users and refresh them if needed with logging
|
||||||
|
async function getGoogleAccessTokens() {
|
||||||
|
console.log("Fetching Google access tokens for all users...");
|
||||||
|
const tokens = {};
|
||||||
|
const profilesSnapshot = await db.collection("Profiles").get();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
profilesSnapshot.docs.map(async (doc) => {
|
||||||
|
const profileData = doc.data();
|
||||||
|
const googleAccounts = profileData?.googleAccounts || {};
|
||||||
|
|
||||||
|
for (const googleEmail of Object.keys(googleAccounts)) {
|
||||||
|
// Check if the googleAccount entry exists and has a refreshToken
|
||||||
|
const accountInfo = googleAccounts[googleEmail];
|
||||||
|
const refreshToken = accountInfo?.refreshToken;
|
||||||
|
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
console.log(`Refreshing token for user ${doc.id} (email: ${googleEmail})`);
|
||||||
|
const {refreshedGoogleToken} = await refreshGoogleToken(refreshToken);
|
||||||
|
tokens[doc.id] = accessToken;
|
||||||
|
console.log(`Token refreshed successfully for user ${doc.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
tokens[doc.id] = accountInfo?.accessToken;
|
||||||
|
console.error(`Failed to refresh token for user ${doc.id}:`, error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`No refresh token available for user ${doc.id} (email: ${googleEmail})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Access tokens fetched and refreshed as needed");
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to watch Google Calendar events with additional logging
|
||||||
|
const watchCalendarEvents = async (userId, token) => {
|
||||||
|
const url = `https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events/watch`;
|
||||||
|
|
||||||
|
// Verify the token is valid
|
||||||
|
console.log(`Attempting to watch calendar for user ${userId}`);
|
||||||
|
console.log(`Token being used: ${token ? 'present' : 'missing'}`);
|
||||||
|
console.log(`Calendar ID: ${GOOGLE_CALENDAR_ID}`);
|
||||||
|
console.log(`Webhook URL: ${WEBHOOK_URL}?userId=${userId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test the token first
|
||||||
|
const testResponse = await fetch(
|
||||||
|
`https://www.googleapis.com/calendar/v3/calendars/${GOOGLE_CALENDAR_ID}/events?maxResults=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!testResponse.ok) {
|
||||||
|
console.error(`Token validation failed for user ${userId}:`, await testResponse.text());
|
||||||
|
throw new Error('Token validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Token validated successfully for user ${userId}`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: `${CHANNEL_ID}-${userId}`,
|
||||||
|
type: "web_hook",
|
||||||
|
address: `${WEBHOOK_URL}?userId=${userId}`,
|
||||||
|
params: {
|
||||||
|
ttl: "80000",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log(`Watch response for user ${userId}:`, responseText);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to watch calendar for user ${userId}:`, responseText);
|
||||||
|
throw new Error(`Failed to watch calendar: ${responseText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(responseText);
|
||||||
|
console.log(`Successfully set up Google Calendar watch for user ${userId}`, result);
|
||||||
|
|
||||||
|
// Store the watch details in Firestore for monitoring
|
||||||
|
await db.collection('CalendarWatches').doc(userId).set({
|
||||||
|
watchId: result.id,
|
||||||
|
resourceId: result.resourceId,
|
||||||
|
expiration: result.expiration,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in watchCalendarEvents for user ${userId}:`, error);
|
||||||
|
// Store the error in Firestore for monitoring
|
||||||
|
await db.collection('CalendarWatchErrors').add({
|
||||||
|
userId,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this to test webhook connectivity
|
||||||
|
exports.testWebhook = functions.https.onRequest(async (req, res) => {
|
||||||
|
console.log('Test webhook received');
|
||||||
|
console.log('Headers:', req.headers);
|
||||||
|
console.log('Body:', req.body);
|
||||||
|
console.log('Query:', req.query);
|
||||||
|
|
||||||
|
res.status(200).send('Test webhook received successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule function to renew Google Calendar watch every 20 hours for each user with logging
|
||||||
|
exports.renewGoogleCalendarWatch = functions.pubsub.schedule("every 10 minutes").onRun(async (context) => {
|
||||||
|
console.log("Starting Google Calendar watch renewal process...");
|
||||||
|
try {
|
||||||
|
const tokens = await getGoogleAccessTokens();
|
||||||
|
console.log("Tokens: ", tokens);
|
||||||
|
|
||||||
|
for (const [userId, token] of Object.entries(tokens)) {
|
||||||
|
try {
|
||||||
|
await watchCalendarEvents(userId, token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error renewing Google Calendar watch for user ${userId}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Google Calendar watch renewal process completed");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in renewGoogleCalendarWatch function:", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to handle notifications from Google Calendar with additional logging
|
||||||
|
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||||
|
const userId = req.query.userId; // Extract userId from query params
|
||||||
|
const calendarId = req.body.resourceId;
|
||||||
|
|
||||||
|
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user profile data for the specific user
|
||||||
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
|
||||||
|
// Ensure pushTokens is an array
|
||||||
|
let pushTokens = [];
|
||||||
|
if (userData && userData.pushToken) {
|
||||||
|
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pushTokens.length === 0) {
|
||||||
|
console.log(`No push tokens found for user ${userId}`);
|
||||||
|
res.status(200).send("No push tokens found for user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call calendarSync with necessary parameters
|
||||||
|
const {googleAccounts} = userData;
|
||||||
|
const email = Object.keys(googleAccounts || {})[0]; // Assuming the first account is the primary
|
||||||
|
const accountData = googleAccounts[email] || {};
|
||||||
|
const token = accountData.accessToken;
|
||||||
|
const refreshToken = accountData.refreshToken;
|
||||||
|
const familyId = userData.familyId;
|
||||||
|
|
||||||
|
console.log("Starting calendar sync...");
|
||||||
|
await calendarSync({userId, email, token, refreshToken, familyId});
|
||||||
|
console.log("Calendar sync completed.");
|
||||||
|
|
||||||
|
// Prepare and send push notifications after sync
|
||||||
|
// const syncMessage = "New events have been synced.";
|
||||||
|
//
|
||||||
|
// let messages = pushTokens.map(pushToken => {
|
||||||
|
// if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
|
// console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// to: pushToken,
|
||||||
|
// sound: "default",
|
||||||
|
// title: "Event Sync",
|
||||||
|
// body: syncMessage,
|
||||||
|
// data: { userId, calendarId },
|
||||||
|
// };
|
||||||
|
// }).filter(Boolean);
|
||||||
|
//
|
||||||
|
// let chunks = expo.chunkPushNotifications(messages);
|
||||||
|
// let tickets = [];
|
||||||
|
//
|
||||||
|
// for (let chunk of chunks) {
|
||||||
|
// try {
|
||||||
|
// let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
||||||
|
// tickets.push(...ticketChunk);
|
||||||
|
//
|
||||||
|
// for (let ticket of ticketChunk) {
|
||||||
|
// if (ticket.status === "ok") {
|
||||||
|
// console.log("Notification successfully sent:", ticket.id);
|
||||||
|
// } else if (ticket.status === "error") {
|
||||||
|
// console.error(`Notification error: ${ticket.message}`);
|
||||||
|
// if (ticket.details?.error === "DeviceNotRegistered") {
|
||||||
|
// await removeInvalidPushToken(ticket.to);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error sending notification:", error.message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// console.log(`Sync notification sent for user ${userId}`);
|
||||||
|
res.status(200).send("Sync notification sent.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||||
|
res.status(500).send("Failed to send sync notification.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId }) {
|
||||||
|
const baseDate = new Date();
|
||||||
|
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||||
|
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||||
|
|
||||||
|
let events = [];
|
||||||
|
let pageToken = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching events for user: ${email}`);
|
||||||
|
|
||||||
|
// Fetch all events from Google Calendar within the specified time range
|
||||||
|
do {
|
||||||
|
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||||
|
url.searchParams.set("singleEvents", "true");
|
||||||
|
url.searchParams.set("timeMin", timeMin);
|
||||||
|
url.searchParams.set("timeMax", timeMax);
|
||||||
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log(`Token expired for user: ${email}, attempting to refresh`);
|
||||||
|
const refreshedToken = await refreshGoogleToken(refreshToken);
|
||||||
|
token = refreshedToken;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to refresh token for user: ${email}`);
|
||||||
|
await clearToken(email);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing events for user: ${email}`);
|
||||||
|
data.items?.forEach((item) => {
|
||||||
|
const googleEvent = {
|
||||||
|
id: item.id,
|
||||||
|
title: item.summary || "",
|
||||||
|
startDate: item.start?.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date),
|
||||||
|
endDate: item.end?.dateTime ? new Date(item.end.dateTime) : new Date(item.end.date),
|
||||||
|
allDay: !item.start?.dateTime,
|
||||||
|
familyId,
|
||||||
|
email,
|
||||||
|
creatorId,
|
||||||
|
externalOrigin: "google",
|
||||||
|
};
|
||||||
|
events.push(googleEvent);
|
||||||
|
console.log(`Processed event: ${JSON.stringify(googleEvent)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
pageToken = data.nextPageToken;
|
||||||
|
} while (pageToken);
|
||||||
|
|
||||||
|
console.log(`Saving events to Firestore for user: ${email}`);
|
||||||
|
await saveEventsToFirestore(events);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching Google Calendar events for ${email}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEventsToFirestore(events) {
|
||||||
|
const batch = db.batch();
|
||||||
|
events.forEach((event) => {
|
||||||
|
const eventRef = db.collection("Events").doc(event.id);
|
||||||
|
batch.set(eventRef, event, { merge: true });
|
||||||
|
});
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calendarSync({ userId, email, token, refreshToken, familyId }) {
|
||||||
|
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
|
||||||
|
try {
|
||||||
|
await fetchAndSaveGoogleEvents({
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
email,
|
||||||
|
familyId,
|
||||||
|
creatorId: userId,
|
||||||
|
});
|
||||||
|
console.log("Calendar events synced successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error syncing calendar for user ${userId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.log(`Finished calendar sync for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||||
|
const userId = req.query.userId;
|
||||||
|
const calendarId = req.body.resourceId;
|
||||||
|
|
||||||
|
console.log(`Received notification for user ${userId} with calendar ID ${calendarId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userDoc = await db.collection("Profiles").doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
|
||||||
|
let pushTokens = [];
|
||||||
|
if (userData && userData.pushToken) {
|
||||||
|
pushTokens = Array.isArray(userData.pushToken) ? userData.pushToken : [userData.pushToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pushTokens.length === 0) {
|
||||||
|
console.log(`No push tokens found for user ${userId}`);
|
||||||
|
res.status(200).send("No push tokens found for user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { googleAccounts } = userData;
|
||||||
|
const email = Object.keys(googleAccounts || {})[0];
|
||||||
|
const accountData = googleAccounts[email] || {};
|
||||||
|
const token = accountData.accessToken;
|
||||||
|
const refreshToken = accountData.refreshToken;
|
||||||
|
const familyId = userData.familyId;
|
||||||
|
|
||||||
|
console.log("Starting calendar sync...");
|
||||||
|
await calendarSync({ userId, email, token, refreshToken, familyId });
|
||||||
|
console.log("Calendar sync completed.");
|
||||||
|
|
||||||
|
res.status(200).send("Sync notification sent.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||||
|
res.status(500).send("Failed to send sync notification.");
|
||||||
|
}
|
||||||
|
});
|
@ -17,20 +17,23 @@ export const useClearTokens = () => {
|
|||||||
if (provider === "google") {
|
if (provider === "google") {
|
||||||
let googleAccounts = profileData?.googleAccounts;
|
let googleAccounts = profileData?.googleAccounts;
|
||||||
if (googleAccounts) {
|
if (googleAccounts) {
|
||||||
googleAccounts[email] = null;
|
const newGoogleAccounts = {...googleAccounts}
|
||||||
newUserData.googleAccounts = googleAccounts;
|
delete newGoogleAccounts[email];
|
||||||
|
newUserData.googleAccounts = newGoogleAccounts;
|
||||||
}
|
}
|
||||||
} else if (provider === "outlook") {
|
} else if (provider === "outlook") {
|
||||||
let microsoftAccounts = profileData?.microsoftAccounts;
|
let microsoftAccounts = profileData?.microsoftAccounts;
|
||||||
if (microsoftAccounts) {
|
if (microsoftAccounts) {
|
||||||
microsoftAccounts[email] = null;
|
const newMicrosoftAccounts = {...microsoftAccounts}
|
||||||
newUserData.microsoftAccounts = microsoftAccounts;
|
delete microsoftAccounts[email];
|
||||||
|
newUserData.microsoftAccounts = newMicrosoftAccounts;
|
||||||
}
|
}
|
||||||
} else if (provider === "apple") {
|
} else if (provider === "apple") {
|
||||||
let appleAccounts = profileData?.appleAccounts;
|
let appleAccounts = profileData?.appleAccounts;
|
||||||
if (appleAccounts) {
|
if (appleAccounts) {
|
||||||
appleAccounts[email] = null;
|
const newAppleAccounts = {...appleAccounts}
|
||||||
newUserData.appleAccounts = appleAccounts;
|
delete newAppleAccounts[email];
|
||||||
|
newUserData.appleAccounts = newAppleAccounts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await updateUserData({newUserData});
|
await updateUserData({newUserData});
|
||||||
|
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 { useAtomValue } from "jotai";
|
||||||
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
export const useGetEvents = () => {
|
export const useGetEvents = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
@ -20,22 +21,22 @@ export const useGetEvents = () => {
|
|||||||
|
|
||||||
// If family view is active, include family, creator, and attendee events
|
// If family view is active, include family, creator, and attendee events
|
||||||
if (isFamilyView) {
|
if (isFamilyView) {
|
||||||
const familyQuery = db.collection("Events").where("familyID", "==", familyId);
|
const familyQuery = db.collection("Events").where("familyId", "==", familyId);
|
||||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
|
||||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
||||||
|
|
||||||
const [familySnapshot, creatorSnapshot, attendeeSnapshot] = await Promise.all([
|
const [familySnapshot, attendeeSnapshot] = await Promise.all([
|
||||||
familyQuery.get(),
|
familyQuery.get(),
|
||||||
creatorQuery.get(),
|
|
||||||
attendeeQuery.get(),
|
attendeeQuery.get(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Collect all events
|
// Collect all events
|
||||||
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
||||||
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
|
|
||||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
||||||
|
|
||||||
allEvents = [...familyEvents, ...creatorEvents, ...attendeeEvents];
|
// console.log("Family events not in creator query: ", familyEvents.filter(event => !creatorEvents.some(creatorEvent => creatorEvent.id === event.id)));
|
||||||
|
|
||||||
|
|
||||||
|
allEvents = [...familyEvents, ...attendeeEvents];
|
||||||
} else {
|
} else {
|
||||||
// Only include creator and attendee events when family view is off
|
// Only include creator and attendee events when family view is off
|
||||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
||||||
@ -58,7 +59,7 @@ export const useGetEvents = () => {
|
|||||||
if (event.id) {
|
if (event.id) {
|
||||||
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
|
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
|
||||||
} else {
|
} else {
|
||||||
uniqueEventsMap.set(Math.random().toString(36), event); // Generate a temp key for events without ID
|
uniqueEventsMap.set(uuidv4(), event); // Generate a temp key for events without ID
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const uniqueEvents = Array.from(uniqueEventsMap.values());
|
const uniqueEvents = Array.from(uniqueEventsMap.values());
|
||||||
@ -83,7 +84,7 @@ export const useGetEvents = () => {
|
|||||||
const eventColor = profileData?.eventColor || colorMap.pink;
|
const eventColor = profileData?.eventColor || colorMap.pink;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.id || Math.random().toString(36).substr(2, 9), // Generate temp ID if missing
|
id: event.id || Math.random().toString(36).slice(2, 9), // Generate temp ID if missing
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start: new Date(event.startDate.seconds * 1000),
|
start: new Date(event.startDate.seconds * 1000),
|
||||||
end: new Date(event.endDate.seconds * 1000),
|
end: new Date(event.endDate.seconds * 1000),
|
||||||
@ -96,5 +97,6 @@ export const useGetEvents = () => {
|
|||||||
},
|
},
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
cacheTime: Infinity,
|
cacheTime: Infinity,
|
||||||
|
keepPreviousData: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
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 Google from "expo-auth-session/providers/google";
|
||||||
import * as AuthSession from "expo-auth-session";
|
import * as AuthSession from "expo-auth-session";
|
||||||
import * as AppleAuthentication from "expo-apple-authentication";
|
import * as AppleAuthentication from "expo-apple-authentication";
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import {useQueryClient} from "react-query";
|
||||||
|
|
||||||
const googleConfig = {
|
const googleConfig = {
|
||||||
androidClientId:
|
androidClientId:
|
||||||
@ -44,16 +46,12 @@ const microsoftConfig = {
|
|||||||
|
|
||||||
export const useCalSync = () => {
|
export const useCalSync = () => {
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
|
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
|
||||||
useFetchAndSaveGoogleEvents();
|
const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
|
||||||
const {
|
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
|
||||||
mutateAsync: fetchAndSaveOutlookEvents,
|
|
||||||
isLoading: isSyncingOutlook,
|
|
||||||
} = useFetchAndSaveOutlookEvents();
|
|
||||||
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
|
|
||||||
useFetchAndSaveAppleEvents();
|
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
||||||
@ -74,6 +72,7 @@ export const useCalSync = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(response)
|
||||||
const userInfo = await userInfoResponse.json();
|
const userInfo = await userInfoResponse.json();
|
||||||
const googleMail = userInfo.email;
|
const googleMail = userInfo.email;
|
||||||
|
|
||||||
@ -83,12 +82,15 @@ export const useCalSync = () => {
|
|||||||
[googleMail]: {accessToken, refreshToken},
|
[googleMail]: {accessToken, refreshToken},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log({refreshToken})
|
||||||
|
|
||||||
await updateUserData({
|
await updateUserData({
|
||||||
newUserData: {googleAccounts: updatedGoogleAccounts},
|
newUserData: {googleAccounts: updatedGoogleAccounts},
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetchAndSaveGoogleEvents({
|
await fetchAndSaveGoogleEvents({
|
||||||
token: accessToken,
|
token: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
email: googleMail,
|
email: googleMail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -238,6 +240,38 @@ export const useCalSync = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const resyncAllCalendars = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const syncPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (profileData?.googleAccounts) {
|
||||||
|
console.log(profileData.googleAccounts)
|
||||||
|
for (const [email, emailAcc] of Object.entries(profileData.googleAccounts)) {
|
||||||
|
if(emailAcc?.accessToken) {
|
||||||
|
syncPromises.push(fetchAndSaveGoogleEvents({ token: emailAcc?.accessToken, refreshToken: emailAcc?.refreshToken, email }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileData?.microsoftAccounts) {
|
||||||
|
for (const [email, accessToken] of Object.entries(profileData.microsoftAccounts)) {
|
||||||
|
syncPromises.push(fetchAndSaveOutlookEvents(accessToken, email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileData?.appleAccounts) {
|
||||||
|
for (const [email, token] of Object.entries(profileData.appleAccounts)) {
|
||||||
|
syncPromises.push(fetchAndSaveAppleEvents({ token, email }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
console.log("All calendars have been resynced.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resyncing calendars:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let isConnectedToGoogle = false;
|
let isConnectedToGoogle = false;
|
||||||
if (profileData?.googleAccounts) {
|
if (profileData?.googleAccounts) {
|
||||||
Object.values(profileData?.googleAccounts).forEach((item) => {
|
Object.values(profileData?.googleAccounts).forEach((item) => {
|
||||||
@ -270,6 +304,20 @@ export const useCalSync = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
|
const eventId = notification?.request?.content?.data?.eventId;
|
||||||
|
|
||||||
|
// await resyncAllCalendars();
|
||||||
|
queryClient.invalidateQueries(["events"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleAppleSignIn,
|
handleAppleSignIn,
|
||||||
handleMicrosoftSignIn,
|
handleMicrosoftSignIn,
|
||||||
@ -283,6 +331,8 @@ export const useCalSync = () => {
|
|||||||
isConnectedToGoogle,
|
isConnectedToGoogle,
|
||||||
isSyncingOutlook,
|
isSyncingOutlook,
|
||||||
isSyncingGoogle,
|
isSyncingGoogle,
|
||||||
isSyncingApple
|
isSyncingApple,
|
||||||
|
resyncAllCalendars,
|
||||||
|
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,11 +9,12 @@ export const useFetchAndSaveAppleEvents = () => {
|
|||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["fetchAndSaveAppleEvents"],
|
mutationKey: ["fetchAndSaveAppleEvents", "sync"],
|
||||||
mutationFn: async ({token, email}: { token?: string, email?: string }) => {
|
mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
|
||||||
console.log("CALLL")
|
const baseDate = date || new Date();
|
||||||
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
|
const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
|
||||||
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
|
const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchiPhoneCalendarEvents(
|
const response = await fetchiPhoneCalendarEvents(
|
||||||
profileData?.familyId!,
|
profileData?.familyId!,
|
||||||
|
@ -1,56 +1,107 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
|
import { fetchGoogleCalendarEvents } from "@/calendar-integration/google-calendar-utils";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent";
|
||||||
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
|
import { useClearTokens } from "@/hooks/firebase/useClearTokens";
|
||||||
|
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
||||||
|
|
||||||
export const useFetchAndSaveGoogleEvents = () => {
|
export const useFetchAndSaveGoogleEvents = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient();
|
||||||
const {profileData} = useAuthContext();
|
const { profileData } = useAuthContext();
|
||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider();
|
||||||
const {mutateAsync: clearToken} = useClearTokens();
|
const { mutateAsync: clearToken } = useClearTokens();
|
||||||
|
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["fetchAndSaveGoogleEvents"],
|
mutationKey: ["fetchAndSaveGoogleEvents", "sync"],
|
||||||
mutationFn: async ({token, email}: { token?: string; email?: string }) => {
|
mutationFn: async ({ token, email, date, refreshToken }: { token?: string; refreshToken?: string; email?: string; date?: Date }) => {
|
||||||
console.log("Fetching Google Calendar events...");
|
const baseDate = date || new Date();
|
||||||
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
|
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString().slice(0, -5) + "Z";
|
||||||
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
|
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString().slice(0, -5) + "Z";
|
||||||
|
|
||||||
console.log("Token: ", token);
|
console.log("Token: ", token);
|
||||||
|
|
||||||
try {
|
const tryFetchEvents = async (isRetry = false) => {
|
||||||
const response = await fetchGoogleCalendarEvents(
|
try {
|
||||||
token,
|
const response = await fetchGoogleCalendarEvents(
|
||||||
email,
|
token,
|
||||||
profileData?.familyId,
|
email,
|
||||||
timeMin.toISOString().slice(0, -5) + "Z",
|
profileData?.familyId,
|
||||||
timeMax.toISOString().slice(0, -5) + "Z"
|
timeMin,
|
||||||
);
|
timeMax
|
||||||
|
);
|
||||||
|
|
||||||
if(!response.success) {
|
if (!response.success) {
|
||||||
await clearToken({email: email!, provider: "google"})
|
await clearToken({ email: email!, provider: "google" });
|
||||||
return
|
return; // Stop refetching if clearing the token
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Google Calendar events fetched:", response);
|
|
||||||
|
|
||||||
const items = response?.googleEvents?.map((item) => {
|
|
||||||
if (item.allDay) {
|
|
||||||
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
|
|
||||||
item.endDate = item.startDate;
|
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
}) || [];
|
|
||||||
|
|
||||||
await createEventsFromProvider(items);
|
console.log("Google Calendar events fetched:", response);
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching Google Calendar events:", error);
|
const items = response?.googleEvents?.map((item) => {
|
||||||
throw error; // Ensure errors are propagated to the mutation
|
if (item.allDay) {
|
||||||
}
|
item.startDate = new Date(item.startDate.setHours(0, 0, 0, 0));
|
||||||
|
item.endDate = item.startDate;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
await createEventsFromProvider(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Google Calendar events:", error);
|
||||||
|
|
||||||
|
if (!isRetry) {
|
||||||
|
const refreshedToken = await handleRefreshToken(email, refreshToken);
|
||||||
|
if (refreshedToken) {
|
||||||
|
await updateUserData({
|
||||||
|
newUserData: {
|
||||||
|
googleAccounts: {
|
||||||
|
...profileData.googleAccounts,
|
||||||
|
[email!]: { ...profileData.googleAccounts[email!], accessToken: refreshedToken },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return tryFetchEvents(true); // Retry once after refreshing
|
||||||
|
} else {
|
||||||
|
await clearToken({ email: email!, provider: "google" });
|
||||||
|
console.error(`Token refresh failed; token cleared for ${email}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Retry failed after refreshing token for user ${profileData?.email}:`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return tryFetchEvents();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["events"])
|
queryClient.invalidateQueries(["events"]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleRefreshToken(email: string, refreshToken: string) {
|
||||||
|
if (!refreshToken) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Google token:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -9,10 +9,11 @@ export const useFetchAndSaveOutlookEvents = () => {
|
|||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["fetchAndSaveOutlookEvents"],
|
mutationKey: ["fetchAndSaveOutlookEvents", "sync"],
|
||||||
mutationFn: async ({token, email}: { token?: string; email?: string }) => {
|
mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
|
||||||
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
|
const baseDate = date || new Date();
|
||||||
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));
|
const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
|
||||||
|
const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
|
||||||
|
|
||||||
console.log("Token: ", token ?? profileData?.microsoftToken);
|
console.log("Token: ", token ?? profileData?.microsoftToken);
|
||||||
|
|
||||||
|
85
hooks/useSyncOnScroll.ts
Normal file
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";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = Cally;
|
PRODUCT_NAME = "Cally";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@ -484,10 +484,10 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = Cally;
|
PRODUCT_NAME = "Cally";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>60</string>
|
<string>74</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
@ -136,6 +136,24 @@
|
|||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
19314
package-lock.json
generated
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": "yarn run prebuild && yarn run build-ios && yarn run submit",
|
||||||
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
||||||
"prebuild-build-submit-cicd": "yarn build-cicd",
|
"prebuild-build-submit-cicd": "yarn build-cicd",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package",
|
||||||
|
"functions-deploy": "cd firebase/functions && firebase deploy --only functions"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
@ -33,19 +34,19 @@
|
|||||||
"@expo-google-fonts/poppins": "^0.2.3",
|
"@expo-google-fonts/poppins": "^0.2.3",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-native-community/blur": "^4.4.0",
|
"@react-native-community/blur": "^4.4.0",
|
||||||
"@react-native-community/datetimepicker": "^8.2.0",
|
"@react-native-community/datetimepicker": "8.0.1",
|
||||||
"@react-native-firebase/app": "^20.3.0",
|
"@react-native-firebase/app": "^20.3.0",
|
||||||
"@react-native-firebase/auth": "^20.3.0",
|
"@react-native-firebase/auth": "^20.3.0",
|
||||||
"@react-native-firebase/crashlytics": "^20.3.0",
|
"@react-native-firebase/crashlytics": "^20.3.0",
|
||||||
"@react-native-firebase/firestore": "^20.4.0",
|
"@react-native-firebase/firestore": "^20.4.0",
|
||||||
"@react-native-firebase/functions": "^20.4.0",
|
"@react-native-firebase/functions": "^20.4.0",
|
||||||
"@react-native-firebase/storage": "^21.0.0",
|
"@react-native-firebase/storage": "^20.4.0",
|
||||||
"@react-native-menu/menu": "^1.1.6",
|
"@react-native-menu/menu": "^1.1.6",
|
||||||
"@react-navigation/drawer": "^6.7.2",
|
"@react-navigation/drawer": "^6.7.2",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"debounce": "^2.1.1",
|
"debounce": "^2.1.1",
|
||||||
"expo": "~51.0.24",
|
"expo": "~51.0.38",
|
||||||
"expo-app-loading": "^2.1.1",
|
"expo-app-loading": "^2.1.1",
|
||||||
"expo-apple-authentication": "~6.4.2",
|
"expo-apple-authentication": "~6.4.2",
|
||||||
"expo-auth-session": "^5.5.2",
|
"expo-auth-session": "^5.5.2",
|
||||||
@ -55,15 +56,15 @@
|
|||||||
"expo-calendar": "~13.0.5",
|
"expo-calendar": "~13.0.5",
|
||||||
"expo-camera": "~15.0.16",
|
"expo-camera": "~15.0.16",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
"expo-dev-client": "~4.0.27",
|
"expo-dev-client": "~4.0.28",
|
||||||
"expo-device": "~6.0.2",
|
"expo-device": "~6.0.2",
|
||||||
"expo-font": "~12.0.10",
|
"expo-font": "~12.0.10",
|
||||||
"expo-image-picker": "~15.0.7",
|
"expo-image-picker": "~15.0.7",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-localization": "~15.0.3",
|
"expo-localization": "~15.0.3",
|
||||||
"expo-notifications": "~0.28.18",
|
"expo-notifications": "~0.28.19",
|
||||||
"expo-router": "~3.5.20",
|
"expo-router": "~3.5.20",
|
||||||
"expo-splash-screen": "~0.27.5",
|
"expo-splash-screen": "~0.27.6",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-updates": "~0.25.27",
|
"expo-updates": "~0.25.27",
|
||||||
@ -75,7 +76,7 @@
|
|||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.5",
|
||||||
"react-native-app-auth": "^8.0.0",
|
"react-native-app-auth": "^8.0.0",
|
||||||
"react-native-big-calendar": "^4.15.1",
|
"react-native-big-calendar": "^4.15.1",
|
||||||
"react-native-calendars": "^1.1306.0",
|
"react-native-calendars": "^1.1306.0",
|
||||||
@ -89,7 +90,7 @@
|
|||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-svg": "^15.7.1",
|
"react-native-svg": "15.2.0",
|
||||||
"react-native-svg-icon": "^0.10.0",
|
"react-native-svg-icon": "^0.10.0",
|
||||||
"react-native-toast-message": "^2.2.1",
|
"react-native-toast-message": "^2.2.1",
|
||||||
"react-native-ui-lib": "^7.27.0",
|
"react-native-ui-lib": "^7.27.0",
|
||||||
@ -98,12 +99,16 @@
|
|||||||
"timezonecomplete": "^5.13.1",
|
"timezonecomplete": "^5.13.1",
|
||||||
"tzdata": "^1.0.42"
|
"tzdata": "^1.0.42"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@react-native/assets-registry": "0.74.83"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
"@types/react-native-onboarding-swiper": "^1.1.9",
|
"@types/react-native-onboarding-swiper": "^1.1.9",
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"jest-expo": "~51.0.3",
|
"jest-expo": "~51.0.3",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
|
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
|
||||||
index 848ceba..57fbaed 100644
|
index 848ceba..f326b8e 100644
|
||||||
--- a/node_modules/react-native-big-calendar/build/index.js
|
--- a/node_modules/react-native-big-calendar/build/index.js
|
||||||
+++ b/node_modules/react-native-big-calendar/build/index.js
|
+++ b/node_modules/react-native-big-calendar/build/index.js
|
||||||
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
|
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
|
||||||
@ -184,3 +184,12 @@ index 848ceba..57fbaed 100644
|
|||||||
return finalEvents_1;
|
return finalEvents_1;
|
||||||
}
|
}
|
||||||
}, [events, sortedMonthView]);
|
}, [events, sortedMonthView]);
|
||||||
|
@@ -1311,7 +1326,7 @@ function _CalendarHeader(_a) {
|
||||||
|
!stringHasContent(dayHeaderHighlightColor) &&
|
||||||
|
u['mt-6'],
|
||||||
|
] }, date.format('D')))),
|
||||||
|
- showAllDayEventCell ? (React__namespace.createElement(reactNative.View, { style: [
|
||||||
|
+ showAllDayEventCell ? (React__namespace.createElement(reactNative.ScrollView, { style: [
|
||||||
|
u['border-l'],
|
||||||
|
{ borderColor: theme.palette.gray['200'] },
|
||||||
|
{ height: cellHeight },
|
||||||
|
Reference in New Issue
Block a user