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 { TouchableOpacity } from "react-native-gesture-handler"; import { useAuthContext } from "@/contexts/AuthContext"; import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData"; import debounce from "debounce"; import AppleIcon from "@/assets/svgs/AppleIcon"; import GoogleIcon from "@/assets/svgs/GoogleIcon"; 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"; import * as AppleAuthentication from "expo-apple-authentication"; import ExpoLocalization from "expo-localization/src/ExpoLocalization"; import { colorMap } from "@/constants/colorMap"; import { useAtom } from "jotai"; import { settingsPageIndex } from "../calendar/atoms"; import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog"; const googleConfig = { androidClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", iosClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", webClientId: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", scopes: [ "email", "profile", "https://www.googleapis.com/auth/calendar.events.owned", ], }; const microsoftConfig = { clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", redirectUri: AuthSession.makeRedirectUri({ path: "settings" }), scopes: [ "openid", "profile", "email", "offline_access", "Calendars.ReadWrite", "User.Read", ], authorizationEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token", }; const CalendarSettingsPage = () => { const { profileData } = useAuthContext(); const [pageIndex, setPageIndex] = useAtom(settingsPageIndex); const [firstDayOfWeek, setFirstDayOfWeek] = useState( profileData?.firstDayOfWeek ?? ExpoLocalization.getCalendars()[0].firstWeekday === 1 ? "Mondays" : "Sundays" ); const [isModalVisible, setModalVisible] = useState(false); const [selectedService, setSelectedService] = useState< "google" | "outlook" | "apple" >("google"); const [selectedEmail, setSelectedEmail] = useState(""); const showConfirmationDialog = ( serviceName: "google" | "outlook" | "apple", email: string ) => { setSelectedService(serviceName); setSelectedEmail(email); setModalVisible(true); }; const handleConfirm = () => { clearToken(selectedService, selectedEmail); setModalVisible(false); }; const handleCancel = () => { setModalVisible(false); }; const [selectedColor, setSelectedColor] = useState( profileData?.eventColor ?? colorMap.pink ); const [previousSelectedColor, setPreviousSelectedColor] = useState( profileData?.eventColor ?? colorMap.pink ); 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); useEffect(() => { signInWithGoogle(); }, [response]); const signInWithGoogle = async () => { try { if (response?.type === "success") { const accessToken = response.authentication?.accessToken; const userInfoResponse = await fetch( "https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${accessToken}` }, } ); const userInfo = await userInfoResponse.json(); const googleMail = userInfo.email; let googleAccounts = profileData?.googleAccounts; const updatedGoogleAccounts = googleAccounts ? { ...googleAccounts, [googleMail]: accessToken } : { [googleMail]: accessToken }; await updateUserData({ newUserData: { googleAccounts: updatedGoogleAccounts }, }); await fetchAndSaveGoogleEvents({ token: accessToken, email: googleMail, }); } } catch (error) { console.error("Error during Google sign-in:", error); } }; 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 User.Read" )}`, }); console.log("Token response status:", tokenResponse.status); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error("Token exchange failed:", errorText); return; } const tokenData = await tokenResponse.json(); console.log("Token data received:", tokenData); if (tokenData?.access_token) { console.log("Access token received, fetching user info..."); // Fetch user info from Microsoft Graph API to get the email const userInfoResponse = await fetch( "https://graph.microsoft.com/v1.0/me", { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, } ); const userInfo = await userInfoResponse.json(); console.log("User info received:", userInfo); if (userInfo.error) { console.error("Error fetching user info:", userInfo.error); } else { const outlookMail = userInfo.mail || userInfo.userPrincipalName; let microsoftAccounts = profileData?.microsoftAccounts; const updatedMicrosoftAccounts = microsoftAccounts ? { ...microsoftAccounts, [outlookMail]: tokenData.access_token } : { [outlookMail]: tokenData.access_token }; // Update user data with Microsoft token and email await updateUserData({ newUserData: { microsoftAccounts: updatedMicrosoftAccounts }, }); await fetchAndSaveOutlookEvents( tokenData.access_token, outlookMail ); console.log("User data updated successfully."); } } } else { console.warn("Authentication was not successful:", authResult); } } catch (error) { console.error("Error during Microsoft sign-in:", error); } }; 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..."); await updateUserData({ newUserData: { appleToken, appleMail }, }); console.log("User data updated with Apple ID token."); await fetchAndSaveAppleEvents({ token: appleToken, email: appleMail! }); } 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 { await updateUserData({ newUserData: { eventColor: color, }, }); } catch (error) { console.error("Failed to update color:", error); setSelectedColor(previousSelectedColor); } }, 500), [] ); 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 = (firstDayOfWeek: string) => { setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"); debouncedUpdateFirstDayOfWeek( firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays" ); }; const handleChangeColor = (color: string) => { setPreviousSelectedColor(selectedColor); setSelectedColor(color); debouncedUpdateUserData(color); }; const clearToken = async ( provider: "google" | "outlook" | "apple", email: string ) => { const newUserData: Partial = {}; 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) => { if (item !== null) { isConnectedToGoogle = true; return; } }); } let isConnectedToMicrosoft = false; const microsoftAccounts = profileData?.microsoftAccounts; if (microsoftAccounts) { Object.values(profileData?.microsoftAccounts).forEach((item) => { if (item !== null) { isConnectedToMicrosoft = true; return; } }); } let isConnectedToApple = false; if (profileData?.appleAccounts) { Object.values(profileData?.appleAccounts).forEach((item) => { if (item !== null) { isConnectedToApple = true; return; } }); } return ( setPageIndex(0)}> Return to main settings Calendar settings Event Color Preference handleChangeColor(colorMap.pink)}> {selectedColor == colorMap.pink && ( )} handleChangeColor(colorMap.orange)} > {selectedColor == colorMap.orange && ( )} handleChangeColor(colorMap.green)}> {selectedColor == colorMap.green && ( )} handleChangeColor(colorMap.teal)}> {selectedColor == colorMap.teal && ( )} handleChangeColor(colorMap.purple)} > {selectedColor == colorMap.purple && ( )} Weekly Start Date handleChangeFirstDayOfWeek("Sundays")} /> Sundays {" "} (default) handleChangeFirstDayOfWeek("Mondays")} /> Mondays Add Calendar