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 { useAtom, useSetAtom } from "jotai";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
@ -32,6 +33,7 @@ import {
export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex);
@ -79,6 +81,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavCalendarIcon />}
/>
@ -91,6 +94,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavGroceryIcon />}
/>
@ -118,6 +122,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavToDosIcon />}
/>
@ -130,6 +135,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavBrainDumpIcon />}
/>
@ -142,6 +148,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
label={"Manage Settings"}
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 {Button, Image, Text, View} from "react-native-ui-lib";
import React from "react";
import {useRouter} from "expo-router";
export default function Screen() {
const router = useRouter()
return (
<SafeAreaView>
<Entry/>
<SafeAreaView style={{flex: 1}}>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
<View>
<Image source={require("../../assets/images/splash.png")}/>
</View>
<View center gap-13>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
Welcome to Cally
</Text>
<Text center color={"#919191"} style={{fontSize: 20, maxWidth: 250}}>
Lightening Mental Loads,
One Family at a Time
</Text>
</View>
<View flexG/>
<View width={"100%"}>
<Button
label="Continue"
marginT-50
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => router.push("/(unauth)/get_started")}
style={{height: 50}}
backgroundColor="#fd1775"
/>
</View>
</View>
</SafeAreaView>
)
}

View File

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

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

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

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

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

View File

@ -5,13 +5,14 @@ const CloseXIcon: React.FC<SvgProps> = (props) => (
width={15}
height={15}
fill="none"
viewBox="0 0 15 15"
{...props}
>
<Path
stroke="#AAA"
strokeLinecap="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"
/>
</Svg>

View File

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

View File

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

View File

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

View File

@ -1,21 +1,12 @@
import {
View,
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 {Button, Dialog, TextField, TextFieldRef, TouchableOpacity, View,} from "react-native-ui-lib";
import React, {useEffect, useRef, useState} from "react";
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 {useBrainDumpContext} from "@/contexts/DumpContext";
import KeyboardManager from "react-native-keyboard-manager";
interface IAddBrainDumpProps {
isVisible: boolean;
setIsVisible: (value: boolean) => void;
@ -42,14 +33,16 @@ const AddBrainDump = ({
}, [addBrainDumpProps.isVisible]);
useEffect(() => {
if (addBrainDumpProps.isVisible) {
setTimeout(() => {
titleRef?.current?.focus()
}, 500)
}, []);
}
}, [addBrainDumpProps.isVisible]);
useEffect(() => {
KeyboardManager.setEnableAutoToolbar(false);
},[])
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []);
return (
<Dialog
@ -78,7 +71,15 @@ const AddBrainDump = ({
label="Save"
style={styles.topBtn}
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);
}}
/>
@ -144,7 +145,7 @@ const styles = StyleSheet.create({
description: {
fontFamily: "Manrope_400Regular",
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 {
Button,
Dialog,
View,
Text,
TextField,
TouchableOpacity,
TouchableOpacity, TextFieldRef,
} from "react-native-ui-lib";
import { Dimensions, StyleSheet } from "react-native";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
@ -30,6 +30,7 @@ const MoveBrainDump = (props: {
props.item.description
);
const [modalVisible, setModalVisible] = useState<boolean>(false);
const descriptionRef = useRef<TextFieldRef>(null)
const { width } = Dimensions.get("screen");
@ -37,6 +38,14 @@ const MoveBrainDump = (props: {
updateBrainDumpItem(props.item.id, { description: description });
}, [description]);
useEffect(() => {
if (props.isVisible) {
setTimeout(() => {
descriptionRef?.current?.focus()
}, 500)
}
}, [props.isVisible]);
const showConfirmationDialog = () => {
setModalVisible(true);
};
@ -112,7 +121,6 @@ const MoveBrainDump = (props: {
<TextField
textAlignVertical="top"
multiline
autoFocus
fieldStyle={{
width: "94%",
}}
@ -123,6 +131,7 @@ const MoveBrainDump = (props: {
onChangeText={(value) => {
setDescription(value);
}}
ref={descriptionRef}
returnKeyType="default"
/>
</View>

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import {
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import { isWithinInterval, subDays, addDays, compareAsc } from "date-fns";
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
interface EventCalendarProps {
calendarHeight: number;
@ -37,21 +37,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const [isRendering, setIsRendering] = useState(true);
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
const todaysDate = new Date();
useEffect(() => {
if (events && mode) {
setIsRendering(true);
const timeout = setTimeout(() => {
setIsRendering(false);
}, 10);
return () => clearTimeout(timeout);
}
}, [events, mode]);
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week") {
@ -95,6 +84,8 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
[profileData]
);
console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek})
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
@ -154,13 +145,12 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
overlapCount: 0
});
// Sort events for this dateKey from oldest to newest by event.start
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
return acc;
}, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now(); // End timer
const endTime = Date.now();
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
return {enrichedEvents, filteredEvents};
@ -218,7 +208,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading || isRendering) {
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff"/>
@ -235,7 +225,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
mode={mode}
enableEnrichedEvents={true}
sortedMonthView
enrichedEventsByDate={enrichedEvents}
// enrichedEventsByDate={enrichedEvents}
events={filteredEvents}
// eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent}

View File

@ -103,6 +103,7 @@ export const ManuallyAddEventModal = () => {
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
const {data: members} = useGetFamilyMembers(true);
const titleRef = useRef<TextFieldRef>(null)
const isLoading = isDeleting || isAdding
@ -135,6 +136,14 @@ export const ManuallyAddEventModal = () => {
setRepeatInterval([]);
}, [editEvent, selectedNewEventDate]);
useEffect(() => {
if(show && !editEvent) {
setTimeout(() => {
titleRef?.current?.focus()
}, 500);
}
}, [selectedNewEventDate]);
if (!show) return null;
const formatDateTime = (date?: Date | string) => {
@ -342,8 +351,8 @@ export const ManuallyAddEventModal = () => {
<ScrollView style={{minHeight: "85%"}}>
<TextField
placeholder="Add event title"
ref={titleRef}
value={title}
autoFocus
onChangeText={(text) => {
setTitle(text);
}}
@ -555,14 +564,15 @@ export const ManuallyAddEventModal = () => {
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerH>
<View row center>
<LockIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
marginL-12
center
>
Mark as Private
</Text>
@ -609,7 +619,7 @@ export const ManuallyAddEventModal = () => {
<Button
disabled
marginH-30
marginB-15
marginB-30
label="Create event from image"
text70
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 {Button, View,} from "react-native-ui-lib";
import {useGroceryContext} from "@/contexts/GroceryContext";
import {FontAwesome6} from "@expo/vector-icons";
import PlusIcon from "@/assets/svgs/PlusIcon";
const { width } = Dimensions.get("screen");
const AddGroceryItem = () => {
const {setIsAddingGrocery} = useGroceryContext();
@ -65,8 +67,14 @@ const styles = StyleSheet.create({
marginVertical: 10,
},
btnContainer: {
width: "100%",
position:"absolute",
bottom: 30,
width: width,
padding: 20,
paddingBottom: 0,
justifyContent: "center",
alignItems:"center",
zIndex: 10,
},
finishShopBtn: {
width: "100%",

View File

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

View File

@ -74,10 +74,11 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
}, [groceries]);
return (
<View marginH-20 marginB-20>
<View marginH-20 marginB-45>
<HeaderTemplate
message={"Welcome to your grocery list"}
isWelcome={false}
isGroceries={true}
>
<View row centerV>
<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 {useSignIn} from "@/hooks/firebase/useSignIn";
import {StyleSheet} from "react-native";
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
import {isLoading} from "expo-font";
export const ResetPasswordPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
export const ResetPasswordPage = () => {
const [email, setEmail] = useState<string>("");
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();

View File

@ -1,7 +1,9 @@
import {
Button,
ButtonSize,
Dialog,
Colors,
KeyboardAwareScrollView,
LoaderScreen,
Text,
TextField,
TextFieldRef,
@ -9,26 +11,22 @@ import {
} from "react-native-ui-lib";
import React, {useRef, useState} from "react";
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 { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
import { Camera, CameraView } from "expo-camera";
import KeyboardManager from "react-native-keyboard-manager";
import {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
const SignInPage = ({
setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => {
KeyboardManager.setEnableAutoToolbar(true);
const SignInPage = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const passwordRef = useRef<TextFieldRef>(null);
const { mutateAsync: signIn, error, isError } = useSignIn();
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode();
const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
const router = useRouter()
const handleSignIn = async () => {
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 (
<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
placeholder="Email"
value={email}
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
onSubmitEditing={() => {
// Move focus to the description field
passwordRef.current?.focus();
@ -87,11 +80,17 @@ const SignInPage = ({
<TextField
ref={passwordRef}
placeholder="Password"
textContentType={"oneTimeCode"}
value={password}
onChangeText={setPassword}
secureTextEntry
style={styles.textfield}
autoCorrect={false}
/>
</KeyboardAvoidingView>
<View flexG/>
<Button
label="Log in"
marginT-50
@ -103,18 +102,7 @@ const SignInPage = ({
style={{marginBottom: 20, height: 50}}
backgroundColor="#fd1775"
/>
<Button
label="Log in with a QR Code"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true));
}}
style={{ marginBottom: 20, height: 50 }}
backgroundColor="#fd1775"
/>
{isError && (
<Text center style={{marginBottom: 20}}>{`${
error?.toString()?.split("]")?.[1]
@ -124,7 +112,7 @@ const SignInPage = ({
<View row centerH marginB-5 gap-5>
<Text style={styles.jakartaLight}>Don't have an account?</Text>
<Button
onPress={() => setTab("register")}
onPress={() => router.replace("/(unauth)/sign_up")}
label="Sign Up"
labelStyle={[
styles.jakartaMedium,
@ -140,56 +128,33 @@ const SignInPage = ({
/>
</View>
<View row centerH marginB-5 gap-5>
<Text text70>Forgot your password?</Text>
<Button
onPress={() => setTab("reset-password")}
label="Reset password"
labelStyle={[
styles.jakartaMedium,
{ textDecorationLine: "none", color: "#fd1575" },
]}
link
size={ButtonSize.xSmall}
padding-0
margin-0
text70
left
avoidInnerPadding
color="#fd1775"
/>
</View>
{/*<View row centerH marginB-5 gap-5>*/}
{/* <Text text70>Forgot your password?</Text>*/}
{/* <Button*/}
{/* onPress={() => router.replace("/(unauth)/sign_up")}*/}
{/* label="Reset password"*/}
{/* labelStyle={[*/}
{/* styles.jakartaMedium,*/}
{/* {textDecorationLine: "none", color: "#fd1575"},*/}
{/* ]}*/}
{/* link*/}
{/* size={ButtonSize.xSmall}*/}
{/* padding-0*/}
{/* margin-0*/}
{/* text70*/}
{/* left*/}
{/* avoidInnerPadding*/}
{/* color="#fd1775"*/}
{/* />*/}
{/*</View>*/}
{/* Camera Dialog */}
<Dialog
visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)}
bottom
width="100%"
height="70%"
containerStyle={{ padding: 15, backgroundColor:"white" }}
>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
) : !hasPermission ? (
<Text>No access to camera</Text>
) : (
<CameraView
style={{ flex: 1, borderRadius: 15 }}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
}}
/>
{isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white}
color={Colors.grey40}/>
)}
<Button
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{ margin: 10, marginBottom: 30 }}
/>
</Dialog>
</View>
</KeyboardAwareScrollView>
</SafeAreaView>
);
};

View File

@ -3,6 +3,9 @@ import {
Button,
ButtonSize,
Checkbox,
Colors,
KeyboardAwareScrollView,
LoaderScreen,
Text,
TextField,
TextFieldRef,
@ -10,16 +13,15 @@ import {
View,
} from "react-native-ui-lib";
import {useSignUp} from "@/hooks/firebase/useSignUp";
import {StyleSheet} from "react-native";
import {KeyboardAvoidingView, StyleSheet} from "react-native";
import {AntDesign} from "@expo/vector-icons";
import KeyboardManager from "react-native-keyboard-manager";
import {SafeAreaView} from "react-native-safe-area-context";
import {useRouter} from "expo-router";
const SignUpPage = ({
setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => {
KeyboardManager.setEnableAutoToolbar(true);
const SignUpPage = () => {
const [email, setEmail] = useState<string>("");
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
@ -28,22 +30,33 @@ const SignUpPage = ({
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
const {mutateAsync: signUp} = useSignUp();
const {mutateAsync: signUp, isLoading} = useSignUp();
const lnameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
const passwordRef = useRef<TextFieldRef>(null);
const router = useRouter()
const handleSignUp = async () => {
await signUp({email, password, firstName, lastName});
router.replace("/(unauth)/cal_sync")
};
return (
<View height={"100%"} padding-15 marginT-30>
<Text style={styles.title}>Get started with Cally</Text>
<Text style={styles.subtitle} marginT-15 color="#919191">
<SafeAreaView style={{flex: 1}}>
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
<View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
Get started with Cally
</Text>
<Text color={"#919191"} style={{fontSize: 20}}>
Please enter your details.
</Text>
</View>
<KeyboardAvoidingView style={{width: '100%'}}>
<TextField
marginT-30
autoFocus
@ -55,6 +68,12 @@ const SignUpPage = ({
lnameRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="First name input"
accessibilityHint="Enter your first name"
accessible
returnKeyType="next"
textContentType="givenName"
importantForAccessibility="yes"
/>
<TextField
ref={lnameRef}
@ -66,18 +85,29 @@ const SignUpPage = ({
emailRef.current?.focus();
}}
blurOnSubmit={false}
accessibilityLabel="Last name input"
accessibilityHint="Enter your last name"
accessible
returnKeyType="next"
textContentType="familyName"
importantForAccessibility="yes"
/>
<TextField
ref={emailRef}
placeholder="Email"
value={email}
keyboardType={"email-address"}
returnKeyType={"next"}
textContentType={"emailAddress"}
defaultValue={email}
onChangeText={setEmail}
style={styles.textfield}
autoComplete={"email"}
autoCorrect={false}
ref={emailRef}
onSubmitEditing={() => {
passwordRef.current?.focus();
}}
blurOnSubmit={false}
/>
<View
centerV
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
@ -102,6 +132,9 @@ const SignUpPage = ({
}
/>
</View>
</KeyboardAvoidingView>
<View gap-5 marginT-15>
<View row centerV>
<Checkbox
@ -123,8 +156,8 @@ const SignUpPage = ({
value={acceptTerms}
onValueChange={(value) => setAcceptTerms(value)}
/>
<View row>
<Text style={styles.jakartaLight} marginL-10>
<View row style={{flexWrap: "wrap", marginLeft: 10}}>
<Text style={styles.jakartaLight}>
I accept the
</Text>
<TouchableOpacity>
@ -143,16 +176,20 @@ const SignUpPage = ({
</View>
</View>
</View>
<View flex-1/>
<View style={styles.bottomView}>
<View flexG style={{minHeight: 50}}/>
<View>
<Button
label="Register"
disabled={!acceptTerms}
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
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>
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
@ -171,11 +208,18 @@ const SignUpPage = ({
color="#fd1775"
size={ButtonSize.small}
text70
onPress={() => setTab("login")}
onPress={() => router.replace("/(unauth)/sign_in")}
/>
</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,
color: "#919191",
},
//mora da se izmeni kako treba
bottomView: {marginTop: "auto", marginBottom: 30, marginTop: "auto"},
jakartaLight: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 13,

View File

@ -1,7 +1,7 @@
import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, { useCallback, useEffect, useState } from "react";
import React, {useCallback, useState} from "react";
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
import { ActivityIndicator, Alert, ScrollView, StyleSheet } from "react-native";
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import {TouchableOpacity} from "react-native-gesture-handler";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
@ -9,53 +9,20 @@ import debounce from "debounce";
import AppleIcon from "@/assets/svgs/AppleIcon";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
import OutlookIcon from "@/assets/svgs/OutlookIcon";
import * as AuthSession from "expo-auth-session";
import * as Google from "expo-auth-session/providers/google";
import * as WebBrowser from "expo-web-browser";
import { UserProfile } from "@firebase/auth";
import { useFetchAndSaveGoogleEvents } from "@/hooks/useFetchAndSaveGoogleEvents";
import { useFetchAndSaveOutlookEvents } from "@/hooks/useFetchAndSaveOutlookEvents";
import { useFetchAndSaveAppleEvents } from "@/hooks/useFetchAndSaveAppleEvents";
import * as AppleAuthentication from "expo-apple-authentication";
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
import {colorMap} from "@/constants/colorMap";
import { useAtom } from "jotai";
import {useSetAtom} from "jotai";
import {settingsPageIndex} from "../calendar/atoms";
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
import {useCalSync} from "@/hooks/useCalSync";
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 {profileData} = useAuthContext();
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const setPageIndex = useSetAtom(settingsPageIndex);
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
profileData?.firstDayOfWeek ??
ExpoLocalization.getCalendars()[0].firstWeekday === 1
@ -77,8 +44,8 @@ const CalendarSettingsPage = () => {
setModalVisible(true);
};
const handleConfirm = () => {
clearToken(selectedService, selectedEmail);
const handleConfirm = async () => {
await clearToken({email: selectedEmail, provider: selectedService});
setModalVisible(false);
};
@ -94,188 +61,22 @@ const CalendarSettingsPage = () => {
);
const {mutateAsync: updateUserData} = useUpdateUserData();
const { mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle } =
useFetchAndSaveGoogleEvents();
const {mutateAsync: clearToken} = useClearTokens();
const {
mutateAsync: fetchAndSaveOutlookEvents,
isLoading: isSyncingOutlook,
} = useFetchAndSaveOutlookEvents();
const { mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple } =
useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
useEffect(() => {
signInWithGoogle();
}, [response]);
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
const accessToken = response.authentication?.accessToken;
const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
let googleAccounts = profileData?.googleAccounts;
const updatedGoogleAccounts = googleAccounts
? { ...googleAccounts, [googleMail]: accessToken }
: { [googleMail]: accessToken };
await updateUserData({
newUserData: { googleAccounts: updatedGoogleAccounts },
});
await fetchAndSaveGoogleEvents({
token: accessToken,
email: googleMail,
});
}
} catch (error) {
console.error("Error during Google sign-in:", error);
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Token exchange failed:", errorText);
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.access_token) {
console.log("Access token received, fetching user info...");
// Fetch user info from Microsoft Graph API to get the email
const userInfoResponse = await fetch(
"https://graph.microsoft.com/v1.0/me",
{
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
}
);
const userInfo = await userInfoResponse.json();
console.log("User info received:", userInfo);
if (userInfo.error) {
console.error("Error fetching user info:", userInfo.error);
} else {
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
let microsoftAccounts = profileData?.microsoftAccounts;
const updatedMicrosoftAccounts = microsoftAccounts
? { ...microsoftAccounts, [outlookMail]: tokenData.access_token }
: { [outlookMail]: tokenData.access_token };
// Update user data with Microsoft token and email
await updateUserData({
newUserData: { microsoftAccounts: updatedMicrosoftAccounts },
});
await fetchAndSaveOutlookEvents(
tokenData.access_token,
outlookMail
);
console.log("User data updated successfully.");
}
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const handleAppleSignIn = async () => {
try {
console.log("Starting Apple Sign-in...");
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
console.log("Apple sign-in result:", credential);
const appleToken = credential.identityToken;
const appleMail = credential.email;
if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed...");
await updateUserData({
newUserData: { appleToken, appleMail },
});
console.log("User data updated with Apple ID token.");
await fetchAndSaveAppleEvents({ token: appleToken, email: appleMail! });
} else {
console.warn(
"Apple authentication was not successful or email was hidden."
);
}
} catch (error) {
console.error("Error during Apple Sign-in:", error);
}
};
isSyncingGoogle,
isSyncingOutlook,
isConnectedToGoogle,
isConnectedToMicrosoft,
isConnectedToApple,
handleAppleSignIn,
isSyncingApple,
handleMicrosoftSignIn,
fetchAndSaveOutlookEvents,
fetchAndSaveGoogleEvents,
handleStartGoogleSignIn,
fetchAndSaveAppleEvents
} = useCalSync()
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
@ -309,10 +110,8 @@ const CalendarSettingsPage = () => {
);
const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
debouncedUpdateFirstDayOfWeek(
firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"
);
setFirstDayOfWeek(firstDayOfWeek);
debouncedUpdateFirstDayOfWeek(firstDayOfWeek);
};
const handleChangeColor = (color: string) => {
@ -321,64 +120,6 @@ const CalendarSettingsPage = () => {
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 (
<ScrollView>
<TouchableOpacity onPress={() => setPageIndex(0)}>
@ -479,8 +220,8 @@ const CalendarSettingsPage = () => {
</Text>
<Button
onPress={() => promptAsync()}
label={"Connect Google"}
onPress={() => handleStartGoogleSignIn()}
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
@ -494,35 +235,8 @@ const CalendarSettingsPage = () => {
color="black"
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
onPress={() => handleAppleSignIn()}
label={"Connect Apple"}
@ -539,36 +253,11 @@ const CalendarSettingsPage = () => {
color="black"
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
onPress={() => handleMicrosoftSignIn()}
label={"Connect Outlook"}
label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2,
@ -582,37 +271,6 @@ const CalendarSettingsPage = () => {
color="black"
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 ||
isConnectedToMicrosoft ||
@ -622,56 +280,68 @@ const CalendarSettingsPage = () => {
Connected Calendars
</Text>
<View style={styles.noPaddingCard}>
<View style={[styles.noPaddingCard, {marginBottom: 100}]}>
<View style={{marginTop: 20}}>
{profileData?.googleAccounts &&
Object.keys(profileData?.googleAccounts)?.map(
(googleEmail) => {
const googleToken =
profileData?.googleAccounts?.[googleEmail];
profileData?.googleAccounts?.[googleEmail]?.accessToken;
return (
googleToken && (
<TouchableOpacity
onPress={() =>
fetchAndSaveGoogleEvents({
token: googleToken,
email: googleEmail,
})
}
>
<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}
<View row paddingR-5 center>
<View
style={{
backgroundColor: "#ffffff",
marginBottom: 15,
paddingLeft: 15,
}}
color="black"
text70BL
/>
row
centerV
width="100%"
spread
>
{isSyncingGoogle ? (
<View marginR-5>
<ActivityIndicator/>
</View>
) : (
<Ionicons
<View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveGoogleEvents({
token: googleToken,
email: googleEmail,
})
}
iconSource={() => <Ionicons
name={"refresh"}
size={20}
color={"#000000"}
/>}
/>
)}
</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 &&
Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
console.log(profileData?.appleAccounts)
const appleToken = profileData?.appleAccounts?.[appleEmail];
return (
appleToken && (
<TouchableOpacity
onPress={() =>
fetchAndSaveAppleEvents({
email: appleEmail,
token: appleToken,
})
}
>
<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}
<View row paddingR-5 center>
<View
style={{
backgroundColor: "#ffffff",
marginBottom: 15,
paddingLeft: 15,
}}
color="black"
text70BL
/>
row
centerV
width="100%"
spread
>
<View marginR-5>
<AppleIcon/>
</View>
<Text style={styles.addCalLbl}>
{appleEmail}
</Text>
{isSyncingApple ? (
<View marginR-5>
<ActivityIndicator/>
</View>
) : (
<Ionicons
<View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveAppleEvents({
email: appleEmail,
token: appleToken,
})
}
iconSource={() => <Ionicons
name={"refresh"}
size={20}
color={"#000000"}
/>}
/>
)}
</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];
return (
microsoftToken && (
<TouchableOpacity
onPress={() =>
fetchAndSaveOutlookEvents({
token: microsoftToken,
email: microsoftEmail,
})
}
>
<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}
<View row paddingR-5 center>
<View
style={{
backgroundColor: "#ffffff",
marginBottom: 15,
paddingLeft: 15,
}}
color="black"
text70BL
/>
row
centerV
width="100%"
spread
>
{isSyncingOutlook ? (
<View marginR-5>
<ActivityIndicator/>
</View>
) : (
<Ionicons
<View marginR-5>
<Button
style={{backgroundColor: "#ffffff"}}
color="black"
onPress={() =>
fetchAndSaveOutlookEvents({
token: microsoftToken,
email: microsoftEmail,
})
}
iconSource={() => <Ionicons
name={"refresh"}
size={20}
color={"#000000"}
/>}
/>
)}
</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,
fontFamily: "PlusJakartaSan_500Medium",
flexWrap: "wrap",
width: "75%",
width: "70%",
textAlign: "left",
lineHeight: 20,
overflow: "visible",
overflow: "hidden",
},
subTitle: {
fontFamily: "Manrope_600SemiBold",

View File

@ -1,6 +1,6 @@
import { Button, Text, View } from "react-native-ui-lib";
import React, { useState } from "react";
import { StyleSheet } from "react-native";
import {Linking, StyleSheet} from "react-native";
import { Octicons } from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings";
@ -21,11 +21,23 @@ const pageIndex = {
policy: 4,
};
const PRIVACY_POLICY_URL = 'https://callyapp.com';
const SettingsPage = () => {
const { profileData } = useAuthContext();
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
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 (
<View flexG>
{pageIndex == 0 && (
@ -73,7 +85,8 @@ const SettingsPage = () => {
}}
/>
<Button
disabled={isntParent}
disabled
// disabled={isntParent}
backgroundColor="white"
style={styles.mainBtn}
children={
@ -84,7 +97,7 @@ const SettingsPage = () => {
color="#ff9900"
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
</Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} />
@ -95,6 +108,7 @@ const SettingsPage = () => {
<Button
backgroundColor="white"
style={styles.mainBtn}
onPress={openPrivacyPolicy}
children={
<View row centerV width={"100%"}>
<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 {Ionicons} from "@expo/vector-icons";
import {ScrollView, StyleSheet} from "react-native";
import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup";
import { useAtom } from "jotai";
import {useAtom, useSetAtom} from "jotai";
import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
import { AuthContextProvider } from "@/contexts/AuthContext";
import PlusIcon from "@/assets/svgs/PlusIcon";
const UserSettings = () => {
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const setPageIndex = useSetAtom(settingsPageIndex);
const [userView, setUserView] = useAtom(userSettingsView);
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
return (
<AuthContextProvider>
<View flexG>
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
<TouchableOpacity
@ -73,21 +74,36 @@ const UserSettings = () => {
</TouchableOpacity>
</View>
{userView && <MyProfile/>}
{!userView && <MyGroup />}
{!userView && <MyGroup onNewUserClick={onNewUserClick} setOnNewUserClick={setOnNewUserClick}/>}
</View>
</ScrollView>
{!userView && (
<View>
<Text>selview</Text>
</View>
<FloatingButton
fullWidth
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>
</AuthContextProvider>
);
};
const styles = StyleSheet.create({
bottomButton: {
position: "absolute",
bottom: 15,
marginHorizontal: 28,
width: 337,
backgroundColor: "#e8156c",
height: 53.26,
},
buttonSwitch: {
borderRadius: 50,
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,
Card,
Colors,
Dialog,
FloatingButton,
Dialog, Image,
KeyboardAwareScrollView,
PanningProvider,
Picker,
@ -15,7 +14,7 @@ import {
View,
} from "react-native-ui-lib";
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 {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
@ -28,11 +27,18 @@ import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
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 [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue
>(ProfileType.CHILD);
@ -40,6 +46,8 @@ const MyGroup = () => {
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [newUserId, setNewUserId] = useState("")
const lNameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
@ -50,6 +58,7 @@ const MyGroup = () => {
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
const {data: familyMembers} = useGetFamilyMembers(true);
const {user} = useAuthContext();
const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
@ -90,19 +99,26 @@ const MyGroup = () => {
console.log(res);
if (!isError) {
setShowNewUserInfoDialog(false);
setOnNewUserClick(false);
if (res?.data?.userId) {
if (profileImageAsset) {
await changeProfilePicture(profileImageAsset)
setShowQRCodeDialog(res.data.userId);
} else {
setTimeout(() => {
setShowQRCodeDialog(res.data.userId);
}, 500);
}
handleClearImage()
}
}
};
useEffect(() => {
KeyboardManager.setEnableAutoToolbar(true);
},[])
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
}, []);
useEffect(() => {
setFirstName("");
@ -110,12 +126,11 @@ const MyGroup = () => {
setEmail("");
}, []);
// @ts-ignore
return (
<View style={{ flex: 1, minHeight: 500 }}>
<View marginB-70>
<ScrollView>
<View>
<ScrollView style={styles.card}>
{!parents.length && !children.length && !caregivers.length && (
<Text text70 marginV-10>
{isLoading ? "Loading...." : "No user devices added"}
@ -123,7 +138,7 @@ const MyGroup = () => {
)}
{(!!parents.length || !!children.length) && (
<>
<View style={styles.card}>
<Text style={styles.subTit} marginB-10>
Family
</Text>
@ -144,7 +159,9 @@ const MyGroup = () => {
source={{uri: member.pfp || undefined}}
/>
) : (
<View style={[styles.pfp, {backgroundColor: "#ea156d"}]} />
<View
style={[styles.pfp, {backgroundColor: "#ea156d"}]}
/>
)}
<View row marginL-10 centerV>
<Text style={styles.name}>
@ -155,9 +172,7 @@ const MyGroup = () => {
<View row centerV gap-10>
<Text style={styles.userType}>
{member.userType === ProfileType.PARENT
? `Admin${
member.uid === user?.uid ? " (You)" : ""
}`
? `Admin${member.uid === user?.uid ? " (You)" : ""}`
: "Child"}
</Text>
<UserMenu
@ -168,11 +183,11 @@ const MyGroup = () => {
</View>
</Card>
))}
</>
</View>
)}
{!!caregivers.length && (
<>
<View style={styles.card}>
<Text style={styles.subTit} marginB-10 marginT-15>
Caregivers
</Text>
@ -209,7 +224,7 @@ const MyGroup = () => {
/>
</Card>
))}
</>
</View>
)}
{!!familyDevices.length && (
@ -250,19 +265,8 @@ const MyGroup = () => {
))}
</>
)}
</ScrollView>
</View>
<FloatingButton
fullWidth
hideBackgroundOverlay
visible
button={{
label: "+ Add a user device",
onPress: () => setShowNewUserInfoDialog(true),
style: styles.bottomButton,
}}
/>
</ScrollView>
<Dialog
visible={showAddUserDialog}
@ -315,8 +319,8 @@ const MyGroup = () => {
<Dialog
panDirection={PanningProvider.Directions.DOWN}
visible={showNewUserInfoDialog}
onDismiss={() => setShowNewUserInfoDialog(false)}
visible={onNewUserClick}
onDismiss={() => setOnNewUserClick(false)}
>
<PreviousNextView>
<KeyboardAwareScrollView>
@ -327,7 +331,7 @@ const MyGroup = () => {
</Text>
<TouchableOpacity
onPress={() => {
setShowNewUserInfoDialog(false);
setOnNewUserClick(false);
}}
>
<CircledXIcon/>
@ -336,6 +340,14 @@ const MyGroup = () => {
<View style={styles.divider} spread/>
<View row centerV gap-20 marginV-20>
{pfpUri ? (
<Image
height={65.54}
width={65.54}
style={{borderRadius: 25}}
source={{uri: pfpUri}}
/>
) : (
<View
height={65.54}
width={65.54}
@ -346,11 +358,22 @@ const MyGroup = () => {
style={{borderRadius: 25}}
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>
Upload User Profile Photo
</Text>
</TouchableOpacity>
)}
</View>
<Text style={styles.jakarta12}>Member Status</Text>
@ -498,7 +521,8 @@ const styles = StyleSheet.create({
},
bottomButton: {
position: "absolute",
bottom: 80,
bottom: 50,
backgroundColor: "#e8156c",
width: "100%",
},
familyCard: {

View File

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

View File

@ -32,7 +32,10 @@ const UserMenu = ({
panDirection={PanningDirectionsEnum.DOWN}
>
<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}/>
<Button
marginT-20

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import FamilyChart from "./FamilyChart";
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
import { Ionicons } from "@expo/vector-icons";
const FamilyChoresProgress = ({
setPageIndex,
@ -12,7 +13,20 @@ const FamilyChoresProgress = ({
return (
<View marginT-20 marginH-5>
<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>
<View centerH>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>

View File

@ -30,7 +30,20 @@ const UserChoresProgress = ({
showsHorizontalScrollIndicator={false}
>
<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>
<View>
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>

View File

@ -1,18 +1,37 @@
import { Image, Text, View } from "react-native-ui-lib";
import React from "react";
import { useAuthContext } from "@/contexts/AuthContext";
import React, { useEffect, useState } from "react";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { StyleSheet } from "react-native";
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: {
message: string;
isWelcome: boolean;
children?: React.ReactNode;
link?: React.ReactNode;
isCalendar?: boolean;
isToDos?: boolean;
isBrainDump?: boolean;
isGroceries?: boolean;
}) => {
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({
pfp: {
@ -26,14 +45,71 @@ const HeaderTemplate = (props: {
pfpTxt: {
fontFamily: "Manrope_500Medium",
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 (
<View row centerV marginV-15>
<View row centerV marginV-15 style={styles.bottomMarg}>
{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>
<Text style={styles.pfpTxt}>

View File

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

View File

@ -24,6 +24,7 @@ interface IAuthContext {
profileType?: ProfileType,
profileData?: UserProfile,
setProfileData: (profileData: UserProfile) => void,
setRedirectOverride: (val: boolean) => void,
refreshProfileData: () => Promise<void>
}
@ -59,7 +60,7 @@ async function registerForPushNotificationsAsync() {
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
// alert('Failed to get push token for push notification!');
return;
}
@ -76,11 +77,11 @@ async function registerForPushNotificationsAsync() {
console.log('Push Token:', token);
return token;
} catch (error) {
alert(`Error getting push token: ${error}`);
// alert(`Error getting push token: ${error}`);
throw error;
}
} 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 [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
const [redirectOverride, setRedirectOverride] = useState(false);
const {replace} = useRouter();
const ready = !initializing;
const queryClient = useQueryClient();
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
setUser(authUser);
if (!redirectOverride) {
setUser(authUser);
if (authUser) {
await refreshProfileData(authUser);
@ -109,6 +113,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}
if (initializing) setInitializing(false);
}
};
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}, [initializing]);
useEffect(() => {
if (ready && user) {
if (ready && user && !redirectOverride) {
replace({pathname: "/(auth)/calendar"});
} else if (ready && !user) {
} else if (ready && !user && !redirectOverride) {
replace({pathname: "/(unauth)"});
}
}, [user, ready]);
}, [user, ready, redirectOverride]);
useEffect(() => {
const sub = Notifications.addNotificationReceivedListener(notification => {
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}
return (
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
<AuthContext.Provider
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
{children}
</AuthContext.Provider>
);

View File

@ -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...');
const profilesSnapshot = await db.collection('Profiles').get();
@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleAccounts) {
try {
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
const googleToken = profileData?.googleAccounts?.[googleEmail];
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
return null;
});
// Function to refresh Google token
async function refreshGoogleToken(token) {
// Assuming you use OAuth2 token refresh flow
async function refreshGoogleToken(refreshToken) {
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously
client_id: 'YOUR_GOOGLE_CLIENT_ID',
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
});
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', {
grant_type: 'refresh_token',
refresh_token: token, // Add refresh token stored previously
client_id: 'YOUR_MICROSOFT_CLIENT_ID',
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
refresh_token: refreshToken,
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
});
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() {

View File

@ -5,7 +5,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import * as ImagePicker from "expo-image-picker";
import {Platform} from "react-native";
export const useChangeProfilePicture = () => {
export const useChangeProfilePicture = (customUserId?: string) => {
const queryClient = useQueryClient();
const {user, refreshProfileData} = useAuthContext();
@ -38,20 +38,24 @@ export const useChangeProfilePicture = () => {
const downloadURL = await reference.getDownloadURL();
console.log("Download URL:", downloadURL);
if(!customUserId) {
await firestore()
.collection("Profiles")
.doc(user?.uid)
.update({pfp: downloadURL});
}
} catch (e) {
console.error("Error uploading profile picture:", e.message);
console.error("Error uploading profile picture:", e);
throw e;
}
},
onSuccess: () => {
// Invalidate queries to refresh profile data
if (!customUserId) {
queryClient.invalidateQueries("Profiles");
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"],
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
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);
// Check if the event already exists
const snapshot = await firestore()
.collection("Events")
.where("id", "==", eventData.id)
.get();
if (snapshot.empty) {
await firestore()
// Event doesn't exist, so add it
return firestore()
.collection("Events")
.add({ ...eventData, creatorId: currentUser?.uid });
} else {
console.log("Event already exists, updating...");
// Event exists, update it
const docId = snapshot.docs[0].id;
await firestore()
return firestore()
.collection("Events")
.doc(docId)
.set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
}
}
});
// Execute all promises in parallel
await Promise.all(promises);
} catch (e) {
console.error("Error creating/updating events: ", e);
}

View File

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

View File

@ -1,11 +1,12 @@
import {useMutation} from "react-query";
import auth from "@react-native-firebase/auth";
import { ProfileType } from "@/contexts/AuthContext";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useSetUserData} from "./useSetUserData";
import {uuidv4} from "@firebase/util";
import * as Localization from "expo-localization";
export const useSignUp = () => {
const {setRedirectOverride} = useAuthContext()
const {mutateAsync: setUserData} = useSetUserData();
return useMutation({
@ -21,6 +22,8 @@ export const useSignUp = () => {
firstName: string;
lastName: string;
}) => {
setRedirectOverride(true)
await auth()
.createUserWithEmailAndPassword(email, password)
.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 {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient()
const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
const {mutateAsync: clearToken} = useClearTokens();
return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"],
@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
timeMax.toISOString().slice(0, -5) + "Z"
);
if(!response.success) {
await clearToken({email: email!, provider: "google"})
return
}
console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => {
const items = response?.googleEvents?.map((item) => {
if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
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";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "Cally";
PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -484,7 +484,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "Cally";
PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View File

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

258
yarn.lock
View File

@ -960,6 +960,89 @@
wrap-ansi "^7.0.0"
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":
version "0.0.5"
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"
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":
version "5.0.4"
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"
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":
version "7.0.3"
resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz"
@ -1285,6 +1449,23 @@
semver "^7.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":
version "1.1.1"
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:
"@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:
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"
@ -3659,6 +3853,22 @@ babel-preset-expo@~11.0.14:
babel-plugin-react-native-web "~0.19.10"
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:
version "29.6.3"
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"
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:
version "13.0.5"
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"
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:
version "1.12.24"
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:
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:
version "0.28.18"
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"
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:
version "51.0.34"
resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz"