Apple calendar sync, timezone, first day of week additions

This commit is contained in:
Milan Paunovic
2024-10-19 22:56:55 +02:00
parent 00b6225a1c
commit 3653400a92
25 changed files with 1154 additions and 689 deletions
@@ -10,6 +10,7 @@ import {
selectedDateAtom,
selectedNewEventDateAtom
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
interface EventCalendarProps {
calendarHeight: number;
@@ -17,6 +18,7 @@ interface EventCalendarProps {
export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight}) => {
const {data: events} = useGetEvents();
const {profileData} = useAuthContext()
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom)
const mode = useAtomValue(modeAtom)
const setEditVisible = useSetAtom(editVisibleAtom)
@@ -35,6 +37,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight
setEditVisible(true);
setEventForEdit(event);
}}
weekStartsOn={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}
+1 -1
View File
@@ -20,7 +20,7 @@ export const InnerCalendar = () => {
return (
<>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30}}
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 60}}
ref={calendarContainerRef}
onLayout={onLayout}
>
@@ -1,12 +1,9 @@
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 {ScrollView, StyleSheet} from "react-native";
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import {colorMap} from "@/contexts/SettingsContext";
import {TouchableOpacity} from "react-native-gesture-handler";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce";
@@ -17,6 +14,11 @@ 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";
import * as AppleAuthentication from 'expo-apple-authentication';
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
const googleConfig = {
androidClientId:
@@ -50,8 +52,8 @@ const microsoftConfig = {
const CalendarSettingsPage = (props: {
setSelectedPage: (page: number) => void;
}) => {
const [startDate, setStartDate] = useState<boolean>(false);
const {profileData} = useAuthContext();
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(profileData?.firstDayOfWeek ?? ExpoLocalization.getCalendars()[0].firstWeekday === 1 ? "Mondays" : "Sundays");
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
@@ -60,8 +62,11 @@ const CalendarSettingsPage = (props: {
profileData?.eventColor ?? colorMap.pink
);
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
@@ -70,49 +75,6 @@ const CalendarSettingsPage = (props: {
signInWithGoogle();
}, [response]);
const fetchAndSaveGoogleEvents = async (token?: string, email?: string) => {
console.log("Fetching Google Calendar events...");
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
console.log("Token: ", token ?? profileData?.googleToken)
fetchGoogleCalendarEvents(
token ?? profileData?.googleToken,
email ?? profileData?.googleMail,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then(async (response) => {
console.log("Google Calendar events fetched:", response);
const items = response?.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);
}).catch((error) => {
console.error("Error fetching Google Calendar events:", error);
});
};
const fetchAndSaveMicrosoftEvents = async (token?: string, email?: string) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));
console.log("Token: ", token ?? profileData?.microsoftToken)
fetchMicrosoftCalendarEvents(
token ?? profileData?.microsoftToken,
email ?? profileData?.outlookMail,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then(async (response) => {
console.log(response);
const items = response ?? [];
await createEventsFromProvider(items);
});
};
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
@@ -214,7 +176,7 @@ const CalendarSettingsPage = (props: {
newUserData: {microsoftToken: tokenData.access_token, outlookMail: outlookMail},
});
await fetchAndSaveMicrosoftEvents(tokenData.access_token, outlookMail)
await fetchAndSaveOutlookEvents(tokenData.access_token, outlookMail)
console.log("User data updated successfully.");
}
}
@@ -226,6 +188,39 @@ const CalendarSettingsPage = (props: {
}
};
const handleAppleSignIn = async () => {
try {
console.log("Starting Apple Sign-in...");
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
console.log("Apple sign-in result:", credential);
const appleToken = credential.identityToken;
const appleMail = credential.email;
if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed...");
// Example: Store user token and email
await updateUserData({
newUserData: {appleToken, appleMail},
});
console.log("User data updated with Apple ID token.");
} else {
console.warn("Apple authentication was not successful or email was hidden.");
}
} catch (error) {
console.error("Error during Apple Sign-in:", error);
}
};
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
try {
@@ -242,6 +237,26 @@ const CalendarSettingsPage = (props: {
[]
);
const debouncedUpdateFirstDayOfWeek = useCallback(
debounce(async (firstDayOfWeek: string) => {
try {
await updateUserData({
newUserData: {
firstDayOfWeek,
},
});
} catch (error) {
console.error("Failed to update first day of week:", error);
}
}, 500),
[]
);
const handleChangeFirstDayOfWeek = () => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
}
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
@@ -333,10 +348,10 @@ const CalendarSettingsPage = (props: {
<Text style={styles.cardTitle}>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={startDate}
value={firstDayOfWeek === "Sundays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(true)}
onValueChange={() => handleChangeFirstDayOfWeek("Sundays")}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
@@ -348,10 +363,10 @@ const CalendarSettingsPage = (props: {
</View>
<View row marginV-5>
<Checkbox
value={!startDate}
value={firstDayOfWeek === "Mondays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(false)}
onValueChange={() => handleChangeFirstDayOfWeek("Mondays")}
/>
<Text text70 marginL-8>
Mondays
@@ -379,7 +394,8 @@ const CalendarSettingsPage = (props: {
text70BL
/>
<Button
label="Connect Apple"
onPress={() => !profileData?.appleToken ? handleAppleSignIn() : clearToken("google")}
label={profileData?.appleToken ? `Disconnect ${profileData.appleMail}` : "Connect Apple"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2
@@ -416,40 +432,93 @@ const CalendarSettingsPage = (props: {
Connected Calendars
</Text>
<View style={styles.card}>
<View style={styles.noPaddingCard}>
<View style={{marginTop: 20}}>
{!!profileData?.googleMail && (
<Button
onPress={() => fetchAndSaveGoogleEvents()}
label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 2}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<TouchableOpacity
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingGoogle}
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingGoogle ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.appleMail && (
<TouchableOpacity>
<View row paddingR-20 center>
<Button
disabled={isSyncingApple}
onPress={() => fetchAndSaveAppleEvents(undefined, undefined)}
label={`Sync ${profileData?.appleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingApple ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.outlookMail && (
<Button
onPress={() => fetchAndSaveMicrosoftEvents()}
label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 2}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<TouchableOpacity
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingOutlook}
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingOutlook ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
</View>
</View>
@@ -480,6 +549,12 @@ const styles = StyleSheet.create({
marginTop: 20,
borderRadius: 12,
},
noPaddingCard: {
backgroundColor: "white",
width: "100%",
marginTop: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
File diff suppressed because it is too large Load Diff
@@ -1,122 +1,207 @@
import { Text, TextField, View } from "react-native-ui-lib";
import React, { useState } from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import {Colors, Picker, Text, TextField, View} from "react-native-ui-lib";
import React, {useEffect, useRef, useState} from "react";
import {ImageBackground, StyleSheet} from "react-native";
import {ScrollView} from "react-native-gesture-handler";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import Ionicons from "@expo/vector-icons/Ionicons";
import * as tz from 'tzdata';
import * as Localization from 'expo-localization';
import debounce from "debounce";
const MyProfile = () => {
const { user, profileData } = useAuthContext();
const {user, profileData} = useAuthContext();
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const [timeZone, setTimeZone] = useState<string>(profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone);
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const { mutateAsync: updateUserData } = useUpdateUserData();
return (
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
const {mutateAsync: updateUserData} = useUpdateUserData();
const isFirstRender = useRef(true);
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
await updateUserData({ newUserData: { firstName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
await updateUserData({ newUserData: { lastName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text text80 marginT-20 marginB-7 style={styles.label}>
Time Zone
</Text>
<TextField text70 placeholder="Time Zone" style={styles.txtBox} />
</View>
</ScrollView>
);
const handleUpdateUserData = async () => {
await updateUserData({newUserData: {firstName, lastName, timeZone}});
}
const debouncedUserDataUpdate = debounce(handleUpdateUserData, 500);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
debouncedUserDataUpdate();
}, [timeZone, lastName, firstName]);
return (
<ScrollView style={{paddingBottom: 100, flex: 1}}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
editable={false}
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text style={styles.jakarta12}>Time Zone</Text>
<View style={styles.viewPicker}>
<Picker
// editable={!isLoading}
value={timeZone}
onChange={(item) => {
setTimeZone(item as string)
}}
showSearch
floatingPlaceholder
style={styles.inViewPicker}
trailingAccessory={
<View style={{
justifyContent: "center",
alignItems: "center",
height: "100%",
marginTop: -38,
paddingRight: 15
}}>
<Ionicons name={"chevron-down"} style={{alignSelf: "center"}} size={20}
color={"#000000"}/>
</View>
}
>
{timeZoneItems}
</Picker>
</View>
</View>
</ScrollView>
);
};
const timeZoneItems = Object.keys(tz.zones).sort().map((zone) => (
<Picker.Item key={zone} label={zone.replace("/", " / ").replace("_", " ")} value={zone}/>
));
const styles = StyleSheet.create({
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfp: {
aspectRatio: 1,
width: 65.54,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1"
},
photoSet:{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07
}
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfp: {
aspectRatio: 1,
width: 65.54,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1"
},
photoSet: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07
},
jakarta12: {
paddingVertical: 10,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1",
},
picker: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
marginTop: -20,
height: 40,
zIndex: 10,
},
viewPicker: {
borderRadius: 50,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
marginTop: 0,
height: 40,
zIndex: 10,
},
inViewPicker: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 16,
marginTop: -20,
height: 40,
zIndex: 10,
},
});
export default MyProfile;