mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 00:24:53 +00:00
Merge branch 'dev'
# Conflicts: # yarn.lock
This commit is contained in:
@ -23,8 +23,9 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||
import {useAtom, useSetAtom} from "jotai";
|
||||
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
246
app/(unauth)/cal_sync.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {Button, Text, View} from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {StyleSheet} from "react-native";
|
||||
|
||||
export default function Screen() {
|
||||
const {profileData, setRedirectOverride} = useAuthContext()
|
||||
const {handleStartGoogleSignIn, handleAppleSignIn, handleMicrosoftSignIn} = useCalSync()
|
||||
|
||||
const hasSomeCalendarsSynced =
|
||||
!!profileData?.appleAccounts || !!profileData?.microsoftAccounts || !!profileData?.googleAccounts
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||
Let's get started!
|
||||
</Text>
|
||||
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||
Add your calendar below to sync events to your Cally calendar
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View width={"100%"} gap-1>
|
||||
{!profileData?.googleAccounts && (
|
||||
<Button
|
||||
onPress={() => handleStartGoogleSignIn()}
|
||||
label={"Connect Google account"}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<GoogleIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)}
|
||||
|
||||
{profileData?.googleAccounts
|
||||
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
|
||||
const googleToken = profileData?.googleAccounts?.[googleMail]?.accessToken;
|
||||
return (
|
||||
googleToken && (
|
||||
<Button
|
||||
key={googleMail}
|
||||
disabled
|
||||
label={`Connected ${googleMail}`}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<GoogleIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
{!profileData?.appleAccounts && (
|
||||
<Button
|
||||
onPress={() => handleAppleSignIn()}
|
||||
label={"Connect Apple"}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<AppleIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)}
|
||||
|
||||
{profileData?.appleAccounts
|
||||
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
|
||||
const appleToken = profileData?.appleAccounts?.[appleEmail!];
|
||||
return (
|
||||
appleToken && (
|
||||
<Button
|
||||
key={appleEmail}
|
||||
disabled
|
||||
label={`Connected Apple Calendar`}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<AppleIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
{!profileData?.microsoftAccounts && (
|
||||
<Button
|
||||
onPress={() => handleMicrosoftSignIn()}
|
||||
label={"Connect Outlook"}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<OutlookIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)}
|
||||
|
||||
{profileData?.microsoftAccounts
|
||||
? Object.keys(profileData?.microsoftAccounts)?.map(
|
||||
(microsoftEmail) => {
|
||||
const microsoftToken =
|
||||
profileData?.microsoftAccounts?.[microsoftEmail];
|
||||
return (
|
||||
microsoftToken && (
|
||||
<Button
|
||||
key={microsoftEmail}
|
||||
|
||||
label={`Connected ${microsoftEmail}`}
|
||||
labelStyle={styles.addCalLbl}
|
||||
labelProps={{
|
||||
numberOfLines: 2,
|
||||
}}
|
||||
iconSource={() => (
|
||||
<View marginR-15>
|
||||
<OutlookIcon/>
|
||||
</View>
|
||||
)}
|
||||
style={styles.addCalBtn}
|
||||
color="black"
|
||||
text70BL
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
: null}
|
||||
</View>
|
||||
|
||||
<View flexG/>
|
||||
|
||||
<View width={"100%"}>
|
||||
<Button
|
||||
label={hasSomeCalendarsSynced ? "Continue" : "Skip this step"}
|
||||
onPress={() => setRedirectOverride(false)}
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
style={{height: 50}}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
addCalBtn: {
|
||||
backgroundColor: "#ffffff",
|
||||
marginBottom: 15,
|
||||
justifyContent: "flex-start",
|
||||
paddingLeft: 25,
|
||||
},
|
||||
backBtn: {
|
||||
backgroundColor: "red",
|
||||
marginLeft: -2,
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "white",
|
||||
width: "100%",
|
||||
padding: 20,
|
||||
paddingBottom: 30,
|
||||
marginTop: 20,
|
||||
borderRadius: 12,
|
||||
},
|
||||
noPaddingCard: {
|
||||
backgroundColor: "white",
|
||||
width: "100%",
|
||||
marginTop: 20,
|
||||
borderRadius: 12,
|
||||
},
|
||||
colorBox: {
|
||||
aspectRatio: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 51,
|
||||
borderRadius: 12,
|
||||
},
|
||||
checkbox: {
|
||||
borderRadius: 50,
|
||||
},
|
||||
addCalLbl: {
|
||||
fontSize: 16,
|
||||
fontFamily: "PlusJakartaSan_500Medium",
|
||||
flexWrap: "wrap",
|
||||
width: "75%",
|
||||
textAlign: "left",
|
||||
lineHeight: 20,
|
||||
overflow: "visible",
|
||||
},
|
||||
subTitle: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 18,
|
||||
},
|
||||
cardTitle: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import Entry from "@/components/pages/main/Entry";
|
||||
|
||||
export default function Screen() {
|
||||
return <Entry />;
|
||||
return <Entry />;
|
||||
}
|
||||
169
app/(unauth)/get_started.tsx
Normal file
169
app/(unauth)/get_started.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,10 +1,44 @@
|
||||
import Entry from "@/components/pages/main/Entry";
|
||||
import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {Button, Image, Text, View} from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import {useRouter} from "expo-router";
|
||||
|
||||
export default function Screen() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Entry/>
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||
<View>
|
||||
<Image source={require("../../assets/images/splash.png")}/>
|
||||
</View>
|
||||
|
||||
<View center gap-13>
|
||||
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||
Welcome to Cally
|
||||
</Text>
|
||||
<Text center color={"#919191"} style={{fontSize: 20, maxWidth: 250}}>
|
||||
Lightening Mental Loads,
|
||||
One Family at a Time
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View flexG/>
|
||||
|
||||
<View width={"100%"}>
|
||||
<Button
|
||||
label="Continue"
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={() => router.push("/(unauth)/get_started")}
|
||||
style={{height: 50}}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
6
app/(unauth)/reset_password.tsx
Normal file
6
app/(unauth)/reset_password.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import {ResetPasswordPage} from "@/components/pages/main/ResetPasswordPage";
|
||||
|
||||
export default function Screen() {
|
||||
return <ResetPasswordPage/>
|
||||
}
|
||||
6
app/(unauth)/sign_in.tsx
Normal file
6
app/(unauth)/sign_in.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import SignInPage from "@/components/pages/main/SignInPage";
|
||||
import React from "react";
|
||||
|
||||
export default function Screen() {
|
||||
return <SignInPage/>
|
||||
}
|
||||
8
app/(unauth)/sign_up.tsx
Normal file
8
app/(unauth)/sign_up.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import SignUpPage from "@/components/pages/main/SignUpPage";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<SignUpPage/>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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};
|
||||
}
|
||||
|
||||
@ -1,126 +1,127 @@
|
||||
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 { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||
import { Dimensions, Keyboard, StyleSheet } from "react-native";
|
||||
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, Platform, StyleSheet} from "react-native";
|
||||
|
||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||
import {useBrainDumpContext} from "@/contexts/DumpContext";
|
||||
import KeyboardManager from "react-native-keyboard-manager";
|
||||
|
||||
|
||||
interface IAddBrainDumpProps {
|
||||
isVisible: boolean;
|
||||
setIsVisible: (value: boolean) => void;
|
||||
isVisible: boolean;
|
||||
setIsVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const AddBrainDump = ({
|
||||
addBrainDumpProps,
|
||||
}: {
|
||||
addBrainDumpProps: IAddBrainDumpProps;
|
||||
addBrainDumpProps,
|
||||
}: {
|
||||
addBrainDumpProps: IAddBrainDumpProps;
|
||||
}) => {
|
||||
const { addBrainDump } = useBrainDumpContext();
|
||||
const [dumpTitle, setDumpTitle] = useState<string>("");
|
||||
const [dumpDesc, setDumpDesc] = useState<string>("");
|
||||
const { width } = Dimensions.get("screen");
|
||||
const {addBrainDump} = useBrainDumpContext();
|
||||
const [dumpTitle, setDumpTitle] = useState<string>("");
|
||||
const [dumpDesc, setDumpDesc] = useState<string>("");
|
||||
const {width} = Dimensions.get("screen");
|
||||
|
||||
|
||||
// Refs for the two TextFields
|
||||
const descriptionRef = useRef<TextFieldRef>(null);
|
||||
const titleRef = useRef<TextFieldRef>(null);
|
||||
// Refs for the two TextFields
|
||||
const descriptionRef = useRef<TextFieldRef>(null);
|
||||
const titleRef = useRef<TextFieldRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDumpDesc("");
|
||||
setDumpTitle("");
|
||||
}, [addBrainDumpProps.isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (addBrainDumpProps.isVisible) {
|
||||
setTimeout(() => {
|
||||
titleRef?.current?.focus()
|
||||
}, 500)
|
||||
}
|
||||
}, [addBrainDumpProps.isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
setDumpDesc("");
|
||||
setDumpTitle("");
|
||||
}, [addBrainDumpProps.isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
titleRef?.current?.focus()
|
||||
}, 500)
|
||||
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
KeyboardManager.setEnableAutoToolbar(false);
|
||||
},[])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
bottom={true}
|
||||
height={"90%"}
|
||||
width={width}
|
||||
panDirection={PanningDirectionsEnum.DOWN}
|
||||
onDismiss={() => addBrainDumpProps.setIsVisible(false)}
|
||||
containerStyle={styles.dialogContainer}
|
||||
visible={addBrainDumpProps.isVisible}
|
||||
>
|
||||
<View row spread style={styles.topBtns} marginB-20>
|
||||
<Button
|
||||
color="#05a8b6"
|
||||
label="Cancel"
|
||||
style={styles.topBtn}
|
||||
onPress={() => {
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
||||
<DropModalIcon style={{ marginTop: 15 }} />
|
||||
</TouchableOpacity>
|
||||
<Button
|
||||
color="#05a8b6"
|
||||
label="Save"
|
||||
style={styles.topBtn}
|
||||
onPress={() => {
|
||||
addBrainDump({ id: 99, title: dumpTitle.trimEnd().trimStart(), description: dumpDesc.trimEnd().trimStart() });
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View marginH-20>
|
||||
<TextField
|
||||
value={dumpTitle}
|
||||
ref={titleRef}
|
||||
placeholder="Set Title"
|
||||
text60R
|
||||
onChangeText={(text) => {
|
||||
setDumpTitle(text);
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
// Move focus to the description field
|
||||
descriptionRef.current?.focus();
|
||||
}}
|
||||
style={styles.title}
|
||||
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
||||
returnKeyType="next"
|
||||
/>
|
||||
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
|
||||
<TextField
|
||||
ref={descriptionRef}
|
||||
value={dumpDesc}
|
||||
placeholder="Write Description"
|
||||
text70
|
||||
onChangeText={(text) => {
|
||||
setDumpDesc(text);
|
||||
}}
|
||||
style={styles.description}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
maxLength={255}
|
||||
onEndEditing={() => {
|
||||
descriptionRef.current?.blur();
|
||||
}}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</View>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
bottom={true}
|
||||
height={"90%"}
|
||||
width={width}
|
||||
panDirection={PanningDirectionsEnum.DOWN}
|
||||
onDismiss={() => addBrainDumpProps.setIsVisible(false)}
|
||||
containerStyle={styles.dialogContainer}
|
||||
visible={addBrainDumpProps.isVisible}
|
||||
>
|
||||
<View row spread style={styles.topBtns} marginB-20>
|
||||
<Button
|
||||
color="#05a8b6"
|
||||
label="Cancel"
|
||||
style={styles.topBtn}
|
||||
onPress={() => {
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
||||
<DropModalIcon style={{marginTop: 15}}/>
|
||||
</TouchableOpacity>
|
||||
<Button
|
||||
color="#05a8b6"
|
||||
label="Save"
|
||||
style={styles.topBtn}
|
||||
onPress={() => {
|
||||
addBrainDump({
|
||||
|
||||
id: 99,
|
||||
|
||||
title: dumpTitle.trimEnd().trimStart(),
|
||||
|
||||
description: dumpDesc.trimEnd().trimStart(),
|
||||
|
||||
});
|
||||
addBrainDumpProps.setIsVisible(false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View marginH-20>
|
||||
<TextField
|
||||
value={dumpTitle}
|
||||
ref={titleRef}
|
||||
placeholder="Set Title"
|
||||
text60R
|
||||
onChangeText={(text) => {
|
||||
setDumpTitle(text);
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
// Move focus to the description field
|
||||
descriptionRef.current?.focus();
|
||||
}}
|
||||
style={styles.title}
|
||||
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
||||
returnKeyType="next"
|
||||
/>
|
||||
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
|
||||
<TextField
|
||||
ref={descriptionRef}
|
||||
value={dumpDesc}
|
||||
placeholder="Write Description"
|
||||
text70
|
||||
onChangeText={(text) => {
|
||||
setDumpDesc(text);
|
||||
}}
|
||||
style={styles.description}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
maxLength={255}
|
||||
onEndEditing={() => {
|
||||
descriptionRef.current?.blur();
|
||||
}}
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</View>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@ -144,7 +145,7 @@ const styles = StyleSheet.create({
|
||||
description: {
|
||||
fontFamily: "Manrope_400Regular",
|
||||
fontSize: 14,
|
||||
textAlignVertical: 'top'
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -13,6 +13,7 @@ export default function CalendarPage() {
|
||||
<HeaderTemplate
|
||||
message={"Let's get your week started!"}
|
||||
isWelcome
|
||||
isCalendar={true}
|
||||
/>
|
||||
<InnerCalendar/>
|
||||
</View>
|
||||
|
||||
@ -1,90 +1,92 @@
|
||||
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
||||
import React, {useState} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
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 (
|
||||
return (
|
||||
<View
|
||||
row
|
||||
spread
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// iOS shadow
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
// Android shadow (elevation)
|
||||
elevation: 6,
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setIsFamilyView(true);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
row
|
||||
spread
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
borderRadius: 30,
|
||||
backgroundColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// iOS shadow
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {width: 0, height: 2},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
// Android shadow (elevation)
|
||||
elevation: 6,
|
||||
}}
|
||||
centerV
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setCalView(true);
|
||||
viewSwitch(true);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={calView ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
|
||||
Family View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setCalView(false);
|
||||
viewSwitch(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={!calView ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
|
||||
My View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<Text
|
||||
color={isFamilyView ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
Family View
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setIsFamilyView(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
centerV
|
||||
centerH
|
||||
height={40}
|
||||
paddingH-15
|
||||
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||
>
|
||||
<Text
|
||||
color={!isFamilyView ? "white" : "#a1a1a1"}
|
||||
style={styles.switchTxt}
|
||||
>
|
||||
My View
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarViewSwitch;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
switchBtnActive: {
|
||||
backgroundColor: "#a1a1a1",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchTxt: {
|
||||
fontSize: 16,
|
||||
fontFamily: 'Manrope_600SemiBold'
|
||||
}
|
||||
switchBtnActive: {
|
||||
backgroundColor: "#a1a1a1",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 50,
|
||||
},
|
||||
switchTxt: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
|
||||
@ -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() &&
|
||||
@ -127,7 +118,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
}, [mode]);
|
||||
|
||||
|
||||
const { enrichedEvents, filteredEvents } = useMemo(() => {
|
||||
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||
const startTime = Date.now(); // Start timer
|
||||
|
||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
@ -154,16 +145,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
overlapCount: 0
|
||||
});
|
||||
|
||||
// Sort events for this dateKey from oldest to newest by event.start
|
||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, CalendarEvent[]>);
|
||||
|
||||
const endTime = Date.now(); // End timer
|
||||
const endTime = Date.now();
|
||||
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||
|
||||
return { enrichedEvents, filteredEvents };
|
||||
return {enrichedEvents, filteredEvents};
|
||||
}, [events, selectedDate, mode]);
|
||||
|
||||
const renderCustomDateForMonth = (date: Date) => {
|
||||
@ -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}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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%",
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,218 +1,183 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
Dialog,
|
||||
Text,
|
||||
TextField,
|
||||
TextFieldRef,
|
||||
View,
|
||||
Button,
|
||||
ButtonSize,
|
||||
Colors,
|
||||
KeyboardAwareScrollView,
|
||||
LoaderScreen,
|
||||
Text,
|
||||
TextField,
|
||||
TextFieldRef,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useSignIn } from "@/hooks/firebase/useSignIn";
|
||||
import { StyleSheet } from "react-native";
|
||||
import React, {useRef, useState} from "react";
|
||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
||||
import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
|
||||
import { Camera, CameraView } from "expo-camera";
|
||||
import KeyboardManager from "react-native-keyboard-manager";
|
||||
import {SafeAreaView} from "react-native-safe-area-context";
|
||||
import {useRouter} from "expo-router";
|
||||
|
||||
const SignInPage = ({
|
||||
setTab,
|
||||
}: {
|
||||
setTab: React.Dispatch<
|
||||
React.SetStateAction<"register" | "login" | "reset-password">
|
||||
>;
|
||||
}) => {
|
||||
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);
|
||||
KeyboardManager.setEnableAutoToolbar(true);
|
||||
|
||||
const { mutateAsync: signIn, error, isError } = useSignIn();
|
||||
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode();
|
||||
const SignInPage = () => {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const passwordRef = useRef<TextFieldRef>(null);
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await signIn({ email, password });
|
||||
if (!isError) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Login successful!",
|
||||
});
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Error logging in",
|
||||
text2: `${error}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
|
||||
|
||||
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 router = useRouter()
|
||||
|
||||
const getCameraPermissions = async (callback: () => void) => {
|
||||
const { status } = await Camera.requestCameraPermissionsAsync();
|
||||
setHasPermission(status === "granted");
|
||||
if (status === "granted") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const handleSignIn = async () => {
|
||||
await signIn({email, password});
|
||||
if (!isError) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Login successful!",
|
||||
});
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Error logging in",
|
||||
text2: `${error}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View padding-10 centerV height={"100%"}>
|
||||
<TextField
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
|
||||
onChangeText={setEmail}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
// Move focus to the description field
|
||||
passwordRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
style={styles.textfield}
|
||||
/>
|
||||
<Button
|
||||
label="Log in"
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={handleSignIn}
|
||||
style={{ marginBottom: 20, height: 50 }}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
<Button
|
||||
label="Log in with a QR Code"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={() => {
|
||||
getCameraPermissions(() => setShowCameraDialog(true));
|
||||
}}
|
||||
style={{ marginBottom: 20, height: 50 }}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
{isError && (
|
||||
<Text center style={{ marginBottom: 20 }}>{`${
|
||||
error?.toString()?.split("]")?.[1]
|
||||
}`}</Text>
|
||||
)}
|
||||
return (
|
||||
<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>
|
||||
|
||||
<View row centerH marginB-5 gap-5>
|
||||
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
||||
<Button
|
||||
onPress={() => setTab("register")}
|
||||
label="Sign Up"
|
||||
labelStyle={[
|
||||
styles.jakartaMedium,
|
||||
{ textDecorationLine: "none", color: "#fd1575" },
|
||||
]}
|
||||
link
|
||||
size={ButtonSize.xSmall}
|
||||
padding-0
|
||||
margin-0
|
||||
text70
|
||||
left
|
||||
color="#fd1775"
|
||||
/>
|
||||
</View>
|
||||
<KeyboardAvoidingView style={{width: "100%"}}
|
||||
contentContainerStyle={{justifyContent: "center"}}
|
||||
keyboardVerticalOffset={50}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<TextField
|
||||
placeholder="Email"
|
||||
keyboardType={"email-address"}
|
||||
returnKeyType={"next"}
|
||||
textContentType={"emailAddress"}
|
||||
defaultValue={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.textfield}
|
||||
autoComplete={"email"}
|
||||
autoCorrect={false}
|
||||
onSubmitEditing={() => {
|
||||
// Move focus to the description field
|
||||
passwordRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
textContentType={"oneTimeCode"}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
style={styles.textfield}
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
<View 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 flexG/>
|
||||
|
||||
{/* Camera Dialog */}
|
||||
<Dialog
|
||||
visible={showCameraDialog}
|
||||
onDismiss={() => setShowCameraDialog(false)}
|
||||
bottom
|
||||
width="100%"
|
||||
height="70%"
|
||||
containerStyle={{ padding: 15, backgroundColor:"white" }}
|
||||
>
|
||||
{hasPermission === null ? (
|
||||
<Text>Requesting camera permissions...</Text>
|
||||
) : !hasPermission ? (
|
||||
<Text>No access to camera</Text>
|
||||
) : (
|
||||
<CameraView
|
||||
style={{ flex: 1, borderRadius: 15 }}
|
||||
onBarcodeScanned={handleQrCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr"],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
label="Cancel"
|
||||
onPress={() => setShowCameraDialog(false)}
|
||||
backgroundColor="#fd1775"
|
||||
style={{ margin: 10, marginBottom: 30 }}
|
||||
/>
|
||||
</Dialog>
|
||||
</View>
|
||||
);
|
||||
<Button
|
||||
label="Log in"
|
||||
marginT-50
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={handleSignIn}
|
||||
style={{marginBottom: 20, height: 50}}
|
||||
backgroundColor="#fd1775"
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<Text center style={{marginBottom: 20}}>{`${
|
||||
error?.toString()?.split("]")?.[1]
|
||||
}`}</Text>
|
||||
)}
|
||||
|
||||
<View row centerH marginB-5 gap-5>
|
||||
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
||||
<Button
|
||||
onPress={() => router.replace("/(unauth)/sign_up")}
|
||||
label="Sign Up"
|
||||
labelStyle={[
|
||||
styles.jakartaMedium,
|
||||
{textDecorationLine: "none", color: "#fd1575"},
|
||||
]}
|
||||
link
|
||||
size={ButtonSize.xSmall}
|
||||
padding-0
|
||||
margin-0
|
||||
text70
|
||||
left
|
||||
color="#fd1775"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/*<View row centerH marginB-5 gap-5>*/}
|
||||
{/* <Text text70>Forgot your password?</Text>*/}
|
||||
{/* <Button*/}
|
||||
{/* onPress={() => router.replace("/(unauth)/sign_up")}*/}
|
||||
{/* label="Reset password"*/}
|
||||
{/* labelStyle={[*/}
|
||||
{/* styles.jakartaMedium,*/}
|
||||
{/* {textDecorationLine: "none", color: "#fd1575"},*/}
|
||||
{/* ]}*/}
|
||||
{/* link*/}
|
||||
{/* size={ButtonSize.xSmall}*/}
|
||||
{/* padding-0*/}
|
||||
{/* margin-0*/}
|
||||
{/* text70*/}
|
||||
{/* left*/}
|
||||
{/* avoidInnerPadding*/}
|
||||
{/* color="#fd1775"*/}
|
||||
{/* />*/}
|
||||
{/*</View>*/}
|
||||
|
||||
{isLoading && (
|
||||
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white}
|
||||
color={Colors.grey40}/>
|
||||
)}
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 10,
|
||||
padding: 30,
|
||||
height: 45,
|
||||
borderRadius: 50,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
},
|
||||
jakartaLight: {
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 16,
|
||||
color: "#484848",
|
||||
},
|
||||
jakartaMedium: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
color: "#919191",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 10,
|
||||
padding: 30,
|
||||
height: 45,
|
||||
borderRadius: 50,
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
},
|
||||
jakartaLight: {
|
||||
fontFamily: "PlusJakartaSans_300Light",
|
||||
fontSize: 16,
|
||||
color: "#484848",
|
||||
},
|
||||
jakartaMedium: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
color: "#919191",
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
|
||||
export default SignInPage;
|
||||
|
||||
@ -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,154 +30,196 @@ const SignUpPage = ({
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||
const {mutateAsync: signUp} = useSignUp();
|
||||
const {mutateAsync: signUp, isLoading} = useSignUp();
|
||||
|
||||
const lnameRef = useRef<TextFieldRef>(null);
|
||||
const emailRef = useRef<TextFieldRef>(null);
|
||||
const passwordRef = useRef<TextFieldRef>(null);
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignUp = async () => {
|
||||
await signUp({email, password, firstName, lastName});
|
||||
router.replace("/(unauth)/cal_sync")
|
||||
};
|
||||
|
||||
return (
|
||||
<View height={"100%"} padding-15 marginT-30>
|
||||
<Text style={styles.title}>Get started with Cally</Text>
|
||||
<Text style={styles.subtitle} marginT-15 color="#919191">
|
||||
Please enter your details.
|
||||
</Text>
|
||||
<TextField
|
||||
marginT-30
|
||||
autoFocus
|
||||
placeholder="First name"
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
lnameRef.current?.focus();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TextField
|
||||
ref={lnameRef}
|
||||
placeholder="Last name"
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
emailRef.current?.focus();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TextField
|
||||
ref={emailRef}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
passwordRef.current?.focus();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<View
|
||||
centerV
|
||||
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
|
||||
>
|
||||
<TextField
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
style={styles.jakartaLight}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
trailingAccessory={
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
>
|
||||
<AntDesign
|
||||
name={isPasswordVisible ? "eye" : "eyeo"}
|
||||
size={24}
|
||||
color="gray"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View gap-5 marginT-15>
|
||||
<View row centerV>
|
||||
<Checkbox
|
||||
style={[styles.check]}
|
||||
color="#919191"
|
||||
value={allowFaceID}
|
||||
onValueChange={(value) => {
|
||||
setAllowFaceID(value);
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.jakartaLight} marginL-10>
|
||||
Allow FaceID for login in future
|
||||
</Text>
|
||||
</View>
|
||||
<View row centerV>
|
||||
<Checkbox
|
||||
style={styles.check}
|
||||
color="#919191"
|
||||
value={acceptTerms}
|
||||
onValueChange={(value) => setAcceptTerms(value)}
|
||||
/>
|
||||
<View row>
|
||||
<Text style={styles.jakartaLight} marginL-10>
|
||||
I accept the
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
|
||||
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
|
||||
<View gap-13 width={"100%"} marginB-20>
|
||||
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||
Get started with Cally
|
||||
</Text>
|
||||
<TouchableOpacity>
|
||||
<Text text90 style={styles.jakartaMedium}>
|
||||
{" "}
|
||||
terms and conditions
|
||||
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||
Please enter your details.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView style={{width: '100%'}}>
|
||||
<TextField
|
||||
marginT-30
|
||||
autoFocus
|
||||
placeholder="First name"
|
||||
value={firstName}
|
||||
onChangeText={setFirstName}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
lnameRef.current?.focus();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
accessibilityLabel="First name input"
|
||||
accessibilityHint="Enter your first name"
|
||||
accessible
|
||||
returnKeyType="next"
|
||||
textContentType="givenName"
|
||||
importantForAccessibility="yes"
|
||||
/>
|
||||
<TextField
|
||||
ref={lnameRef}
|
||||
placeholder="Last name"
|
||||
value={lastName}
|
||||
onChangeText={setLastName}
|
||||
style={styles.textfield}
|
||||
onSubmitEditing={() => {
|
||||
emailRef.current?.focus();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
accessibilityLabel="Last name input"
|
||||
accessibilityHint="Enter your last name"
|
||||
accessible
|
||||
returnKeyType="next"
|
||||
textContentType="familyName"
|
||||
importantForAccessibility="yes"
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Email"
|
||||
keyboardType={"email-address"}
|
||||
returnKeyType={"next"}
|
||||
textContentType={"emailAddress"}
|
||||
defaultValue={email}
|
||||
onChangeText={setEmail}
|
||||
style={styles.textfield}
|
||||
autoComplete={"email"}
|
||||
autoCorrect={false}
|
||||
ref={emailRef}
|
||||
onSubmitEditing={() => {
|
||||
passwordRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
centerV
|
||||
style={[styles.textfield, {padding: 0, paddingHorizontal: 30}]}
|
||||
>
|
||||
<TextField
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
style={styles.jakartaLight}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
trailingAccessory={
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
>
|
||||
<AntDesign
|
||||
name={isPasswordVisible ? "eye" : "eyeo"}
|
||||
size={24}
|
||||
color="gray"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
<View gap-5 marginT-15>
|
||||
<View row centerV>
|
||||
<Checkbox
|
||||
style={[styles.check]}
|
||||
color="#919191"
|
||||
value={allowFaceID}
|
||||
onValueChange={(value) => {
|
||||
setAllowFaceID(value);
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.jakartaLight} marginL-10>
|
||||
Allow FaceID for login in future
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.jakartaLight}> and </Text>
|
||||
<TouchableOpacity>
|
||||
<Text text90 style={styles.jakartaMedium}>
|
||||
{" "}
|
||||
privacy policy
|
||||
</View>
|
||||
<View row centerV>
|
||||
<Checkbox
|
||||
style={styles.check}
|
||||
color="#919191"
|
||||
value={acceptTerms}
|
||||
onValueChange={(value) => setAcceptTerms(value)}
|
||||
/>
|
||||
<View row style={{flexWrap: "wrap", marginLeft: 10}}>
|
||||
<Text style={styles.jakartaLight}>
|
||||
I accept the
|
||||
</Text>
|
||||
<TouchableOpacity>
|
||||
<Text text90 style={styles.jakartaMedium}>
|
||||
{" "}
|
||||
terms and conditions
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.jakartaLight}> and </Text>
|
||||
<TouchableOpacity>
|
||||
<Text text90 style={styles.jakartaMedium}>
|
||||
{" "}
|
||||
privacy policy
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View flexG style={{minHeight: 50}}/>
|
||||
|
||||
<View>
|
||||
<Button
|
||||
label="Register"
|
||||
disabled={!acceptTerms}
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={handleSignUp}
|
||||
backgroundColor={"#fd1775"}
|
||||
style={{marginBottom: 0, height: 50}}
|
||||
/>
|
||||
<View row centerH marginT-10 marginB-2 gap-5>
|
||||
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
|
||||
Already have an account?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Button
|
||||
label="Log in"
|
||||
labelStyle={[
|
||||
styles.jakartaMedium,
|
||||
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
|
||||
]}
|
||||
flexS
|
||||
margin-0
|
||||
link
|
||||
color="#fd1775"
|
||||
size={ButtonSize.small}
|
||||
text70
|
||||
onPress={() => router.replace("/(unauth)/sign_in")}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View flex-1/>
|
||||
<View style={styles.bottomView}>
|
||||
<Button
|
||||
label="Register"
|
||||
labelStyle={{
|
||||
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onPress={handleSignUp}
|
||||
style={{marginBottom: 0, backgroundColor: "#fd1775", height: 50}}
|
||||
/>
|
||||
<View row centerH marginT-10 marginB-2 gap-5>
|
||||
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center>
|
||||
Already have an account?
|
||||
</Text>
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<Button
|
||||
label="Log in"
|
||||
labelStyle={[
|
||||
styles.jakartaMedium,
|
||||
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
|
||||
]}
|
||||
flexS
|
||||
margin-0
|
||||
link
|
||||
color="#fd1775"
|
||||
size={ButtonSize.small}
|
||||
text70
|
||||
onPress={() => setTab("login")}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{isLoading && (
|
||||
<LoaderScreen overlay message={"Signing up..."} backgroundColor={Colors.white}
|
||||
color={Colors.grey40}/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 }} />
|
||||
|
||||
@ -1,115 +1,131 @@
|
||||
import { 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 {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 { settingsPageIndex, userSettingsView } from "../calendar/atoms";
|
||||
import { AuthContextProvider } from "@/contexts/AuthContext";
|
||||
import {useAtom, useSetAtom} from "jotai";
|
||||
import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
|
||||
const UserSettings = () => {
|
||||
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
||||
const [userView, setUserView] = useAtom(userSettingsView);
|
||||
return (
|
||||
<AuthContextProvider>
|
||||
<View flexG>
|
||||
<ScrollView style={{ paddingBottom: 20, minHeight: "100%" }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setPageIndex(0);
|
||||
setUserView(true);
|
||||
}}
|
||||
>
|
||||
<View row marginT-20 marginB-20 marginL-20 centerV>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={14}
|
||||
color="#979797"
|
||||
style={{ paddingBottom: 3 }}
|
||||
/>
|
||||
<Text
|
||||
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
||||
color="#979797"
|
||||
>
|
||||
Return to main settings
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View marginH-26 flexG style={{ minHeight: "90%" }}>
|
||||
<Text text60R marginB-25>
|
||||
User Management
|
||||
</Text>
|
||||
<View style={styles.buttonSwitch} spread row>
|
||||
<TouchableOpacity
|
||||
onPress={() => setUserView(true)}
|
||||
centerV
|
||||
centerH
|
||||
style={userView == true ? styles.btnSelected : styles.btnNot}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.btnTxt}
|
||||
color={userView ? "white" : "black"}
|
||||
>
|
||||
My Profile
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => setUserView(false)}
|
||||
centerV
|
||||
centerH
|
||||
style={userView == false ? styles.btnSelected : styles.btnNot}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.btnTxt}
|
||||
color={!userView ? "white" : "black"}
|
||||
>
|
||||
My Group
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{userView && <MyProfile />}
|
||||
{!userView && <MyGroup />}
|
||||
</View>
|
||||
</ScrollView>
|
||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||
const [userView, setUserView] = useAtom(userSettingsView);
|
||||
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
|
||||
|
||||
{!userView && (
|
||||
<View>
|
||||
<Text>selview</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</AuthContextProvider>
|
||||
);
|
||||
return (
|
||||
<View flexG>
|
||||
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setPageIndex(0);
|
||||
setUserView(true);
|
||||
}}
|
||||
>
|
||||
<View row marginT-20 marginB-20 marginL-20 centerV>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={14}
|
||||
color="#979797"
|
||||
style={{paddingBottom: 3}}
|
||||
/>
|
||||
<Text
|
||||
style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
|
||||
color="#979797"
|
||||
>
|
||||
Return to main settings
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View marginH-26 flexG style={{minHeight: "90%"}}>
|
||||
<Text text60R marginB-25>
|
||||
User Management
|
||||
</Text>
|
||||
<View style={styles.buttonSwitch} spread row>
|
||||
<TouchableOpacity
|
||||
onPress={() => setUserView(true)}
|
||||
centerV
|
||||
centerH
|
||||
style={userView == true ? styles.btnSelected : styles.btnNot}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.btnTxt}
|
||||
color={userView ? "white" : "black"}
|
||||
>
|
||||
My Profile
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => setUserView(false)}
|
||||
centerV
|
||||
centerH
|
||||
style={userView == false ? styles.btnSelected : styles.btnNot}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.btnTxt}
|
||||
color={!userView ? "white" : "black"}
|
||||
>
|
||||
My Group
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{userView && <MyProfile/>}
|
||||
{!userView && <MyGroup onNewUserClick={onNewUserClick} setOnNewUserClick={setOnNewUserClick}/>}
|
||||
</View>
|
||||
</ScrollView>
|
||||
{!userView && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonSwitch: {
|
||||
borderRadius: 50,
|
||||
width: "100%",
|
||||
backgroundColor: "#ebebeb",
|
||||
height: 45,
|
||||
},
|
||||
btnSelected: {
|
||||
backgroundColor: "#05a8b6",
|
||||
height: "100%",
|
||||
width: "50%",
|
||||
borderRadius: 50,
|
||||
},
|
||||
btnTxt: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 15,
|
||||
},
|
||||
btnNot: {
|
||||
height: "100%",
|
||||
width: "50%",
|
||||
borderRadius: 50,
|
||||
},
|
||||
title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
|
||||
bottomButton: {
|
||||
position: "absolute",
|
||||
bottom: 15,
|
||||
marginHorizontal: 28,
|
||||
width: 337,
|
||||
backgroundColor: "#e8156c",
|
||||
height: 53.26,
|
||||
},
|
||||
buttonSwitch: {
|
||||
borderRadius: 50,
|
||||
width: "100%",
|
||||
backgroundColor: "#ebebeb",
|
||||
height: 45,
|
||||
},
|
||||
btnSelected: {
|
||||
backgroundColor: "#05a8b6",
|
||||
height: "100%",
|
||||
width: "50%",
|
||||
borderRadius: 50,
|
||||
},
|
||||
btnTxt: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 15,
|
||||
},
|
||||
btnNot: {
|
||||
height: "100%",
|
||||
width: "50%",
|
||||
borderRadius: 50,
|
||||
},
|
||||
title: {fontFamily: "Manrope_600SemiBold", fontSize: 18},
|
||||
});
|
||||
|
||||
export default UserSettings;
|
||||
|
||||
@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -22,7 +22,7 @@ const AddChore = () => {
|
||||
>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
marginH-20
|
||||
marginB-30
|
||||
size={ButtonSize.large}
|
||||
style={styles.button}
|
||||
onPress={() => setIsVisible(!isVisible)}
|
||||
|
||||
@ -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";
|
||||
@ -60,15 +61,14 @@ const ToDoItem = (props: {
|
||||
|
||||
return (
|
||||
<View
|
||||
key={props.item.id}
|
||||
key={props.item.id}
|
||||
centerV
|
||||
paddingV-10
|
||||
paddingH-13
|
||||
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) => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }}>
|
||||
|
||||
@ -30,8 +30,21 @@ const UserChoresProgress = ({
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||
<Text style={{ fontSize: 14 }}>Back to ToDos</Text>
|
||||
</TouchableOpacity>
|
||||
<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 }}>
|
||||
Your To Do's Progress Report
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,7 +8,7 @@ import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as Device from 'expo-device';
|
||||
import Constants from 'expo-constants';
|
||||
import { Platform } from 'react-native';
|
||||
import {Platform} from 'react-native';
|
||||
import {useQueryClient} from "react-query";
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ interface IAuthContext {
|
||||
profileType?: ProfileType,
|
||||
profileData?: UserProfile,
|
||||
setProfileData: (profileData: UserProfile) => void,
|
||||
setRedirectOverride: (val: boolean) => void,
|
||||
refreshProfileData: () => Promise<void>
|
||||
}
|
||||
|
||||
@ -50,16 +51,16 @@ async function registerForPushNotificationsAsync() {
|
||||
}
|
||||
|
||||
if (Device.isDevice) {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
const {status: existingStatus} = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
const {status} = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
|
||||
if (finalStatus !== 'granted') {
|
||||
alert('Failed to get push token for push notification!');
|
||||
// alert('Failed to get push token for push notification!');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,15 +73,15 @@ async function registerForPushNotificationsAsync() {
|
||||
}
|
||||
|
||||
try {
|
||||
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
|
||||
const token = (await Notifications.getExpoPushTokenAsync({projectId})).data;
|
||||
console.log('Push Token:', token);
|
||||
return token;
|
||||
} catch (error) {
|
||||
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,24 +92,28 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
||||
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
||||
const [redirectOverride, setRedirectOverride] = useState(false);
|
||||
|
||||
const {replace} = useRouter();
|
||||
const ready = !initializing;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
||||
setUser(authUser);
|
||||
if (!redirectOverride) {
|
||||
|
||||
setUser(authUser);
|
||||
|
||||
if (authUser) {
|
||||
await refreshProfileData(authUser);
|
||||
const pushToken = await registerForPushNotificationsAsync();
|
||||
if (pushToken) {
|
||||
await savePushTokenToFirestore(authUser.uid, pushToken);
|
||||
if (authUser) {
|
||||
await refreshProfileData(authUser);
|
||||
const pushToken = await registerForPushNotificationsAsync();
|
||||
if (pushToken) {
|
||||
await savePushTokenToFirestore(authUser.uid, pushToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initializing) setInitializing(false);
|
||||
if (initializing) setInitializing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
||||
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
||||
}, [initializing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ready && user) {
|
||||
if (ready && user && !redirectOverride) {
|
||||
replace({pathname: "/(auth)/calendar"});
|
||||
} else if (ready && !user) {
|
||||
} else if (ready && !user && !redirectOverride) {
|
||||
replace({pathname: "/(unauth)"});
|
||||
}
|
||||
}, [user, ready]);
|
||||
}, [user, ready, redirectOverride]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
||||
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
|
||||
<AuthContext.Provider
|
||||
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@ -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
|
||||
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',
|
||||
});
|
||||
async function refreshGoogleToken(refreshToken) {
|
||||
try {
|
||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
||||
grant_type: 'refresh_token',
|
||||
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) {
|
||||
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',
|
||||
});
|
||||
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: 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() {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import {useMutation, useQueryClient} from "react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import storage from "@react-native-firebase/storage";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { Platform } from "react-native";
|
||||
import {Platform} from "react-native";
|
||||
|
||||
export const useChangeProfilePicture = () => {
|
||||
export const useChangeProfilePicture = (customUserId?: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { user, refreshProfileData } = useAuthContext();
|
||||
const {user, refreshProfileData} = useAuthContext();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["changeProfilePicture"],
|
||||
@ -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 });
|
||||
.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
|
||||
queryClient.invalidateQueries("Profiles");
|
||||
refreshProfileData();
|
||||
if (!customUserId) {
|
||||
queryClient.invalidateQueries("Profiles");
|
||||
refreshProfileData();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
39
hooks/firebase/useClearTokens.ts
Normal file
39
hooks/firebase/useClearTokens.ts
Normal 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});
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -45,34 +45,41 @@ export const useCreateEvent = () => {
|
||||
}
|
||||
|
||||
export const useCreateEventsFromProvider = () => {
|
||||
const {user: currentUser} = useAuthContext();
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
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});
|
||||
.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});
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import {useMutation} from "react-query";
|
||||
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
|
||||
export const useLoginWithQrCode = () => {
|
||||
const {setRedirectOverride} = useAuthContext()
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["loginWithQrCode"],
|
||||
mutationFn: async ({userId}: { userId: string }) => {
|
||||
try {
|
||||
setRedirectOverride(true)
|
||||
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
||||
token: string
|
||||
}>
|
||||
|
||||
@ -1,44 +1,47 @@
|
||||
import { useMutation } from "react-query";
|
||||
import {useMutation} from "react-query";
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import { ProfileType } from "@/contexts/AuthContext";
|
||||
import { useSetUserData } from "./useSetUserData";
|
||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useSetUserData} from "./useSetUserData";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import * as Localization from "expo-localization";
|
||||
|
||||
export const useSignUp = () => {
|
||||
const { mutateAsync: setUserData } = useSetUserData();
|
||||
const {setRedirectOverride} = useAuthContext()
|
||||
const {mutateAsync: setUserData} = useSetUserData();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["signUp"],
|
||||
mutationFn: async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) => {
|
||||
await auth()
|
||||
.createUserWithEmailAndPassword(email, password)
|
||||
.then(async (res) => {
|
||||
try {
|
||||
await setUserData({
|
||||
newUserData: {
|
||||
userType: ProfileType.PARENT,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
familyId: uuidv4(),
|
||||
timeZone: Localization.getCalendars()[0].timeZone,
|
||||
},
|
||||
customUser: res.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
return useMutation({
|
||||
mutationKey: ["signUp"],
|
||||
mutationFn: async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) => {
|
||||
setRedirectOverride(true)
|
||||
|
||||
await auth()
|
||||
.createUserWithEmailAndPassword(email, password)
|
||||
.then(async (res) => {
|
||||
try {
|
||||
await setUserData({
|
||||
newUserData: {
|
||||
userType: ProfileType.PARENT,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
familyId: uuidv4(),
|
||||
timeZone: Localization.getCalendars()[0].timeZone,
|
||||
},
|
||||
customUser: res.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
288
hooks/useCalSync.ts
Normal file
288
hooks/useCalSync.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useEffect} from "react";
|
||||
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
|
||||
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
|
||||
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import * as Google from "expo-auth-session/providers/google";
|
||||
import * as AuthSession from "expo-auth-session";
|
||||
import * as AppleAuthentication from "expo-apple-authentication";
|
||||
|
||||
const googleConfig = {
|
||||
androidClientId:
|
||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||
iosClientId:
|
||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||
webClientId:
|
||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||
scopes: [
|
||||
"email",
|
||||
"profile",
|
||||
"https://www.googleapis.com/auth/calendar.events.owned",
|
||||
],
|
||||
extraParams: {
|
||||
access_type: "offline",
|
||||
},
|
||||
};
|
||||
|
||||
const microsoftConfig = {
|
||||
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
||||
redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
|
||||
scopes: [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"offline_access",
|
||||
"Calendars.ReadWrite",
|
||||
"User.Read",
|
||||
],
|
||||
authorizationEndpoint:
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
};
|
||||
|
||||
export const useCalSync = () => {
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
|
||||
useFetchAndSaveGoogleEvents();
|
||||
const {
|
||||
mutateAsync: fetchAndSaveOutlookEvents,
|
||||
isLoading: isSyncingOutlook,
|
||||
} = useFetchAndSaveOutlookEvents();
|
||||
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
|
||||
useFetchAndSaveAppleEvents();
|
||||
|
||||
WebBrowser.maybeCompleteAuthSession();
|
||||
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
||||
|
||||
useEffect(() => {
|
||||
signInWithGoogle();
|
||||
}, [response]);
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
try {
|
||||
if (response?.type === "success") {
|
||||
const {accessToken, refreshToken} = response?.authentication!;
|
||||
|
||||
const userInfoResponse = await fetch(
|
||||
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
{
|
||||
headers: {Authorization: `Bearer ${accessToken}`},
|
||||
}
|
||||
);
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
const googleMail = userInfo.email;
|
||||
|
||||
let googleAccounts = profileData?.googleAccounts || {};
|
||||
const updatedGoogleAccounts = {
|
||||
...googleAccounts,
|
||||
[googleMail]: {accessToken, refreshToken},
|
||||
};
|
||||
|
||||
await updateUserData({
|
||||
newUserData: {googleAccounts: updatedGoogleAccounts},
|
||||
});
|
||||
|
||||
await fetchAndSaveGoogleEvents({
|
||||
token: accessToken,
|
||||
email: googleMail,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during Google sign-in:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMicrosoftSignIn = async () => {
|
||||
try {
|
||||
console.log("Starting Microsoft sign-in...");
|
||||
|
||||
const authRequest = new AuthSession.AuthRequest({
|
||||
clientId: microsoftConfig.clientId,
|
||||
scopes: microsoftConfig.scopes,
|
||||
redirectUri: microsoftConfig.redirectUri,
|
||||
responseType: AuthSession.ResponseType.Code,
|
||||
usePKCE: true, // Enable PKCE
|
||||
});
|
||||
|
||||
console.log("Auth request created:", authRequest);
|
||||
|
||||
const authResult = await authRequest.promptAsync({
|
||||
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
|
||||
});
|
||||
|
||||
console.log("Auth result:", authResult);
|
||||
|
||||
if (authResult.type === "success" && authResult.params?.code) {
|
||||
const code = authResult.params.code;
|
||||
console.log("Authorization code received:", code);
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `client_id=${
|
||||
microsoftConfig.clientId
|
||||
}&redirect_uri=${encodeURIComponent(
|
||||
microsoftConfig.redirectUri
|
||||
)}&grant_type=authorization_code&code=${code}&code_verifier=${
|
||||
authRequest.codeVerifier
|
||||
}&scope=${encodeURIComponent(
|
||||
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
|
||||
)}`,
|
||||
});
|
||||
|
||||
console.log("Token response status:", tokenResponse.status);
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text();
|
||||
console.error("Token exchange failed:", errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
console.log("Token data received:", tokenData);
|
||||
|
||||
if (tokenData?.access_token) {
|
||||
console.log("Access token received, fetching user info...");
|
||||
|
||||
// Fetch user info from Microsoft Graph API to get the email
|
||||
const userInfoResponse = await fetch(
|
||||
"https://graph.microsoft.com/v1.0/me",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const userInfo = await userInfoResponse.json();
|
||||
console.log("User info received:", userInfo);
|
||||
|
||||
if (userInfo.error) {
|
||||
console.error("Error fetching user info:", userInfo.error);
|
||||
} else {
|
||||
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
|
||||
|
||||
let microsoftAccounts = profileData?.microsoftAccounts;
|
||||
const updatedMicrosoftAccounts = microsoftAccounts
|
||||
? {...microsoftAccounts, [outlookMail]: tokenData.access_token}
|
||||
: {[outlookMail]: tokenData.access_token};
|
||||
|
||||
await updateUserData({
|
||||
newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
|
||||
});
|
||||
|
||||
await fetchAndSaveOutlookEvents(
|
||||
tokenData.access_token,
|
||||
outlookMail
|
||||
);
|
||||
console.log("User data updated successfully.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("Authentication was not successful:", authResult);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during Microsoft sign-in:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAppleSignIn = async () => {
|
||||
try {
|
||||
console.log("Starting Apple Sign-in...");
|
||||
|
||||
const credential = await AppleAuthentication.signInAsync({
|
||||
requestedScopes: [
|
||||
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
||||
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
||||
],
|
||||
});
|
||||
|
||||
console.log("Apple sign-in result:", credential);
|
||||
|
||||
alert(JSON.stringify(credential))
|
||||
|
||||
const appleToken = credential.identityToken;
|
||||
const appleMail = credential.email!;
|
||||
|
||||
|
||||
if (appleToken) {
|
||||
console.log("Apple ID token received. Fetch user info if needed...");
|
||||
|
||||
let appleAcounts = profileData?.appleAccounts;
|
||||
const updatedAppleAccounts = appleAcounts
|
||||
? {...appleAcounts, [appleMail]: appleToken}
|
||||
: {[appleMail]: appleToken};
|
||||
|
||||
await updateUserData({
|
||||
newUserData: {appleAccounts: updatedAppleAccounts},
|
||||
});
|
||||
|
||||
console.log("User data updated with Apple ID token.");
|
||||
await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
|
||||
} else {
|
||||
console.warn(
|
||||
"Apple authentication was not successful or email was hidden."
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during Apple Sign-in:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let isConnectedToGoogle = false;
|
||||
if (profileData?.googleAccounts) {
|
||||
Object.values(profileData?.googleAccounts).forEach((item) => {
|
||||
if (item !== null) {
|
||||
isConnectedToGoogle = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let isConnectedToMicrosoft = false;
|
||||
const microsoftAccounts = profileData?.microsoftAccounts;
|
||||
if (microsoftAccounts) {
|
||||
Object.values(profileData?.microsoftAccounts).forEach((item) => {
|
||||
if (item !== null) {
|
||||
isConnectedToMicrosoft = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let isConnectedToApple = false;
|
||||
if (profileData?.appleAccounts) {
|
||||
Object.values(profileData?.appleAccounts).forEach((item) => {
|
||||
if (item !== null) {
|
||||
isConnectedToApple = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
handleAppleSignIn,
|
||||
handleMicrosoftSignIn,
|
||||
handleGoogleSignIn: signInWithGoogle,
|
||||
handleStartGoogleSignIn: promptAsync,
|
||||
fetchAndSaveOutlookEvents,
|
||||
fetchAndSaveAppleEvents,
|
||||
fetchAndSaveGoogleEvents,
|
||||
isConnectedToApple,
|
||||
isConnectedToMicrosoft,
|
||||
isConnectedToGoogle,
|
||||
isSyncingOutlook,
|
||||
isSyncingGoogle,
|
||||
isSyncingApple
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
62
hooks/useUploadProfilePicture.ts
Normal file
62
hooks/useUploadProfilePicture.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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
258
yarn.lock
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user