merge resolve

This commit is contained in:
ivic00
2024-10-31 22:35:32 +01:00
13 changed files with 1255 additions and 1145 deletions

View File

@ -2,8 +2,9 @@ import * as Calendar from 'expo-calendar';
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) { export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
try { try {
const {status} = await Calendar.requestCalendarPermissionsAsync(); const {granted} = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') {
if (!granted) {
throw new Error("Calendar permission not granted"); throw new Error("Calendar permission not granted");
} }
@ -22,7 +23,11 @@ export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endD
return events.map((event) => { return events.map((event) => {
let isAllDay = event.allDay || false; let isAllDay = event.allDay || false;
const startDateTime = new Date(event.startDate); const startDateTime = new Date(event.startDate);
const endDateTime = new Date(event.endDate); let endDateTime = new Date(event.endDate);
if (isAllDay) {
endDateTime = startDateTime
}
return { return {
id: event.id, id: event.id,

View File

@ -8,7 +8,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
}, },
); );
const data = await response.json(); const data = await response.json();
const googleEvents = []; const googleEvents = [];
data.items?.forEach((item) => { data.items?.forEach((item) => {
let isAllDay = false; let isAllDay = false;
@ -49,5 +51,5 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
googleEvents.push(googleEvent); googleEvents.push(googleEvent);
}); });
return googleEvents; return {googleEvents, success: response.ok};
} }

View File

@ -1,128 +1,127 @@
import { import {Button, Dialog, TextField, TextFieldRef, TouchableOpacity, View,} from "react-native-ui-lib";
View, import React, {useEffect, useRef, useState} from "react";
Text, import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
Button, import {Dimensions, StyleSheet} from "react-native";
TextField,
TextFieldRef,
TouchableOpacity,
} from "react-native-ui-lib";
import React, { useEffect, useState, useRef } from "react";
import { Dialog } from "react-native-ui-lib";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import { Dimensions, Keyboard, Platform, StyleSheet } from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon"; import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { useBrainDumpContext } from "@/contexts/DumpContext"; import {useBrainDumpContext} from "@/contexts/DumpContext";
import KeyboardManager from "react-native-keyboard-manager"; import KeyboardManager from "react-native-keyboard-manager";
interface IAddBrainDumpProps { interface IAddBrainDumpProps {
isVisible: boolean; isVisible: boolean;
setIsVisible: (value: boolean) => void; setIsVisible: (value: boolean) => void;
} }
const AddBrainDump = ({ const AddBrainDump = ({
addBrainDumpProps, addBrainDumpProps,
}: { }: {
addBrainDumpProps: IAddBrainDumpProps; addBrainDumpProps: IAddBrainDumpProps;
}) => { }) => {
const { addBrainDump } = useBrainDumpContext(); const {addBrainDump} = useBrainDumpContext();
const [dumpTitle, setDumpTitle] = useState<string>(""); const [dumpTitle, setDumpTitle] = useState<string>("");
const [dumpDesc, setDumpDesc] = useState<string>(""); const [dumpDesc, setDumpDesc] = useState<string>("");
const { width } = Dimensions.get("screen"); const {width} = Dimensions.get("screen");
// Refs for the two TextFields
const descriptionRef = useRef<TextFieldRef>(null);
const titleRef = useRef<TextFieldRef>(null);
useEffect(() => { // Refs for the two TextFields
setDumpDesc(""); const descriptionRef = useRef<TextFieldRef>(null);
setDumpTitle(""); const titleRef = useRef<TextFieldRef>(null);
}, [addBrainDumpProps.isVisible]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setDumpDesc("");
titleRef?.current?.focus(); setDumpTitle("");
}, 500); }, [addBrainDumpProps.isVisible]);
}, []);
useEffect(() => {
if (addBrainDumpProps.isVisible) {
setTimeout(() => {
titleRef?.current?.focus()
}, 500)
}
}, [addBrainDumpProps.isVisible]);
useEffect(() => { useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false); if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []); }, []);
return ( return (
<Dialog <Dialog
bottom={true} bottom={true}
height={"90%"} height={"90%"}
width={width} width={width}
panDirection={PanningDirectionsEnum.DOWN} panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => addBrainDumpProps.setIsVisible(false)} onDismiss={() => addBrainDumpProps.setIsVisible(false)}
containerStyle={styles.dialogContainer} containerStyle={styles.dialogContainer}
visible={addBrainDumpProps.isVisible} visible={addBrainDumpProps.isVisible}
> >
<View row spread style={styles.topBtns} marginB-20> <View row spread style={styles.topBtns} marginB-20>
<Button <Button
color="#05a8b6" color="#05a8b6"
label="Cancel" label="Cancel"
style={styles.topBtn} style={styles.topBtn}
onPress={() => { onPress={() => {
addBrainDumpProps.setIsVisible(false); addBrainDumpProps.setIsVisible(false);
}} }}
/> />
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}> <TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
<DropModalIcon style={{ marginTop: 15 }} /> <DropModalIcon style={{marginTop: 15}}/>
</TouchableOpacity> </TouchableOpacity>
<Button <Button
color="#05a8b6" color="#05a8b6"
label="Save" label="Save"
style={styles.topBtn} style={styles.topBtn}
onPress={() => { onPress={() => {
addBrainDump({ addBrainDump({
id: 99,
title: dumpTitle.trimEnd().trimStart(), id: 99,
description: dumpDesc.trimEnd().trimStart(),
}); title: dumpTitle.trimEnd().trimStart(),
addBrainDumpProps.setIsVisible(false);
}} description: dumpDesc.trimEnd().trimStart(),
/>
</View> });
<View marginH-20> addBrainDumpProps.setIsVisible(false);
<TextField }}
value={dumpTitle} />
ref={titleRef} </View>
placeholder="Set Title" <View marginH-20>
text60R <TextField
onChangeText={(text) => { value={dumpTitle}
setDumpTitle(text); ref={titleRef}
}} placeholder="Set Title"
onSubmitEditing={() => { text60R
// Move focus to the description field onChangeText={(text) => {
descriptionRef.current?.focus(); setDumpTitle(text);
}} }}
style={styles.title} onSubmitEditing={() => {
blurOnSubmit={false} // Keep the keyboard open when moving focus // Move focus to the description field
returnKeyType="next" descriptionRef.current?.focus();
/> }}
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 /> style={styles.title}
<TextField blurOnSubmit={false} // Keep the keyboard open when moving focus
ref={descriptionRef} returnKeyType="next"
value={dumpDesc} />
placeholder="Write Description" <View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
text70 <TextField
onChangeText={(text) => { ref={descriptionRef}
setDumpDesc(text); value={dumpDesc}
}} placeholder="Write Description"
style={styles.description} text70
multiline onChangeText={(text) => {
numberOfLines={4} setDumpDesc(text);
maxLength={255} }}
onEndEditing={() => { style={styles.description}
descriptionRef.current?.blur(); multiline
}} numberOfLines={4}
returnKeyType="done" maxLength={255}
/> onEndEditing={() => {
</View> descriptionRef.current?.blur();
</Dialog> }}
); returnKeyType="done"
/>
</View>
</Dialog>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react"; import React, {useEffect, useRef, useState} from "react";
import { import {
Button, Button,
Dialog, Dialog,
View, View,
Text, Text,
TextField, TextField,
TouchableOpacity, TouchableOpacity, TextFieldRef,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { Dimensions, StyleSheet } from "react-native"; import { Dimensions, StyleSheet } from "react-native";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView"; import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
@ -30,6 +30,7 @@ const MoveBrainDump = (props: {
props.item.description props.item.description
); );
const [modalVisible, setModalVisible] = useState<boolean>(false); const [modalVisible, setModalVisible] = useState<boolean>(false);
const descriptionRef = useRef<TextFieldRef>(null)
const { width } = Dimensions.get("screen"); const { width } = Dimensions.get("screen");
@ -37,6 +38,14 @@ const MoveBrainDump = (props: {
updateBrainDumpItem(props.item.id, { description: description }); updateBrainDumpItem(props.item.id, { description: description });
}, [description]); }, [description]);
useEffect(() => {
if (props.isVisible) {
setTimeout(() => {
descriptionRef?.current?.focus()
}, 500)
}
}, [props.isVisible]);
const showConfirmationDialog = () => { const showConfirmationDialog = () => {
setModalVisible(true); setModalVisible(true);
}; };
@ -112,7 +121,6 @@ const MoveBrainDump = (props: {
<TextField <TextField
textAlignVertical="top" textAlignVertical="top"
multiline multiline
autoFocus
fieldStyle={{ fieldStyle={{
width: "94%", width: "94%",
}} }}
@ -123,6 +131,7 @@ const MoveBrainDump = (props: {
onChangeText={(value) => { onChangeText={(value) => {
setDescription(value); setDescription(value);
}} }}
ref={descriptionRef}
returnKeyType="default" returnKeyType="default"
/> />
</View> </View>

View File

@ -1,10 +1,12 @@
import {StyleSheet} from "react-native"; import {Dimensions, StyleSheet} from "react-native";
import React from "react"; import React from "react";
import {Button, View,} from "react-native-ui-lib"; import {Button, View,} from "react-native-ui-lib";
import {useGroceryContext} from "@/contexts/GroceryContext"; import {useGroceryContext} from "@/contexts/GroceryContext";
import {FontAwesome6} from "@expo/vector-icons"; import {FontAwesome6} from "@expo/vector-icons";
import PlusIcon from "@/assets/svgs/PlusIcon"; import PlusIcon from "@/assets/svgs/PlusIcon";
const { width } = Dimensions.get("screen");
const AddGroceryItem = () => { const AddGroceryItem = () => {
const {setIsAddingGrocery} = useGroceryContext(); const {setIsAddingGrocery} = useGroceryContext();
@ -65,8 +67,14 @@ const styles = StyleSheet.create({
marginVertical: 10, marginVertical: 10,
}, },
btnContainer: { btnContainer: {
width: "100%", position:"absolute",
bottom: 30,
width: width,
padding: 20,
paddingBottom: 0,
justifyContent: "center", justifyContent: "center",
alignItems:"center",
zIndex: 10,
}, },
finishShopBtn: { finishShopBtn: {
width: "100%", width: "100%",

View File

@ -1,218 +1,240 @@
import { import {
Button, Button,
ButtonSize, ButtonSize, Colors,
Dialog, Dialog,
Text, KeyboardAwareScrollView, LoaderScreen, Modal,
TextField, Text,
TextFieldRef, TextField,
View, TextFieldRef,
View,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import React, { useRef, useState } from "react"; import React, {useRef, useState} from "react";
import { useSignIn } from "@/hooks/firebase/useSignIn"; import {useSignIn} from "@/hooks/firebase/useSignIn";
import { StyleSheet } from "react-native"; import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode"; import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
import { Camera, CameraView } from "expo-camera"; import {Camera, CameraView} from "expo-camera";
const SignInPage = ({ const SignInPage = ({
setTab, setTab,
}: { }: {
setTab: React.Dispatch< setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password"> React.SetStateAction<"register" | "login" | "reset-password">
>; >;
}) => { }) => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null); const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false); const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const passwordRef = useRef<TextFieldRef>(null); const passwordRef = useRef<TextFieldRef>(null);
const { mutateAsync: signIn, error, isError } = useSignIn(); const {mutateAsync: signIn, error, isError, isLoading: isSigninIn} = useSignIn();
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode(); const {mutateAsync: signInWithQrCode, isLoading: isQRCodeLoggingIn} = useLoginWithQrCode();
const handleSignIn = async () => { const isLoading = isSigninIn || isQRCodeLoggingIn
await signIn({ email, password });
if (!isError) {
Toast.show({
type: "success",
text1: "Login successful!",
});
} else {
Toast.show({
type: "error",
text1: "Error logging in",
text2: `${error}`,
});
}
};
const handleQrCodeScanned = async ({ data }: { data: string }) => { const handleSignIn = async () => {
setShowCameraDialog(false); await signIn({email, password});
try { if (!isError) {
await signInWithQrCode({ userId: data }); Toast.show({
Toast.show({ type: "success",
type: "success", text1: "Login successful!",
text1: "Login successful with QR code!", });
}); } else {
} catch (err) { Toast.show({
Toast.show({ type: "error",
type: "error", text1: "Error logging in",
text1: "Error logging in with QR code", text2: `${error}`,
text2: `${err}`, });
}); }
} };
};
const getCameraPermissions = async (callback: () => void) => { const handleQrCodeScanned = async ({data}: { data: string }) => {
const { status } = await Camera.requestCameraPermissionsAsync(); setShowCameraDialog(false);
setHasPermission(status === "granted"); try {
if (status === "granted") { await signInWithQrCode({userId: data});
callback(); Toast.show({
} type: "success",
}; text1: "Login successful with QR code!",
});
} catch (err) {
Toast.show({
type: "error",
text1: "Error logging in with QR code",
text2: `${err}`,
});
}
};
return ( const getCameraPermissions = async (callback: () => void) => {
<View padding-10 centerV height={"100%"}> const {status} = await Camera.requestCameraPermissionsAsync();
<TextField setHasPermission(status === "granted");
placeholder="Email" if (status === "granted") {
value={email} callback();
}
};
onChangeText={setEmail} return (
style={styles.textfield} <View padding-10 centerV height={"100%%"} style={{justifyContent: "center"}}>
onSubmitEditing={() => { <KeyboardAvoidingView
// Move focus to the description field contentContainerStyle={{ justifyContent:"center"}}
passwordRef.current?.focus(); keyboardVerticalOffset={50}
}} behavior={Platform.OS === "ios" ? "padding" : "height"}
/> >
<TextField <TextField
ref={passwordRef} placeholder="Email"
placeholder="Password" keyboardType={"email-address"}
value={password} returnKeyType={"next"}
onChangeText={setPassword} textContentType={"emailAddress"}
secureTextEntry defaultValue={email}
style={styles.textfield} onChangeText={setEmail}
/> style={styles.textfield}
<Button autoComplete={"email"}
label="Log in" autoCorrect={false}
marginT-50 onSubmitEditing={() => {
labelStyle={{ // Move focus to the description field
fontFamily: "PlusJakartaSans_600SemiBold", passwordRef.current?.focus();
fontSize: 16, }}
}} />
onPress={handleSignIn} <TextField
style={{ marginBottom: 20, height: 50 }} ref={passwordRef}
backgroundColor="#fd1775" placeholder="Password"
/> textContentType={"oneTimeCode"}
<Button value={password}
label="Log in with a QR Code" onChangeText={setPassword}
labelStyle={{ secureTextEntry
fontFamily: "PlusJakartaSans_600SemiBold", style={styles.textfield}
fontSize: 16, autoCorrect={false}
}} />
onPress={() => { <Button
getCameraPermissions(() => setShowCameraDialog(true)); label="Log in"
}} marginT-50
style={{ marginBottom: 20, height: 50 }} labelStyle={{
backgroundColor="#fd1775" fontFamily: "PlusJakartaSans_600SemiBold",
/> fontSize: 16,
{isError && ( }}
<Text center style={{ marginBottom: 20 }}>{`${ onPress={handleSignIn}
error?.toString()?.split("]")?.[1] style={{marginBottom: 20, height: 50}}
}`}</Text> backgroundColor="#fd1775"
)} />
<Button
label="Log in with a QR Code"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true));
}}
style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775"
/>
<View row centerH marginB-5 gap-5> {isError && (
<Text style={styles.jakartaLight}>Don't have an account?</Text> <Text center style={{marginBottom: 20}}>{`${
<Button error?.toString()?.split("]")?.[1]
onPress={() => setTab("register")} }`}</Text>
label="Sign Up" )}
labelStyle={[
styles.jakartaMedium,
{ textDecorationLine: "none", color: "#fd1575" },
]}
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
color="#fd1775"
/>
</View>
<View row centerH marginB-5 gap-5> <View row centerH marginB-5 gap-5>
<Text text70>Forgot your password?</Text> <Text style={styles.jakartaLight}>Don't have an account?</Text>
<Button <Button
onPress={() => setTab("reset-password")} onPress={() => setTab("register")}
label="Reset password" label="Sign Up"
labelStyle={[ labelStyle={[
styles.jakartaMedium, styles.jakartaMedium,
{ textDecorationLine: "none", color: "#fd1575" }, {textDecorationLine: "none", color: "#fd1575"},
]} ]}
link link
size={ButtonSize.xSmall} size={ButtonSize.xSmall}
padding-0 padding-0
margin-0 margin-0
text70 text70
left left
avoidInnerPadding color="#fd1775"
color="#fd1775" />
/> </View>
</View>
{/* Camera Dialog */} <View row centerH marginB-5 gap-5>
<Dialog <Text text70>Forgot your password?</Text>
visible={showCameraDialog} <Button
onDismiss={() => setShowCameraDialog(false)} onPress={() => setTab("reset-password")}
bottom label="Reset password"
width="100%" labelStyle={[
height="70%" styles.jakartaMedium,
containerStyle={{ padding: 15, backgroundColor:"white" }} {textDecorationLine: "none", color: "#fd1575"},
> ]}
{hasPermission === null ? ( link
<Text>Requesting camera permissions...</Text> size={ButtonSize.xSmall}
) : !hasPermission ? ( padding-0
<Text>No access to camera</Text> margin-0
) : ( text70
<CameraView left
style={{ flex: 1, borderRadius: 15 }} avoidInnerPadding
onBarcodeScanned={handleQrCodeScanned} color="#fd1775"
barcodeScannerSettings={{ />
barcodeTypes: ["qr"], </View>
}} </KeyboardAvoidingView>
/>
)} {/* Camera Dialog */}
<Button <Dialog
label="Cancel" visible={showCameraDialog}
onPress={() => setShowCameraDialog(false)} onDismiss={() => setShowCameraDialog(false)}
backgroundColor="#fd1775" bottom
style={{ margin: 10, marginBottom: 30 }} width="100%"
/> height="70%"
</Dialog> containerStyle={{padding: 15, backgroundColor: "white"}}
</View> >
); {hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{flex: 1, borderRadius: 15}}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
}}
/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{margin: 10, marginBottom: 30}}
/>
</Dialog>
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
)}
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
textfield: { textfield: {
backgroundColor: "white", backgroundColor: "white",
marginVertical: 10, marginVertical: 10,
padding: 30, padding: 30,
height: 45, height: 45,
borderRadius: 50, borderRadius: 50,
fontFamily: "PlusJakartaSans_300Light", fontFamily: "PlusJakartaSans_300Light",
}, },
jakartaLight: { jakartaLight: {
fontFamily: "PlusJakartaSans_300Light", fontFamily: "PlusJakartaSans_300Light",
fontSize: 16, fontSize: 16,
color: "#484848", color: "#484848",
}, },
jakartaMedium: { jakartaMedium: {
fontFamily: "PlusJakartaSans_500Medium", fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16, fontSize: 16,
color: "#919191", color: "#919191",
textDecorationLine: "underline", textDecorationLine: "underline",
}, },
}); });
export default SignInPage; export default SignInPage;

View File

@ -55,6 +55,12 @@ const SignUpPage = ({
lnameRef.current?.focus(); lnameRef.current?.focus();
}} }}
blurOnSubmit={false} blurOnSubmit={false}
accessibilityLabel="First name input"
accessibilityHint="Enter your first name"
accessible
returnKeyType="next"
textContentType="givenName"
importantForAccessibility="yes"
/> />
<TextField <TextField
ref={lnameRef} ref={lnameRef}
@ -66,17 +72,27 @@ const SignUpPage = ({
emailRef.current?.focus(); emailRef.current?.focus();
}} }}
blurOnSubmit={false} blurOnSubmit={false}
accessibilityLabel="Last name input"
accessibilityHint="Enter your last name"
accessible
returnKeyType="next"
textContentType="familyName"
importantForAccessibility="yes"
/> />
<TextField <TextField
ref={emailRef}
placeholder="Email" placeholder="Email"
value={email} keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail} onChangeText={setEmail}
style={styles.textfield} style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
ref={emailRef}
onSubmitEditing={() => { onSubmitEditing={() => {
passwordRef.current?.focus(); passwordRef.current?.focus();
}} }}
blurOnSubmit={false}
/> />
<View <View
centerV centerV

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ const AddChore = () => {
> >
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
marginH-20 marginB-30
size={ButtonSize.large} size={ButtonSize.large}
style={styles.button} style={styles.button}
onPress={() => setIsVisible(!isVisible)} onPress={() => setIsVisible(!isVisible)}

View File

@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
} }
}); });
exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => { exports.refreshTokens = functions.pubsub.schedule('every 1 hours').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();
@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleAccounts) { if (profileData.googleAccounts) {
try { try {
for (const googleEmail of Object.keys(profileData?.googleAccounts)) { for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail]; const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) { if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken); const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken}; const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
return null; return null;
}); });
// Function to refresh Google token async function refreshGoogleToken(refreshToken) {
async function refreshGoogleToken(token) { try {
// Assuming you use OAuth2 token refresh flow const response = await axios.post('https://oauth2.googleapis.com/token', {
const response = await axios.post('https://oauth2.googleapis.com/token', { grant_type: 'refresh_token',
grant_type: 'refresh_token', refresh_token: refreshToken,
refresh_token: token, // Add refresh token stored previously client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
client_id: 'YOUR_GOOGLE_CLIENT_ID', });
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
});
return response.data.access_token; // Return new access token return response.data.access_token; // Return the new access token
} catch (error) {
console.error("Error refreshing Google token:", error);
throw error;
}
} }
async function refreshMicrosoftToken(token) { async function refreshMicrosoftToken(refreshToken) {
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { try {
grant_type: 'refresh_token', const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
refresh_token: token, // Add refresh token stored previously grant_type: 'refresh_token',
client_id: 'YOUR_MICROSOFT_CLIENT_ID', refresh_token: refreshToken,
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET', client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access', scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
}); });
return response.data.access_token; // Return new access token return response.data.access_token; // Return the new access token
} catch (error) {
console.error("Error refreshing Microsoft token:", error);
throw error;
}
} }
async function getPushTokensForEvent() { async function getPushTokensForEvent() {

View File

@ -0,0 +1,39 @@
import {useMutation} from "react-query";
import {UserProfile} from "@firebase/auth";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
export const useClearTokens = () => {
const {profileData} = useAuthContext();
const {mutateAsync: updateUserData} = useUpdateUserData();
return useMutation({
mutationKey: ["clearTokens"],
mutationFn: async ({provider, email}: {
provider: "google" | "outlook" | "apple",
email: string
}) => {
const newUserData: Partial<UserProfile> = {};
if (provider === "google") {
let googleAccounts = profileData?.googleAccounts;
if (googleAccounts) {
googleAccounts[email] = null;
newUserData.googleAccounts = googleAccounts;
}
} else if (provider === "outlook") {
let microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) {
microsoftAccounts[email] = null;
newUserData.microsoftAccounts = microsoftAccounts;
}
} else if (provider === "apple") {
let appleAccounts = profileData?.appleAccounts;
if (appleAccounts) {
appleAccounts[email] = null;
newUserData.appleAccounts = appleAccounts;
}
}
await updateUserData({newUserData});
},
})
}

View File

@ -45,34 +45,41 @@ export const useCreateEvent = () => {
} }
export const useCreateEventsFromProvider = () => { export const useCreateEventsFromProvider = () => {
const {user: currentUser} = useAuthContext(); const { user: currentUser } = useAuthContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationKey: ["createEventsFromProvider"], mutationKey: ["createEventsFromProvider"],
mutationFn: async (eventDataArray: Partial<EventData>[]) => { mutationFn: async (eventDataArray: Partial<EventData>[]) => {
try { try {
for (const eventData of eventDataArray) { // Create an array of promises for each event's Firestore read/write operation
const promises = eventDataArray.map(async (eventData) => {
console.log("Processing EventData: ", eventData); console.log("Processing EventData: ", eventData);
// Check if the event already exists
const snapshot = await firestore() const snapshot = await firestore()
.collection("Events") .collection("Events")
.where("id", "==", eventData.id) .where("id", "==", eventData.id)
.get(); .get();
if (snapshot.empty) { if (snapshot.empty) {
await firestore() // Event doesn't exist, so add it
return firestore()
.collection("Events") .collection("Events")
.add({...eventData, creatorId: currentUser?.uid}); .add({ ...eventData, creatorId: currentUser?.uid });
} else { } else {
console.log("Event already exists, updating..."); // Event exists, update it
const docId = snapshot.docs[0].id; const docId = snapshot.docs[0].id;
await firestore() return firestore()
.collection("Events") .collection("Events")
.doc(docId) .doc(docId)
.set({...eventData, creatorId: currentUser?.uid}, {merge: true}); .set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
} }
} });
// Execute all promises in parallel
await Promise.all(promises);
} catch (e) { } catch (e) {
console.error("Error creating/updating events: ", e); console.error("Error creating/updating events: ", e);
} }

View File

@ -2,11 +2,13 @@ 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";
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();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"], mutationKey: ["fetchAndSaveGoogleEvents"],
@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
timeMax.toISOString().slice(0, -5) + "Z" timeMax.toISOString().slice(0, -5) + "Z"
); );
if(!response.success) {
await clearToken({email: email!, provider: "google"})
return
}
console.log("Google Calendar events fetched:", response); console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => { const items = response?.googleEvents?.map((item) => {
if (item.allDay) { if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0)); item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
item.endDate = item.startDate; item.endDate = item.startDate;