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