New onboarding flow, calendar sync logic refactor

This commit is contained in:
Milan Paunovic
2024-11-01 03:18:50 +01:00
parent 539cbd9f10
commit 7f68f3acf8
18 changed files with 1153 additions and 671 deletions

246
app/(unauth)/cal_sync.tsx Normal file
View File

@ -0,0 +1,246 @@
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Text, View} from "react-native-ui-lib";
import React from "react";
import {useCalSync} from "@/hooks/useCalSync";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
import AppleIcon from "@/assets/svgs/AppleIcon";
import OutlookIcon from "@/assets/svgs/OutlookIcon";
import {useAuthContext} from "@/contexts/AuthContext";
import {StyleSheet} from "react-native";
export default function Screen() {
const {profileData, setRedirectOverride} = useAuthContext()
const {handleStartGoogleSignIn, handleAppleSignIn, handleMicrosoftSignIn} = useCalSync()
const hasSomeCalendarsSynced =
!!profileData?.appleAccounts || !!profileData?.microsoftAccounts || !!profileData?.googleAccounts
return (
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
Let's get started!
</Text>
<Text color={"#919191"} style={{fontSize: 20}}>
Add your calendar below to sync events to your Cally calendar
</Text>
</View>
<View width={"100%"} gap-1>
{!profileData?.googleAccounts && (
<Button
onPress={() => handleStartGoogleSignIn()}
label={"Connect Google account"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)}
{profileData?.googleAccounts
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
const googleToken = profileData?.googleAccounts?.[googleMail]?.accessToken;
return (
googleToken && (
<Button
key={googleMail}
disabled
label={`Connected ${googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
})
: null}
{!profileData?.appleAccounts && (
<Button
onPress={() => handleAppleSignIn()}
label={"Connect Apple"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)}
{profileData?.appleAccounts
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
const appleToken = profileData?.appleAccounts?.[appleEmail!];
return (
appleToken && (
<Button
key={appleEmail}
disabled
label={`Connected Apple Calendar`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
})
: null}
{!profileData?.microsoftAccounts && (
<Button
onPress={() => handleMicrosoftSignIn()}
label={"Connect Outlook"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)}
{profileData?.microsoftAccounts
? Object.keys(profileData?.microsoftAccounts)?.map(
(microsoftEmail) => {
const microsoftToken =
profileData?.microsoftAccounts?.[microsoftEmail];
return (
microsoftToken && (
<Button
key={microsoftEmail}
label={`Connected ${microsoftEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
}
)
: null}
</View>
<View flexG/>
<View width={"100%"}>
<Button
label={hasSomeCalendarsSynced ? "Continue" : "Skip this step"}
onPress={() => setRedirectOverride(false)}
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
style={{height: 50}}
backgroundColor="#fd1775"
/>
</View>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
addCalBtn: {
backgroundColor: "#ffffff",
marginBottom: 15,
justifyContent: "flex-start",
paddingLeft: 25,
},
backBtn: {
backgroundColor: "red",
marginLeft: -2,
justifyContent: "flex-start",
},
card: {
backgroundColor: "white",
width: "100%",
padding: 20,
paddingBottom: 30,
marginTop: 20,
borderRadius: 12,
},
noPaddingCard: {
backgroundColor: "white",
width: "100%",
marginTop: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 51,
borderRadius: 12,
},
checkbox: {
borderRadius: 50,
},
addCalLbl: {
fontSize: 16,
fontFamily: "PlusJakartaSan_500Medium",
flexWrap: "wrap",
width: "75%",
textAlign: "left",
lineHeight: 20,
overflow: "visible",
},
subTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
},
cardTitle: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
});

View File

@ -1,5 +1,5 @@
import Entry from "@/components/pages/main/Entry";
export default function Screen() {
return <Entry />;
return <Entry />;
}

View File

@ -0,0 +1,166 @@
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Colors, Dialog, LoaderScreen, Text, View} from "react-native-ui-lib";
import React, {useState} from "react";
import {useRouter} from "expo-router";
import QRIcon from "@/assets/svgs/QRIcon";
import Toast from "react-native-toast-message";
import {Camera, CameraView} from "expo-camera";
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
export default function Screen() {
const router = useRouter()
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const {mutateAsync: signInWithQrCode, isLoading} = useLoginWithQrCode();
const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false);
try {
await signInWithQrCode({userId: data});
Toast.show({
type: "success",
text1: "Login successful with QR code!",
});
} catch (err) {
Toast.show({
type: "error",
text1: "Error logging in with QR code",
text2: `${err}`,
});
}
};
const getCameraPermissions = async (callback: () => void) => {
const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted");
if (status === "granted") {
callback();
}
};
const handleOpenQrCodeDialog = () => {
getCameraPermissions(() => setShowCameraDialog(true));
}
return (
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View center>
<Text style={{fontSize: 30, fontFamily: 'Manrope_600SemiBold'}}>
Get started with Cally
</Text>
</View>
<View width={"100%"} gap-30>
<View>
<Button
label="Scan QR Code"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10
}}
iconSource={() => <QRIcon color={"#07B8C7"}/>}
onPress={handleOpenQrCodeDialog}
style={{height: 50}}
color={Colors.black}
backgroundColor={Colors.white}
/>
{/* GOOGLE LOGIN HERE */}
</View>
<View row center gap-20>
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
<Text style={{fontSize: 16, fontFamily: 'PlusJakartaSans_300Light', color: "#7A7A7A"}}>
or
</Text>
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
</View>
<View>
<Button
label="Contine with Email"
labelStyle={{
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginLeft: 10
}}
onPress={() => router.push("/(unauth)/sign_up")}
style={{height: 50, borderStyle: "solid", borderColor: "#E2E2E2", borderWidth: 2}}
color={Colors.black}
backgroundColor={"transparent"}
/>
</View>
</View>
<View flexG/>
<View row centerH gap-5>
<Text style={{
fontFamily: "PlusJakartaSans_300Light",
fontSize: 16,
color: "#484848"
}} center>
Already have an account?
</Text>
<Button
label="Log in"
link
onPress={() => router.push("/(unauth)/sign_in")}
labelStyle={[
{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
color: "#919191",
},
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
]}
/>
</View>
</View>
{/* Legacy, move into separate component */}
{/* Camera Dialog */}
<Dialog
visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)}
bottom
width="100%"
height="70%"
containerStyle={{padding: 15, backgroundColor: "white"}}
>
<Text center style={{fontSize: 16}} marginB-15>
Scan a QR code presented from your family member of provider.
</Text>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{flex: 1, borderRadius: 15}}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
}}
/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{margin: 10, marginBottom: 30}}
/>
</Dialog>
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
)}
</SafeAreaView>
)
}

View File

@ -1,10 +1,44 @@
import Entry from "@/components/pages/main/Entry";
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Image, Text, View} from "react-native-ui-lib";
import React from "react";
import {useRouter} from "expo-router";
export default function Screen() {
const router = useRouter()
return (
<SafeAreaView>
<Entry/>
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View>
<Image source={require("../../assets/images/splash.png")}/>
</View>
<View center gap-13>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
Welcome to Cally
</Text>
<Text center color={"#919191"} style={{fontSize: 20, maxWidth: 250}}>
Lightening Mental Loads,
One Family at a Time
</Text>
</View>
<View flexG/>
<View width={"100%"}>
<Button
label="Continue"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => router.push("/(unauth)/get_started")}
style={{height: 50}}
backgroundColor="#fd1775"
/>
</View>
</View>
</SafeAreaView>
)
}
}

View File

@ -0,0 +1,6 @@
import React from "react";
import {ResetPasswordPage} from "@/components/pages/main/ResetPasswordPage";
export default function Screen() {
return <ResetPasswordPage/>
}

6
app/(unauth)/sign_in.tsx Normal file
View File

@ -0,0 +1,6 @@
import SignInPage from "@/components/pages/main/SignInPage";
import React from "react";
export default function Screen() {
return <SignInPage/>
}

8
app/(unauth)/sign_up.tsx Normal file
View File

@ -0,0 +1,8 @@
import React from "react";
import SignUpPage from "@/components/pages/main/SignUpPage";
export default function Screen() {
return (
<SignUpPage/>
)
}

View File

@ -13,7 +13,7 @@ import {
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import { isWithinInterval, subDays, addDays, compareAsc } from "date-fns";
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
interface EventCalendarProps {
calendarHeight: number;
@ -37,21 +37,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const [isRendering, setIsRendering] = useState(true);
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
const todaysDate = new Date();
useEffect(() => {
if (events && mode) {
setIsRendering(true);
const timeout = setTimeout(() => {
setIsRendering(false);
}, 10);
return () => clearTimeout(timeout);
}
}, [events, mode]);
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week") {
@ -129,7 +118,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
}, [mode]);
const { enrichedEvents, filteredEvents } = useMemo(() => {
const {enrichedEvents, filteredEvents} = useMemo(() => {
const startTime = Date.now(); // Start timer
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
@ -156,16 +145,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
overlapCount: 0
});
// Sort events for this dateKey from oldest to newest by event.start
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
return acc;
}, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now(); // End timer
const endTime = Date.now();
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
return { enrichedEvents, filteredEvents };
return {enrichedEvents, filteredEvents};
}, [events, selectedDate, mode]);
const renderCustomDateForMonth = (date: Date) => {
@ -220,7 +208,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading || isRendering) {
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff"/>

View File

@ -103,6 +103,7 @@ export const ManuallyAddEventModal = () => {
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
const {data: members} = useGetFamilyMembers(true);
const titleRef = useRef<TextFieldRef>(null)
const isLoading = isDeleting || isAdding
@ -135,6 +136,14 @@ export const ManuallyAddEventModal = () => {
setRepeatInterval([]);
}, [editEvent, selectedNewEventDate]);
useEffect(() => {
if(show && !editEvent) {
setTimeout(() => {
titleRef?.current?.focus()
}, 500);
}
}, [selectedNewEventDate]);
if (!show) return null;
const formatDateTime = (date?: Date | string) => {
@ -342,6 +351,7 @@ export const ManuallyAddEventModal = () => {
<ScrollView style={{minHeight: "85%"}}>
<TextField
placeholder="Add event title"
ref={titleRef}
value={title}
onChangeText={(text) => {
setTitle(text);

View File

@ -1,11 +1,9 @@
import {Button, ButtonSize, Text, TextField, View} from "react-native-ui-lib";
import {Button, Text, TextField, View} from "react-native-ui-lib";
import React, {useState} from "react";
import {useSignIn} from "@/hooks/firebase/useSignIn";
import {StyleSheet} from "react-native";
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
import {isLoading} from "expo-font";
export const ResetPasswordPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
export const ResetPasswordPage = () => {
const [email, setEmail] = useState<string>("");
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();

View File

@ -2,7 +2,7 @@ import {
Button,
ButtonSize,
Colors,
Dialog,
KeyboardAwareScrollView,
LoaderScreen,
Text,
TextField,
@ -13,29 +13,20 @@ import React, {useRef, useState} from "react";
import {useSignIn} from "@/hooks/firebase/useSignIn";
import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
import Toast from "react-native-toast-message";
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
import {Camera, CameraView} from "expo-camera";
import KeyboardManager from "react-native-keyboard-manager";
import {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
KeyboardManager.setEnableAutoToolbar(true);
const SignInPage = ({
setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => {
const SignInPage = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const passwordRef = useRef<TextFieldRef>(null);
const {mutateAsync: signIn, error, isError, isLoading: isSigninIn} = useSignIn();
const {mutateAsync: signInWithQrCode, isLoading: isQRCodeLoggingIn} = useLoginWithQrCode();
const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
const isLoading = isSigninIn || isQRCodeLoggingIn
const router = useRouter()
const handleSignIn = async () => {
await signIn({email, password});
@ -53,169 +44,117 @@ const SignInPage = ({
}
};
const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false);
try {
await signInWithQrCode({userId: data});
Toast.show({
type: "success",
text1: "Login successful with QR code!",
});
} catch (err) {
Toast.show({
type: "error",
text1: "Error logging in with QR code",
text2: `${err}`,
});
}
};
const getCameraPermissions = async (callback: () => void) => {
const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === "granted");
if (status === "granted") {
callback();
}
};
return (
<View padding-10 centerV height={"100%%"} style={{justifyContent: "center"}}>
<KeyboardAvoidingView
contentContainerStyle={{justifyContent: "center"}}
keyboardVerticalOffset={50}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TextField
placeholder="Email"
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
onSubmitEditing={() => {
// Move focus to the description field
passwordRef.current?.focus();
}}
/>
<TextField
ref={passwordRef}
placeholder="Password"
textContentType={"oneTimeCode"}
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
autoCorrect={false}
/>
<Button
label="Log in"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={handleSignIn}
style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775"
/>
<Button
label="Log in with a QR Code"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true));
}}
style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775"
/>
<SafeAreaView style={{flex: 1}}>
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
<View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
Jump back into Cally
</Text>
<Text color={"#919191"} style={{fontSize: 20}}>
Please enter your details.
</Text>
</View>
{isError && (
<Text center style={{marginBottom: 20}}>{`${
error?.toString()?.split("]")?.[1]
}`}</Text>
)}
<KeyboardAvoidingView style={{width: "100%"}}
contentContainerStyle={{justifyContent: "center"}}
keyboardVerticalOffset={50}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TextField
placeholder="Email"
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
onSubmitEditing={() => {
// Move focus to the description field
passwordRef.current?.focus();
}}
/>
<TextField
ref={passwordRef}
placeholder="Password"
textContentType={"oneTimeCode"}
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
autoCorrect={false}
/>
</KeyboardAvoidingView>
<View flexG/>
<View row centerH marginB-5 gap-5>
<Text style={styles.jakartaLight}>Don't have an account?</Text>
<Button
onPress={() => setTab("register")}
label="Sign Up"
labelStyle={[
styles.jakartaMedium,
{textDecorationLine: "none", color: "#fd1575"},
]}
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
color="#fd1775"
/>
</View>
<View row centerH marginB-5 gap-5>
<Text text70>Forgot your password?</Text>
<Button
onPress={() => setTab("reset-password")}
label="Reset password"
labelStyle={[
styles.jakartaMedium,
{textDecorationLine: "none", color: "#fd1575"},
]}
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
avoidInnerPadding
color="#fd1775"
/>
</View>
</KeyboardAvoidingView>
{/* Camera Dialog */}
<Dialog
visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)}
bottom
width="100%"
height="70%"
containerStyle={{padding: 15, backgroundColor: "white"}}
>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{flex: 1, borderRadius: 15}}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
label="Log in"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={handleSignIn}
style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775"
/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{margin: 10, marginBottom: 30}}
/>
</Dialog>
{isError && (
<Text center style={{marginBottom: 20}}>{`${
error?.toString()?.split("]")?.[1]
}`}</Text>
)}
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
)}
<View row centerH marginB-5 gap-5>
<Text style={styles.jakartaLight}>Don't have an account?</Text>
<Button
onPress={() => router.replace("/(unauth)/sign_up")}
label="Sign Up"
labelStyle={[
styles.jakartaMedium,
{textDecorationLine: "none", color: "#fd1575"},
]}
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
color="#fd1775"
/>
</View>
</View>
{/*<View row centerH marginB-5 gap-5>*/}
{/* <Text text70>Forgot your password?</Text>*/}
{/* <Button*/}
{/* onPress={() => router.replace("/(unauth)/sign_up")}*/}
{/* label="Reset password"*/}
{/* labelStyle={[*/}
{/* styles.jakartaMedium,*/}
{/* {textDecorationLine: "none", color: "#fd1575"},*/}
{/* ]}*/}
{/* link*/}
{/* size={ButtonSize.xSmall}*/}
{/* padding-0*/}
{/* margin-0*/}
{/* text70*/}
{/* left*/}
{/* avoidInnerPadding*/}
{/* color="#fd1775"*/}
{/* />*/}
{/*</View>*/}
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white}
color={Colors.grey40}/>
)}
</View>
</KeyboardAwareScrollView>
</SafeAreaView>
);
};

View File

@ -3,6 +3,9 @@ import {
Button,
ButtonSize,
Checkbox,
Colors,
KeyboardAwareScrollView,
LoaderScreen,
Text,
TextField,
TextFieldRef,
@ -10,19 +13,15 @@ import {
View,
} from "react-native-ui-lib";
import {useSignUp} from "@/hooks/firebase/useSignUp";
import {StyleSheet} from "react-native";
import {KeyboardAvoidingView, StyleSheet} from "react-native";
import {AntDesign} from "@expo/vector-icons";
import KeyboardManager from "react-native-keyboard-manager";
import {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
KeyboardManager.setEnableAutoToolbar(true);
const SignUpPage = ({
setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => {
const SignUpPage = () => {
const [email, setEmail] = useState<string>("");
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
@ -31,170 +30,196 @@ const SignUpPage = ({
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
const {mutateAsync: signUp} = useSignUp();
const {mutateAsync: signUp, isLoading} = useSignUp();
const lnameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
const passwordRef = useRef<TextFieldRef>(null);
const router = useRouter()
const handleSignUp = async () => {
await signUp({email, password, firstName, lastName});
router.replace("/(unauth)/cal_sync")
};
return (
<View height={"100%"} padding-15 marginT-30>
<Text style={styles.title}>Get started with Cally</Text>
<Text style={styles.subtitle} marginT-15 color="#919191">
Please enter your details.
</Text>
<TextField
marginT-30
autoFocus
placeholder="First name"
value={firstName}
onChangeText={setFirstName}
style={styles.textfield}
onSubmitEditing={() => {
lnameRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="First name input"
accessibilityHint="Enter your first name"
accessible
returnKeyType="next"
textContentType="givenName"
importantForAccessibility="yes"
/>
<TextField
ref={lnameRef}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.textfield}
onSubmitEditing={() => {
emailRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="Last name input"
accessibilityHint="Enter your last name"
accessible
returnKeyType="next"
textContentType="familyName"
importantForAccessibility="yes"
/>
<TextField
placeholder="Email"
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
ref={emailRef}
onSubmitEditing={() => {
passwordRef.current?.focus();
}}
/>
<View
centerV
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
>
<TextField
ref={passwordRef}
placeholder="Password"
style={styles.jakartaLight}
value={password}
onChangeText={setPassword}
secureTextEntry={!isPasswordVisible}
trailingAccessory={
<TouchableOpacity
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
>
<AntDesign
name={isPasswordVisible ? "eye" : "eyeo"}
size={24}
color="gray"
/>
</TouchableOpacity>
}
/>
</View>
<View gap-5 marginT-15>
<View row centerV>
<Checkbox
style={[styles.check]}
color="#919191"
value={allowFaceID}
onValueChange={(value) => {
setAllowFaceID(value);
}}
/>
<Text style={styles.jakartaLight} marginL-10>
Allow FaceID for login in future
</Text>
</View>
<View row centerV>
<Checkbox
style={styles.check}
color="#919191"
value={acceptTerms}
onValueChange={(value) => setAcceptTerms(value)}
/>
<View row>
<Text style={styles.jakartaLight} marginL-10>
I accept the
<SafeAreaView style={{flex: 1}}>
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
<View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
Get started with Cally
</Text>
<TouchableOpacity>
<Text text90 style={styles.jakartaMedium}>
{" "}
terms and conditions
<Text color={"#919191"} style={{fontSize: 20}}>
Please enter your details.
</Text>
</View>
<KeyboardAvoidingView style={{width: '100%'}}>
<TextField
marginT-30
autoFocus
placeholder="First name"
value={firstName}
onChangeText={setFirstName}
style={styles.textfield}
onSubmitEditing={() => {
lnameRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="First name input"
accessibilityHint="Enter your first name"
accessible
returnKeyType="next"
textContentType="givenName"
importantForAccessibility="yes"
/>
<TextField
ref={lnameRef}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.textfield}
onSubmitEditing={() => {
emailRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="Last name input"
accessibilityHint="Enter your last name"
accessible
returnKeyType="next"
textContentType="familyName"
importantForAccessibility="yes"
/>
<TextField
placeholder="Email"
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
ref={emailRef}
onSubmitEditing={() => {
passwordRef.current?.focus();
}}
/>
<View
centerV
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
>
<TextField
ref={passwordRef}
placeholder="Password"
style={styles.jakartaLight}
value={password}
onChangeText={setPassword}
secureTextEntry={!isPasswordVisible}
trailingAccessory={
<TouchableOpacity
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
>
<AntDesign
name={isPasswordVisible ? "eye" : "eyeo"}
size={24}
color="gray"
/>
</TouchableOpacity>
}
/>
</View>
</KeyboardAvoidingView>
<View gap-5 marginT-15>
<View row centerV>
<Checkbox
style={[styles.check]}
color="#919191"
value={allowFaceID}
onValueChange={(value) => {
setAllowFaceID(value);
}}
/>
<Text style={styles.jakartaLight} marginL-10>
Allow FaceID for login in future
</Text>
</TouchableOpacity>
<Text style={styles.jakartaLight}> and </Text>
<TouchableOpacity>
<Text text90 style={styles.jakartaMedium}>
{" "}
privacy policy
</View>
<View row centerV>
<Checkbox
style={styles.check}
color="#919191"
value={acceptTerms}
onValueChange={(value) => setAcceptTerms(value)}
/>
<View row style={{flexWrap: "wrap", marginLeft: 10}}>
<Text style={styles.jakartaLight}>
I accept the
</Text>
<TouchableOpacity>
<Text text90 style={styles.jakartaMedium}>
{" "}
terms and conditions
</Text>
</TouchableOpacity>
<Text style={styles.jakartaLight}> and </Text>
<TouchableOpacity>
<Text text90 style={styles.jakartaMedium}>
{" "}
privacy policy
</Text>
</TouchableOpacity>
</View>
</View>
</View>
<View flexG style={{minHeight: 50}}/>
<View>
<Button
label="Register"
disabled={!acceptTerms}
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={handleSignUp}
backgroundColor={"#fd1775"}
style={{marginBottom: 0, height: 50}}
/>
<View row centerH marginT-10 marginB-2 gap-5>
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
Already have an account?
</Text>
</TouchableOpacity>
<Button
label="Log in"
labelStyle={[
styles.jakartaMedium,
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
]}
flexS
margin-0
link
color="#fd1775"
size={ButtonSize.small}
text70
onPress={() => router.replace("/(unauth)/sign_in")}
/>
</View>
</View>
</View>
</View>
<View flex-1/>
<View style={styles.bottomView}>
<Button
label="Register"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={handleSignUp}
style={{marginBottom: 0, backgroundColor: "#fd1775", height: 50}}
/>
<View row centerH marginT-10 marginB-2 gap-5>
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
Already have an account?
</Text>
</KeyboardAwareScrollView>
<Button
label="Log in"
labelStyle={[
styles.jakartaMedium,
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
]}
flexS
margin-0
link
color="#fd1775"
size={ButtonSize.small}
text70
onPress={() => setTab("login")}
/>
</View>
</View>
</View>
{isLoading && (
<LoaderScreen overlay message={"Signing up..."} backgroundColor={Colors.white}
color={Colors.grey40}/>
)}
</SafeAreaView>
);
};
@ -211,8 +236,6 @@ const styles = StyleSheet.create({
fontSize: 13,
color: "#919191",
},
//mora da se izmeni kako treba
bottomView: {marginTop: "auto", marginBottom: 30, marginTop: "auto"},
jakartaLight: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 13,

View File

@ -1,5 +1,5 @@
import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, {useCallback, useEffect, useState} from "react";
import React, {useCallback, useState} from "react";
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import {TouchableOpacity} from "react-native-gesture-handler";
@ -9,56 +9,19 @@ 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 {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 {useSetAtom} from "jotai";
import {settingsPageIndex} from "../calendar/atoms";
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
import {useCalSync} from "@/hooks/useCalSync";
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",
],
extraParams: {
access_type: "offline",
},
};
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 setPageIndex = useSetAtom(settingsPageIndex);
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
profileData?.firstDayOfWeek ??
ExpoLocalization.getCalendars()[0].firstWeekday === 1
@ -80,8 +43,8 @@ const CalendarSettingsPage = () => {
setModalVisible(true);
};
const handleConfirm = () => {
clearToken(selectedService, selectedEmail);
const handleConfirm = async () => {
await clearToken({email: selectedEmail, provider: selectedService});
setModalVisible(false);
};
@ -98,196 +61,21 @@ const CalendarSettingsPage = () => {
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: clearToken} = useClearTokens();
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, refreshToken } = response?.authentication!;
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,
[googleMail]: { accessToken, refreshToken },
};
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};
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);
alert(JSON.stringify(credential))
const appleToken = credential.identityToken;
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: {appleAccounts: updatedAppleAccounts},
});
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);
}
};
isSyncingGoogle,
isSyncingOutlook,
isConnectedToGoogle,
isConnectedToMicrosoft,
isConnectedToApple,
handleAppleSignIn,
isSyncingApple,
handleMicrosoftSignIn,
fetchAndSaveOutlookEvents,
fetchAndSaveGoogleEvents,
handleStartGoogleSignIn,
fetchAndSaveAppleEvents
} = useCalSync()
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
@ -331,37 +119,6 @@ const CalendarSettingsPage = () => {
debouncedUpdateUserData(color);
};
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 (
<ScrollView>
<TouchableOpacity onPress={() => setPageIndex(0)}>
@ -462,7 +219,7 @@ const CalendarSettingsPage = () => {
</Text>
<Button
onPress={() => promptAsync()}
onPress={() => handleStartGoogleSignIn()}
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
labelStyle={styles.addCalLbl}
labelProps={{

View File

@ -8,7 +8,7 @@ import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
import {Platform} from 'react-native';
import {useQueryClient} from "react-query";
@ -24,6 +24,7 @@ interface IAuthContext {
profileType?: ProfileType,
profileData?: UserProfile,
setProfileData: (profileData: UserProfile) => void,
setRedirectOverride: (val: boolean) => void,
refreshProfileData: () => Promise<void>
}
@ -50,11 +51,11 @@ async function registerForPushNotificationsAsync() {
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
const {status: existingStatus} = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
const {status} = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
@ -72,7 +73,7 @@ async function registerForPushNotificationsAsync() {
}
try {
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
const token = (await Notifications.getExpoPushTokenAsync({projectId})).data;
console.log('Push Token:', token);
return token;
} catch (error) {
@ -91,24 +92,28 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
const [initializing, setInitializing] = useState(true);
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
const [redirectOverride, setRedirectOverride] = useState(false);
const {replace} = useRouter();
const ready = !initializing;
const queryClient = useQueryClient();
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
setUser(authUser);
if (!redirectOverride) {
setUser(authUser);
if (authUser) {
await refreshProfileData(authUser);
const pushToken = await registerForPushNotificationsAsync();
if (pushToken) {
await savePushTokenToFirestore(authUser.uid, pushToken);
if (authUser) {
await refreshProfileData(authUser);
const pushToken = await registerForPushNotificationsAsync();
if (pushToken) {
await savePushTokenToFirestore(authUser.uid, pushToken);
}
}
}
if (initializing) setInitializing(false);
if (initializing) setInitializing(false);
}
};
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}, [initializing]);
useEffect(() => {
if (ready && user) {
if (ready && user && !redirectOverride) {
replace({pathname: "/(auth)/calendar"});
} else if (ready && !user) {
} else if (ready && !user && !redirectOverride) {
replace({pathname: "/(unauth)"});
}
}, [user, ready]);
}, [user, ready, redirectOverride]);
useEffect(() => {
const sub = Notifications.addNotificationReceivedListener(notification => {
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}
return (
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
<AuthContext.Provider
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
{children}
</AuthContext.Provider>
);

View File

@ -1,12 +1,16 @@
import {useMutation} from "react-query";
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
import auth from "@react-native-firebase/auth";
import {useAuthContext} from "@/contexts/AuthContext";
export const useLoginWithQrCode = () => {
const {setRedirectOverride} = useAuthContext()
return useMutation({
mutationKey: ["loginWithQrCode"],
mutationFn: async ({userId}: { userId: string }) => {
try {
setRedirectOverride(true)
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
token: string
}>

View File

@ -1,44 +1,47 @@
import { useMutation } from "react-query";
import {useMutation} from "react-query";
import auth from "@react-native-firebase/auth";
import { ProfileType } from "@/contexts/AuthContext";
import { useSetUserData } from "./useSetUserData";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useSetUserData} from "./useSetUserData";
import {uuidv4} from "@firebase/util";
import * as Localization from "expo-localization";
export const useSignUp = () => {
const { mutateAsync: setUserData } = useSetUserData();
const {setRedirectOverride} = useAuthContext()
const {mutateAsync: setUserData} = useSetUserData();
return useMutation({
mutationKey: ["signUp"],
mutationFn: async ({
email,
password,
firstName,
lastName,
}: {
email: string;
password: string;
firstName: string;
lastName: string;
}) => {
await auth()
.createUserWithEmailAndPassword(email, password)
.then(async (res) => {
try {
await setUserData({
newUserData: {
userType: ProfileType.PARENT,
firstName: firstName,
lastName: lastName,
familyId: uuidv4(),
timeZone: Localization.getCalendars()[0].timeZone,
},
customUser: res.user,
});
} catch (error) {
console.error(error);
}
});
},
});
return useMutation({
mutationKey: ["signUp"],
mutationFn: async ({
email,
password,
firstName,
lastName,
}: {
email: string;
password: string;
firstName: string;
lastName: string;
}) => {
setRedirectOverride(true)
await auth()
.createUserWithEmailAndPassword(email, password)
.then(async (res) => {
try {
await setUserData({
newUserData: {
userType: ProfileType.PARENT,
firstName: firstName,
lastName: lastName,
familyId: uuidv4(),
timeZone: Localization.getCalendars()[0].timeZone,
},
customUser: res.user,
});
} catch (error) {
console.error(error);
}
});
},
});
};

288
hooks/useCalSync.ts Normal file
View File

@ -0,0 +1,288 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useEffect} from "react";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
import * as WebBrowser from "expo-web-browser";
import * as Google from "expo-auth-session/providers/google";
import * as AuthSession from "expo-auth-session";
import * as AppleAuthentication from "expo-apple-authentication";
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",
],
extraParams: {
access_type: "offline",
},
};
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",
};
export const useCalSync = () => {
const {profileData} = useAuthContext();
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, refreshToken} = response?.authentication!;
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,
[googleMail]: {accessToken, refreshToken},
};
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};
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);
alert(JSON.stringify(credential))
const appleToken = credential.identityToken;
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: {appleAccounts: updatedAppleAccounts},
});
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);
}
};
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 {
handleAppleSignIn,
handleMicrosoftSignIn,
handleGoogleSignIn: signInWithGoogle,
handleStartGoogleSignIn: promptAsync,
fetchAndSaveOutlookEvents,
fetchAndSaveAppleEvents,
fetchAndSaveGoogleEvents,
isConnectedToApple,
isConnectedToMicrosoft,
isConnectedToGoogle,
isSyncingOutlook,
isSyncingGoogle,
isSyncingApple
}
}