Merge branch 'dev'

# Conflicts:
#	yarn.lock
This commit is contained in:
Milan Paunovic
2024-11-01 04:46:19 +01:00
52 changed files with 3395 additions and 2170 deletions

View File

@ -25,6 +25,7 @@ import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon"; import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { import {
isFamilyViewAtom,
settingsPageIndex, settingsPageIndex,
toDosPageIndex, toDosPageIndex,
userSettingsView, userSettingsView,
@ -32,6 +33,7 @@ import {
export default function TabLayout() { export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut(); const { mutateAsync: signOut } = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex); const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView); const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex); const setToDosIndex = useSetAtom(toDosPageIndex);
@ -79,6 +81,7 @@ export default function TabLayout() {
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false);
}} }}
icon={<NavCalendarIcon />} icon={<NavCalendarIcon />}
/> />
@ -91,6 +94,7 @@ export default function TabLayout() {
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false);
}} }}
icon={<NavGroceryIcon />} icon={<NavGroceryIcon />}
/> />
@ -118,6 +122,7 @@ export default function TabLayout() {
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false);
}} }}
icon={<NavToDosIcon />} icon={<NavToDosIcon />}
/> />
@ -130,6 +135,7 @@ export default function TabLayout() {
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false);
}} }}
icon={<NavBrainDumpIcon />} icon={<NavBrainDumpIcon />}
/> />
@ -142,6 +148,7 @@ export default function TabLayout() {
setPageIndex(0); setPageIndex(0);
setToDosIndex(0); setToDosIndex(0);
setUserView(true); setUserView(true);
setIsFamilyView(false);
}} }}
label={"Manage Settings"} label={"Manage Settings"}
labelStyle={styles.label} labelStyle={styles.label}

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

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

View File

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

View File

@ -1,10 +1,44 @@
import Entry from "@/components/pages/main/Entry";
import {SafeAreaView} from "react-native-safe-area-context"; import {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>
) )
} }

View File

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

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

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

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

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

View File

@ -5,13 +5,14 @@ const CloseXIcon: React.FC<SvgProps> = (props) => (
width={15} width={15}
height={15} height={15}
fill="none" fill="none"
viewBox="0 0 15 15"
{...props} {...props}
> >
<Path <Path
stroke="#AAA" stroke="#AAA"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={1.394} strokeWidth={props.strokeWidth || 1.394}
d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543" d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543"
/> />
</Svg> </Svg>

View File

@ -4,6 +4,7 @@ const PlusIcon = (props: SvgProps) => (
<Svg <Svg
width={props.width || 14} width={props.width || 14}
height={props.height || 15} height={props.height || 15}
viewBox="0 0 14 15"
fill="none" fill="none"
{...props} {...props}
> >

View File

@ -2,8 +2,9 @@ import * as Calendar from 'expo-calendar';
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) { export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
try { try {
const {status} = await Calendar.requestCalendarPermissionsAsync(); const {granted} = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') {
if (!granted) {
throw new Error("Calendar permission not granted"); throw new Error("Calendar permission not granted");
} }
@ -22,7 +23,11 @@ export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endD
return events.map((event) => { return events.map((event) => {
let isAllDay = event.allDay || false; let isAllDay = event.allDay || false;
const startDateTime = new Date(event.startDate); const startDateTime = new Date(event.startDate);
const endDateTime = new Date(event.endDate); let endDateTime = new Date(event.endDate);
if (isAllDay) {
endDateTime = startDateTime
}
return { return {
id: event.id, id: event.id,

View File

@ -8,7 +8,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
}, },
); );
const data = await response.json(); const data = await response.json();
const googleEvents = []; const googleEvents = [];
data.items?.forEach((item) => { data.items?.forEach((item) => {
let isAllDay = false; let isAllDay = false;
@ -49,5 +51,5 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
googleEvents.push(googleEvent); googleEvents.push(googleEvent);
}); });
return googleEvents; return {googleEvents, success: response.ok};
} }

View File

@ -1,21 +1,12 @@
import { import {Button, Dialog, TextField, TextFieldRef, TouchableOpacity, View,} from "react-native-ui-lib";
View, import React, {useEffect, useRef, useState} from "react";
Text,
Button,
TextField,
TextFieldRef,
TouchableOpacity,
} from "react-native-ui-lib";
import React, { useEffect, useState, useRef } from "react";
import { Dialog } from "react-native-ui-lib";
import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView"; import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
import { Dimensions, Keyboard, StyleSheet } from "react-native"; import {Dimensions, Platform, StyleSheet} from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon"; import DropModalIcon from "@/assets/svgs/DropModalIcon";
import {useBrainDumpContext} from "@/contexts/DumpContext"; import {useBrainDumpContext} from "@/contexts/DumpContext";
import KeyboardManager from "react-native-keyboard-manager"; import KeyboardManager from "react-native-keyboard-manager";
interface IAddBrainDumpProps { interface IAddBrainDumpProps {
isVisible: boolean; isVisible: boolean;
setIsVisible: (value: boolean) => void; setIsVisible: (value: boolean) => void;
@ -42,14 +33,16 @@ const AddBrainDump = ({
}, [addBrainDumpProps.isVisible]); }, [addBrainDumpProps.isVisible]);
useEffect(() => { useEffect(() => {
if (addBrainDumpProps.isVisible) {
setTimeout(() => { setTimeout(() => {
titleRef?.current?.focus() titleRef?.current?.focus()
}, 500) }, 500)
}, []); }
}, [addBrainDumpProps.isVisible]);
useEffect(() => { useEffect(() => {
KeyboardManager.setEnableAutoToolbar(false); if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
},[]) }, []);
return ( return (
<Dialog <Dialog
@ -78,7 +71,15 @@ const AddBrainDump = ({
label="Save" label="Save"
style={styles.topBtn} style={styles.topBtn}
onPress={() => { onPress={() => {
addBrainDump({ id: 99, title: dumpTitle.trimEnd().trimStart(), description: dumpDesc.trimEnd().trimStart() }); addBrainDump({
id: 99,
title: dumpTitle.trimEnd().trimStart(),
description: dumpDesc.trimEnd().trimStart(),
});
addBrainDumpProps.setIsVisible(false); addBrainDumpProps.setIsVisible(false);
}} }}
/> />
@ -144,7 +145,7 @@ const styles = StyleSheet.create({
description: { description: {
fontFamily: "Manrope_400Regular", fontFamily: "Manrope_400Regular",
fontSize: 14, fontSize: 14,
textAlignVertical: 'top' textAlignVertical: "top",
}, },
}); });

View File

@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react"; import React, {useEffect, useRef, useState} from "react";
import { import {
Button, Button,
Dialog, Dialog,
View, View,
Text, Text,
TextField, TextField,
TouchableOpacity, TouchableOpacity, TextFieldRef,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { Dimensions, StyleSheet } from "react-native"; import { Dimensions, StyleSheet } from "react-native";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView"; import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
@ -30,6 +30,7 @@ const MoveBrainDump = (props: {
props.item.description props.item.description
); );
const [modalVisible, setModalVisible] = useState<boolean>(false); const [modalVisible, setModalVisible] = useState<boolean>(false);
const descriptionRef = useRef<TextFieldRef>(null)
const { width } = Dimensions.get("screen"); const { width } = Dimensions.get("screen");
@ -37,6 +38,14 @@ const MoveBrainDump = (props: {
updateBrainDumpItem(props.item.id, { description: description }); updateBrainDumpItem(props.item.id, { description: description });
}, [description]); }, [description]);
useEffect(() => {
if (props.isVisible) {
setTimeout(() => {
descriptionRef?.current?.focus()
}, 500)
}
}, [props.isVisible]);
const showConfirmationDialog = () => { const showConfirmationDialog = () => {
setModalVisible(true); setModalVisible(true);
}; };
@ -112,7 +121,6 @@ const MoveBrainDump = (props: {
<TextField <TextField
textAlignVertical="top" textAlignVertical="top"
multiline multiline
autoFocus
fieldStyle={{ fieldStyle={{
width: "94%", width: "94%",
}} }}
@ -123,6 +131,7 @@ const MoveBrainDump = (props: {
onChangeText={(value) => { onChangeText={(value) => {
setDescription(value); setDescription(value);
}} }}
ref={descriptionRef}
returnKeyType="default" returnKeyType="default"
/> />
</View> </View>

View File

@ -13,6 +13,7 @@ export default function CalendarPage() {
<HeaderTemplate <HeaderTemplate
message={"Let's get your week started!"} message={"Let's get your week started!"}
isWelcome isWelcome
isCalendar={true}
/> />
<InnerCalendar/> <InnerCalendar/>
</View> </View>

View File

@ -1,13 +1,11 @@
import { Text, TouchableOpacity, View } from "react-native-ui-lib"; import { Text, TouchableOpacity, View } from "react-native-ui-lib";
import React, {useState} from "react"; import React from "react";
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import {useSetAtom} from "jotai"; import { useAtom } from "jotai";
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms"; import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
const CalendarViewSwitch = () => { const CalendarViewSwitch = () => {
const [calView, setCalView] = useState<boolean>(false); const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
const viewSwitch = useSetAtom(isFamilyViewAtom)
return ( return (
<View <View
@ -33,8 +31,7 @@ const CalendarViewSwitch = () => {
> >
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
setCalView(true); setIsFamilyView(true);
viewSwitch(true);
}} }}
> >
<View <View
@ -42,9 +39,12 @@ const CalendarViewSwitch = () => {
centerH centerH
height={40} height={40}
paddingH-15 paddingH-15
style={calView ? styles.switchBtnActive : styles.switchBtn} style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
> >
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
Family View Family View
</Text> </Text>
</View> </View>
@ -52,8 +52,7 @@ const CalendarViewSwitch = () => {
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
setCalView(false); setIsFamilyView(false);
viewSwitch(false);
}} }}
> >
<View <View
@ -61,9 +60,12 @@ const CalendarViewSwitch = () => {
centerH centerH
height={40} height={40}
paddingH-15 paddingH-15
style={!calView ? styles.switchBtnActive : styles.switchBtn} style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={!isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
> >
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
My View My View
</Text> </Text>
</View> </View>
@ -85,6 +87,6 @@ const styles = StyleSheet.create({
}, },
switchTxt: { switchTxt: {
fontSize: 16, fontSize: 16,
fontFamily: 'Manrope_600SemiBold' fontFamily: "Manrope_600SemiBold",
} },
}); });

View File

@ -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") {
@ -95,6 +84,8 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
[profileData] [profileData]
); );
console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek})
const isSameDate = useCallback((date1: Date, date2: Date) => { const isSameDate = useCallback((date1: Date, date2: Date) => {
return ( return (
date1.getDate() === date2.getDate() && date1.getDate() === date2.getDate() &&
@ -154,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};
@ -218,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"/>
@ -235,7 +225,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
mode={mode} mode={mode}
enableEnrichedEvents={true} enableEnrichedEvents={true}
sortedMonthView sortedMonthView
enrichedEventsByDate={enrichedEvents} // enrichedEventsByDate={enrichedEvents}
events={filteredEvents} events={filteredEvents}
// eventCellStyle={memoizedEventCellStyle} // eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent} onPressEvent={handlePressEvent}

View File

@ -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,8 +351,8 @@ 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}
autoFocus
onChangeText={(text) => { onChangeText={(text) => {
setTitle(text); setTitle(text);
}} }}
@ -555,14 +564,15 @@ export const ManuallyAddEventModal = () => {
</View> </View>
<View style={styles.divider}/> <View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV> <View marginH-30 marginB-0 row spread centerV>
<View row centerH> <View row center>
<LockIcon/> <LockIcon/>
<Text <Text
style={{ style={{
fontFamily: "PlusJakartaSans_500Medium", fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16, fontSize: 16,
}} }}
marginL-10 marginL-12
center
> >
Mark as Private Mark as Private
</Text> </Text>
@ -609,7 +619,7 @@ export const ManuallyAddEventModal = () => {
<Button <Button
disabled disabled
marginH-30 marginH-30
marginB-15 marginB-30
label="Create event from image" label="Create event from image"
text70 text70
style={{height: 47}} style={{height: 47}}

View File

@ -1,10 +1,12 @@
import {StyleSheet} from "react-native"; import {Dimensions, StyleSheet} from "react-native";
import React from "react"; import React from "react";
import {Button, View,} from "react-native-ui-lib"; import {Button, View,} from "react-native-ui-lib";
import {useGroceryContext} from "@/contexts/GroceryContext"; import {useGroceryContext} from "@/contexts/GroceryContext";
import {FontAwesome6} from "@expo/vector-icons"; import {FontAwesome6} from "@expo/vector-icons";
import PlusIcon from "@/assets/svgs/PlusIcon"; import PlusIcon from "@/assets/svgs/PlusIcon";
const { width } = Dimensions.get("screen");
const AddGroceryItem = () => { const AddGroceryItem = () => {
const {setIsAddingGrocery} = useGroceryContext(); const {setIsAddingGrocery} = useGroceryContext();
@ -65,8 +67,14 @@ const styles = StyleSheet.create({
marginVertical: 10, marginVertical: 10,
}, },
btnContainer: { btnContainer: {
width: "100%", position:"absolute",
bottom: 30,
width: width,
padding: 20,
paddingBottom: 0,
justifyContent: "center", justifyContent: "center",
alignItems:"center",
zIndex: 10,
}, },
finishShopBtn: { finishShopBtn: {
width: "100%", width: "100%",

View File

@ -73,6 +73,8 @@ const GroceryItem = ({
marginVertical: 5, marginVertical: 5,
paddingHorizontal: isEditingTitle ? 0 : 13, paddingHorizontal: isEditingTitle ? 0 : 13,
paddingVertical: isEditingTitle ? 0 : 10, paddingVertical: isEditingTitle ? 0 : 10,
height: 44.64,
backgroundColor: item.bought ? "#cbcbcb" : "white",
}} }}
backgroundColor="white" backgroundColor="white"
centerV centerV
@ -103,12 +105,25 @@ const GroceryItem = ({
<View> <View>
{isParent ? ( {isParent ? (
<TouchableOpacity onPress={() => setIsEditingTitle(true)}> <TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70T black style={styles.title}> <Text
text70T
black
style={[
styles.title,
{
textDecorationLine: item.bought ? "line-through" : "none",
},
]}
>
{item.title} {item.title}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<Text text70T black style={styles.title}> <Text
text70T
black
style={[styles.title, { color: item.bought ? "red" : "black" }]}
>
{item.title} {item.title}
</Text> </Text>
)} )}

View File

@ -74,10 +74,11 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
}, [groceries]); }, [groceries]);
return ( return (
<View marginH-20 marginB-20> <View marginH-20 marginB-45>
<HeaderTemplate <HeaderTemplate
message={"Welcome to your grocery list"} message={"Welcome to your grocery list"}
isWelcome={false} isWelcome={false}
isGroceries={true}
> >
<View row centerV> <View row centerV>
<View <View

View File

@ -1,11 +1,9 @@
import {Button, ButtonSize, Text, TextField, View} from "react-native-ui-lib"; import {Button, Text, TextField, View} from "react-native-ui-lib";
import React, {useState} from "react"; import 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();

View File

@ -1,7 +1,9 @@
import { import {
Button, Button,
ButtonSize, ButtonSize,
Dialog, Colors,
KeyboardAwareScrollView,
LoaderScreen,
Text, Text,
TextField, TextField,
TextFieldRef, TextFieldRef,
@ -9,26 +11,22 @@ import {
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import React, {useRef, useState} from "react"; import React, {useRef, useState} from "react";
import {useSignIn} from "@/hooks/firebase/useSignIn"; import {useSignIn} from "@/hooks/firebase/useSignIn";
import { 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 KeyboardManager from "react-native-keyboard-manager";
import { Camera, CameraView } from "expo-camera"; import {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
const SignInPage = ({ KeyboardManager.setEnableAutoToolbar(true);
setTab,
}: { const SignInPage = () => {
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 } = useSignIn(); const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode();
const router = useRouter()
const handleSignIn = async () => { const handleSignIn = async () => {
await signIn({email, password}); await signIn({email, password});
@ -46,39 +44,34 @@ 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%"}> <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>
<KeyboardAvoidingView style={{width: "100%"}}
contentContainerStyle={{justifyContent: "center"}}
keyboardVerticalOffset={50}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TextField <TextField
placeholder="Email" placeholder="Email"
value={email} keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail} onChangeText={setEmail}
style={styles.textfield} style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
onSubmitEditing={() => { onSubmitEditing={() => {
// Move focus to the description field // Move focus to the description field
passwordRef.current?.focus(); passwordRef.current?.focus();
@ -87,11 +80,17 @@ const SignInPage = ({
<TextField <TextField
ref={passwordRef} ref={passwordRef}
placeholder="Password" placeholder="Password"
textContentType={"oneTimeCode"}
value={password} value={password}
onChangeText={setPassword} onChangeText={setPassword}
secureTextEntry secureTextEntry
style={styles.textfield} style={styles.textfield}
autoCorrect={false}
/> />
</KeyboardAvoidingView>
<View flexG/>
<Button <Button
label="Log in" label="Log in"
marginT-50 marginT-50
@ -103,18 +102,7 @@ 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}}>{`${
error?.toString()?.split("]")?.[1] error?.toString()?.split("]")?.[1]
@ -124,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,
@ -140,56 +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>*/}
{/* Camera Dialog */} {isLoading && (
<Dialog <LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white}
visible={showCameraDialog} color={Colors.grey40}/>
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>
</View> </View>
</KeyboardAwareScrollView>
</SafeAreaView>
); );
}; };

View File

@ -3,6 +3,9 @@ import {
Button, Button,
ButtonSize, ButtonSize,
Checkbox, Checkbox,
Colors,
KeyboardAwareScrollView,
LoaderScreen,
Text, Text,
TextField, TextField,
TextFieldRef, TextFieldRef,
@ -10,16 +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 {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
const SignUpPage = ({ KeyboardManager.setEnableAutoToolbar(true);
setTab,
}: { const SignUpPage = () => {
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>("");
@ -28,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
@ -55,6 +68,12 @@ const SignUpPage = ({
lnameRef.current?.focus(); lnameRef.current?.focus();
}} }}
blurOnSubmit={false} blurOnSubmit={false}
accessibilityLabel="First name input"
accessibilityHint="Enter your first name"
accessible
returnKeyType="next"
textContentType="givenName"
importantForAccessibility="yes"
/> />
<TextField <TextField
ref={lnameRef} ref={lnameRef}
@ -66,18 +85,29 @@ const SignUpPage = ({
emailRef.current?.focus(); emailRef.current?.focus();
}} }}
blurOnSubmit={false} blurOnSubmit={false}
accessibilityLabel="Last name input"
accessibilityHint="Enter your last name"
accessible
returnKeyType="next"
textContentType="familyName"
importantForAccessibility="yes"
/> />
<TextField <TextField
ref={emailRef}
placeholder="Email" placeholder="Email"
value={email} keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail} onChangeText={setEmail}
style={styles.textfield} style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
ref={emailRef}
onSubmitEditing={() => { onSubmitEditing={() => {
passwordRef.current?.focus(); passwordRef.current?.focus();
}} }}
blurOnSubmit={false}
/> />
<View <View
centerV centerV
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]} style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
@ -102,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
@ -123,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>
@ -143,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>
@ -171,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>
); );
}; };
@ -192,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,

View File

@ -1,7 +1,7 @@
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, Alert, 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";
import {useAuthContext} from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData"; import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
@ -9,53 +9,20 @@ 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 { UserProfile } from "@firebase/auth";
import { useFetchAndSaveGoogleEvents } from "@/hooks/useFetchAndSaveGoogleEvents";
import { useFetchAndSaveOutlookEvents } from "@/hooks/useFetchAndSaveOutlookEvents";
import { useFetchAndSaveAppleEvents } from "@/hooks/useFetchAndSaveAppleEvents";
import * as AppleAuthentication from "expo-apple-authentication";
import ExpoLocalization from "expo-localization/src/ExpoLocalization"; import 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 {useCalSync} from "@/hooks/useCalSync";
import Feather from "@expo/vector-icons/Feather";
const googleConfig = {
androidClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: [
"email",
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
};
const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
redirectUri: AuthSession.makeRedirectUri({ path: "settings" }),
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite",
"User.Read",
],
authorizationEndpoint:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
};
const CalendarSettingsPage = () => { const 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
@ -77,8 +44,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);
}; };
@ -94,188 +61,22 @@ const CalendarSettingsPage = () => {
); );
const {mutateAsync: updateUserData} = useUpdateUserData(); const {mutateAsync: updateUserData} = useUpdateUserData();
const { mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle } = const {mutateAsync: clearToken} = useClearTokens();
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 = response.authentication?.accessToken;
const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
let googleAccounts = profileData?.googleAccounts;
const updatedGoogleAccounts = googleAccounts
? { ...googleAccounts, [googleMail]: accessToken }
: { [googleMail]: accessToken };
await updateUserData({
newUserData: { googleAccounts: updatedGoogleAccounts },
});
await fetchAndSaveGoogleEvents({
token: accessToken,
email: googleMail,
});
}
} catch (error) {
console.error("Error during Google sign-in:", error);
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Token exchange failed:", errorText);
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.access_token) {
console.log("Access token received, fetching user info...");
// Fetch user info from Microsoft Graph API to get the email
const userInfoResponse = await fetch(
"https://graph.microsoft.com/v1.0/me",
{
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
}
);
const userInfo = await userInfoResponse.json();
console.log("User info received:", userInfo);
if (userInfo.error) {
console.error("Error fetching user info:", userInfo.error);
} else {
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
let microsoftAccounts = profileData?.microsoftAccounts;
const updatedMicrosoftAccounts = microsoftAccounts
? { ...microsoftAccounts, [outlookMail]: tokenData.access_token }
: { [outlookMail]: tokenData.access_token };
// Update user data with Microsoft token and email
await updateUserData({
newUserData: { microsoftAccounts: updatedMicrosoftAccounts },
});
await fetchAndSaveOutlookEvents(
tokenData.access_token,
outlookMail
);
console.log("User data updated successfully.");
}
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const handleAppleSignIn = async () => {
try {
console.log("Starting Apple Sign-in...");
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
console.log("Apple sign-in result:", credential);
const appleToken = credential.identityToken;
const appleMail = credential.email;
if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed...");
await updateUserData({
newUserData: { appleToken, appleMail },
});
console.log("User data updated with Apple ID token.");
await fetchAndSaveAppleEvents({ token: appleToken, email: appleMail! });
} else {
console.warn(
"Apple authentication was not successful or email was hidden."
);
}
} catch (error) {
console.error("Error during Apple Sign-in:", error);
}
};
const debouncedUpdateUserData = useCallback( const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => { debounce(async (color: string) => {
@ -309,10 +110,8 @@ const CalendarSettingsPage = () => {
); );
const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => { const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"); setFirstDayOfWeek(firstDayOfWeek);
debouncedUpdateFirstDayOfWeek( debouncedUpdateFirstDayOfWeek(firstDayOfWeek);
firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"
);
}; };
const handleChangeColor = (color: string) => { const handleChangeColor = (color: string) => {
@ -321,64 +120,6 @@ const CalendarSettingsPage = () => {
debouncedUpdateUserData(color); debouncedUpdateUserData(color);
}; };
const clearToken = async (
provider: "google" | "outlook" | "apple",
email: string
) => {
const newUserData: Partial<UserProfile> = {};
if (provider === "google") {
let googleAccounts = profileData?.googleAccounts;
if (googleAccounts) {
googleAccounts[email] = null;
newUserData.googleAccounts = googleAccounts;
}
} else if (provider === "outlook") {
let microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) {
microsoftAccounts[email] = null;
newUserData.microsoftAccounts = microsoftAccounts;
}
} else if (provider === "apple") {
let appleAccounts = profileData?.appleAccounts;
if (appleAccounts) {
appleAccounts[email] = null;
newUserData.appleAccounts = appleAccounts;
}
}
await updateUserData({ newUserData });
};
let isConnectedToGoogle = false;
if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => {
if (item !== null) {
isConnectedToGoogle = true;
return;
}
});
}
let isConnectedToMicrosoft = false;
const microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) {
Object.values(profileData?.microsoftAccounts).forEach((item) => {
if (item !== null) {
isConnectedToMicrosoft = true;
return;
}
});
}
let isConnectedToApple = false;
if (profileData?.appleAccounts) {
Object.values(profileData?.appleAccounts).forEach((item) => {
if (item !== null) {
isConnectedToApple = true;
return;
}
});
}
return ( return (
<ScrollView> <ScrollView>
<TouchableOpacity onPress={() => setPageIndex(0)}> <TouchableOpacity onPress={() => setPageIndex(0)}>
@ -479,8 +220,8 @@ const CalendarSettingsPage = () => {
</Text> </Text>
<Button <Button
onPress={() => promptAsync()} onPress={() => handleStartGoogleSignIn()}
label={"Connect Google"} label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ labelProps={{
numberOfLines: 2, numberOfLines: 2,
@ -494,35 +235,8 @@ const CalendarSettingsPage = () => {
color="black" color="black"
text70BL text70BL
/> />
{profileData?.googleAccounts
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
const googleToken = profileData?.googleAccounts?.[googleMail];
return (
googleToken && (
<Button
key={googleMail}
onPress={() => {
showConfirmationDialog("google", googleMail);
}}
label={`Disconnect ${googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
})
: null}
{!profileData?.appleAccounts && (
<Button <Button
onPress={() => handleAppleSignIn()} onPress={() => handleAppleSignIn()}
label={"Connect Apple"} label={"Connect Apple"}
@ -539,36 +253,11 @@ const CalendarSettingsPage = () => {
color="black" color="black"
text70BL text70BL
/> />
{profileData?.appleAccounts
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
const appleToken = profileData?.appleAccounts?.[appleEmail];
return (
appleToken && (
<Button
key={appleEmail}
onPress={() => showConfirmationDialog("apple", appleEmail)}
label={`Disconnect ${appleEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<AppleIcon />
</View>
)} )}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
})
: null}
<Button <Button
onPress={() => handleMicrosoftSignIn()} onPress={() => handleMicrosoftSignIn()}
label={"Connect Outlook"} label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{ labelProps={{
numberOfLines: 2, numberOfLines: 2,
@ -582,37 +271,6 @@ const CalendarSettingsPage = () => {
color="black" color="black"
text70BL text70BL
/> />
{profileData?.microsoftAccounts
? Object.keys(profileData?.microsoftAccounts)?.map(
(microsoftEmail) => {
const microsoftToken =
profileData?.microsoftAccounts?.[microsoftEmail];
return (
microsoftToken && (
<Button
key={microsoftEmail}
onPress={() => {
showConfirmationDialog("outlook", microsoftEmail);
}}
label={`Disconnect ${microsoftEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
}}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
)
);
}
)
: null}
{(isConnectedToGoogle || {(isConnectedToGoogle ||
isConnectedToMicrosoft || isConnectedToMicrosoft ||
@ -622,56 +280,68 @@ const CalendarSettingsPage = () => {
Connected Calendars Connected Calendars
</Text> </Text>
<View style={styles.noPaddingCard}> <View style={[styles.noPaddingCard, {marginBottom: 100}]}>
<View style={{marginTop: 20}}> <View style={{marginTop: 20}}>
{profileData?.googleAccounts && {profileData?.googleAccounts &&
Object.keys(profileData?.googleAccounts)?.map( Object.keys(profileData?.googleAccounts)?.map(
(googleEmail) => { (googleEmail) => {
const googleToken = const googleToken =
profileData?.googleAccounts?.[googleEmail]; profileData?.googleAccounts?.[googleEmail]?.accessToken;
return ( return (
googleToken && ( googleToken && (
<TouchableOpacity <View row paddingR-5 center>
onPress={() => <View
fetchAndSaveGoogleEvents({ style={{
token: googleToken, backgroundColor: "#ffffff",
email: googleEmail, marginBottom: 15,
}) paddingLeft: 15,
} }}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingGoogle}
onPress={() =>
fetchAndSaveGoogleEvents({
token: googleToken,
email: googleEmail,
})
}
label={`Sync ${googleEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
</View>
)}
style={styles.addCalBtn}
color="black" color="black"
text70BL text70BL
/> row
centerV
width="100%"
spread
>
{isSyncingGoogle ? ( {isSyncingGoogle ? (
<View marginR-5>
<ActivityIndicator/> <ActivityIndicator/>
</View>
) : ( ) : (
<Ionicons <View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveGoogleEvents({
token: googleToken,
email: googleEmail,
})
}
iconSource={() => <Ionicons
name={"refresh"} name={"refresh"}
size={20} size={20}
color={"#000000"} color={"#000000"}
/>}
/> />
)}
</View> </View>
</TouchableOpacity> )}
<View marginR-5>
<GoogleIcon/>
</View>
<Text style={styles.addCalLbl}>
{googleEmail}
</Text>
<Button
style={{backgroundColor: "#ffffff", marginRight: 5}}
color="black"
onPress={
() => showConfirmationDialog("google", googleEmail)
}
iconSource={() => <Feather name="x" size={24} />}
/>
</View>
</View>
) )
); );
} }
@ -679,49 +349,62 @@ const CalendarSettingsPage = () => {
{profileData?.appleAccounts && {profileData?.appleAccounts &&
Object.keys(profileData?.appleAccounts)?.map((appleEmail) => { Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
console.log(profileData?.appleAccounts)
const appleToken = profileData?.appleAccounts?.[appleEmail]; const appleToken = profileData?.appleAccounts?.[appleEmail];
return ( return (
appleToken && ( appleToken && (
<TouchableOpacity <View row paddingR-5 center>
onPress={() => <View
fetchAndSaveAppleEvents({ style={{
email: appleEmail, backgroundColor: "#ffffff",
token: appleToken, marginBottom: 15,
}) paddingLeft: 15,
} }}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingApple}
onPress={() =>
fetchAndSaveAppleEvents({
email: appleEmail,
token: appleToken,
})
}
label={`Sync ${appleEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }}
iconSource={() => (
<View marginR-15>
<AppleIcon />
</View>
)}
style={styles.addCalBtn}
color="black" color="black"
text70BL text70BL
/> row
centerV
width="100%"
spread
>
<View marginR-5>
<AppleIcon/>
</View>
<Text style={styles.addCalLbl}>
{appleEmail}
</Text>
{isSyncingApple ? ( {isSyncingApple ? (
<View marginR-5>
<ActivityIndicator/> <ActivityIndicator/>
</View>
) : ( ) : (
<Ionicons <View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveAppleEvents({
email: appleEmail,
token: appleToken,
})
}
iconSource={() => <Ionicons
name={"refresh"} name={"refresh"}
size={20} size={20}
color={"#000000"} color={"#000000"}
/>}
/> />
)}
</View> </View>
</TouchableOpacity> )}
<Button
style={{backgroundColor: "#ffffff", marginRight: 5}}
color="black"
onPress={() => showConfirmationDialog("apple", appleEmail)}
iconSource={() => <Feather name="x" size={24} />}
/>
</View>
</View>
) )
); );
})} })}
@ -733,46 +416,59 @@ const CalendarSettingsPage = () => {
profileData?.microsoftAccounts?.[microsoftEmail]; profileData?.microsoftAccounts?.[microsoftEmail];
return ( return (
microsoftToken && ( microsoftToken && (
<TouchableOpacity <View row paddingR-5 center>
onPress={() => <View
fetchAndSaveOutlookEvents({ style={{
token: microsoftToken, backgroundColor: "#ffffff",
email: microsoftEmail, marginBottom: 15,
}) paddingLeft: 15,
} }}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingOutlook}
onPress={() =>
fetchAndSaveOutlookEvents({
token: microsoftToken,
email: microsoftEmail,
})
}
label={`Sync ${microsoftEmail}`}
labelStyle={styles.addCalLbl}
labelProps={{ numberOfLines: 3 }}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
</View>
)}
style={styles.addCalBtn}
color="black" color="black"
text70BL text70BL
/> row
centerV
width="100%"
spread
>
{isSyncingOutlook ? ( {isSyncingOutlook ? (
<View marginR-5>
<ActivityIndicator/> <ActivityIndicator/>
</View>
) : ( ) : (
<Ionicons <View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveOutlookEvents({
token: microsoftToken,
email: microsoftEmail,
})
}
iconSource={() => <Ionicons
name={"refresh"} name={"refresh"}
size={20} size={20}
color={"#000000"} color={"#000000"}
/>}
/> />
)}
</View> </View>
</TouchableOpacity> )}
<View marginR-5>
<OutlookIcon/>
</View>
<Text style={styles.addCalLbl}>
{microsoftEmail}
</Text>
<Button
style={{backgroundColor: "#ffffff", marginRight: 5}}
color="black"
onPress={
() => showConfirmationDialog("outlook", microsoftEmail)
}
iconSource={() => <Feather name="x" size={24} />}
/>
</View>
</View>
) )
); );
} }
@ -833,10 +529,10 @@ const styles = StyleSheet.create({
fontSize: 16, fontSize: 16,
fontFamily: "PlusJakartaSan_500Medium", fontFamily: "PlusJakartaSan_500Medium",
flexWrap: "wrap", flexWrap: "wrap",
width: "75%", width: "70%",
textAlign: "left", textAlign: "left",
lineHeight: 20, lineHeight: 20,
overflow: "visible", overflow: "hidden",
}, },
subTitle: { subTitle: {
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",

View File

@ -1,6 +1,6 @@
import { Button, Text, View } from "react-native-ui-lib"; import { Button, Text, View } from "react-native-ui-lib";
import React, { useState } from "react"; import React, { useState } from "react";
import { StyleSheet } from "react-native"; import {Linking, StyleSheet} from "react-native";
import { Octicons } from "@expo/vector-icons"; import { Octicons } from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage"; import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings"; import ChoreRewardSettings from "./ChoreRewardSettings";
@ -21,11 +21,23 @@ const pageIndex = {
policy: 4, policy: 4,
}; };
const PRIVACY_POLICY_URL = 'https://callyapp.com';
const SettingsPage = () => { const SettingsPage = () => {
const { profileData } = useAuthContext(); const { profileData } = useAuthContext();
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex); const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const isntParent = profileData?.userType !== ProfileType.PARENT; const isntParent = profileData?.userType !== ProfileType.PARENT;
const openPrivacyPolicy = async () => {
const supported = await Linking.canOpenURL(PRIVACY_POLICY_URL);
if (supported) {
await Linking.openURL(PRIVACY_POLICY_URL);
} else {
console.log("Don't know how to open this URL:", PRIVACY_POLICY_URL);
}
};
return ( return (
<View flexG> <View flexG>
{pageIndex == 0 && ( {pageIndex == 0 && (
@ -73,7 +85,8 @@ const SettingsPage = () => {
}} }}
/> />
<Button <Button
disabled={isntParent} disabled
// disabled={isntParent}
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
children={ children={
@ -84,7 +97,7 @@ const SettingsPage = () => {
color="#ff9900" color="#ff9900"
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
/> />
<Text style={[styles.label, isntParent ? styles.disabledText : {color: "#ff9900"}]}> <Text style={[styles.label, true ? styles.disabledText : {color: "#ff9900"}]}>
To-Do Reward Settings To-Do Reward Settings
</Text> </Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} /> <ArrowRightIcon style={{ marginLeft: "auto" }} />
@ -95,6 +108,7 @@ const SettingsPage = () => {
<Button <Button
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
onPress={openPrivacyPolicy}
children={ children={
<View row centerV width={"100%"}> <View row centerV width={"100%"}>
<PrivacyPolicyIcon style={{ marginRight: 10 }} /> <PrivacyPolicyIcon style={{ marginRight: 10 }} />

View File

@ -1,18 +1,19 @@
import { Text, TouchableOpacity, View } from "react-native-ui-lib"; import {FloatingButton, Text, TouchableOpacity, View,} from "react-native-ui-lib";
import React, {useState} from "react"; import React, {useState} from "react";
import {Ionicons} from "@expo/vector-icons"; import {Ionicons} from "@expo/vector-icons";
import {ScrollView, StyleSheet} from "react-native"; import {ScrollView, StyleSheet} from "react-native";
import MyProfile from "./user_settings_views/MyProfile"; import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup"; import MyGroup from "./user_settings_views/MyGroup";
import { useAtom } from "jotai"; import {useAtom, useSetAtom} from "jotai";
import {settingsPageIndex, userSettingsView} from "../calendar/atoms"; import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
import { AuthContextProvider } from "@/contexts/AuthContext"; import PlusIcon from "@/assets/svgs/PlusIcon";
const UserSettings = () => { const UserSettings = () => {
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex); const setPageIndex = useSetAtom(settingsPageIndex);
const [userView, setUserView] = useAtom(userSettingsView); const [userView, setUserView] = useAtom(userSettingsView);
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
return ( return (
<AuthContextProvider>
<View flexG> <View flexG>
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}> <ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
<TouchableOpacity <TouchableOpacity
@ -73,21 +74,36 @@ const UserSettings = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{userView && <MyProfile/>} {userView && <MyProfile/>}
{!userView && <MyGroup />} {!userView && <MyGroup onNewUserClick={onNewUserClick} setOnNewUserClick={setOnNewUserClick}/>}
</View> </View>
</ScrollView> </ScrollView>
{!userView && ( {!userView && (
<View> <FloatingButton
<Text>selview</Text> fullWidth
</View> hideBackgroundOverlay
visible
button={{
label: " Add a user device",
iconSource: () => <PlusIcon height={13} width={14}/>,
onPress: () => setOnNewUserClick(true),
style: styles.bottomButton,
labelStyle: {fontFamily: "Manrope_600SemiBold", fontSize: 15},
}}
/>
)} )}
</View> </View>
</AuthContextProvider>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
bottomButton: {
position: "absolute",
bottom: 15,
marginHorizontal: 28,
width: 337,
backgroundColor: "#e8156c",
height: 53.26,
},
buttonSwitch: { buttonSwitch: {
borderRadius: 50, borderRadius: 50,
width: "100%", width: "100%",

View File

@ -0,0 +1,128 @@
import React, { useState } from "react";
import { Dialog, Button, Text, View } from "react-native-ui-lib";
import { StyleSheet } from "react-native";
import { Feather } from "@expo/vector-icons";
interface ConfirmationDialogProps {
visible: boolean;
onDismiss: () => void;
onFirstYes: () => void;
onConfirm: () => void;
}
const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
visible,
onDismiss,
onFirstYes,
onConfirm,
}) => {
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
return (
<>
<Dialog
visible={visible}
onDismiss={onDismiss}
containerStyle={styles.dialog}
>
<View centerH>
<Feather name="alert-triangle" size={70} color="#FF5449" />
</View>
<Text center style={styles.title}>
Are you sure?
</Text>
<Text
style={{
fontSize: 18,
fontFamily: "PlusJakartaSans_700Bold",
color: "#979797",
marginBottom: 20,
}}
center
>
This action will permanently delete all your data, you won't be able
to recover it!
</Text>
<View centerV></View>
<View row right gap-8>
<Button
label="Cancel"
onPress={onDismiss}
style={styles.cancelBtn}
color="#999999"
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
/>
<Button
label="Yes"
onPress={() => {
setTimeout(() => setConfirmationDialog(true), 300);
onFirstYes();
}}
style={styles.confirmBtn}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
/>
</View>
</Dialog>
<Dialog
visible={confirmationDialog}
onDismiss={() => setConfirmationDialog(false)}
containerStyle={styles.dialog}
>
<View center paddingH-10 paddingT-15 paddingB-5>
<Text style={styles.title}>
We're sorry to see you go, are you really sure you want to delete
everything?
</Text>
<View row right gap-8 marginT-15>
<Button
label="Cancel"
onPress={() => {
setConfirmationDialog(false);
}}
style={styles.cancelBtn}
color="#999999"
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
/>
<Button
label="Yes"
onPress={() => {
onConfirm();
setConfirmationDialog(false);
}}
style={styles.confirmBtn}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
/>
</View>
</View>
</Dialog>
</>
);
};
// Empty stylesheet for future styles
const styles = StyleSheet.create({
confirmBtn: {
backgroundColor: "#FF5449",
},
cancelBtn: {
backgroundColor: "white",
},
dialog: {
backgroundColor: "white",
paddingHorizontal: 25,
paddingTop: 25,
paddingBottom: 17,
borderRadius: 20,
},
title: {
fontFamily: "Manrope_600SemiBold",
fontSize: 22,
marginBottom: 5,
},
text: {
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginBottom: 0,
},
});
export default DeleteProfileDialogs;

View File

@ -3,8 +3,7 @@ import {
Button, Button,
Card, Card,
Colors, Colors,
Dialog, Dialog, Image,
FloatingButton,
KeyboardAwareScrollView, KeyboardAwareScrollView,
PanningProvider, PanningProvider,
Picker, Picker,
@ -15,7 +14,7 @@ import {
View, View,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import { ImageBackground, ScrollView, StyleSheet } from "react-native"; import {ImageBackground, Platform, StyleSheet} from "react-native";
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types"; import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser"; import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext"; import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
@ -28,11 +27,18 @@ import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon"; import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon"; import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import Ionicons from "@expo/vector-icons/Ionicons"; import Ionicons from "@expo/vector-icons/Ionicons";
import KeyboardManager, { PreviousNextView } from "react-native-keyboard-manager"; import KeyboardManager, {PreviousNextView,} from "react-native-keyboard-manager";
import {ScrollView} from "react-native-gesture-handler";
import {useUploadProfilePicture} from "@/hooks/useUploadProfilePicture";
import {ImagePickerAsset} from "expo-image-picker";
const MyGroup = () => { type MyGroupProps = {
onNewUserClick: boolean;
setOnNewUserClick: React.Dispatch<React.SetStateAction<boolean>>;
};
const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) => {
const [showAddUserDialog, setShowAddUserDialog] = useState(false); const [showAddUserDialog, setShowAddUserDialog] = useState(false);
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState< const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue string | PickerSingleValue
>(ProfileType.CHILD); >(ProfileType.CHILD);
@ -40,6 +46,8 @@ const MyGroup = () => {
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [newUserId, setNewUserId] = useState("")
const lNameRef = useRef<TextFieldRef>(null); const lNameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null); const emailRef = useRef<TextFieldRef>(null);
@ -50,6 +58,7 @@ const MyGroup = () => {
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser(); const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
const {data: familyMembers} = useGetFamilyMembers(true); const {data: familyMembers} = useGetFamilyMembers(true);
const {user} = useAuthContext(); const {user} = useAuthContext();
const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
const parents = const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? []; familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
@ -90,19 +99,26 @@ const MyGroup = () => {
console.log(res); console.log(res);
if (!isError) { if (!isError) {
setShowNewUserInfoDialog(false); setOnNewUserClick(false);
if (res?.data?.userId) { if (res?.data?.userId) {
if (profileImageAsset) {
await changeProfilePicture(profileImageAsset)
setShowQRCodeDialog(res.data.userId);
} else {
setTimeout(() => { setTimeout(() => {
setShowQRCodeDialog(res.data.userId); setShowQRCodeDialog(res.data.userId);
}, 500); }, 500);
} }
handleClearImage()
}
} }
}; };
useEffect(() => { useEffect(() => {
KeyboardManager.setEnableAutoToolbar(true); if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
},[]) }, []);
useEffect(() => { useEffect(() => {
setFirstName(""); setFirstName("");
@ -110,12 +126,11 @@ const MyGroup = () => {
setEmail(""); setEmail("");
}, []); }, []);
// @ts-ignore // @ts-ignore
return ( return (
<View style={{ flex: 1, minHeight: 500 }}> <View marginB-70>
<ScrollView>
<View> <View>
<ScrollView style={styles.card}>
{!parents.length && !children.length && !caregivers.length && ( {!parents.length && !children.length && !caregivers.length && (
<Text text70 marginV-10> <Text text70 marginV-10>
{isLoading ? "Loading...." : "No user devices added"} {isLoading ? "Loading...." : "No user devices added"}
@ -123,7 +138,7 @@ const MyGroup = () => {
)} )}
{(!!parents.length || !!children.length) && ( {(!!parents.length || !!children.length) && (
<> <View style={styles.card}>
<Text style={styles.subTit} marginB-10> <Text style={styles.subTit} marginB-10>
Family Family
</Text> </Text>
@ -144,7 +159,9 @@ const MyGroup = () => {
source={{uri: member.pfp || undefined}} source={{uri: member.pfp || undefined}}
/> />
) : ( ) : (
<View style={[styles.pfp, {backgroundColor: "#ea156d"}]} /> <View
style={[styles.pfp, {backgroundColor: "#ea156d"}]}
/>
)} )}
<View row marginL-10 centerV> <View row marginL-10 centerV>
<Text style={styles.name}> <Text style={styles.name}>
@ -155,9 +172,7 @@ const MyGroup = () => {
<View row centerV gap-10> <View row centerV gap-10>
<Text style={styles.userType}> <Text style={styles.userType}>
{member.userType === ProfileType.PARENT {member.userType === ProfileType.PARENT
? `Admin${ ? `Admin${member.uid === user?.uid ? " (You)" : ""}`
member.uid === user?.uid ? " (You)" : ""
}`
: "Child"} : "Child"}
</Text> </Text>
<UserMenu <UserMenu
@ -168,11 +183,11 @@ const MyGroup = () => {
</View> </View>
</Card> </Card>
))} ))}
</> </View>
)} )}
{!!caregivers.length && ( {!!caregivers.length && (
<> <View style={styles.card}>
<Text style={styles.subTit} marginB-10 marginT-15> <Text style={styles.subTit} marginB-10 marginT-15>
Caregivers Caregivers
</Text> </Text>
@ -209,7 +224,7 @@ const MyGroup = () => {
/> />
</Card> </Card>
))} ))}
</> </View>
)} )}
{!!familyDevices.length && ( {!!familyDevices.length && (
@ -250,19 +265,8 @@ const MyGroup = () => {
))} ))}
</> </>
)} )}
</ScrollView>
</View> </View>
</ScrollView>
<FloatingButton
fullWidth
hideBackgroundOverlay
visible
button={{
label: "+ Add a user device",
onPress: () => setShowNewUserInfoDialog(true),
style: styles.bottomButton,
}}
/>
<Dialog <Dialog
visible={showAddUserDialog} visible={showAddUserDialog}
@ -315,8 +319,8 @@ const MyGroup = () => {
<Dialog <Dialog
panDirection={PanningProvider.Directions.DOWN} panDirection={PanningProvider.Directions.DOWN}
visible={showNewUserInfoDialog} visible={onNewUserClick}
onDismiss={() => setShowNewUserInfoDialog(false)} onDismiss={() => setOnNewUserClick(false)}
> >
<PreviousNextView> <PreviousNextView>
<KeyboardAwareScrollView> <KeyboardAwareScrollView>
@ -327,7 +331,7 @@ const MyGroup = () => {
</Text> </Text>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
setShowNewUserInfoDialog(false); setOnNewUserClick(false);
}} }}
> >
<CircledXIcon/> <CircledXIcon/>
@ -336,6 +340,14 @@ const MyGroup = () => {
<View style={styles.divider} spread/> <View style={styles.divider} spread/>
<View row centerV gap-20 marginV-20> <View row centerV gap-20 marginV-20>
{pfpUri ? (
<Image
height={65.54}
width={65.54}
style={{borderRadius: 25}}
source={{uri: pfpUri}}
/>
) : (
<View <View
height={65.54} height={65.54}
width={65.54} width={65.54}
@ -346,11 +358,22 @@ const MyGroup = () => {
style={{borderRadius: 25}} style={{borderRadius: 25}}
center center
/> />
<TouchableOpacity onPress={() => {}}> )}
{pfpUri ? (
<TouchableOpacity onPress={handleClearImage}>
<Text color={Colors.red40} style={styles.jakarta13} marginL-15>
Clear user photo
</Text>
</TouchableOpacity>
) : (
<TouchableOpacity onPress={pickImage}>
<Text color="#50be0c" style={styles.jakarta13} marginL-15> <Text color="#50be0c" style={styles.jakarta13} marginL-15>
Upload User Profile Photo Upload User Profile Photo
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
)}
</View> </View>
<Text style={styles.jakarta12}>Member Status</Text> <Text style={styles.jakarta12}>Member Status</Text>
@ -498,7 +521,8 @@ const styles = StyleSheet.create({
}, },
bottomButton: { bottomButton: {
position: "absolute", position: "absolute",
bottom: 80, bottom: 50,
backgroundColor: "#e8156c",
width: "100%", width: "100%",
}, },
familyCard: { familyCard: {

View File

@ -3,6 +3,7 @@ import { StyleSheet, TouchableOpacity } from "react-native";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView } from "react-native-gesture-handler";
import * as ImagePicker from "expo-image-picker"; import * as ImagePicker from "expo-image-picker";
import { import {
Button,
Colors, Colors,
Image, Image,
Picker, Picker,
@ -18,6 +19,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData"; import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture"; import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
import { colorMap } from "@/constants/colorMap"; import { colorMap } from "@/constants/colorMap";
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
const MyProfile = () => { const MyProfile = () => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
@ -32,6 +34,15 @@ const MyProfile = () => {
string | ImagePicker.ImagePickerAsset | null string | ImagePicker.ImagePickerAsset | null
>(profileData?.pfp || null); >(profileData?.pfp || null);
const [showDeleteDialog, setShowDeleteDialog] = useState<boolean>(false);
const handleHideDeleteDialog = () => {
setShowDeleteDialog(false);
};
const handleShowDeleteDialog = () => {
setShowDeleteDialog(true);
};
const { mutateAsync: updateUserData } = useUpdateUserData(); const { mutateAsync: updateUserData } = useUpdateUserData();
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture(); const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
@ -48,13 +59,12 @@ const MyProfile = () => {
return; return;
} }
debouncedUserDataUpdate(); debouncedUserDataUpdate();
}, [timeZone, lastName, firstName, profileImage]); }, [timeZone, lastName, firstName]);
useEffect(() => { useEffect(() => {
if (profileData) { if (profileData) {
setFirstName(profileData.firstName || ""); setFirstName(profileData.firstName || "");
setLastName(profileData.lastName || ""); setLastName(profileData.lastName || "");
// setProfileImage(profileData.pfp || null);
setTimeZone( setTimeZone(
profileData.timeZone || Localization.getCalendars()[0].timeZone! profileData.timeZone || Localization.getCalendars()[0].timeZone!
); );
@ -78,7 +88,7 @@ const MyProfile = () => {
if (!result.canceled) { if (!result.canceled) {
setProfileImage(result.assets[0].uri); setProfileImage(result.assets[0].uri);
changeProfilePicture(result.assets[0]); await changeProfilePicture(result.assets[0]);
} }
}; };
@ -93,7 +103,7 @@ const MyProfile = () => {
: profileImage; : profileImage;
return ( return (
<ScrollView style={{ paddingBottom: 100, flex: 1 }}> <ScrollView style={{ paddingBottom: 20, flex: 1 }}>
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text> <Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15> <View row spread paddingH-15 centerV marginV-15>
@ -205,6 +215,22 @@ const MyProfile = () => {
</Picker> </Picker>
</View> </View>
</View> </View>
<Button
size="large"
backgroundColor="#FF5449"
label="Delete Profile"
style={{ marginTop: 10 }}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
onPress={handleShowDeleteDialog}
/>
<DeleteProfileDialogs
onFirstYes={() => {
setShowDeleteDialog(false);
}}
visible={showDeleteDialog}
onDismiss={handleHideDeleteDialog}
onConfirm={() => {console.log('delete account here')}}
/>
</ScrollView> </ScrollView>
); );
}; };

View File

@ -32,7 +32,10 @@ const UserMenu = ({
panDirection={PanningDirectionsEnum.DOWN} panDirection={PanningDirectionsEnum.DOWN}
> >
<Card padding-20 center> <Card padding-20 center>
<Text marginB-10>Scan this QR Code to Login:</Text> <Text center marginB-10 style={{fontSize: 16, maxWidth: "80%"}}>Ask your family to download the app
and then scan the
QR Code to join the family group:
</Text>
<QRCode value={userId} size={150}/> <QRCode value={userId} size={150}/>
<Button <Button
marginT-20 marginT-20

View File

@ -22,7 +22,7 @@ const AddChore = () => {
> >
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button
marginH-20 marginB-30
size={ButtonSize.large} size={ButtonSize.large}
style={styles.button} style={styles.button}
onPress={() => setIsVisible(!isVisible)} onPress={() => setIsVisible(!isVisible)}

View File

@ -1,12 +1,13 @@
import { import {
View, View,
Text, Text,
Checkbox,
TouchableOpacity, TouchableOpacity,
Dialog, Dialog,
Button, Button,
ButtonSize, ButtonSize,
Checkbox,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import React, { useState } from "react"; import React, { useState } from "react";
import { useToDosContext } from "@/contexts/ToDosContext"; import { useToDosContext } from "@/contexts/ToDosContext";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
@ -67,8 +68,7 @@ const ToDoItem = (props: {
marginV-10 marginV-10
style={{ style={{
borderRadius: 17, borderRadius: 17,
backgroundColor: props.item.done ? "#e0e0e0" : "white", backgroundColor: "white",
opacity: props.item.done ? 0.3 : 1,
}} }}
> >
{visible && ( {visible && (
@ -84,6 +84,7 @@ const ToDoItem = (props: {
style={{ style={{
textDecorationLine: props.item.done ? "line-through" : "none", textDecorationLine: props.item.done ? "line-through" : "none",
fontFamily: "Manrope_500Medium", fontFamily: "Manrope_500Medium",
color: props.item.done? "#a09f9f": "black",
fontSize: 15, fontSize: 15,
}} }}
onPress={() => { onPress={() => {
@ -96,6 +97,7 @@ const ToDoItem = (props: {
value={props.item.done} value={props.item.done}
containerStyle={[styles.checkbox, { borderRadius: 50 }]} containerStyle={[styles.checkbox, { borderRadius: 50 }]}
style={styles.checked} style={styles.checked}
size={26.64}
borderRadius={50} borderRadius={50}
color="#fd1575" color="#fd1575"
onValueChange={(value) => { onValueChange={(value) => {

View File

@ -42,6 +42,7 @@ const ToDosPage = () => {
message="Here are your To Do's" message="Here are your To Do's"
isWelcome={true} isWelcome={true}
link={profileData?.userType == ProfileType.PARENT && pageLink} link={profileData?.userType == ProfileType.PARENT && pageLink}
isToDos={true}
/> />
{profileData?.userType == ProfileType.CHILD && ( {profileData?.userType == ProfileType.CHILD && (
<View marginB-25> <View marginB-25>

View File

@ -3,6 +3,7 @@ import React from "react";
import { ImageBackground, StyleSheet } from "react-native"; import { ImageBackground, StyleSheet } from "react-native";
import FamilyChart from "./FamilyChart"; import FamilyChart from "./FamilyChart";
import { TouchableOpacity } from "react-native-ui-lib/src/incubator"; import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
import { Ionicons } from "@expo/vector-icons";
const FamilyChoresProgress = ({ const FamilyChoresProgress = ({
setPageIndex, setPageIndex,
@ -12,7 +13,20 @@ const FamilyChoresProgress = ({
return ( return (
<View marginT-20 marginH-5> <View marginT-20 marginH-5>
<TouchableOpacity onPress={() => setPageIndex(0)}> <TouchableOpacity onPress={() => setPageIndex(0)}>
<Text style={{ fontFamily: "Manrope_200", fontSize: 12 }}>Back to ToDos</Text> <View row marginT-4 marginB-10 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to To Do's
</Text>
</View>
</TouchableOpacity> </TouchableOpacity>
<View centerH> <View centerH>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}> <Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>

View File

@ -30,7 +30,20 @@ const UserChoresProgress = ({
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
> >
<TouchableOpacity onPress={() => setPageIndex(0)}> <TouchableOpacity onPress={() => setPageIndex(0)}>
<Text style={{ fontSize: 14 }}>Back to ToDos</Text> <View row marginT-4 marginB-10 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to To Do's
</Text>
</View>
</TouchableOpacity> </TouchableOpacity>
<View> <View>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}> <Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>

View File

@ -1,18 +1,37 @@
import { Image, Text, View } from "react-native-ui-lib"; import { Image, Text, View } from "react-native-ui-lib";
import React from "react"; import React, { useEffect, useState } from "react";
import { useAuthContext } from "@/contexts/AuthContext"; import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap"; import { colorMap } from "@/constants/colorMap";
import { useAtom } from "jotai";
import { isFamilyViewAtom } from "../pages/calendar/atoms";
import { useGetChildrenByParentId } from "@/hooks/firebase/useGetChildrenByParentId";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
import { child } from "@react-native-firebase/storage";
import CachedImage from 'expo-cached-image'
const HeaderTemplate = (props: { const HeaderTemplate = (props: {
message: string; message: string;
isWelcome: boolean; isWelcome: boolean;
children?: React.ReactNode; children?: React.ReactNode;
link?: React.ReactNode; link?: React.ReactNode;
isCalendar?: boolean;
isToDos?: boolean;
isBrainDump?: boolean;
isGroceries?: boolean;
}) => { }) => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
const headerHeight: number = 72; const { data: members } = useGetFamilyMembers();
const [children, setChildren] = useState<UserProfile[]>([]);
const [isFamilyView] = useAtom(isFamilyViewAtom);
const headerHeight: number =
(props.isCalendar && 65.54) ||
(props.isToDos && 84) ||
(props.isGroceries && 72.09) ||
65.54;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
pfp: { pfp: {
@ -26,14 +45,71 @@ const HeaderTemplate = (props: {
pfpTxt: { pfpTxt: {
fontFamily: "Manrope_500Medium", fontFamily: "Manrope_500Medium",
fontSize: 30, fontSize: 30,
color: 'white', color: "white",
},
childrenPfpArr: {
width: 65.54,
position: "absolute",
bottom: -12.44,
left: (children.length > 3 && -9) || 0,
height: 27.32,
},
childrenPfp: {
aspectRatio: 1,
width: 27.32,
backgroundColor: "#fd1575",
borderRadius: 50,
position: "absolute",
borderWidth: 2,
borderColor: "#f2f2f2",
},
bottomMarg: {
marginBottom: isFamilyView ? 30 : 15,
}, },
}); });
useEffect(() => {
if (members) {
const childrenMembers = members.filter(
(member) => member.userType === ProfileType.CHILD
);
setChildren(childrenMembers);
}
}, []);
return ( return (
<View row centerV marginV-15> <View row centerV marginV-15 style={styles.bottomMarg}>
{profileData?.pfp ? ( {profileData?.pfp ? (
<Image source={{ uri: profileData.pfp }} style={styles.pfp} /> <View>
<CachedImage source={{ uri: profileData.pfp, }} style={styles.pfp} cacheKey={profileData.pfp}/>
{isFamilyView && props.isCalendar && (
<View style={styles.childrenPfpArr} row>
{children?.slice(0, 3).map((child, index) => {
return child.pfp ? (
<Image
source={{ uri: child.pfp }}
style={[styles.childrenPfp, { left: index * 19 }]}
/>
) : (
<View
style={[styles.childrenPfp, { left: index * 19 }]}
center
>
<Text style={{ color: "white" }}>
{child?.firstName?.at(0)}
{child?.firstName?.at(1)}
</Text>
</View>
);
})}
{children?.length > 3 && (
<View style={[styles.childrenPfp, { left: 3 * 19 }]} center>
<Text style={{ color: "white", fontFamily: "Manrope_600SemiBold", fontSize: 12 }}>+{children.length - 3}</Text>
</View>
)}
</View>
)}
</View>
) : ( ) : (
<View style={styles.pfp} center> <View style={styles.pfp} center>
<Text style={styles.pfpTxt}> <Text style={styles.pfpTxt}>

View File

@ -6,7 +6,7 @@ import { View } from "react-native-ui-lib";
const RemoveAssigneeBtn = () => { const RemoveAssigneeBtn = () => {
return ( return (
<View style={styles.removeBtn} center> <View style={styles.removeBtn} center>
<CloseXIcon /> <CloseXIcon width={9} height={9} strokeWidth={2} />
</View> </View>
); );
}; };

View File

@ -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>
} }
@ -59,7 +60,7 @@ async function registerForPushNotificationsAsync() {
} }
if (finalStatus !== 'granted') { if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!'); // alert('Failed to get push token for push notification!');
return; return;
} }
@ -76,11 +77,11 @@ async function registerForPushNotificationsAsync() {
console.log('Push Token:', token); console.log('Push Token:', token);
return token; return token;
} catch (error) { } catch (error) {
alert(`Error getting push token: ${error}`); // alert(`Error getting push token: ${error}`);
throw error; throw error;
} }
} else { } else {
alert('Must use a physical device for push notifications'); // alert('Must use a physical device for push notifications');
} }
} }
@ -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>
); );

View File

@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
} }
}); });
exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => { exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
console.log('Running token refresh job...'); console.log('Running token refresh job...');
const profilesSnapshot = await db.collection('Profiles').get(); const profilesSnapshot = await db.collection('Profiles').get();
@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleAccounts) { if (profileData.googleAccounts) {
try { try {
for (const googleEmail of Object.keys(profileData?.googleAccounts)) { for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail]; const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) { if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken); const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken}; const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
return null; return null;
}); });
// Function to refresh Google token async function refreshGoogleToken(refreshToken) {
async function refreshGoogleToken(token) { try {
// Assuming you use OAuth2 token refresh flow
const response = await axios.post('https://oauth2.googleapis.com/token', { const response = await axios.post('https://oauth2.googleapis.com/token', {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously refresh_token: refreshToken,
client_id: 'YOUR_GOOGLE_CLIENT_ID', client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
}); });
return response.data.access_token; // Return new access token return response.data.access_token; // Return the new access token
} catch (error) {
console.error("Error refreshing Google token:", error);
throw error;
}
} }
async function refreshMicrosoftToken(token) { async function refreshMicrosoftToken(refreshToken) {
try {
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously refresh_token: refreshToken,
client_id: 'YOUR_MICROSOFT_CLIENT_ID', client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET', scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
}); });
return response.data.access_token; // Return new access token return response.data.access_token; // Return the new access token
} catch (error) {
console.error("Error refreshing Microsoft token:", error);
throw error;
}
} }
async function getPushTokensForEvent() { async function getPushTokensForEvent() {

View File

@ -5,7 +5,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import * as ImagePicker from "expo-image-picker"; import * as ImagePicker from "expo-image-picker";
import {Platform} from "react-native"; import {Platform} from "react-native";
export const useChangeProfilePicture = () => { export const useChangeProfilePicture = (customUserId?: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const {user, refreshProfileData} = useAuthContext(); const {user, refreshProfileData} = useAuthContext();
@ -38,20 +38,24 @@ export const useChangeProfilePicture = () => {
const downloadURL = await reference.getDownloadURL(); const downloadURL = await reference.getDownloadURL();
console.log("Download URL:", downloadURL); console.log("Download URL:", downloadURL);
if(!customUserId) {
await firestore() await firestore()
.collection("Profiles") .collection("Profiles")
.doc(user?.uid) .doc(user?.uid)
.update({pfp: downloadURL}); .update({pfp: downloadURL});
}
} catch (e) { } catch (e) {
console.error("Error uploading profile picture:", e.message); console.error("Error uploading profile picture:", e);
throw e; throw e;
} }
}, },
onSuccess: () => { onSuccess: () => {
// Invalidate queries to refresh profile data // Invalidate queries to refresh profile data
if (!customUserId) {
queryClient.invalidateQueries("Profiles"); queryClient.invalidateQueries("Profiles");
refreshProfileData(); refreshProfileData();
}
}, },
}); });
}; };

View File

@ -0,0 +1,39 @@
import {useMutation} from "react-query";
import {UserProfile} from "@firebase/auth";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
export const useClearTokens = () => {
const {profileData} = useAuthContext();
const {mutateAsync: updateUserData} = useUpdateUserData();
return useMutation({
mutationKey: ["clearTokens"],
mutationFn: async ({provider, email}: {
provider: "google" | "outlook" | "apple",
email: string
}) => {
const newUserData: Partial<UserProfile> = {};
if (provider === "google") {
let googleAccounts = profileData?.googleAccounts;
if (googleAccounts) {
googleAccounts[email] = null;
newUserData.googleAccounts = googleAccounts;
}
} else if (provider === "outlook") {
let microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) {
microsoftAccounts[email] = null;
newUserData.microsoftAccounts = microsoftAccounts;
}
} else if (provider === "apple") {
let appleAccounts = profileData?.appleAccounts;
if (appleAccounts) {
appleAccounts[email] = null;
newUserData.appleAccounts = appleAccounts;
}
}
await updateUserData({newUserData});
},
})
}

View File

@ -52,27 +52,34 @@ export const useCreateEventsFromProvider = () => {
mutationKey: ["createEventsFromProvider"], mutationKey: ["createEventsFromProvider"],
mutationFn: async (eventDataArray: Partial<EventData>[]) => { mutationFn: async (eventDataArray: Partial<EventData>[]) => {
try { try {
for (const eventData of eventDataArray) { // Create an array of promises for each event's Firestore read/write operation
const promises = eventDataArray.map(async (eventData) => {
console.log("Processing EventData: ", eventData); console.log("Processing EventData: ", eventData);
// Check if the event already exists
const snapshot = await firestore() const snapshot = await firestore()
.collection("Events") .collection("Events")
.where("id", "==", eventData.id) .where("id", "==", eventData.id)
.get(); .get();
if (snapshot.empty) { if (snapshot.empty) {
await firestore() // Event doesn't exist, so add it
return firestore()
.collection("Events") .collection("Events")
.add({ ...eventData, creatorId: currentUser?.uid }); .add({ ...eventData, creatorId: currentUser?.uid });
} else { } else {
console.log("Event already exists, updating..."); // Event exists, update it
const docId = snapshot.docs[0].id; const docId = snapshot.docs[0].id;
await firestore() return firestore()
.collection("Events") .collection("Events")
.doc(docId) .doc(docId)
.set({ ...eventData, creatorId: currentUser?.uid }, { merge: true }); .set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
} }
} });
// Execute all promises in parallel
await Promise.all(promises);
} catch (e) { } catch (e) {
console.error("Error creating/updating events: ", e); console.error("Error creating/updating events: ", e);
} }

View File

@ -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
}> }>

View File

@ -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
View File

@ -0,0 +1,288 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useEffect} from "react";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
import * as WebBrowser from "expo-web-browser";
import * as Google from "expo-auth-session/providers/google";
import * as AuthSession from "expo-auth-session";
import * as AppleAuthentication from "expo-apple-authentication";
const googleConfig = {
androidClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: [
"email",
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
extraParams: {
access_type: "offline",
},
};
const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite",
"User.Read",
],
authorizationEndpoint:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
};
export const useCalSync = () => {
const {profileData} = useAuthContext();
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
useFetchAndSaveGoogleEvents();
const {
mutateAsync: fetchAndSaveOutlookEvents,
isLoading: isSyncingOutlook,
} = useFetchAndSaveOutlookEvents();
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
useEffect(() => {
signInWithGoogle();
}, [response]);
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
const {accessToken, refreshToken} = response?.authentication!;
const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: {Authorization: `Bearer ${accessToken}`},
}
);
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
let googleAccounts = profileData?.googleAccounts || {};
const updatedGoogleAccounts = {
...googleAccounts,
[googleMail]: {accessToken, refreshToken},
};
await updateUserData({
newUserData: {googleAccounts: updatedGoogleAccounts},
});
await fetchAndSaveGoogleEvents({
token: accessToken,
email: googleMail,
});
}
} catch (error) {
console.error("Error during Google sign-in:", error);
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Token exchange failed:", errorText);
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.access_token) {
console.log("Access token received, fetching user info...");
// Fetch user info from Microsoft Graph API to get the email
const userInfoResponse = await fetch(
"https://graph.microsoft.com/v1.0/me",
{
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
}
);
const userInfo = await userInfoResponse.json();
console.log("User info received:", userInfo);
if (userInfo.error) {
console.error("Error fetching user info:", userInfo.error);
} else {
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
let microsoftAccounts = profileData?.microsoftAccounts;
const updatedMicrosoftAccounts = microsoftAccounts
? {...microsoftAccounts, [outlookMail]: tokenData.access_token}
: {[outlookMail]: tokenData.access_token};
await updateUserData({
newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
});
await fetchAndSaveOutlookEvents(
tokenData.access_token,
outlookMail
);
console.log("User data updated successfully.");
}
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const handleAppleSignIn = async () => {
try {
console.log("Starting Apple Sign-in...");
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
console.log("Apple sign-in result:", credential);
alert(JSON.stringify(credential))
const appleToken = credential.identityToken;
const appleMail = credential.email!;
if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed...");
let appleAcounts = profileData?.appleAccounts;
const updatedAppleAccounts = appleAcounts
? {...appleAcounts, [appleMail]: appleToken}
: {[appleMail]: appleToken};
await updateUserData({
newUserData: {appleAccounts: updatedAppleAccounts},
});
console.log("User data updated with Apple ID token.");
await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
} else {
console.warn(
"Apple authentication was not successful or email was hidden."
);
}
} catch (error) {
console.error("Error during Apple Sign-in:", error);
}
};
let isConnectedToGoogle = false;
if (profileData?.googleAccounts) {
Object.values(profileData?.googleAccounts).forEach((item) => {
if (item !== null) {
isConnectedToGoogle = true;
return;
}
});
}
let isConnectedToMicrosoft = false;
const microsoftAccounts = profileData?.microsoftAccounts;
if (microsoftAccounts) {
Object.values(profileData?.microsoftAccounts).forEach((item) => {
if (item !== null) {
isConnectedToMicrosoft = true;
return;
}
});
}
let isConnectedToApple = false;
if (profileData?.appleAccounts) {
Object.values(profileData?.appleAccounts).forEach((item) => {
if (item !== null) {
isConnectedToApple = true;
return;
}
});
}
return {
handleAppleSignIn,
handleMicrosoftSignIn,
handleGoogleSignIn: signInWithGoogle,
handleStartGoogleSignIn: promptAsync,
fetchAndSaveOutlookEvents,
fetchAndSaveAppleEvents,
fetchAndSaveGoogleEvents,
isConnectedToApple,
isConnectedToMicrosoft,
isConnectedToGoogle,
isSyncingOutlook,
isSyncingGoogle,
isSyncingApple
}
}

View File

@ -2,11 +2,13 @@ import {useMutation, useQueryClient} from "react-query";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils"; import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {useAuthContext} from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent"; import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
export const useFetchAndSaveGoogleEvents = () => { export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {profileData} = useAuthContext(); const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
const {mutateAsync: clearToken} = useClearTokens();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"], mutationKey: ["fetchAndSaveGoogleEvents"],
@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
timeMax.toISOString().slice(0, -5) + "Z" timeMax.toISOString().slice(0, -5) + "Z"
); );
if(!response.success) {
await clearToken({email: email!, provider: "google"})
return
}
console.log("Google Calendar events fetched:", response); console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => { const items = response?.googleEvents?.map((item) => {
if (item.allDay) { if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0)); item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
item.endDate = item.startDate; item.endDate = item.startDate;

View File

@ -0,0 +1,62 @@
import {useState} from "react";
import * as ImagePicker from "expo-image-picker";
import {useChangeProfilePicture} from "@/hooks/firebase/useChangeProfilePicture";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
export const useUploadProfilePicture = (customUserId?: string, existingPfp?: string) => {
const [profileImage, setProfileImage] = useState<
string | ImagePicker.ImagePickerAsset | null
>(existingPfp || null);
const [profileImageAsset, setProfileImageAsset] = useState<
ImagePicker.ImagePickerAsset | null
>(null);
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: changeProfilePicture} = useChangeProfilePicture(customUserId);
const pickImage = async () => {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permissionResult.granted) {
alert("Permission to access camera roll is required!");
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.canceled) {
setProfileImage(result.assets[0].uri);
setProfileImageAsset(result.assets[0]);
if (!customUserId) {
await changeProfilePicture(result.assets[0]);
}
}
};
const handleClearImage = async () => {
if (!customUserId) {
await updateUserData({newUserData: {pfp: null}});
}
setProfileImage(null);
};
const pfpUri =
profileImage && typeof profileImage === "object" && "uri" in profileImage
? profileImage.uri
: profileImage;
return {
pfpUri,
profileImage,
profileImageAsset,
handleClearImage,
pickImage,
changeProfilePicture
}
}

View File

@ -450,7 +450,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "Cally"; PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -484,7 +484,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "Cally"; PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

View File

@ -50,6 +50,7 @@
"expo-auth-session": "^5.5.2", "expo-auth-session": "^5.5.2",
"expo-barcode-scanner": "~13.0.1", "expo-barcode-scanner": "~13.0.1",
"expo-build-properties": "~0.12.4", "expo-build-properties": "~0.12.4",
"expo-cached-image": "^51.0.19",
"expo-calendar": "~13.0.5", "expo-calendar": "~13.0.5",
"expo-camera": "~15.0.16", "expo-camera": "~15.0.16",
"expo-constants": "~16.0.2", "expo-constants": "~16.0.2",

258
yarn.lock
View File

@ -960,6 +960,89 @@
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
ws "^8.12.1" ws "^8.12.1"
"@expo/cli@0.18.30":
version "0.18.30"
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.18.30.tgz#0cb4829aa11e98ae350a5c15958b9816e9a1d2f0"
integrity sha512-V90TUJh9Ly8stYo8nwqIqNWCsYjE28GlVFWEhAFCUOp99foiQr8HSTpiiX5GIrprcPoWmlGoY+J5fQA29R4lFg==
dependencies:
"@babel/runtime" "^7.20.0"
"@expo/code-signing-certificates" "0.0.5"
"@expo/config" "~9.0.0-beta.0"
"@expo/config-plugins" "~8.0.8"
"@expo/devcert" "^1.0.0"
"@expo/env" "~0.3.0"
"@expo/image-utils" "^0.5.0"
"@expo/json-file" "^8.3.0"
"@expo/metro-config" "0.18.11"
"@expo/osascript" "^2.0.31"
"@expo/package-manager" "^1.5.0"
"@expo/plist" "^0.1.0"
"@expo/prebuild-config" "7.0.9"
"@expo/rudder-sdk-node" "1.1.1"
"@expo/spawn-async" "^1.7.2"
"@expo/xcpretty" "^4.3.0"
"@react-native/dev-middleware" "0.74.85"
"@urql/core" "2.3.6"
"@urql/exchange-retry" "0.3.0"
accepts "^1.3.8"
arg "5.0.2"
better-opn "~3.0.2"
bplist-creator "0.0.7"
bplist-parser "^0.3.1"
cacache "^18.0.2"
chalk "^4.0.0"
ci-info "^3.3.0"
connect "^3.7.0"
debug "^4.3.4"
env-editor "^0.4.1"
fast-glob "^3.3.2"
find-yarn-workspace-root "~2.0.0"
form-data "^3.0.1"
freeport-async "2.0.0"
fs-extra "~8.1.0"
getenv "^1.0.0"
glob "^7.1.7"
graphql "15.8.0"
graphql-tag "^2.10.1"
https-proxy-agent "^5.0.1"
internal-ip "4.3.0"
is-docker "^2.0.0"
is-wsl "^2.1.1"
js-yaml "^3.13.1"
json-schema-deref-sync "^0.13.0"
lodash.debounce "^4.0.8"
md5hex "^1.0.0"
minimatch "^3.0.4"
node-fetch "^2.6.7"
node-forge "^1.3.1"
npm-package-arg "^7.0.0"
open "^8.3.0"
ora "3.4.0"
picomatch "^3.0.1"
pretty-bytes "5.6.0"
progress "2.0.3"
prompts "^2.3.2"
qrcode-terminal "0.11.0"
require-from-string "^2.0.2"
requireg "^0.2.2"
resolve "^1.22.2"
resolve-from "^5.0.0"
resolve.exports "^2.0.2"
semver "^7.6.0"
send "^0.18.0"
slugify "^1.3.4"
source-map-support "~0.5.21"
stacktrace-parser "^0.1.10"
structured-headers "^0.4.1"
tar "^6.0.5"
temp-dir "^2.0.0"
tempy "^0.7.1"
terminal-link "^2.1.1"
text-table "^0.2.0"
url-join "4.0.0"
wrap-ansi "^7.0.0"
ws "^8.12.1"
"@expo/code-signing-certificates@0.0.5": "@expo/code-signing-certificates@0.0.5":
version "0.0.5" version "0.0.5"
resolved "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz" resolved "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz"
@ -968,6 +1051,48 @@
node-forge "^1.2.1" node-forge "^1.2.1"
nullthrows "^1.1.1" nullthrows "^1.1.1"
"@expo/config-plugins@8.0.10":
version "8.0.10"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-8.0.10.tgz#5cda076f38bc04675cb42d8acdd23d6e460a62de"
integrity sha512-KG1fnSKRmsudPU9BWkl59PyE0byrE2HTnqbOrgwr2FAhqh7tfr9nRs6A9oLS/ntpGzmFxccTEcsV0L4apsuxxg==
dependencies:
"@expo/config-types" "^51.0.3"
"@expo/json-file" "~8.3.0"
"@expo/plist" "^0.1.0"
"@expo/sdk-runtime-versions" "^1.0.0"
chalk "^4.1.2"
debug "^4.3.1"
find-up "~5.0.0"
getenv "^1.0.0"
glob "7.1.6"
resolve-from "^5.0.0"
semver "^7.5.4"
slash "^3.0.0"
slugify "^1.6.6"
xcode "^3.0.1"
xml2js "0.6.0"
"@expo/config-plugins@8.0.9", "@expo/config-plugins@~8.0.0-beta.0", "@expo/config-plugins@~8.0.8":
version "8.0.9"
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.9.tgz"
integrity sha512-dNCG45C7BbDPV9MdWvCbsFtJtVn4w/TJbb5b7Yr6FA8HYIlaaVM0wqUMzTPmGj54iYXw8X/Vge8uCPxg7RWgeA==
dependencies:
"@expo/config-types" "^51.0.0-unreleased"
"@expo/json-file" "~8.3.0"
"@expo/plist" "^0.1.0"
"@expo/sdk-runtime-versions" "^1.0.0"
chalk "^4.1.2"
debug "^4.3.1"
find-up "~5.0.0"
getenv "^1.0.0"
glob "7.1.6"
resolve-from "^5.0.0"
semver "^7.5.4"
slash "^3.0.0"
slugify "^1.6.6"
xcode "^3.0.1"
xml2js "0.6.0"
"@expo/config-plugins@~5.0.3": "@expo/config-plugins@~5.0.3":
version "5.0.4" version "5.0.4"
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-5.0.4.tgz" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-5.0.4.tgz"
@ -1020,6 +1145,45 @@
resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.2.tgz" resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.2.tgz"
integrity sha512-IglkIoiDwJMY01lYkF/ZSBoe/5cR+O3+Gx6fpLFjLfgZGBTdyPkKa1g8NWoWQCk+D3cKL2MDbszT2DyRRB0YqQ== integrity sha512-IglkIoiDwJMY01lYkF/ZSBoe/5cR+O3+Gx6fpLFjLfgZGBTdyPkKa1g8NWoWQCk+D3cKL2MDbszT2DyRRB0YqQ==
"@expo/config-types@^51.0.3":
version "51.0.3"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450"
integrity sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA==
"@expo/config@9.0.3", "@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0":
version "9.0.3"
resolved "https://registry.npmjs.org/@expo/config/-/config-9.0.3.tgz"
integrity sha512-eOTNM8eOC8gZNHgenySRlc/lwmYY1NOgvjwA8LHuvPT7/eUwD93zrxu3lPD1Cc/P6C/2BcVdfH4hf0tLmDxnsg==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "~8.0.8"
"@expo/config-types" "^51.0.0-unreleased"
"@expo/json-file" "^8.3.0"
getenv "^1.0.0"
glob "7.1.6"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
semver "^7.6.0"
slugify "^1.3.4"
sucrase "3.34.0"
"@expo/config@9.0.4":
version "9.0.4"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.4.tgz#52f0a94edd0e2c36dfb5e284cc1a6d99d9d2af97"
integrity sha512-g5ns5u1JSKudHYhjo1zaSfkJ/iZIcWmUmIQptMJZ6ag1C0ShL2sj8qdfU8MmAMuKLOgcIfSaiWlQnm4X3VJVkg==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "~8.0.8"
"@expo/config-types" "^51.0.3"
"@expo/json-file" "^8.3.0"
getenv "^1.0.0"
glob "7.1.6"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
semver "^7.6.0"
slugify "^1.3.4"
sucrase "3.34.0"
"@expo/config@~7.0.2": "@expo/config@~7.0.2":
version "7.0.3" version "7.0.3"
resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz" resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz"
@ -1285,6 +1449,23 @@
semver "^7.6.0" semver "^7.6.0"
xml2js "0.6.0" xml2js "0.6.0"
"@expo/prebuild-config@7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-7.0.9.tgz#7abd489e18ed6514a0c9cd214eb34c0d5efda799"
integrity sha512-9i6Cg7jInpnGEHN0jxnW0P+0BexnePiBzmbUvzSbRXpdXihYUX2AKMu73jgzxn5P1hXOSkzNS7umaY+BZ+aBag==
dependencies:
"@expo/config" "~9.0.0-beta.0"
"@expo/config-plugins" "~8.0.8"
"@expo/config-types" "^51.0.3"
"@expo/image-utils" "^0.5.0"
"@expo/json-file" "^8.3.0"
"@react-native/normalize-colors" "0.74.85"
debug "^4.3.1"
fs-extra "^9.0.0"
resolve-from "^5.0.0"
semver "^7.6.0"
xml2js "0.6.0"
"@expo/rudder-sdk-node@1.1.1": "@expo/rudder-sdk-node@1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz" resolved "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz"
@ -3592,6 +3773,19 @@ babel-plugin-polyfill-regenerator@^0.6.1:
dependencies: dependencies:
"@babel/helper-define-polyfill-provider" "^0.6.2" "@babel/helper-define-polyfill-provider" "^0.6.2"
babel-plugin-react-compiler@0.0.0-experimental-592953e-20240517:
version "0.0.0-experimental-592953e-20240517"
resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-592953e-20240517.tgz#e800fa1550d03573cd5637218dc711f12f642249"
integrity sha512-OjG1SVaeQZaJrqkMFJatg8W/MTow8Ak5rx2SI0ETQBO1XvOk/XZGMbltNCPdFJLKghBYoBjC+Y3Ap/Xr7B01mA==
dependencies:
"@babel/generator" "7.2.0"
"@babel/types" "^7.19.0"
chalk "4"
invariant "^2.2.4"
pretty-format "^24"
zod "^3.22.4"
zod-validation-error "^2.1.0"
babel-plugin-react-compiler@^0.0.0-experimental-592953e-20240517: babel-plugin-react-compiler@^0.0.0-experimental-592953e-20240517:
version "0.0.0-experimental-7d62301-20240821" version "0.0.0-experimental-7d62301-20240821"
resolved "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-7d62301-20240821.tgz" resolved "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-7d62301-20240821.tgz"
@ -3659,6 +3853,22 @@ babel-preset-expo@~11.0.14:
babel-plugin-react-native-web "~0.19.10" babel-plugin-react-native-web "~0.19.10"
react-refresh "^0.14.2" react-refresh "^0.14.2"
babel-preset-expo@~11.0.15:
version "11.0.15"
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-11.0.15.tgz#f29b1ac1f59f8739f63c80515906186586c24d3c"
integrity sha512-rgiMTYwqIPULaO7iZdqyL7aAff9QLOX6OWUtLZBlOrOTreGY1yHah/5+l8MvI6NVc/8Zj5LY4Y5uMSnJIuzTLw==
dependencies:
"@babel/plugin-proposal-decorators" "^7.12.9"
"@babel/plugin-transform-export-namespace-from" "^7.22.11"
"@babel/plugin-transform-object-rest-spread" "^7.12.13"
"@babel/plugin-transform-parameters" "^7.22.15"
"@babel/preset-react" "^7.22.15"
"@babel/preset-typescript" "^7.23.0"
"@react-native/babel-preset" "0.74.87"
babel-plugin-react-compiler "0.0.0-experimental-592953e-20240517"
babel-plugin-react-native-web "~0.19.10"
react-refresh "^0.14.2"
babel-preset-jest@^29.6.3: babel-preset-jest@^29.6.3:
version "29.6.3" version "29.6.3"
resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz"
@ -5115,6 +5325,13 @@ expo-build-properties@~0.12.4:
ajv "^8.11.0" ajv "^8.11.0"
semver "^7.6.0" semver "^7.6.0"
expo-cached-image@^51.0.19:
version "51.0.19"
resolved "https://registry.yarnpkg.com/expo-cached-image/-/expo-cached-image-51.0.19.tgz#27447d761a4b7414a2e5fee2e25c9436dd6f073e"
integrity sha512-HcIKolCKyrYcfimWp64S25Tv8YneUsKV47yJ93L4l4NVA7GJulqSS/fr2jf6B3mzw5rZNDU+eDAf1nzcxavfkg==
dependencies:
expo "51"
expo-calendar@~13.0.5: expo-calendar@~13.0.5:
version "13.0.5" version "13.0.5"
resolved "https://registry.npmjs.org/expo-calendar/-/expo-calendar-13.0.5.tgz" resolved "https://registry.npmjs.org/expo-calendar/-/expo-calendar-13.0.5.tgz"
@ -5259,6 +5476,19 @@ expo-modules-autolinking@1.11.2:
require-from-string "^2.0.2" require-from-string "^2.0.2"
resolve-from "^5.0.0" resolve-from "^5.0.0"
expo-modules-autolinking@1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.11.3.tgz#bc64d278c04015014bb5802e3cfcd942d7c07168"
integrity sha512-oYh8EZEvYF5TYppxEKUTTJmbr8j7eRRnrIxzZtMvxLTXoujThVPMFS/cbnSnf2bFm1lq50TdDNABhmEi7z0ngQ==
dependencies:
chalk "^4.1.0"
commander "^7.2.0"
fast-glob "^3.2.5"
find-up "^5.0.0"
fs-extra "^9.1.0"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
expo-modules-core@1.12.24: expo-modules-core@1.12.24:
version "1.12.24" version "1.12.24"
resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz" resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz"
@ -5266,6 +5496,13 @@ expo-modules-core@1.12.24:
dependencies: dependencies:
invariant "^2.2.4" invariant "^2.2.4"
expo-modules-core@1.12.26:
version "1.12.26"
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.26.tgz#86c4087dc6246abfc4d7f5e61097dc8cc4b22262"
integrity sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA==
dependencies:
invariant "^2.2.4"
expo-notifications@~0.28.18: expo-notifications@~0.28.18:
version "0.28.18" version "0.28.18"
resolved "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.18.tgz" resolved "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.18.tgz"
@ -5359,6 +5596,27 @@ expo-web-browser@~13.0.0, expo-web-browser@~13.0.3:
resolved "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-13.0.3.tgz" resolved "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-13.0.3.tgz"
integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ== integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ==
expo@51:
version "51.0.38"
resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.38.tgz#e4127b230454a34a507cfb9f1a2e4b3855cb0579"
integrity sha512-/B9npFkOPmv6WMIhdjQXEY0Z9k/67UZIVkodW8JxGIXwKUZAGHL+z1R5hTtWimpIrvVhyHUFU3f8uhfEKYhHNQ==
dependencies:
"@babel/runtime" "^7.20.0"
"@expo/cli" "0.18.30"
"@expo/config" "9.0.4"
"@expo/config-plugins" "8.0.10"
"@expo/metro-config" "0.18.11"
"@expo/vector-icons" "^14.0.3"
babel-preset-expo "~11.0.15"
expo-asset "~10.0.10"
expo-file-system "~17.0.1"
expo-font "~12.0.10"
expo-keep-awake "~13.0.2"
expo-modules-autolinking "1.11.3"
expo-modules-core "1.12.26"
fbemitter "^3.0.0"
whatwg-url-without-unicode "8.0.0-3"
expo@~51.0.24: expo@~51.0.24:
version "51.0.34" version "51.0.34"
resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz" resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz"