mirror of
https://github.com/urosran/cally.git
synced 2025-08-25 21:59:39 +00:00
Merge branch 'dp/calendar-sync' into dev
# Conflicts: # components/pages/settings/CalendarSettingsPage.tsx # yarn.lock
This commit is contained in:
@ -16,11 +16,11 @@ import {
|
||||
} from "react-native-ui-lib";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal";
|
||||
import AddChore from "../todos/AddChore";
|
||||
import AddChoreDialog from "../todos/AddChoreDialog";
|
||||
import { ToDosContextProvider } from "@/contexts/ToDosContext";
|
||||
import UploadImageDialog from "./UploadImageDialog";
|
||||
|
||||
|
||||
export const AddEventDialog = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
const [showManualInputModal, setShowManualInputModal] = useState(false);
|
||||
|
@ -1,169 +1,349 @@
|
||||
import { AntDesign, Ionicons } from "@expo/vector-icons";
|
||||
import React, { useState } from "react";
|
||||
import { Button, View, Text, Checkbox } from "react-native-ui-lib";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { colorMap } from "@/contexts/SettingsContext";
|
||||
import { ScrollView, TouchableOpacity } from "react-native-gesture-handler";
|
||||
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
||||
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
||||
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
||||
import React, {useState} from "react";
|
||||
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
|
||||
import {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 {useCreateEventFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||
import {GoogleSignin} from "@react-native-google-signin/google-signin";
|
||||
import * as AuthSession from "expo-auth-session";
|
||||
|
||||
GoogleSignin.configure({
|
||||
webClientId:
|
||||
"406146460310-hjadmfa1gg4ptaouira5rkhu0djlo5ut.apps.googleusercontent.com",
|
||||
scopes: ["profile", "email"], // Note: add calendar scope
|
||||
});
|
||||
|
||||
const GoogleLogin = async () => {
|
||||
return await GoogleSignin.signIn();
|
||||
};
|
||||
|
||||
const microsoftConfig = {
|
||||
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Replace with your Microsoft client ID
|
||||
redirectUri: AuthSession.makeRedirectUri({path: "settings"}), // Generate redirect URI automatically for Expo
|
||||
scopes: [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"offline_access",
|
||||
"Calendars.ReadWrite", // Scope for reading calendar events
|
||||
],
|
||||
authorizationEndpoint:
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
||||
};
|
||||
|
||||
const CalendarSettingsPage = (props: {
|
||||
setSelectedPage: (page: number) => void;
|
||||
setSelectedPage: (page: number) => void;
|
||||
}) => {
|
||||
const [selectedColor, setSelectedColor] = useState<string>(colorMap.pink);
|
||||
const [startDate, setStartDate] = useState<boolean>(true);
|
||||
return (
|
||||
<ScrollView>
|
||||
<View marginH-30>
|
||||
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
|
||||
<View row marginT-20 marginB-35 centerV>
|
||||
<Ionicons name="chevron-back" size={22} color="#979797" />
|
||||
<Text text70 color="#979797">
|
||||
Return to main settings
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<Text text60R>Calendar settings</Text>
|
||||
<View style={styles.card}>
|
||||
<Text text70 marginB-14>
|
||||
Event Color Preference
|
||||
</Text>
|
||||
<View row spread>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.pink)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
|
||||
{selectedColor == colorMap.pink && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.orange)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
|
||||
{selectedColor == colorMap.orange && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.green)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.green}>
|
||||
{selectedColor == colorMap.green && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.teal)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
|
||||
{selectedColor == colorMap.teal && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.purple)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
|
||||
{selectedColor == colorMap.purple && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text text70>Weekly Start Date</Text>
|
||||
<View row marginV-5 marginT-20>
|
||||
<Checkbox
|
||||
value={startDate}
|
||||
style={styles.checkbox}
|
||||
color="#ea156d"
|
||||
onValueChange={() => setStartDate(true)}
|
||||
/>
|
||||
<View row marginL-8>
|
||||
<Text text70>Sundays</Text>
|
||||
<Text text70 color="gray">
|
||||
{" "}
|
||||
(default)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View row marginV-5>
|
||||
<Checkbox
|
||||
value={!startDate}
|
||||
style={styles.checkbox}
|
||||
color="#ea156d"
|
||||
onValueChange={() => setStartDate(false)}
|
||||
/>
|
||||
<Text text70 marginL-8>
|
||||
Mondays
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
const [selectedColor, setSelectedColor] = useState<string>(colorMap.pink);
|
||||
const [startDate, setStartDate] = useState<boolean>(true);
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
<Text text60R marginT-30 marginB-25>
|
||||
Add Calendar
|
||||
</Text>
|
||||
const {mutateAsync: createEventFromProvider} = useCreateEventFromProvider();
|
||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||
|
||||
<Button
|
||||
label="Connect Google"
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<GoogleIcon />
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
<Button
|
||||
label="Connect Apple"
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<AppleIcon />
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
<Button
|
||||
label="Connect Outlook"
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<OutlookIcon />
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
const fetchAndSaveGoogleEvents = () => {
|
||||
const timeMin = new Date(new Date().setHours(0, 0, 0, 0));
|
||||
const timeMax = new Date(
|
||||
new Date(new Date().setHours(0, 0, 0, 0)).setDate(timeMin.getDate() + 30)
|
||||
);
|
||||
|
||||
<Text text60R marginT-30 marginB-20>
|
||||
Connected Calendars
|
||||
</Text>
|
||||
fetchGoogleCalendarEvents(
|
||||
profileData?.googleToken,
|
||||
timeMin.toISOString().slice(0, -5) + "Z",
|
||||
timeMax.toISOString().slice(0, -5) + "Z",
|
||||
).then((response) => {
|
||||
response?.forEach((item) => saveData(item));
|
||||
});
|
||||
};
|
||||
|
||||
<View style={styles.card} gap-15>
|
||||
<View row spread centerV>
|
||||
<View row centerV>
|
||||
<GoogleIcon width={30} height={30} style={{marginRight: 5}}/>
|
||||
<Text text70>Google Calendar name</Text>
|
||||
</View>
|
||||
<CloseXIcon />
|
||||
</View>
|
||||
async function saveData(item: any) {
|
||||
await createEventFromProvider(item);
|
||||
}
|
||||
|
||||
<View row spread centerV>
|
||||
<View row centerV>
|
||||
<AppleIcon />
|
||||
<Text text70>Google Calendar name</Text>
|
||||
const fetchAndSaveMicrosoftEvents = () => {
|
||||
const startDateTime = new Date(new Date().setHours(0, 0, 0, 0));
|
||||
const endDateTime = new Date(
|
||||
new Date(new Date().setHours(0, 0, 0, 0)).setDate(
|
||||
startDateTime.getDate() + 30
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
fetchMicrosoftCalendarEvents(
|
||||
profileData?.microsoftToken,
|
||||
startDateTime.toISOString().slice(0, -5) + "Z",
|
||||
endDateTime.toISOString().slice(0, -5) + "Z"
|
||||
).then((response) => {
|
||||
console.log(response)
|
||||
response?.forEach((item) => saveData(item));
|
||||
});
|
||||
};
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
try {
|
||||
const response = await GoogleLogin();
|
||||
if (response) {
|
||||
const googleUserData = response.data;
|
||||
let idToken = googleUserData?.idToken;
|
||||
|
||||
if (idToken) {
|
||||
await updateUserData({newUserData: {googleToken: idToken}});
|
||||
}
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.log(apiError || "Something went wrong");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMicrosoftSignIn = async () => {
|
||||
try {
|
||||
console.log("Starting Microsoft sign-in...");
|
||||
|
||||
const authRequest = new AuthSession.AuthRequest({
|
||||
clientId: microsoftConfig.clientId,
|
||||
scopes: microsoftConfig.scopes,
|
||||
redirectUri: microsoftConfig.redirectUri,
|
||||
responseType: AuthSession.ResponseType.Code,
|
||||
usePKCE: true, // Enable PKCE
|
||||
});
|
||||
|
||||
console.log("Auth request created:", authRequest);
|
||||
|
||||
const authResult = await authRequest.promptAsync({
|
||||
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
|
||||
});
|
||||
|
||||
console.log("Auth result:", authResult);
|
||||
|
||||
if (authResult.type === "success" && authResult.params?.code) {
|
||||
const code = authResult.params.code;
|
||||
console.log("Authorization code received:", code);
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `client_id=${microsoftConfig.clientId}&redirect_uri=${encodeURIComponent(
|
||||
microsoftConfig.redirectUri
|
||||
)}&grant_type=authorization_code&code=${code}&code_verifier=${
|
||||
authRequest.codeVerifier
|
||||
}&scope=${encodeURIComponent("https://graph.microsoft.com/Calendars.ReadWrite offline_access")}`,
|
||||
});
|
||||
|
||||
|
||||
console.log("Token response status:", tokenResponse.status);
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
console.error("Token exchange failed:", await tokenResponse.text());
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
console.log("Token data received:", tokenData);
|
||||
|
||||
if (tokenData?.id_token) {
|
||||
console.log("ID token received, updating user data...");
|
||||
await updateUserData({newUserData: {microsoftToken: tokenData.access_token}});
|
||||
console.log("User data updated successfully.");
|
||||
}
|
||||
} else {
|
||||
console.warn("Authentication was not successful:", authResult);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during Microsoft sign-in:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View marginH-30>
|
||||
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
|
||||
<View row marginT-20 marginB-35 centerV>
|
||||
<Ionicons name="chevron-back" size={22} color="#979797"/>
|
||||
<Text text70 color="#979797">
|
||||
Return to main settings
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<Text text60R>Calendar settings</Text>
|
||||
<View style={styles.card}>
|
||||
<Text text70 marginB-14>
|
||||
Event Color Preference
|
||||
</Text>
|
||||
<View row spread>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.pink)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
|
||||
{selectedColor == colorMap.pink && (
|
||||
<AntDesign name="check" size={30} color="white"/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.orange)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
|
||||
{selectedColor == colorMap.orange && (
|
||||
<AntDesign name="check" size={30} color="white"/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.green)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.green}>
|
||||
{selectedColor == colorMap.green && (
|
||||
<AntDesign name="check" size={30} color="white"/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.teal)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
|
||||
{selectedColor == colorMap.teal && (
|
||||
<AntDesign name="check" size={30} color="white"/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSelectedColor(colorMap.purple)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
|
||||
{selectedColor == colorMap.purple && (
|
||||
<AntDesign name="check" size={30} color="white"/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text text70>Weekly Start Date</Text>
|
||||
<View row marginV-5 marginT-20>
|
||||
<Checkbox
|
||||
value={startDate}
|
||||
style={styles.checkbox}
|
||||
color="#ea156d"
|
||||
onValueChange={() => setStartDate(true)}
|
||||
/>
|
||||
<View row marginL-8>
|
||||
<Text text70>Sundays</Text>
|
||||
<Text text70 color="gray">
|
||||
{" "}
|
||||
(default)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View row marginV-5>
|
||||
<Checkbox
|
||||
value={!startDate}
|
||||
style={styles.checkbox}
|
||||
color="#ea156d"
|
||||
onValueChange={() => setStartDate(false)}
|
||||
/>
|
||||
<Text text70 marginL-8>
|
||||
Mondays
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text text70>Add Calendar</Text>
|
||||
<View style={{marginTop: 20}}>
|
||||
<Button
|
||||
label={"Connect Google"}
|
||||
iconSource={() => (
|
||||
<View
|
||||
backgroundColor="#ededed"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{borderRadius: 50}}
|
||||
marginR-10
|
||||
centerV
|
||||
centerH
|
||||
>
|
||||
<Ionicons name="logo-google" size={22} color="#979797"/>
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
borderRadius={15}
|
||||
onPress={handleGoogleLogin}
|
||||
/>
|
||||
<Button
|
||||
label={"Connect Microsoft"}
|
||||
iconSource={() => (
|
||||
<View
|
||||
backgroundColor="#ededed"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{borderRadius: 50}}
|
||||
marginR-10
|
||||
centerV
|
||||
centerH
|
||||
>
|
||||
<Ionicons name="logo-microsoft" size={22} color="#979797"/>
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
borderRadius={15}
|
||||
onPress={handleMicrosoftSignIn}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text text70>Calendars</Text>
|
||||
<View style={{marginTop: 20}}>
|
||||
<Button
|
||||
label={"Sync Outlook"}
|
||||
iconSource={() => (
|
||||
<View
|
||||
backgroundColor="#ededed"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{borderRadius: 50}}
|
||||
marginR-10
|
||||
centerV
|
||||
centerH
|
||||
>
|
||||
<Ionicons name="logo-microsoft" size={22} color="#979797"/>
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
borderRadius={15}
|
||||
onPress={fetchAndSaveMicrosoftEvents}
|
||||
/>
|
||||
{profileData?.googleToken !== undefined && (
|
||||
<Button
|
||||
label={"Sync Google"}
|
||||
iconSource={() => (
|
||||
<View
|
||||
backgroundColor="#ededed"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{borderRadius: 50}}
|
||||
marginR-10
|
||||
centerV
|
||||
centerH
|
||||
>
|
||||
<Ionicons name="logo-google" size={22} color="#979797"/>
|
||||
</View>
|
||||
)}
|
||||
backgroundColor="white"
|
||||
color="#464039"
|
||||
borderRadius={15}
|
||||
onPress={fetchAndSaveGoogleEvents}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<CloseXIcon />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
addCalBtn: {
|
||||
addCalBtn: {
|
||||
backgroundColor: "#ffffff",
|
||||
marginBottom: 15,
|
||||
justifyContent: "flex-start",
|
||||
|
Reference in New Issue
Block a user