mirror of
https://github.com/urosran/cally.git
synced 2025-07-14 17:25:46 +00:00
New onboarding flow, calendar sync logic refactor
This commit is contained in:
246
app/(unauth)/cal_sync.tsx
Normal file
246
app/(unauth)/cal_sync.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
166
app/(unauth)/get_started.tsx
Normal file
166
app/(unauth)/get_started.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,10 +1,44 @@
|
|||||||
import Entry from "@/components/pages/main/Entry";
|
|
||||||
import {SafeAreaView} from "react-native-safe-area-context";
|
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() {
|
export default function Screen() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<Entry/>
|
<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>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
6
app/(unauth)/reset_password.tsx
Normal file
6
app/(unauth)/reset_password.tsx
Normal 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
6
app/(unauth)/sign_in.tsx
Normal 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
8
app/(unauth)/sign_up.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SignUpPage from "@/components/pages/main/SignUpPage";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return (
|
||||||
|
<SignUpPage/>
|
||||||
|
)
|
||||||
|
}
|
@ -13,7 +13,7 @@ import {
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
||||||
import {Text} from "react-native-ui-lib";
|
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 {
|
interface EventCalendarProps {
|
||||||
calendarHeight: number;
|
calendarHeight: number;
|
||||||
@ -37,21 +37,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
const [isRendering, setIsRendering] = useState(true);
|
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
|
|
||||||
const todaysDate = new Date();
|
const todaysDate = new Date();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (events && mode) {
|
|
||||||
setIsRendering(true);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setIsRendering(false);
|
|
||||||
}, 10);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [events, mode]);
|
|
||||||
|
|
||||||
const handlePressEvent = useCallback(
|
const handlePressEvent = useCallback(
|
||||||
(event: CalendarEvent) => {
|
(event: CalendarEvent) => {
|
||||||
if (mode === "day" || mode === "week") {
|
if (mode === "day" || mode === "week") {
|
||||||
@ -156,13 +145,12 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
overlapCount: 0
|
overlapCount: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort events for this dateKey from oldest to newest by event.start
|
|
||||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, CalendarEvent[]>);
|
}, {} as Record<string, CalendarEvent[]>);
|
||||||
|
|
||||||
const endTime = Date.now(); // End timer
|
const endTime = Date.now();
|
||||||
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
|
|
||||||
return {enrichedEvents, filteredEvents};
|
return {enrichedEvents, filteredEvents};
|
||||||
@ -220,7 +208,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
setOffsetMinutes(getTotalMinutes());
|
setOffsetMinutes(getTotalMinutes());
|
||||||
}, [events, mode]);
|
}, [events, mode]);
|
||||||
|
|
||||||
if (isLoading || isRendering) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color="#0000ff"/>
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
|
@ -103,6 +103,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
|
|
||||||
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
|
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
|
||||||
const {data: members} = useGetFamilyMembers(true);
|
const {data: members} = useGetFamilyMembers(true);
|
||||||
|
const titleRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
const isLoading = isDeleting || isAdding
|
const isLoading = isDeleting || isAdding
|
||||||
|
|
||||||
@ -135,6 +136,14 @@ export const ManuallyAddEventModal = () => {
|
|||||||
setRepeatInterval([]);
|
setRepeatInterval([]);
|
||||||
}, [editEvent, selectedNewEventDate]);
|
}, [editEvent, selectedNewEventDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(show && !editEvent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus()
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [selectedNewEventDate]);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
const formatDateTime = (date?: Date | string) => {
|
const formatDateTime = (date?: Date | string) => {
|
||||||
@ -342,6 +351,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
<ScrollView style={{minHeight: "85%"}}>
|
<ScrollView style={{minHeight: "85%"}}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Add event title"
|
placeholder="Add event title"
|
||||||
|
ref={titleRef}
|
||||||
value={title}
|
value={title}
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setTitle(text);
|
setTitle(text);
|
||||||
|
@ -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 React, {useState} from "react";
|
||||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from "react-native";
|
||||||
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
|
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 [email, setEmail] = useState<string>("");
|
||||||
|
|
||||||
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
Colors,
|
Colors,
|
||||||
Dialog,
|
KeyboardAwareScrollView,
|
||||||
LoaderScreen,
|
LoaderScreen,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
@ -13,29 +13,20 @@ import React, {useRef, useState} from "react";
|
|||||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
||||||
import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
|
import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
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 KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
KeyboardManager.setEnableAutoToolbar(true);
|
KeyboardManager.setEnableAutoToolbar(true);
|
||||||
|
|
||||||
const SignInPage = ({
|
const SignInPage = () => {
|
||||||
setTab,
|
|
||||||
}: {
|
|
||||||
setTab: React.Dispatch<
|
|
||||||
React.SetStateAction<"register" | "login" | "reset-password">
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [password, setPassword] = 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 passwordRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
const {mutateAsync: signIn, error, isError, isLoading: isSigninIn} = useSignIn();
|
const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
|
||||||
const {mutateAsync: signInWithQrCode, isLoading: isQRCodeLoggingIn} = useLoginWithQrCode();
|
|
||||||
|
|
||||||
const isLoading = isSigninIn || isQRCodeLoggingIn
|
const router = useRouter()
|
||||||
|
|
||||||
const handleSignIn = async () => {
|
const handleSignIn = async () => {
|
||||||
await signIn({email, password});
|
await signIn({email, password});
|
||||||
@ -53,34 +44,20 @@ 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 (
|
return (
|
||||||
<View padding-10 centerV height={"100%%"} style={{justifyContent: "center"}}>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<KeyboardAvoidingView
|
<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>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView style={{width: "100%"}}
|
||||||
contentContainerStyle={{justifyContent: "center"}}
|
contentContainerStyle={{justifyContent: "center"}}
|
||||||
keyboardVerticalOffset={50}
|
keyboardVerticalOffset={50}
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
@ -110,6 +87,10 @@ const SignInPage = ({
|
|||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
<View flexG/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label="Log in"
|
label="Log in"
|
||||||
marginT-50
|
marginT-50
|
||||||
@ -121,18 +102,6 @@ const SignInPage = ({
|
|||||||
style={{marginBottom: 20, height: 50}}
|
style={{marginBottom: 20, height: 50}}
|
||||||
backgroundColor="#fd1775"
|
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<Text center style={{marginBottom: 20}}>{`${
|
<Text center style={{marginBottom: 20}}>{`${
|
||||||
@ -143,7 +112,7 @@ const SignInPage = ({
|
|||||||
<View row centerH marginB-5 gap-5>
|
<View row centerH marginB-5 gap-5>
|
||||||
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => setTab("register")}
|
onPress={() => router.replace("/(unauth)/sign_up")}
|
||||||
label="Sign Up"
|
label="Sign Up"
|
||||||
labelStyle={[
|
labelStyle={[
|
||||||
styles.jakartaMedium,
|
styles.jakartaMedium,
|
||||||
@ -159,63 +128,33 @@ const SignInPage = ({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View row centerH marginB-5 gap-5>
|
{/*<View row centerH marginB-5 gap-5>*/}
|
||||||
<Text text70>Forgot your password?</Text>
|
{/* <Text text70>Forgot your password?</Text>*/}
|
||||||
<Button
|
{/* <Button*/}
|
||||||
onPress={() => setTab("reset-password")}
|
{/* onPress={() => router.replace("/(unauth)/sign_up")}*/}
|
||||||
label="Reset password"
|
{/* label="Reset password"*/}
|
||||||
labelStyle={[
|
{/* labelStyle={[*/}
|
||||||
styles.jakartaMedium,
|
{/* styles.jakartaMedium,*/}
|
||||||
{textDecorationLine: "none", color: "#fd1575"},
|
{/* {textDecorationLine: "none", color: "#fd1575"},*/}
|
||||||
]}
|
{/* ]}*/}
|
||||||
link
|
{/* link*/}
|
||||||
size={ButtonSize.xSmall}
|
{/* size={ButtonSize.xSmall}*/}
|
||||||
padding-0
|
{/* padding-0*/}
|
||||||
margin-0
|
{/* margin-0*/}
|
||||||
text70
|
{/* text70*/}
|
||||||
left
|
{/* left*/}
|
||||||
avoidInnerPadding
|
{/* avoidInnerPadding*/}
|
||||||
color="#fd1775"
|
{/* color="#fd1775"*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</View>
|
{/*</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"],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
label="Cancel"
|
|
||||||
onPress={() => setShowCameraDialog(false)}
|
|
||||||
backgroundColor="#fd1775"
|
|
||||||
style={{margin: 10, marginBottom: 30}}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
|
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white}
|
||||||
|
color={Colors.grey40}/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Colors,
|
||||||
|
KeyboardAwareScrollView,
|
||||||
|
LoaderScreen,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
TextFieldRef,
|
TextFieldRef,
|
||||||
@ -10,19 +13,15 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import {useSignUp} from "@/hooks/firebase/useSignUp";
|
import {useSignUp} from "@/hooks/firebase/useSignUp";
|
||||||
import {StyleSheet} from "react-native";
|
import {KeyboardAvoidingView, StyleSheet} from "react-native";
|
||||||
import {AntDesign} from "@expo/vector-icons";
|
import {AntDesign} from "@expo/vector-icons";
|
||||||
import KeyboardManager from "react-native-keyboard-manager";
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
KeyboardManager.setEnableAutoToolbar(true);
|
KeyboardManager.setEnableAutoToolbar(true);
|
||||||
|
|
||||||
const SignUpPage = ({
|
const SignUpPage = () => {
|
||||||
setTab,
|
|
||||||
}: {
|
|
||||||
setTab: React.Dispatch<
|
|
||||||
React.SetStateAction<"register" | "login" | "reset-password">
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [firstName, setFirstName] = useState<string>("");
|
const [firstName, setFirstName] = useState<string>("");
|
||||||
const [lastName, setLastName] = useState<string>("");
|
const [lastName, setLastName] = useState<string>("");
|
||||||
@ -31,22 +30,33 @@ const SignUpPage = ({
|
|||||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||||
const {mutateAsync: signUp} = useSignUp();
|
const {mutateAsync: signUp, isLoading} = useSignUp();
|
||||||
|
|
||||||
const lnameRef = useRef<TextFieldRef>(null);
|
const lnameRef = useRef<TextFieldRef>(null);
|
||||||
const emailRef = useRef<TextFieldRef>(null);
|
const emailRef = useRef<TextFieldRef>(null);
|
||||||
const passwordRef = useRef<TextFieldRef>(null);
|
const passwordRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
await signUp({email, password, firstName, lastName});
|
await signUp({email, password, firstName, lastName});
|
||||||
|
router.replace("/(unauth)/cal_sync")
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View height={"100%"} padding-15 marginT-30>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<Text style={styles.title}>Get started with Cally</Text>
|
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
|
||||||
<Text style={styles.subtitle} marginT-15 color="#919191">
|
<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>
|
||||||
|
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||||
Please enter your details.
|
Please enter your details.
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView style={{width: '100%'}}>
|
||||||
<TextField
|
<TextField
|
||||||
marginT-30
|
marginT-30
|
||||||
autoFocus
|
autoFocus
|
||||||
@ -97,6 +107,7 @@ const SignUpPage = ({
|
|||||||
passwordRef.current?.focus();
|
passwordRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
centerV
|
centerV
|
||||||
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
|
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
|
||||||
@ -121,6 +132,9 @@ const SignUpPage = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
<View gap-5 marginT-15>
|
<View gap-5 marginT-15>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -142,8 +156,8 @@ const SignUpPage = ({
|
|||||||
value={acceptTerms}
|
value={acceptTerms}
|
||||||
onValueChange={(value) => setAcceptTerms(value)}
|
onValueChange={(value) => setAcceptTerms(value)}
|
||||||
/>
|
/>
|
||||||
<View row>
|
<View row style={{flexWrap: "wrap", marginLeft: 10}}>
|
||||||
<Text style={styles.jakartaLight} marginL-10>
|
<Text style={styles.jakartaLight}>
|
||||||
I accept the
|
I accept the
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity>
|
<TouchableOpacity>
|
||||||
@ -162,16 +176,20 @@ const SignUpPage = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View flex-1/>
|
|
||||||
<View style={styles.bottomView}>
|
<View flexG style={{minHeight: 50}}/>
|
||||||
|
|
||||||
|
<View>
|
||||||
<Button
|
<Button
|
||||||
label="Register"
|
label="Register"
|
||||||
|
disabled={!acceptTerms}
|
||||||
labelStyle={{
|
labelStyle={{
|
||||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
onPress={handleSignUp}
|
onPress={handleSignUp}
|
||||||
style={{marginBottom: 0, backgroundColor: "#fd1775", height: 50}}
|
backgroundColor={"#fd1775"}
|
||||||
|
style={{marginBottom: 0, height: 50}}
|
||||||
/>
|
/>
|
||||||
<View row centerH marginT-10 marginB-2 gap-5>
|
<View row centerH marginT-10 marginB-2 gap-5>
|
||||||
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
|
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
|
||||||
@ -190,11 +208,18 @@ const SignUpPage = ({
|
|||||||
color="#fd1775"
|
color="#fd1775"
|
||||||
size={ButtonSize.small}
|
size={ButtonSize.small}
|
||||||
text70
|
text70
|
||||||
onPress={() => setTab("login")}
|
onPress={() => router.replace("/(unauth)/sign_in")}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<LoaderScreen overlay message={"Signing up..."} backgroundColor={Colors.white}
|
||||||
|
color={Colors.grey40}/>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -211,8 +236,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: "#919191",
|
color: "#919191",
|
||||||
},
|
},
|
||||||
//mora da se izmeni kako treba
|
|
||||||
bottomView: {marginTop: "auto", marginBottom: 30, marginTop: "auto"},
|
|
||||||
jakartaLight: {
|
jakartaLight: {
|
||||||
fontFamily: "PlusJakartaSans_300Light",
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
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 {Button, Checkbox, Text, View} from "react-native-ui-lib";
|
||||||
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
|
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
|
||||||
import {TouchableOpacity} from "react-native-gesture-handler";
|
import {TouchableOpacity} from "react-native-gesture-handler";
|
||||||
@ -9,56 +9,19 @@ import debounce from "debounce";
|
|||||||
import AppleIcon from "@/assets/svgs/AppleIcon";
|
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||||
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||||
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
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 ExpoLocalization from "expo-localization/src/ExpoLocalization";
|
||||||
import {colorMap} from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
import {useAtom} from "jotai";
|
import {useSetAtom} from "jotai";
|
||||||
import {settingsPageIndex} from "../calendar/atoms";
|
import {settingsPageIndex} from "../calendar/atoms";
|
||||||
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
|
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
|
||||||
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
|
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 CalendarSettingsPage = () => {
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
|
|
||||||
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
|
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
|
||||||
profileData?.firstDayOfWeek ??
|
profileData?.firstDayOfWeek ??
|
||||||
ExpoLocalization.getCalendars()[0].firstWeekday === 1
|
ExpoLocalization.getCalendars()[0].firstWeekday === 1
|
||||||
@ -80,8 +43,8 @@ const CalendarSettingsPage = () => {
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = async () => {
|
||||||
clearToken(selectedService, selectedEmail);
|
await clearToken({email: selectedEmail, provider: selectedService});
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,196 +61,21 @@ const CalendarSettingsPage = () => {
|
|||||||
|
|
||||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
const {mutateAsync: clearToken} = useClearTokens();
|
const {mutateAsync: clearToken} = useClearTokens();
|
||||||
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
|
|
||||||
useFetchAndSaveGoogleEvents();
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: fetchAndSaveOutlookEvents,
|
isSyncingGoogle,
|
||||||
isLoading: isSyncingOutlook,
|
isSyncingOutlook,
|
||||||
} = useFetchAndSaveOutlookEvents();
|
isConnectedToGoogle,
|
||||||
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
|
isConnectedToMicrosoft,
|
||||||
useFetchAndSaveAppleEvents();
|
isConnectedToApple,
|
||||||
|
handleAppleSignIn,
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
isSyncingApple,
|
||||||
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
handleMicrosoftSignIn,
|
||||||
|
fetchAndSaveOutlookEvents,
|
||||||
useEffect(() => {
|
fetchAndSaveGoogleEvents,
|
||||||
signInWithGoogle();
|
handleStartGoogleSignIn,
|
||||||
}, [response]);
|
fetchAndSaveAppleEvents
|
||||||
|
} = useCalSync()
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedUpdateUserData = useCallback(
|
const debouncedUpdateUserData = useCallback(
|
||||||
debounce(async (color: string) => {
|
debounce(async (color: string) => {
|
||||||
@ -331,37 +119,6 @@ const CalendarSettingsPage = () => {
|
|||||||
debouncedUpdateUserData(color);
|
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 (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
@ -462,7 +219,7 @@ const CalendarSettingsPage = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={() => promptAsync()}
|
onPress={() => handleStartGoogleSignIn()}
|
||||||
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
|
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
|
||||||
labelStyle={styles.addCalLbl}
|
labelStyle={styles.addCalLbl}
|
||||||
labelProps={{
|
labelProps={{
|
||||||
|
@ -24,6 +24,7 @@ interface IAuthContext {
|
|||||||
profileType?: ProfileType,
|
profileType?: ProfileType,
|
||||||
profileData?: UserProfile,
|
profileData?: UserProfile,
|
||||||
setProfileData: (profileData: UserProfile) => void,
|
setProfileData: (profileData: UserProfile) => void,
|
||||||
|
setRedirectOverride: (val: boolean) => void,
|
||||||
refreshProfileData: () => Promise<void>
|
refreshProfileData: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,14 +92,17 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
const [initializing, setInitializing] = useState(true);
|
const [initializing, setInitializing] = useState(true);
|
||||||
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
||||||
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
||||||
|
const [redirectOverride, setRedirectOverride] = useState(false);
|
||||||
|
|
||||||
const {replace} = useRouter();
|
const {replace} = useRouter();
|
||||||
const ready = !initializing;
|
const ready = !initializing;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
||||||
setUser(authUser);
|
if (!redirectOverride) {
|
||||||
|
|
||||||
|
setUser(authUser);
|
||||||
|
|
||||||
if (authUser) {
|
if (authUser) {
|
||||||
await refreshProfileData(authUser);
|
await refreshProfileData(authUser);
|
||||||
@ -109,6 +113,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (initializing) setInitializing(false);
|
if (initializing) setInitializing(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
||||||
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}, [initializing]);
|
}, [initializing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ready && user) {
|
if (ready && user && !redirectOverride) {
|
||||||
replace({pathname: "/(auth)/calendar"});
|
replace({pathname: "/(auth)/calendar"});
|
||||||
} else if (ready && !user) {
|
} else if (ready && !user && !redirectOverride) {
|
||||||
replace({pathname: "/(unauth)"});
|
replace({pathname: "/(unauth)"});
|
||||||
}
|
}
|
||||||
}, [user, ready]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
const sub = Notifications.addNotificationReceivedListener(notification => {
|
||||||
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
|
<AuthContext.Provider
|
||||||
|
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export const useLoginWithQrCode = () => {
|
export const useLoginWithQrCode = () => {
|
||||||
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["loginWithQrCode"],
|
mutationKey: ["loginWithQrCode"],
|
||||||
mutationFn: async ({userId}: { userId: string }) => {
|
mutationFn: async ({userId}: { userId: string }) => {
|
||||||
try {
|
try {
|
||||||
|
setRedirectOverride(true)
|
||||||
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
||||||
token: string
|
token: string
|
||||||
}>
|
}>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useSetUserData} from "./useSetUserData";
|
import {useSetUserData} from "./useSetUserData";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import * as Localization from "expo-localization";
|
import * as Localization from "expo-localization";
|
||||||
|
|
||||||
export const useSignUp = () => {
|
export const useSignUp = () => {
|
||||||
|
const {setRedirectOverride} = useAuthContext()
|
||||||
const {mutateAsync: setUserData} = useSetUserData();
|
const {mutateAsync: setUserData} = useSetUserData();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@ -21,6 +22,8 @@ export const useSignUp = () => {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
setRedirectOverride(true)
|
||||||
|
|
||||||
await auth()
|
await auth()
|
||||||
.createUserWithEmailAndPassword(email, password)
|
.createUserWithEmailAndPassword(email, password)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
288
hooks/useCalSync.ts
Normal file
288
hooks/useCalSync.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user