Small fixes, calendar token refresh for google

This commit is contained in:
Milan Paunovic
2024-10-31 19:57:06 +01:00
parent 1b6a241bbe
commit 8edb8f47f2
9 changed files with 1092 additions and 1032 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,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,22 +1,14 @@
import { import {Button, ButtonSize, Dialog, Text, TextField, TextFieldRef, View,} from "react-native-ui-lib";
Button, import React, {useRef, useState} from "react";
ButtonSize, import {useSignIn} from "@/hooks/firebase/useSignIn";
Dialog, import {StyleSheet} from "react-native";
Text,
TextField,
TextFieldRef,
View,
} from "react-native-ui-lib";
import React, { useRef, useState } from "react";
import { useSignIn } from "@/hooks/firebase/useSignIn";
import { 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">
>; >;
@ -27,11 +19,11 @@ const SignInPage = ({
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} = useSignIn();
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode(); const {mutateAsync: signInWithQrCode} = useLoginWithQrCode();
const handleSignIn = async () => { const handleSignIn = async () => {
await signIn({ email, password }); await signIn({email, password});
if (!isError) { if (!isError) {
Toast.show({ Toast.show({
type: "success", type: "success",
@ -46,10 +38,10 @@ const SignInPage = ({
} }
}; };
const handleQrCodeScanned = async ({ data }: { data: string }) => { const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false); setShowCameraDialog(false);
try { try {
await signInWithQrCode({ userId: data }); await signInWithQrCode({userId: data});
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Login successful with QR code!", text1: "Login successful with QR code!",
@ -64,7 +56,7 @@ const SignInPage = ({
}; };
const getCameraPermissions = async (callback: () => void) => { const getCameraPermissions = async (callback: () => void) => {
const { status } = await Camera.requestCameraPermissionsAsync(); const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted"); setHasPermission(status === "granted");
if (status === "granted") { if (status === "granted") {
callback(); callback();
@ -75,8 +67,10 @@ const SignInPage = ({
<View padding-10 centerV height={"100%"}> <View padding-10 centerV height={"100%"}>
<TextField <TextField
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}
onSubmitEditing={() => { onSubmitEditing={() => {
@ -87,10 +81,12 @@ const SignInPage = ({
<TextField <TextField
ref={passwordRef} ref={passwordRef}
placeholder="Password" placeholder="Password"
textContentType={"oneTimeCode"}
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
secureTextEntry secureTextEntry
style={styles.textfield} style={styles.textfield}
autoCorrect={false}
/> />
<Button <Button
label="Log in" label="Log in"
@ -100,7 +96,7 @@ const SignInPage = ({
fontSize: 16, fontSize: 16,
}} }}
onPress={handleSignIn} onPress={handleSignIn}
style={{ marginBottom: 20, height: 50 }} style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775" backgroundColor="#fd1775"
/> />
<Button <Button
@ -112,11 +108,11 @@ const SignInPage = ({
onPress={() => { onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true)); getCameraPermissions(() => setShowCameraDialog(true));
}} }}
style={{ marginBottom: 20, height: 50 }} style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775" backgroundColor="#fd1775"
/> />
{isError && ( {isError && (
<Text center style={{ marginBottom: 20 }}>{`${ <Text center style={{marginBottom: 20}}>{`${
error?.toString()?.split("]")?.[1] error?.toString()?.split("]")?.[1]
}`}</Text> }`}</Text>
)} )}
@ -128,7 +124,7 @@ const SignInPage = ({
label="Sign Up" 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}
@ -147,7 +143,7 @@ const SignInPage = ({
label="Reset password" label="Reset password"
labelStyle={[ labelStyle={[
styles.jakartaMedium, styles.jakartaMedium,
{ textDecorationLine: "none", color: "#fd1575" }, {textDecorationLine: "none", color: "#fd1575"},
]} ]}
link link
size={ButtonSize.xSmall} size={ButtonSize.xSmall}
@ -167,7 +163,7 @@ const SignInPage = ({
bottom bottom
width="100%" width="100%"
height="70%" height="70%"
containerStyle={{ padding: 15, backgroundColor:"white" }} containerStyle={{padding: 15, backgroundColor: "white"}}
> >
{hasPermission === null ? ( {hasPermission === null ? (
<Text>Requesting camera permissions...</Text> <Text>Requesting camera permissions...</Text>
@ -175,7 +171,7 @@ const SignInPage = ({
<Text>No access to camera</Text> <Text>No access to camera</Text>
) : ( ) : (
<CameraView <CameraView
style={{ flex: 1, borderRadius: 15 }} style={{flex: 1, borderRadius: 15}}
onBarcodeScanned={handleQrCodeScanned} onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{ barcodeScannerSettings={{
barcodeTypes: ["qr"], barcodeTypes: ["qr"],
@ -186,7 +182,7 @@ const SignInPage = ({
label="Cancel" label="Cancel"
onPress={() => setShowCameraDialog(false)} onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775" backgroundColor="#fd1775"
style={{ margin: 10, marginBottom: 30 }} style={{margin: 10, marginBottom: 30}}
/> />
</Dialog> </Dialog>
</View> </View>

View File

@ -1,10 +1,10 @@
import { AntDesign, Ionicons } from "@expo/vector-icons"; import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, { useCallback, useEffect, useState } from "react"; import React, {useCallback, useEffect, useState} from "react";
import { Button, Checkbox, Text, View } from "react-native-ui-lib"; import {Button, Checkbox, Text, View} from "react-native-ui-lib";
import { ActivityIndicator, Alert, ScrollView, StyleSheet } from "react-native"; import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler"; import {TouchableOpacity} from "react-native-gesture-handler";
import { useAuthContext } from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData"; import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce"; import debounce from "debounce";
import AppleIcon from "@/assets/svgs/AppleIcon"; import AppleIcon from "@/assets/svgs/AppleIcon";
import GoogleIcon from "@/assets/svgs/GoogleIcon"; import GoogleIcon from "@/assets/svgs/GoogleIcon";
@ -12,16 +12,16 @@ import OutlookIcon from "@/assets/svgs/OutlookIcon";
import * as AuthSession from "expo-auth-session"; import * as AuthSession from "expo-auth-session";
import * as Google from "expo-auth-session/providers/google"; import * as Google from "expo-auth-session/providers/google";
import * as WebBrowser from "expo-web-browser"; import * as WebBrowser from "expo-web-browser";
import { UserProfile } from "@firebase/auth"; import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
import { useFetchAndSaveGoogleEvents } from "@/hooks/useFetchAndSaveGoogleEvents"; import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
import { useFetchAndSaveOutlookEvents } from "@/hooks/useFetchAndSaveOutlookEvents"; import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
import { useFetchAndSaveAppleEvents } from "@/hooks/useFetchAndSaveAppleEvents";
import * as AppleAuthentication from "expo-apple-authentication"; import * as AppleAuthentication from "expo-apple-authentication";
import ExpoLocalization from "expo-localization/src/ExpoLocalization"; import ExpoLocalization from "expo-localization/src/ExpoLocalization";
import { colorMap } from "@/constants/colorMap"; import {colorMap} from "@/constants/colorMap";
import { useAtom } from "jotai"; import {useAtom} from "jotai";
import { settingsPageIndex } from "../calendar/atoms"; import {settingsPageIndex} from "../calendar/atoms";
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog"; import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
const googleConfig = { const googleConfig = {
androidClientId: androidClientId:
@ -35,11 +35,14 @@ const googleConfig = {
"profile", "profile",
"https://www.googleapis.com/auth/calendar.events.owned", "https://www.googleapis.com/auth/calendar.events.owned",
], ],
extraParams: {
access_type: "offline",
},
}; };
const microsoftConfig = { const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
redirectUri: AuthSession.makeRedirectUri({ path: "settings" }), redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
scopes: [ scopes: [
"openid", "openid",
"profile", "profile",
@ -54,7 +57,7 @@ const microsoftConfig = {
}; };
const CalendarSettingsPage = () => { const CalendarSettingsPage = () => {
const { profileData } = useAuthContext(); const {profileData} = useAuthContext();
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex); const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>( const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
profileData?.firstDayOfWeek ?? profileData?.firstDayOfWeek ??
@ -93,14 +96,15 @@ const CalendarSettingsPage = () => {
profileData?.eventColor ?? colorMap.pink profileData?.eventColor ?? colorMap.pink
); );
const { mutateAsync: updateUserData } = useUpdateUserData(); const {mutateAsync: updateUserData} = useUpdateUserData();
const { mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle } = const {mutateAsync: clearToken} = useClearTokens();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
useFetchAndSaveGoogleEvents(); useFetchAndSaveGoogleEvents();
const { const {
mutateAsync: fetchAndSaveOutlookEvents, mutateAsync: fetchAndSaveOutlookEvents,
isLoading: isSyncingOutlook, isLoading: isSyncingOutlook,
} = useFetchAndSaveOutlookEvents(); } = useFetchAndSaveOutlookEvents();
const { mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple } = const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
useFetchAndSaveAppleEvents(); useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession(); WebBrowser.maybeCompleteAuthSession();
@ -113,7 +117,7 @@ const CalendarSettingsPage = () => {
const signInWithGoogle = async () => { const signInWithGoogle = async () => {
try { try {
if (response?.type === "success") { if (response?.type === "success") {
const accessToken = response.authentication?.accessToken; const { accessToken, refreshToken } = response?.authentication!;
const userInfoResponse = await fetch( const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo", "https://www.googleapis.com/oauth2/v3/userinfo",
@ -125,10 +129,11 @@ const CalendarSettingsPage = () => {
const userInfo = await userInfoResponse.json(); const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email; const googleMail = userInfo.email;
let googleAccounts = profileData?.googleAccounts; let googleAccounts = profileData?.googleAccounts || {};
const updatedGoogleAccounts = googleAccounts const updatedGoogleAccounts = {
? { ...googleAccounts, [googleMail]: accessToken } ...googleAccounts,
: { [googleMail]: accessToken }; [googleMail]: { accessToken, refreshToken },
};
await updateUserData({ await updateUserData({
newUserData: { googleAccounts: updatedGoogleAccounts }, newUserData: { googleAccounts: updatedGoogleAccounts },
@ -219,12 +224,11 @@ const CalendarSettingsPage = () => {
let microsoftAccounts = profileData?.microsoftAccounts; let microsoftAccounts = profileData?.microsoftAccounts;
const updatedMicrosoftAccounts = microsoftAccounts const updatedMicrosoftAccounts = microsoftAccounts
? { ...microsoftAccounts, [outlookMail]: tokenData.access_token } ? {...microsoftAccounts, [outlookMail]: tokenData.access_token}
: { [outlookMail]: tokenData.access_token }; : {[outlookMail]: tokenData.access_token};
// Update user data with Microsoft token and email
await updateUserData({ await updateUserData({
newUserData: { microsoftAccounts: updatedMicrosoftAccounts }, newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
}); });
await fetchAndSaveOutlookEvents( await fetchAndSaveOutlookEvents(
@ -255,18 +259,26 @@ const CalendarSettingsPage = () => {
console.log("Apple sign-in result:", credential); console.log("Apple sign-in result:", credential);
alert(JSON.stringify(credential))
const appleToken = credential.identityToken; const appleToken = credential.identityToken;
const appleMail = credential.email; const appleMail = credential.email!;
if (appleToken) { if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed..."); console.log("Apple ID token received. Fetch user info if needed...");
let appleAcounts = profileData?.appleAccounts;
const updatedAppleAccounts = appleAcounts
? {...appleAcounts, [appleMail]: appleToken}
: {[appleMail]: appleToken};
await updateUserData({ await updateUserData({
newUserData: { appleToken, appleMail }, newUserData: {appleAccounts: updatedAppleAccounts},
}); });
console.log("User data updated with Apple ID token."); console.log("User data updated with Apple ID token.");
await fetchAndSaveAppleEvents({ token: appleToken, email: appleMail! }); await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
} else { } else {
console.warn( console.warn(
"Apple authentication was not successful or email was hidden." "Apple authentication was not successful or email was hidden."
@ -321,33 +333,6 @@ const CalendarSettingsPage = () => {
debouncedUpdateUserData(color); debouncedUpdateUserData(color);
}; };
const clearToken = async (
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 });
};
let isConnectedToGoogle = false; let isConnectedToGoogle = false;
if (profileData?.googleAccounts) { if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => { Object.values(profileData?.googleAccounts).forEach((item) => {
@ -387,10 +372,10 @@ const CalendarSettingsPage = () => {
name="chevron-back" name="chevron-back"
size={14} size={14}
color="#979797" color="#979797"
style={{ paddingBottom: 3 }} style={{paddingBottom: 3}}
/> />
<Text <Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }} style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
color="#979797" color="#979797"
> >
Return to main settings Return to main settings
@ -407,7 +392,7 @@ const CalendarSettingsPage = () => {
<TouchableOpacity onPress={() => handleChangeColor(colorMap.pink)}> <TouchableOpacity onPress={() => handleChangeColor(colorMap.pink)}>
<View style={styles.colorBox} backgroundColor={colorMap.pink}> <View style={styles.colorBox} backgroundColor={colorMap.pink}>
{selectedColor == colorMap.pink && ( {selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white" /> <AntDesign name="check" size={30} color="white"/>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -416,21 +401,21 @@ const CalendarSettingsPage = () => {
> >
<View style={styles.colorBox} backgroundColor={colorMap.orange}> <View style={styles.colorBox} backgroundColor={colorMap.orange}>
{selectedColor == colorMap.orange && ( {selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white" /> <AntDesign name="check" size={30} color="white"/>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}> <TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}>
<View style={styles.colorBox} backgroundColor={colorMap.green}> <View style={styles.colorBox} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && ( {selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white" /> <AntDesign name="check" size={30} color="white"/>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}> <TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}>
<View style={styles.colorBox} backgroundColor={colorMap.teal}> <View style={styles.colorBox} backgroundColor={colorMap.teal}>
{selectedColor == colorMap.teal && ( {selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white" /> <AntDesign name="check" size={30} color="white"/>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -439,7 +424,7 @@ const CalendarSettingsPage = () => {
> >
<View style={styles.colorBox} backgroundColor={colorMap.purple}> <View style={styles.colorBox} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && ( {selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white" /> <AntDesign name="check" size={30} color="white"/>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -480,14 +465,14 @@ const CalendarSettingsPage = () => {
<Button <Button
onPress={() => promptAsync()} onPress={() => promptAsync()}
label={"Connect Google"} label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ labelProps={{
numberOfLines: 2, numberOfLines: 2,
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<GoogleIcon /> <GoogleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -496,7 +481,7 @@ const CalendarSettingsPage = () => {
/> />
{profileData?.googleAccounts {profileData?.googleAccounts
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => { ? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
const googleToken = profileData?.googleAccounts?.[googleMail]; const googleToken = profileData?.googleAccounts?.[googleMail]?.accessToken;
return ( return (
googleToken && ( googleToken && (
<Button <Button
@ -511,7 +496,7 @@ const CalendarSettingsPage = () => {
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<GoogleIcon /> <GoogleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -523,6 +508,7 @@ const CalendarSettingsPage = () => {
}) })
: null} : null}
{!profileData?.appleAccounts && (
<Button <Button
onPress={() => handleAppleSignIn()} onPress={() => handleAppleSignIn()}
label={"Connect Apple"} label={"Connect Apple"}
@ -532,29 +518,31 @@ const CalendarSettingsPage = () => {
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<AppleIcon /> <AppleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
color="black" color="black"
text70BL text70BL
/> />
)}
{profileData?.appleAccounts {profileData?.appleAccounts
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => { ? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
const appleToken = profileData?.appleAccounts?.[appleEmail]; const appleToken = profileData?.appleAccounts?.[appleEmail!];
return ( return (
appleToken && ( appleToken && (
<Button <Button
key={appleEmail} key={appleEmail}
onPress={() => showConfirmationDialog("apple", appleEmail)} onPress={() => showConfirmationDialog("apple", appleEmail)}
label={`Disconnect ${appleEmail}`} label={`Disconnect Apple`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ labelProps={{
numberOfLines: 2, numberOfLines: 2,
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<AppleIcon /> <AppleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -568,14 +556,14 @@ const CalendarSettingsPage = () => {
<Button <Button
onPress={() => handleMicrosoftSignIn()} onPress={() => handleMicrosoftSignIn()}
label={"Connect Outlook"} label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ labelProps={{
numberOfLines: 2, numberOfLines: 2,
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<OutlookIcon /> <OutlookIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -601,7 +589,7 @@ const CalendarSettingsPage = () => {
}} }}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<OutlookIcon /> <OutlookIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -623,7 +611,7 @@ const CalendarSettingsPage = () => {
</Text> </Text>
<View style={styles.noPaddingCard}> <View style={styles.noPaddingCard}>
<View style={{ marginTop: 20 }}> <View style={{marginTop: 20}}>
{profileData?.googleAccounts && {profileData?.googleAccounts &&
Object.keys(profileData?.googleAccounts)?.map( Object.keys(profileData?.googleAccounts)?.map(
(googleEmail) => { (googleEmail) => {
@ -650,10 +638,10 @@ const CalendarSettingsPage = () => {
} }
label={`Sync ${googleEmail}`} label={`Sync ${googleEmail}`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }} labelProps={{numberOfLines: 3}}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<GoogleIcon /> <GoogleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -662,7 +650,7 @@ const CalendarSettingsPage = () => {
/> />
{isSyncingGoogle ? ( {isSyncingGoogle ? (
<ActivityIndicator /> <ActivityIndicator/>
) : ( ) : (
<Ionicons <Ionicons
name={"refresh"} name={"refresh"}
@ -679,6 +667,8 @@ const CalendarSettingsPage = () => {
{profileData?.appleAccounts && {profileData?.appleAccounts &&
Object.keys(profileData?.appleAccounts)?.map((appleEmail) => { Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
console.log(profileData?.appleAccounts)
const appleToken = profileData?.appleAccounts?.[appleEmail]; const appleToken = profileData?.appleAccounts?.[appleEmail];
return ( return (
appleToken && ( appleToken && (
@ -699,12 +689,12 @@ const CalendarSettingsPage = () => {
token: appleToken, token: appleToken,
}) })
} }
label={`Sync ${appleEmail}`} label={isSyncingApple ? "Syncing Events from the Apple calendar..." : `Sync Apple`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }} labelProps={{numberOfLines: 3}}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<AppleIcon /> <AppleIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -712,7 +702,7 @@ const CalendarSettingsPage = () => {
text70BL text70BL
/> />
{isSyncingApple ? ( {isSyncingApple ? (
<ActivityIndicator /> <ActivityIndicator/>
) : ( ) : (
<Ionicons <Ionicons
name={"refresh"} name={"refresh"}
@ -752,10 +742,10 @@ const CalendarSettingsPage = () => {
} }
label={`Sync ${microsoftEmail}`} label={`Sync ${microsoftEmail}`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }} labelProps={{numberOfLines: 3}}
iconSource={() => ( iconSource={() => (
<View marginR-15> <View marginR-15>
<OutlookIcon /> <OutlookIcon/>
</View> </View>
)} )}
style={styles.addCalBtn} style={styles.addCalBtn}
@ -763,7 +753,7 @@ const CalendarSettingsPage = () => {
text70BL text70BL
/> />
{isSyncingOutlook ? ( {isSyncingOutlook ? (
<ActivityIndicator /> <ActivityIndicator/>
) : ( ) : (
<Ionicons <Ionicons
name={"refresh"} name={"refresh"}

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: token, // Add refresh token stored previously refresh_token: refreshToken,
client_id: 'YOUR_GOOGLE_CLIENT_ID', client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
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) {
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', {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously refresh_token: refreshToken,
client_id: 'YOUR_MICROSOFT_CLIENT_ID', client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET', scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
}); });
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;