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

View File

@ -8,7 +8,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
},
);
const data = await response.json();
const googleEvents = [];
data.items?.forEach((item) => {
let isAllDay = false;
@ -49,5 +51,5 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
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 {Button, View,} from "react-native-ui-lib";
import {useGroceryContext} from "@/contexts/GroceryContext";
import {FontAwesome6} from "@expo/vector-icons";
import PlusIcon from "@/assets/svgs/PlusIcon";
const { width } = Dimensions.get("screen");
const AddGroceryItem = () => {
const {setIsAddingGrocery} = useGroceryContext();
@ -65,8 +67,14 @@ const styles = StyleSheet.create({
marginVertical: 10,
},
btnContainer: {
width: "100%",
position:"absolute",
bottom: 30,
width: width,
padding: 20,
paddingBottom: 0,
justifyContent: "center",
alignItems:"center",
zIndex: 10,
},
finishShopBtn: {
width: "100%",

View File

@ -1,12 +1,4 @@
import {
Button,
ButtonSize,
Dialog,
Text,
TextField,
TextFieldRef,
View,
} from "react-native-ui-lib";
import {Button, ButtonSize, Dialog, 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";
@ -75,8 +67,10 @@ const SignInPage = ({
<View padding-10 centerV height={"100%"}>
<TextField
placeholder="Email"
value={email}
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
onSubmitEditing={() => {
@ -87,10 +81,12 @@ const SignInPage = ({
<TextField
ref={passwordRef}
placeholder="Password"
textContentType={"oneTimeCode"}
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
autoCorrect={false}
/>
<Button
label="Log in"

View File

@ -1,7 +1,7 @@
import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, {useCallback, useEffect, useState} from "react";
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 {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
@ -12,7 +12,6 @@ import OutlookIcon from "@/assets/svgs/OutlookIcon";
import * as AuthSession from "expo-auth-session";
import * as Google from "expo-auth-session/providers/google";
import * as WebBrowser from "expo-web-browser";
import { UserProfile } from "@firebase/auth";
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
@ -22,6 +21,7 @@ import { colorMap } from "@/constants/colorMap";
import {useAtom} from "jotai";
import {settingsPageIndex} from "../calendar/atoms";
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
const googleConfig = {
androidClientId:
@ -35,6 +35,9 @@ const googleConfig = {
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
extraParams: {
access_type: "offline",
},
};
const microsoftConfig = {
@ -94,6 +97,7 @@ const CalendarSettingsPage = () => {
);
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: clearToken} = useClearTokens();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
useFetchAndSaveGoogleEvents();
const {
@ -113,7 +117,7 @@ const CalendarSettingsPage = () => {
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
const accessToken = response.authentication?.accessToken;
const { accessToken, refreshToken } = response?.authentication!;
const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
@ -125,10 +129,11 @@ const CalendarSettingsPage = () => {
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
let googleAccounts = profileData?.googleAccounts;
const updatedGoogleAccounts = googleAccounts
? { ...googleAccounts, [googleMail]: accessToken }
: { [googleMail]: accessToken };
let googleAccounts = profileData?.googleAccounts || {};
const updatedGoogleAccounts = {
...googleAccounts,
[googleMail]: { accessToken, refreshToken },
};
await updateUserData({
newUserData: { googleAccounts: updatedGoogleAccounts },
@ -222,7 +227,6 @@ const CalendarSettingsPage = () => {
? {...microsoftAccounts, [outlookMail]: tokenData.access_token}
: {[outlookMail]: tokenData.access_token};
// Update user data with Microsoft token and email
await updateUserData({
newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
});
@ -255,14 +259,22 @@ const CalendarSettingsPage = () => {
console.log("Apple sign-in result:", credential);
alert(JSON.stringify(credential))
const appleToken = credential.identityToken;
const appleMail = credential.email;
const appleMail = credential.email!;
if (appleToken) {
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({
newUserData: { appleToken, appleMail },
newUserData: {appleAccounts: updatedAppleAccounts},
});
console.log("User data updated with Apple ID token.");
@ -321,33 +333,6 @@ const CalendarSettingsPage = () => {
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;
if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => {
@ -480,7 +465,7 @@ const CalendarSettingsPage = () => {
<Button
onPress={() => promptAsync()}
label={"Connect Google"}
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
@ -496,7 +481,7 @@ const CalendarSettingsPage = () => {
/>
{profileData?.googleAccounts
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
const googleToken = profileData?.googleAccounts?.[googleMail];
const googleToken = profileData?.googleAccounts?.[googleMail]?.accessToken;
return (
googleToken && (
<Button
@ -523,6 +508,7 @@ const CalendarSettingsPage = () => {
})
: null}
{!profileData?.appleAccounts && (
<Button
onPress={() => handleAppleSignIn()}
label={"Connect Apple"}
@ -539,15 +525,17 @@ const CalendarSettingsPage = () => {
color="black"
text70BL
/>
)}
{profileData?.appleAccounts
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
const appleToken = profileData?.appleAccounts?.[appleEmail];
const appleToken = profileData?.appleAccounts?.[appleEmail!];
return (
appleToken && (
<Button
key={appleEmail}
onPress={() => showConfirmationDialog("apple", appleEmail)}
label={`Disconnect ${appleEmail}`}
label={`Disconnect Apple`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
@ -568,7 +556,7 @@ const CalendarSettingsPage = () => {
<Button
onPress={() => handleMicrosoftSignIn()}
label={"Connect Outlook"}
label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
@ -679,6 +667,8 @@ const CalendarSettingsPage = () => {
{profileData?.appleAccounts &&
Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
console.log(profileData?.appleAccounts)
const appleToken = profileData?.appleAccounts?.[appleEmail];
return (
appleToken && (
@ -699,7 +689,7 @@ const CalendarSettingsPage = () => {
token: appleToken,
})
}
label={`Sync ${appleEmail}`}
label={isSyncingApple ? "Syncing Events from the Apple calendar..." : `Sync Apple`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (

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...');
const profilesSnapshot = await db.collection('Profiles').get();
@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleAccounts) {
try {
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail];
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
return null;
});
// Function to refresh Google token
async function refreshGoogleToken(token) {
// Assuming you use OAuth2 token refresh flow
async function refreshGoogleToken(refreshToken) {
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously
client_id: 'YOUR_GOOGLE_CLIENT_ID',
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
});
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', {
grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously
client_id: 'YOUR_MICROSOFT_CLIENT_ID',
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
refresh_token: refreshToken,
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
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() {

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

@ -52,27 +52,34 @@ export const useCreateEventsFromProvider = () => {
mutationKey: ["createEventsFromProvider"],
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
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);
// Check if the event already exists
const snapshot = await firestore()
.collection("Events")
.where("id", "==", eventData.id)
.get();
if (snapshot.empty) {
await firestore()
// Event doesn't exist, so add it
return firestore()
.collection("Events")
.add({ ...eventData, creatorId: currentUser?.uid });
} else {
console.log("Event already exists, updating...");
// Event exists, update it
const docId = snapshot.docs[0].id;
await firestore()
return firestore()
.collection("Events")
.doc(docId)
.set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
}
}
});
// Execute all promises in parallel
await Promise.all(promises);
} catch (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 {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient()
const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
const {mutateAsync: clearToken} = useClearTokens();
return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"],
@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
timeMax.toISOString().slice(0, -5) + "Z"
);
if(!response.success) {
await clearToken({email: email!, provider: "google"})
return
}
console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => {
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;