mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 08:24:55 +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 NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||||
import {useAtom, useSetAtom} from "jotai";
|
import { useAtom, useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
|
isFamilyViewAtom,
|
||||||
settingsPageIndex,
|
settingsPageIndex,
|
||||||
toDosPageIndex,
|
toDosPageIndex,
|
||||||
userSettingsView,
|
userSettingsView,
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { mutateAsync: signOut } = useSignOut();
|
const { mutateAsync: signOut } = useSignOut();
|
||||||
|
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
const setUserView = useSetAtom(userSettingsView);
|
const setUserView = useSetAtom(userSettingsView);
|
||||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
@ -79,6 +81,7 @@ export default function TabLayout() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavCalendarIcon />}
|
icon={<NavCalendarIcon />}
|
||||||
/>
|
/>
|
||||||
@ -91,6 +94,7 @@ export default function TabLayout() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavGroceryIcon />}
|
icon={<NavGroceryIcon />}
|
||||||
/>
|
/>
|
||||||
@ -118,6 +122,7 @@ export default function TabLayout() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavToDosIcon />}
|
icon={<NavToDosIcon />}
|
||||||
/>
|
/>
|
||||||
@ -130,6 +135,7 @@ export default function TabLayout() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavBrainDumpIcon />}
|
icon={<NavBrainDumpIcon />}
|
||||||
/>
|
/>
|
||||||
@ -142,6 +148,7 @@ export default function TabLayout() {
|
|||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
setToDosIndex(0);
|
setToDosIndex(0);
|
||||||
setUserView(true);
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
label={"Manage Settings"}
|
label={"Manage Settings"}
|
||||||
labelStyle={styles.label}
|
labelStyle={styles.label}
|
||||||
|
|||||||
246
app/(unauth)/cal_sync.tsx
Normal file
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";
|
import Entry from "@/components/pages/main/Entry";
|
||||||
|
|
||||||
export default function Screen() {
|
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 {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {Button, Image, Text, View} from "react-native-ui-lib";
|
||||||
|
import React from "react";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<Entry/>
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||||
|
<View>
|
||||||
|
<Image source={require("../../assets/images/splash.png")}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View center gap-13>
|
||||||
|
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||||
|
Welcome to Cally
|
||||||
|
</Text>
|
||||||
|
<Text center color={"#919191"} style={{fontSize: 20, maxWidth: 250}}>
|
||||||
|
Lightening Mental Loads,
|
||||||
|
One Family at a Time
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View flexG/>
|
||||||
|
|
||||||
|
<View width={"100%"}>
|
||||||
|
<Button
|
||||||
|
label="Continue"
|
||||||
|
marginT-50
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
onPress={() => router.push("/(unauth)/get_started")}
|
||||||
|
style={{height: 50}}
|
||||||
|
backgroundColor="#fd1775"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
6
app/(unauth)/reset_password.tsx
Normal file
6
app/(unauth)/reset_password.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {ResetPasswordPage} from "@/components/pages/main/ResetPasswordPage";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return <ResetPasswordPage/>
|
||||||
|
}
|
||||||
6
app/(unauth)/sign_in.tsx
Normal file
6
app/(unauth)/sign_in.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import SignInPage from "@/components/pages/main/SignInPage";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return <SignInPage/>
|
||||||
|
}
|
||||||
8
app/(unauth)/sign_up.tsx
Normal file
8
app/(unauth)/sign_up.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SignUpPage from "@/components/pages/main/SignUpPage";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return (
|
||||||
|
<SignUpPage/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -5,13 +5,14 @@ const CloseXIcon: React.FC<SvgProps> = (props) => (
|
|||||||
width={15}
|
width={15}
|
||||||
height={15}
|
height={15}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
viewBox="0 0 15 15"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Path
|
<Path
|
||||||
stroke="#AAA"
|
stroke="#AAA"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={1.394}
|
strokeWidth={props.strokeWidth || 1.394}
|
||||||
d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543"
|
d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543"
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ const PlusIcon = (props: SvgProps) => (
|
|||||||
<Svg
|
<Svg
|
||||||
width={props.width || 14}
|
width={props.width || 14}
|
||||||
height={props.height || 15}
|
height={props.height || 15}
|
||||||
|
viewBox="0 0 14 15"
|
||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import * as Calendar from 'expo-calendar';
|
|||||||
|
|
||||||
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
|
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
|
||||||
try {
|
try {
|
||||||
const {status} = await Calendar.requestCalendarPermissionsAsync();
|
const {granted} = await Calendar.requestCalendarPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
|
||||||
|
if (!granted) {
|
||||||
throw new Error("Calendar permission not granted");
|
throw new Error("Calendar permission not granted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +23,11 @@ export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endD
|
|||||||
return events.map((event) => {
|
return events.map((event) => {
|
||||||
let isAllDay = event.allDay || false;
|
let isAllDay = event.allDay || false;
|
||||||
const startDateTime = new Date(event.startDate);
|
const startDateTime = new Date(event.startDate);
|
||||||
const endDateTime = new Date(event.endDate);
|
let endDateTime = new Date(event.endDate);
|
||||||
|
|
||||||
|
if (isAllDay) {
|
||||||
|
endDateTime = startDateTime
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
|||||||
@ -8,7 +8,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const googleEvents = [];
|
const googleEvents = [];
|
||||||
data.items?.forEach((item) => {
|
data.items?.forEach((item) => {
|
||||||
let isAllDay = false;
|
let isAllDay = false;
|
||||||
@ -49,5 +51,5 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
|
|||||||
googleEvents.push(googleEvent);
|
googleEvents.push(googleEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
return googleEvents;
|
return {googleEvents, success: response.ok};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,126 +1,127 @@
|
|||||||
import {
|
import {Button, Dialog, TextField, TextFieldRef, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
View,
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
Text,
|
import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
|
||||||
Button,
|
import {Dimensions, Platform, StyleSheet} from "react-native";
|
||||||
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 DropModalIcon from "@/assets/svgs/DropModalIcon";
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
import {useBrainDumpContext} from "@/contexts/DumpContext";
|
||||||
import KeyboardManager from "react-native-keyboard-manager";
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
|
||||||
|
|
||||||
interface IAddBrainDumpProps {
|
interface IAddBrainDumpProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
setIsVisible: (value: boolean) => void;
|
setIsVisible: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddBrainDump = ({
|
const AddBrainDump = ({
|
||||||
addBrainDumpProps,
|
addBrainDumpProps,
|
||||||
}: {
|
}: {
|
||||||
addBrainDumpProps: IAddBrainDumpProps;
|
addBrainDumpProps: IAddBrainDumpProps;
|
||||||
}) => {
|
}) => {
|
||||||
const { addBrainDump } = useBrainDumpContext();
|
const {addBrainDump} = useBrainDumpContext();
|
||||||
const [dumpTitle, setDumpTitle] = useState<string>("");
|
const [dumpTitle, setDumpTitle] = useState<string>("");
|
||||||
const [dumpDesc, setDumpDesc] = useState<string>("");
|
const [dumpDesc, setDumpDesc] = useState<string>("");
|
||||||
const { width } = Dimensions.get("screen");
|
const {width} = Dimensions.get("screen");
|
||||||
|
|
||||||
|
|
||||||
// Refs for the two TextFields
|
// Refs for the two TextFields
|
||||||
const descriptionRef = useRef<TextFieldRef>(null);
|
const descriptionRef = useRef<TextFieldRef>(null);
|
||||||
const titleRef = 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(() => {
|
useEffect(() => {
|
||||||
setDumpDesc("");
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||||
setDumpTitle("");
|
|
||||||
}, [addBrainDumpProps.isVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
titleRef?.current?.focus()
|
|
||||||
}, 500)
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
KeyboardManager.setEnableAutoToolbar(false);
|
<Dialog
|
||||||
},[])
|
bottom={true}
|
||||||
|
height={"90%"}
|
||||||
return (
|
width={width}
|
||||||
<Dialog
|
panDirection={PanningDirectionsEnum.DOWN}
|
||||||
bottom={true}
|
onDismiss={() => addBrainDumpProps.setIsVisible(false)}
|
||||||
height={"90%"}
|
containerStyle={styles.dialogContainer}
|
||||||
width={width}
|
visible={addBrainDumpProps.isVisible}
|
||||||
panDirection={PanningDirectionsEnum.DOWN}
|
>
|
||||||
onDismiss={() => addBrainDumpProps.setIsVisible(false)}
|
<View row spread style={styles.topBtns} marginB-20>
|
||||||
containerStyle={styles.dialogContainer}
|
<Button
|
||||||
visible={addBrainDumpProps.isVisible}
|
color="#05a8b6"
|
||||||
>
|
label="Cancel"
|
||||||
<View row spread style={styles.topBtns} marginB-20>
|
style={styles.topBtn}
|
||||||
<Button
|
onPress={() => {
|
||||||
color="#05a8b6"
|
addBrainDumpProps.setIsVisible(false);
|
||||||
label="Cancel"
|
}}
|
||||||
style={styles.topBtn}
|
/>
|
||||||
onPress={() => {
|
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
||||||
addBrainDumpProps.setIsVisible(false);
|
<DropModalIcon style={{marginTop: 15}}/>
|
||||||
}}
|
</TouchableOpacity>
|
||||||
/>
|
<Button
|
||||||
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
color="#05a8b6"
|
||||||
<DropModalIcon style={{ marginTop: 15 }} />
|
label="Save"
|
||||||
</TouchableOpacity>
|
style={styles.topBtn}
|
||||||
<Button
|
onPress={() => {
|
||||||
color="#05a8b6"
|
addBrainDump({
|
||||||
label="Save"
|
|
||||||
style={styles.topBtn}
|
id: 99,
|
||||||
onPress={() => {
|
|
||||||
addBrainDump({ id: 99, title: dumpTitle.trimEnd().trimStart(), description: dumpDesc.trimEnd().trimStart() });
|
title: dumpTitle.trimEnd().trimStart(),
|
||||||
addBrainDumpProps.setIsVisible(false);
|
|
||||||
}}
|
description: dumpDesc.trimEnd().trimStart(),
|
||||||
/>
|
|
||||||
</View>
|
});
|
||||||
<View marginH-20>
|
addBrainDumpProps.setIsVisible(false);
|
||||||
<TextField
|
}}
|
||||||
value={dumpTitle}
|
/>
|
||||||
ref={titleRef}
|
</View>
|
||||||
placeholder="Set Title"
|
<View marginH-20>
|
||||||
text60R
|
<TextField
|
||||||
onChangeText={(text) => {
|
value={dumpTitle}
|
||||||
setDumpTitle(text);
|
ref={titleRef}
|
||||||
}}
|
placeholder="Set Title"
|
||||||
onSubmitEditing={() => {
|
text60R
|
||||||
// Move focus to the description field
|
onChangeText={(text) => {
|
||||||
descriptionRef.current?.focus();
|
setDumpTitle(text);
|
||||||
}}
|
}}
|
||||||
style={styles.title}
|
onSubmitEditing={() => {
|
||||||
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
// Move focus to the description field
|
||||||
returnKeyType="next"
|
descriptionRef.current?.focus();
|
||||||
/>
|
}}
|
||||||
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
|
style={styles.title}
|
||||||
<TextField
|
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
||||||
ref={descriptionRef}
|
returnKeyType="next"
|
||||||
value={dumpDesc}
|
/>
|
||||||
placeholder="Write Description"
|
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
|
||||||
text70
|
<TextField
|
||||||
onChangeText={(text) => {
|
ref={descriptionRef}
|
||||||
setDumpDesc(text);
|
value={dumpDesc}
|
||||||
}}
|
placeholder="Write Description"
|
||||||
style={styles.description}
|
text70
|
||||||
multiline
|
onChangeText={(text) => {
|
||||||
numberOfLines={4}
|
setDumpDesc(text);
|
||||||
maxLength={255}
|
}}
|
||||||
onEndEditing={() => {
|
style={styles.description}
|
||||||
descriptionRef.current?.blur();
|
multiline
|
||||||
}}
|
numberOfLines={4}
|
||||||
returnKeyType="done"
|
maxLength={255}
|
||||||
/>
|
onEndEditing={() => {
|
||||||
</View>
|
descriptionRef.current?.blur();
|
||||||
</Dialog>
|
}}
|
||||||
);
|
returnKeyType="done"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@ -144,7 +145,7 @@ const styles = StyleSheet.create({
|
|||||||
description: {
|
description: {
|
||||||
fontFamily: "Manrope_400Regular",
|
fontFamily: "Manrope_400Regular",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
textAlignVertical: 'top'
|
textAlignVertical: "top",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
TouchableOpacity,
|
TouchableOpacity, TextFieldRef,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import { Dimensions, StyleSheet } from "react-native";
|
import { Dimensions, StyleSheet } from "react-native";
|
||||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||||
@ -30,6 +30,7 @@ const MoveBrainDump = (props: {
|
|||||||
props.item.description
|
props.item.description
|
||||||
);
|
);
|
||||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
|
const descriptionRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
const { width } = Dimensions.get("screen");
|
const { width } = Dimensions.get("screen");
|
||||||
|
|
||||||
@ -37,6 +38,14 @@ const MoveBrainDump = (props: {
|
|||||||
updateBrainDumpItem(props.item.id, { description: description });
|
updateBrainDumpItem(props.item.id, { description: description });
|
||||||
}, [description]);
|
}, [description]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isVisible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
descriptionRef?.current?.focus()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}, [props.isVisible]);
|
||||||
|
|
||||||
const showConfirmationDialog = () => {
|
const showConfirmationDialog = () => {
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
@ -112,7 +121,6 @@ const MoveBrainDump = (props: {
|
|||||||
<TextField
|
<TextField
|
||||||
textAlignVertical="top"
|
textAlignVertical="top"
|
||||||
multiline
|
multiline
|
||||||
autoFocus
|
|
||||||
fieldStyle={{
|
fieldStyle={{
|
||||||
width: "94%",
|
width: "94%",
|
||||||
}}
|
}}
|
||||||
@ -123,6 +131,7 @@ const MoveBrainDump = (props: {
|
|||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
setDescription(value);
|
setDescription(value);
|
||||||
}}
|
}}
|
||||||
|
ref={descriptionRef}
|
||||||
returnKeyType="default"
|
returnKeyType="default"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default function CalendarPage() {
|
|||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Let's get your week started!"}
|
message={"Let's get your week started!"}
|
||||||
isWelcome
|
isWelcome
|
||||||
|
isCalendar={true}
|
||||||
/>
|
/>
|
||||||
<InnerCalendar/>
|
<InnerCalendar/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,90 +1,92 @@
|
|||||||
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React from "react";
|
||||||
import {StyleSheet} from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import {useSetAtom} from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
||||||
|
|
||||||
|
|
||||||
const CalendarViewSwitch = () => {
|
const CalendarViewSwitch = () => {
|
||||||
const [calView, setCalView] = useState<boolean>(false);
|
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
const viewSwitch = useSetAtom(isFamilyViewAtom)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<View
|
||||||
|
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
|
<View
|
||||||
row
|
centerV
|
||||||
spread
|
centerH
|
||||||
style={{
|
height={40}
|
||||||
position: "absolute",
|
paddingH-15
|
||||||
bottom: 20,
|
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||||
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
|
<Text
|
||||||
onPress={() => {
|
color={isFamilyView ? "white" : "#a1a1a1"}
|
||||||
setCalView(true);
|
style={styles.switchTxt}
|
||||||
viewSwitch(true);
|
>
|
||||||
}}
|
Family View
|
||||||
>
|
</Text>
|
||||||
<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>
|
|
||||||
</View>
|
</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;
|
export default CalendarViewSwitch;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
switchBtnActive: {
|
switchBtnActive: {
|
||||||
backgroundColor: "#a1a1a1",
|
backgroundColor: "#a1a1a1",
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
},
|
},
|
||||||
switchBtn: {
|
switchBtn: {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
},
|
},
|
||||||
switchTxt: {
|
switchTxt: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: 'Manrope_600SemiBold'
|
fontFamily: "Manrope_600SemiBold",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
||||||
import {Text} from "react-native-ui-lib";
|
import {Text} from "react-native-ui-lib";
|
||||||
import { isWithinInterval, subDays, addDays, compareAsc } from "date-fns";
|
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
|
||||||
|
|
||||||
interface EventCalendarProps {
|
interface EventCalendarProps {
|
||||||
calendarHeight: number;
|
calendarHeight: number;
|
||||||
@ -37,21 +37,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
const [isRendering, setIsRendering] = useState(true);
|
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
|
|
||||||
const todaysDate = new Date();
|
const todaysDate = new Date();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (events && mode) {
|
|
||||||
setIsRendering(true);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setIsRendering(false);
|
|
||||||
}, 10);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [events, mode]);
|
|
||||||
|
|
||||||
const handlePressEvent = useCallback(
|
const handlePressEvent = useCallback(
|
||||||
(event: CalendarEvent) => {
|
(event: CalendarEvent) => {
|
||||||
if (mode === "day" || mode === "week") {
|
if (mode === "day" || mode === "week") {
|
||||||
@ -95,6 +84,8 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
[profileData]
|
[profileData]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek})
|
||||||
|
|
||||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||||
return (
|
return (
|
||||||
date1.getDate() === date2.getDate() &&
|
date1.getDate() === date2.getDate() &&
|
||||||
@ -127,7 +118,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
|
|
||||||
const { enrichedEvents, filteredEvents } = useMemo(() => {
|
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||||
const startTime = Date.now(); // Start timer
|
const startTime = Date.now(); // Start timer
|
||||||
|
|
||||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||||
@ -154,16 +145,15 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
overlapCount: 0
|
overlapCount: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort events for this dateKey from oldest to newest by event.start
|
|
||||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, CalendarEvent[]>);
|
}, {} as Record<string, CalendarEvent[]>);
|
||||||
|
|
||||||
const endTime = Date.now(); // End timer
|
const endTime = Date.now();
|
||||||
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
|
|
||||||
return { enrichedEvents, filteredEvents };
|
return {enrichedEvents, filteredEvents};
|
||||||
}, [events, selectedDate, mode]);
|
}, [events, selectedDate, mode]);
|
||||||
|
|
||||||
const renderCustomDateForMonth = (date: Date) => {
|
const renderCustomDateForMonth = (date: Date) => {
|
||||||
@ -218,7 +208,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
setOffsetMinutes(getTotalMinutes());
|
setOffsetMinutes(getTotalMinutes());
|
||||||
}, [events, mode]);
|
}, [events, mode]);
|
||||||
|
|
||||||
if (isLoading || isRendering) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color="#0000ff"/>
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
@ -235,7 +225,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
enableEnrichedEvents={true}
|
enableEnrichedEvents={true}
|
||||||
sortedMonthView
|
sortedMonthView
|
||||||
enrichedEventsByDate={enrichedEvents}
|
// enrichedEventsByDate={enrichedEvents}
|
||||||
events={filteredEvents}
|
events={filteredEvents}
|
||||||
// eventCellStyle={memoizedEventCellStyle}
|
// eventCellStyle={memoizedEventCellStyle}
|
||||||
onPressEvent={handlePressEvent}
|
onPressEvent={handlePressEvent}
|
||||||
|
|||||||
@ -103,6 +103,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
|
|
||||||
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
|
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
|
||||||
const {data: members} = useGetFamilyMembers(true);
|
const {data: members} = useGetFamilyMembers(true);
|
||||||
|
const titleRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
const isLoading = isDeleting || isAdding
|
const isLoading = isDeleting || isAdding
|
||||||
|
|
||||||
@ -135,6 +136,14 @@ export const ManuallyAddEventModal = () => {
|
|||||||
setRepeatInterval([]);
|
setRepeatInterval([]);
|
||||||
}, [editEvent, selectedNewEventDate]);
|
}, [editEvent, selectedNewEventDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(show && !editEvent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus()
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [selectedNewEventDate]);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
const formatDateTime = (date?: Date | string) => {
|
const formatDateTime = (date?: Date | string) => {
|
||||||
@ -342,8 +351,8 @@ export const ManuallyAddEventModal = () => {
|
|||||||
<ScrollView style={{minHeight: "85%"}}>
|
<ScrollView style={{minHeight: "85%"}}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Add event title"
|
placeholder="Add event title"
|
||||||
|
ref={titleRef}
|
||||||
value={title}
|
value={title}
|
||||||
autoFocus
|
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setTitle(text);
|
setTitle(text);
|
||||||
}}
|
}}
|
||||||
@ -555,14 +564,15 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.divider}/>
|
<View style={styles.divider}/>
|
||||||
<View marginH-30 marginB-0 row spread centerV>
|
<View marginH-30 marginB-0 row spread centerV>
|
||||||
<View row centerH>
|
<View row center>
|
||||||
<LockIcon/>
|
<LockIcon/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
marginL-10
|
marginL-12
|
||||||
|
center
|
||||||
>
|
>
|
||||||
Mark as Private
|
Mark as Private
|
||||||
</Text>
|
</Text>
|
||||||
@ -609,7 +619,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
marginH-30
|
marginH-30
|
||||||
marginB-15
|
marginB-30
|
||||||
label="Create event from image"
|
label="Create event from image"
|
||||||
text70
|
text70
|
||||||
style={{height: 47}}
|
style={{height: 47}}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import {StyleSheet} from "react-native";
|
import {Dimensions, StyleSheet} from "react-native";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, View,} from "react-native-ui-lib";
|
import {Button, View,} from "react-native-ui-lib";
|
||||||
import {useGroceryContext} from "@/contexts/GroceryContext";
|
import {useGroceryContext} from "@/contexts/GroceryContext";
|
||||||
import {FontAwesome6} from "@expo/vector-icons";
|
import {FontAwesome6} from "@expo/vector-icons";
|
||||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
|
const { width } = Dimensions.get("screen");
|
||||||
|
|
||||||
const AddGroceryItem = () => {
|
const AddGroceryItem = () => {
|
||||||
const {setIsAddingGrocery} = useGroceryContext();
|
const {setIsAddingGrocery} = useGroceryContext();
|
||||||
|
|
||||||
@ -65,8 +67,14 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
},
|
},
|
||||||
btnContainer: {
|
btnContainer: {
|
||||||
width: "100%",
|
position:"absolute",
|
||||||
|
bottom: 30,
|
||||||
|
width: width,
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 0,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
alignItems:"center",
|
||||||
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
finishShopBtn: {
|
finishShopBtn: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|||||||
@ -73,6 +73,8 @@ const GroceryItem = ({
|
|||||||
marginVertical: 5,
|
marginVertical: 5,
|
||||||
paddingHorizontal: isEditingTitle ? 0 : 13,
|
paddingHorizontal: isEditingTitle ? 0 : 13,
|
||||||
paddingVertical: isEditingTitle ? 0 : 10,
|
paddingVertical: isEditingTitle ? 0 : 10,
|
||||||
|
height: 44.64,
|
||||||
|
backgroundColor: item.bought ? "#cbcbcb" : "white",
|
||||||
}}
|
}}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
centerV
|
centerV
|
||||||
@ -103,12 +105,25 @@ const GroceryItem = ({
|
|||||||
<View>
|
<View>
|
||||||
{isParent ? (
|
{isParent ? (
|
||||||
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
|
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
|
||||||
<Text text70T black style={styles.title}>
|
<Text
|
||||||
|
text70T
|
||||||
|
black
|
||||||
|
style={[
|
||||||
|
styles.title,
|
||||||
|
{
|
||||||
|
textDecorationLine: item.bought ? "line-through" : "none",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<Text text70T black style={styles.title}>
|
<Text
|
||||||
|
text70T
|
||||||
|
black
|
||||||
|
style={[styles.title, { color: item.bought ? "red" : "black" }]}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -74,10 +74,11 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
|
|||||||
}, [groceries]);
|
}, [groceries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View marginH-20 marginB-20>
|
<View marginH-20 marginB-45>
|
||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Welcome to your grocery list"}
|
message={"Welcome to your grocery list"}
|
||||||
isWelcome={false}
|
isWelcome={false}
|
||||||
|
isGroceries={true}
|
||||||
>
|
>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import {Button, ButtonSize, Text, TextField, View} from "react-native-ui-lib";
|
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from "react-native";
|
||||||
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
|
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
|
||||||
import {isLoading} from "expo-font";
|
|
||||||
|
|
||||||
export const ResetPasswordPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
|
export const ResetPasswordPage = () => {
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
|
|
||||||
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
||||||
|
|||||||
@ -1,218 +1,183 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
Dialog,
|
Colors,
|
||||||
Text,
|
KeyboardAwareScrollView,
|
||||||
TextField,
|
LoaderScreen,
|
||||||
TextFieldRef,
|
Text,
|
||||||
View,
|
TextField,
|
||||||
|
TextFieldRef,
|
||||||
|
View,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import React, { useRef, useState } from "react";
|
import React, {useRef, useState} from "react";
|
||||||
import { useSignIn } from "@/hooks/firebase/useSignIn";
|
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
||||||
import { StyleSheet } from "react-native";
|
import {KeyboardAvoidingView, Platform, StyleSheet} from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
import { Camera, CameraView } from "expo-camera";
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
const SignInPage = ({
|
KeyboardManager.setEnableAutoToolbar(true);
|
||||||
setTab,
|
|
||||||
}: {
|
|
||||||
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);
|
|
||||||
|
|
||||||
const { mutateAsync: signIn, error, isError } = useSignIn();
|
const SignInPage = () => {
|
||||||
const { mutateAsync: signInWithQrCode } = useLoginWithQrCode();
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const passwordRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
const handleSignIn = async () => {
|
const {mutateAsync: signIn, error, isError, isLoading} = useSignIn();
|
||||||
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 handleQrCodeScanned = async ({ data }: { data: string }) => {
|
const router = useRouter()
|
||||||
setShowCameraDialog(false);
|
|
||||||
try {
|
|
||||||
await signInWithQrCode({ userId: data });
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Login successful with QR code!",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Error logging in with QR code",
|
|
||||||
text2: `${err}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCameraPermissions = async (callback: () => void) => {
|
const handleSignIn = async () => {
|
||||||
const { status } = await Camera.requestCameraPermissionsAsync();
|
await signIn({email, password});
|
||||||
setHasPermission(status === "granted");
|
if (!isError) {
|
||||||
if (status === "granted") {
|
Toast.show({
|
||||||
callback();
|
type: "success",
|
||||||
}
|
text1: "Login successful!",
|
||||||
};
|
});
|
||||||
|
} else {
|
||||||
|
Toast.show({
|
||||||
|
type: "error",
|
||||||
|
text1: "Error logging in",
|
||||||
|
text2: `${error}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View padding-10 centerV height={"100%"}>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<TextField
|
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
|
||||||
placeholder="Email"
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
|
||||||
value={email}
|
<View gap-13 width={"100%"} marginB-20>
|
||||||
|
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||||
onChangeText={setEmail}
|
Jump back into Cally
|
||||||
style={styles.textfield}
|
</Text>
|
||||||
onSubmitEditing={() => {
|
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||||
// Move focus to the description field
|
Please enter your details.
|
||||||
passwordRef.current?.focus();
|
</Text>
|
||||||
}}
|
</View>
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View row centerH marginB-5 gap-5>
|
<KeyboardAvoidingView style={{width: "100%"}}
|
||||||
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
contentContainerStyle={{justifyContent: "center"}}
|
||||||
<Button
|
keyboardVerticalOffset={50}
|
||||||
onPress={() => setTab("register")}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
label="Sign Up"
|
>
|
||||||
labelStyle={[
|
<TextField
|
||||||
styles.jakartaMedium,
|
placeholder="Email"
|
||||||
{ textDecorationLine: "none", color: "#fd1575" },
|
keyboardType={"email-address"}
|
||||||
]}
|
returnKeyType={"next"}
|
||||||
link
|
textContentType={"emailAddress"}
|
||||||
size={ButtonSize.xSmall}
|
defaultValue={email}
|
||||||
padding-0
|
onChangeText={setEmail}
|
||||||
margin-0
|
style={styles.textfield}
|
||||||
text70
|
autoComplete={"email"}
|
||||||
left
|
autoCorrect={false}
|
||||||
color="#fd1775"
|
onSubmitEditing={() => {
|
||||||
/>
|
// Move focus to the description field
|
||||||
</View>
|
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>
|
<View flexG/>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Camera Dialog */}
|
<Button
|
||||||
<Dialog
|
label="Log in"
|
||||||
visible={showCameraDialog}
|
marginT-50
|
||||||
onDismiss={() => setShowCameraDialog(false)}
|
labelStyle={{
|
||||||
bottom
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
width="100%"
|
fontSize: 16,
|
||||||
height="70%"
|
}}
|
||||||
containerStyle={{ padding: 15, backgroundColor:"white" }}
|
onPress={handleSignIn}
|
||||||
>
|
style={{marginBottom: 20, height: 50}}
|
||||||
{hasPermission === null ? (
|
backgroundColor="#fd1775"
|
||||||
<Text>Requesting camera permissions...</Text>
|
/>
|
||||||
) : !hasPermission ? (
|
|
||||||
<Text>No access to camera</Text>
|
{isError && (
|
||||||
) : (
|
<Text center style={{marginBottom: 20}}>{`${
|
||||||
<CameraView
|
error?.toString()?.split("]")?.[1]
|
||||||
style={{ flex: 1, borderRadius: 15 }}
|
}`}</Text>
|
||||||
onBarcodeScanned={handleQrCodeScanned}
|
)}
|
||||||
barcodeScannerSettings={{
|
|
||||||
barcodeTypes: ["qr"],
|
<View row centerH marginB-5 gap-5>
|
||||||
}}
|
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
||||||
/>
|
<Button
|
||||||
)}
|
onPress={() => router.replace("/(unauth)/sign_up")}
|
||||||
<Button
|
label="Sign Up"
|
||||||
label="Cancel"
|
labelStyle={[
|
||||||
onPress={() => setShowCameraDialog(false)}
|
styles.jakartaMedium,
|
||||||
backgroundColor="#fd1775"
|
{textDecorationLine: "none", color: "#fd1575"},
|
||||||
style={{ margin: 10, marginBottom: 30 }}
|
]}
|
||||||
/>
|
link
|
||||||
</Dialog>
|
size={ButtonSize.xSmall}
|
||||||
</View>
|
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({
|
const styles = StyleSheet.create({
|
||||||
textfield: {
|
textfield: {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
padding: 30,
|
padding: 30,
|
||||||
height: 45,
|
height: 45,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
fontFamily: "PlusJakartaSans_300Light",
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
},
|
},
|
||||||
jakartaLight: {
|
jakartaLight: {
|
||||||
fontFamily: "PlusJakartaSans_300Light",
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: "#484848",
|
color: "#484848",
|
||||||
},
|
},
|
||||||
jakartaMedium: {
|
jakartaMedium: {
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: "#919191",
|
color: "#919191",
|
||||||
textDecorationLine: "underline",
|
textDecorationLine: "underline",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default SignInPage;
|
export default SignInPage;
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Colors,
|
||||||
|
KeyboardAwareScrollView,
|
||||||
|
LoaderScreen,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
TextFieldRef,
|
TextFieldRef,
|
||||||
@ -10,16 +13,15 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import {useSignUp} from "@/hooks/firebase/useSignUp";
|
import {useSignUp} from "@/hooks/firebase/useSignUp";
|
||||||
import {StyleSheet} from "react-native";
|
import {KeyboardAvoidingView, StyleSheet} from "react-native";
|
||||||
import {AntDesign} from "@expo/vector-icons";
|
import {AntDesign} from "@expo/vector-icons";
|
||||||
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
const SignUpPage = ({
|
KeyboardManager.setEnableAutoToolbar(true);
|
||||||
setTab,
|
|
||||||
}: {
|
const SignUpPage = () => {
|
||||||
setTab: React.Dispatch<
|
|
||||||
React.SetStateAction<"register" | "login" | "reset-password">
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [firstName, setFirstName] = useState<string>("");
|
const [firstName, setFirstName] = useState<string>("");
|
||||||
const [lastName, setLastName] = useState<string>("");
|
const [lastName, setLastName] = useState<string>("");
|
||||||
@ -28,154 +30,196 @@ const SignUpPage = ({
|
|||||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||||
const {mutateAsync: signUp} = useSignUp();
|
const {mutateAsync: signUp, isLoading} = useSignUp();
|
||||||
|
|
||||||
const lnameRef = useRef<TextFieldRef>(null);
|
const lnameRef = useRef<TextFieldRef>(null);
|
||||||
const emailRef = useRef<TextFieldRef>(null);
|
const emailRef = useRef<TextFieldRef>(null);
|
||||||
const passwordRef = useRef<TextFieldRef>(null);
|
const passwordRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
await signUp({email, password, firstName, lastName});
|
await signUp({email, password, firstName, lastName});
|
||||||
|
router.replace("/(unauth)/cal_sync")
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View height={"100%"} padding-15 marginT-30>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<Text style={styles.title}>Get started with Cally</Text>
|
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid>
|
||||||
<Text style={styles.subtitle} marginT-15 color="#919191">
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}>
|
||||||
Please enter your details.
|
<View gap-13 width={"100%"} marginB-20>
|
||||||
</Text>
|
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||||
<TextField
|
Get started with Cally
|
||||||
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
|
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity>
|
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||||
<Text text90 style={styles.jakartaMedium}>
|
Please enter your details.
|
||||||
{" "}
|
</Text>
|
||||||
terms and conditions
|
</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>
|
</Text>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
<Text style={styles.jakartaLight}> and </Text>
|
<View row centerV>
|
||||||
<TouchableOpacity>
|
<Checkbox
|
||||||
<Text text90 style={styles.jakartaMedium}>
|
style={styles.check}
|
||||||
{" "}
|
color="#919191"
|
||||||
privacy policy
|
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>
|
</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>
|
||||||
</View>
|
</KeyboardAwareScrollView>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Button
|
{isLoading && (
|
||||||
label="Log in"
|
<LoaderScreen overlay message={"Signing up..."} backgroundColor={Colors.white}
|
||||||
labelStyle={[
|
color={Colors.grey40}/>
|
||||||
styles.jakartaMedium,
|
)}
|
||||||
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
|
</SafeAreaView>
|
||||||
]}
|
|
||||||
flexS
|
|
||||||
margin-0
|
|
||||||
link
|
|
||||||
color="#fd1775"
|
|
||||||
size={ButtonSize.small}
|
|
||||||
text70
|
|
||||||
onPress={() => setTab("login")}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -192,8 +236,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: "#919191",
|
color: "#919191",
|
||||||
},
|
},
|
||||||
//mora da se izmeni kako treba
|
|
||||||
bottomView: {marginTop: "auto", marginBottom: 30, marginTop: "auto"},
|
|
||||||
jakartaLight: {
|
jakartaLight: {
|
||||||
fontFamily: "PlusJakartaSans_300Light",
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import { Button, Text, View } from "react-native-ui-lib";
|
import { Button, Text, View } from "react-native-ui-lib";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { StyleSheet } from "react-native";
|
import {Linking, StyleSheet} from "react-native";
|
||||||
import { Octicons } from "@expo/vector-icons";
|
import { Octicons } from "@expo/vector-icons";
|
||||||
import CalendarSettingsPage from "./CalendarSettingsPage";
|
import CalendarSettingsPage from "./CalendarSettingsPage";
|
||||||
import ChoreRewardSettings from "./ChoreRewardSettings";
|
import ChoreRewardSettings from "./ChoreRewardSettings";
|
||||||
@ -21,11 +21,23 @@ const pageIndex = {
|
|||||||
policy: 4,
|
policy: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PRIVACY_POLICY_URL = 'https://callyapp.com';
|
||||||
|
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const SettingsPage = () => {
|
||||||
const { profileData } = useAuthContext();
|
const { profileData } = useAuthContext();
|
||||||
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
||||||
const isntParent = profileData?.userType !== ProfileType.PARENT;
|
const isntParent = profileData?.userType !== ProfileType.PARENT;
|
||||||
|
|
||||||
|
const openPrivacyPolicy = async () => {
|
||||||
|
const supported = await Linking.canOpenURL(PRIVACY_POLICY_URL);
|
||||||
|
if (supported) {
|
||||||
|
await Linking.openURL(PRIVACY_POLICY_URL);
|
||||||
|
} else {
|
||||||
|
console.log("Don't know how to open this URL:", PRIVACY_POLICY_URL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View flexG>
|
<View flexG>
|
||||||
{pageIndex == 0 && (
|
{pageIndex == 0 && (
|
||||||
@ -73,7 +85,8 @@ const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={isntParent}
|
disabled
|
||||||
|
// disabled={isntParent}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
children={
|
children={
|
||||||
@ -84,7 +97,7 @@ const SettingsPage = () => {
|
|||||||
color="#ff9900"
|
color="#ff9900"
|
||||||
style={{ marginRight: 10 }}
|
style={{ marginRight: 10 }}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.label, isntParent ? styles.disabledText : {color: "#ff9900"}]}>
|
<Text style={[styles.label, true ? styles.disabledText : {color: "#ff9900"}]}>
|
||||||
To-Do Reward Settings
|
To-Do Reward Settings
|
||||||
</Text>
|
</Text>
|
||||||
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
||||||
@ -95,6 +108,7 @@ const SettingsPage = () => {
|
|||||||
<Button
|
<Button
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
|
onPress={openPrivacyPolicy}
|
||||||
children={
|
children={
|
||||||
<View row centerV width={"100%"}>
|
<View row centerV width={"100%"}>
|
||||||
<PrivacyPolicyIcon style={{ marginRight: 10 }} />
|
<PrivacyPolicyIcon style={{ marginRight: 10 }} />
|
||||||
|
|||||||
@ -1,115 +1,131 @@
|
|||||||
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
import {FloatingButton, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
import React, { useState } from "react";
|
import React, {useState} from "react";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import {Ionicons} from "@expo/vector-icons";
|
||||||
import { ScrollView, StyleSheet } from "react-native";
|
import {ScrollView, StyleSheet} from "react-native";
|
||||||
import MyProfile from "./user_settings_views/MyProfile";
|
import MyProfile from "./user_settings_views/MyProfile";
|
||||||
import MyGroup from "./user_settings_views/MyGroup";
|
import MyGroup from "./user_settings_views/MyGroup";
|
||||||
import { useAtom } from "jotai";
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
import { settingsPageIndex, userSettingsView } from "../calendar/atoms";
|
import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
|
||||||
import { AuthContextProvider } from "@/contexts/AuthContext";
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
const UserSettings = () => {
|
const UserSettings = () => {
|
||||||
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
const [userView, setUserView] = useAtom(userSettingsView);
|
const [userView, setUserView] = useAtom(userSettingsView);
|
||||||
return (
|
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
|
||||||
<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>
|
|
||||||
|
|
||||||
{!userView && (
|
return (
|
||||||
<View>
|
<View flexG>
|
||||||
<Text>selview</Text>
|
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
|
||||||
</View>
|
<TouchableOpacity
|
||||||
)}
|
onPress={() => {
|
||||||
</View>
|
setPageIndex(0);
|
||||||
</AuthContextProvider>
|
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({
|
const styles = StyleSheet.create({
|
||||||
buttonSwitch: {
|
bottomButton: {
|
||||||
borderRadius: 50,
|
position: "absolute",
|
||||||
width: "100%",
|
bottom: 15,
|
||||||
backgroundColor: "#ebebeb",
|
marginHorizontal: 28,
|
||||||
height: 45,
|
width: 337,
|
||||||
},
|
backgroundColor: "#e8156c",
|
||||||
btnSelected: {
|
height: 53.26,
|
||||||
backgroundColor: "#05a8b6",
|
},
|
||||||
height: "100%",
|
buttonSwitch: {
|
||||||
width: "50%",
|
borderRadius: 50,
|
||||||
borderRadius: 50,
|
width: "100%",
|
||||||
},
|
backgroundColor: "#ebebeb",
|
||||||
btnTxt: {
|
height: 45,
|
||||||
fontFamily: "Manrope_500Medium",
|
},
|
||||||
fontSize: 15,
|
btnSelected: {
|
||||||
},
|
backgroundColor: "#05a8b6",
|
||||||
btnNot: {
|
height: "100%",
|
||||||
height: "100%",
|
width: "50%",
|
||||||
width: "50%",
|
borderRadius: 50,
|
||||||
borderRadius: 50,
|
},
|
||||||
},
|
btnTxt: {
|
||||||
title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
|
fontFamily: "Manrope_500Medium",
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
btnNot: {
|
||||||
|
height: "100%",
|
||||||
|
width: "50%",
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
title: {fontFamily: "Manrope_600SemiBold", fontSize: 18},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default UserSettings;
|
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 { ScrollView } from "react-native-gesture-handler";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Colors,
|
Colors,
|
||||||
Image,
|
Image,
|
||||||
Picker,
|
Picker,
|
||||||
@ -18,6 +19,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
|
|||||||
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
||||||
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
|
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
|
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
|
||||||
|
|
||||||
const MyProfile = () => {
|
const MyProfile = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
@ -32,6 +34,15 @@ const MyProfile = () => {
|
|||||||
string | ImagePicker.ImagePickerAsset | null
|
string | ImagePicker.ImagePickerAsset | null
|
||||||
>(profileData?.pfp || null);
|
>(profileData?.pfp || null);
|
||||||
|
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleHideDeleteDialog = () => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
};
|
||||||
|
const handleShowDeleteDialog = () => {
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||||
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
@ -48,13 +59,12 @@ const MyProfile = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debouncedUserDataUpdate();
|
debouncedUserDataUpdate();
|
||||||
}, [timeZone, lastName, firstName, profileImage]);
|
}, [timeZone, lastName, firstName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profileData) {
|
if (profileData) {
|
||||||
setFirstName(profileData.firstName || "");
|
setFirstName(profileData.firstName || "");
|
||||||
setLastName(profileData.lastName || "");
|
setLastName(profileData.lastName || "");
|
||||||
// setProfileImage(profileData.pfp || null);
|
|
||||||
setTimeZone(
|
setTimeZone(
|
||||||
profileData.timeZone || Localization.getCalendars()[0].timeZone!
|
profileData.timeZone || Localization.getCalendars()[0].timeZone!
|
||||||
);
|
);
|
||||||
@ -78,7 +88,7 @@ const MyProfile = () => {
|
|||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
setProfileImage(result.assets[0].uri);
|
setProfileImage(result.assets[0].uri);
|
||||||
changeProfilePicture(result.assets[0]);
|
await changeProfilePicture(result.assets[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,7 +103,7 @@ const MyProfile = () => {
|
|||||||
: profileImage;
|
: profileImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
|
<ScrollView style={{ paddingBottom: 20, flex: 1 }}>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.subTit}>Your Profile</Text>
|
<Text style={styles.subTit}>Your Profile</Text>
|
||||||
<View row spread paddingH-15 centerV marginV-15>
|
<View row spread paddingH-15 centerV marginV-15>
|
||||||
@ -205,6 +215,22 @@ const MyProfile = () => {
|
|||||||
</Picker>
|
</Picker>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
backgroundColor="#FF5449"
|
||||||
|
label="Delete Profile"
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
|
||||||
|
onPress={handleShowDeleteDialog}
|
||||||
|
/>
|
||||||
|
<DeleteProfileDialogs
|
||||||
|
onFirstYes={() => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
}}
|
||||||
|
visible={showDeleteDialog}
|
||||||
|
onDismiss={handleHideDeleteDialog}
|
||||||
|
onConfirm={() => {console.log('delete account here')}}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,10 @@ const UserMenu = ({
|
|||||||
panDirection={PanningDirectionsEnum.DOWN}
|
panDirection={PanningDirectionsEnum.DOWN}
|
||||||
>
|
>
|
||||||
<Card padding-20 center>
|
<Card padding-20 center>
|
||||||
<Text marginB-10>Scan this QR Code to Login:</Text>
|
<Text center marginB-10 style={{fontSize: 16, maxWidth: "80%"}}>Ask your family to download the app
|
||||||
|
and then scan the
|
||||||
|
QR Code to join the family group:
|
||||||
|
</Text>
|
||||||
<QRCode value={userId} size={150}/>
|
<QRCode value={userId} size={150}/>
|
||||||
<Button
|
<Button
|
||||||
marginT-20
|
marginT-20
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const AddChore = () => {
|
|||||||
>
|
>
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<Button
|
<Button
|
||||||
marginH-20
|
marginB-30
|
||||||
size={ButtonSize.large}
|
size={ButtonSize.large}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={() => setIsVisible(!isVisible)}
|
onPress={() => setIsVisible(!isVisible)}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Checkbox,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Dialog,
|
Dialog,
|
||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
|
Checkbox,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useToDosContext } from "@/contexts/ToDosContext";
|
import { useToDosContext } from "@/contexts/ToDosContext";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
@ -60,15 +61,14 @@ const ToDoItem = (props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={props.item.id}
|
key={props.item.id}
|
||||||
centerV
|
centerV
|
||||||
paddingV-10
|
paddingV-10
|
||||||
paddingH-13
|
paddingH-13
|
||||||
marginV-10
|
marginV-10
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 17,
|
borderRadius: 17,
|
||||||
backgroundColor: props.item.done ? "#e0e0e0" : "white",
|
backgroundColor: "white",
|
||||||
opacity: props.item.done ? 0.3 : 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visible && (
|
{visible && (
|
||||||
@ -84,6 +84,7 @@ const ToDoItem = (props: {
|
|||||||
style={{
|
style={{
|
||||||
textDecorationLine: props.item.done ? "line-through" : "none",
|
textDecorationLine: props.item.done ? "line-through" : "none",
|
||||||
fontFamily: "Manrope_500Medium",
|
fontFamily: "Manrope_500Medium",
|
||||||
|
color: props.item.done? "#a09f9f": "black",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -96,6 +97,7 @@ const ToDoItem = (props: {
|
|||||||
value={props.item.done}
|
value={props.item.done}
|
||||||
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
|
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
|
||||||
style={styles.checked}
|
style={styles.checked}
|
||||||
|
size={26.64}
|
||||||
borderRadius={50}
|
borderRadius={50}
|
||||||
color="#fd1575"
|
color="#fd1575"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const ToDosPage = () => {
|
|||||||
message="Here are your To Do's"
|
message="Here are your To Do's"
|
||||||
isWelcome={true}
|
isWelcome={true}
|
||||||
link={profileData?.userType == ProfileType.PARENT && pageLink}
|
link={profileData?.userType == ProfileType.PARENT && pageLink}
|
||||||
|
isToDos={true}
|
||||||
/>
|
/>
|
||||||
{profileData?.userType == ProfileType.CHILD && (
|
{profileData?.userType == ProfileType.CHILD && (
|
||||||
<View marginB-25>
|
<View marginB-25>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React from "react";
|
|||||||
import { ImageBackground, StyleSheet } from "react-native";
|
import { ImageBackground, StyleSheet } from "react-native";
|
||||||
import FamilyChart from "./FamilyChart";
|
import FamilyChart from "./FamilyChart";
|
||||||
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
|
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
const FamilyChoresProgress = ({
|
const FamilyChoresProgress = ({
|
||||||
setPageIndex,
|
setPageIndex,
|
||||||
@ -12,7 +13,20 @@ const FamilyChoresProgress = ({
|
|||||||
return (
|
return (
|
||||||
<View marginT-20 marginH-5>
|
<View marginT-20 marginH-5>
|
||||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<Text style={{ fontFamily: "Manrope_200", fontSize: 12 }}>Back to ToDos</Text>
|
<View row marginT-4 marginB-10 centerV>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={14}
|
||||||
|
color="#979797"
|
||||||
|
style={{ paddingBottom: 3 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
||||||
|
color="#979797"
|
||||||
|
>
|
||||||
|
Return to To Do's
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View centerH>
|
<View centerH>
|
||||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>
|
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>
|
||||||
|
|||||||
@ -30,8 +30,21 @@ const UserChoresProgress = ({
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<Text style={{ fontSize: 14 }}>Back to ToDos</Text>
|
<View row marginT-4 marginB-10 centerV>
|
||||||
</TouchableOpacity>
|
<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>
|
<View>
|
||||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
||||||
Your To Do's Progress Report
|
Your To Do's Progress Report
|
||||||
|
|||||||
@ -1,18 +1,37 @@
|
|||||||
import { Image, Text, View } from "react-native-ui-lib";
|
import { Image, Text, View } from "react-native-ui-lib";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { isFamilyViewAtom } from "../pages/calendar/atoms";
|
||||||
|
import { useGetChildrenByParentId } from "@/hooks/firebase/useGetChildrenByParentId";
|
||||||
|
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||||
|
import { child } from "@react-native-firebase/storage";
|
||||||
|
import CachedImage from 'expo-cached-image'
|
||||||
|
|
||||||
const HeaderTemplate = (props: {
|
const HeaderTemplate = (props: {
|
||||||
message: string;
|
message: string;
|
||||||
isWelcome: boolean;
|
isWelcome: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
link?: React.ReactNode;
|
link?: React.ReactNode;
|
||||||
|
isCalendar?: boolean;
|
||||||
|
isToDos?: boolean;
|
||||||
|
isBrainDump?: boolean;
|
||||||
|
isGroceries?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
|
|
||||||
const headerHeight: number = 72;
|
const { data: members } = useGetFamilyMembers();
|
||||||
|
const [children, setChildren] = useState<UserProfile[]>([]);
|
||||||
|
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
|
|
||||||
|
const headerHeight: number =
|
||||||
|
(props.isCalendar && 65.54) ||
|
||||||
|
(props.isToDos && 84) ||
|
||||||
|
(props.isGroceries && 72.09) ||
|
||||||
|
65.54;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
pfp: {
|
pfp: {
|
||||||
@ -26,14 +45,71 @@ const HeaderTemplate = (props: {
|
|||||||
pfpTxt: {
|
pfpTxt: {
|
||||||
fontFamily: "Manrope_500Medium",
|
fontFamily: "Manrope_500Medium",
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
color: 'white',
|
color: "white",
|
||||||
|
},
|
||||||
|
childrenPfpArr: {
|
||||||
|
width: 65.54,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -12.44,
|
||||||
|
left: (children.length > 3 && -9) || 0,
|
||||||
|
height: 27.32,
|
||||||
|
},
|
||||||
|
childrenPfp: {
|
||||||
|
aspectRatio: 1,
|
||||||
|
width: 27.32,
|
||||||
|
backgroundColor: "#fd1575",
|
||||||
|
borderRadius: 50,
|
||||||
|
position: "absolute",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#f2f2f2",
|
||||||
|
},
|
||||||
|
bottomMarg: {
|
||||||
|
marginBottom: isFamilyView ? 30 : 15,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (members) {
|
||||||
|
const childrenMembers = members.filter(
|
||||||
|
(member) => member.userType === ProfileType.CHILD
|
||||||
|
);
|
||||||
|
setChildren(childrenMembers);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View row centerV marginV-15>
|
<View row centerV marginV-15 style={styles.bottomMarg}>
|
||||||
{profileData?.pfp ? (
|
{profileData?.pfp ? (
|
||||||
<Image source={{ uri: profileData.pfp }} style={styles.pfp} />
|
<View>
|
||||||
|
<CachedImage source={{ uri: profileData.pfp, }} style={styles.pfp} cacheKey={profileData.pfp}/>
|
||||||
|
{isFamilyView && props.isCalendar && (
|
||||||
|
<View style={styles.childrenPfpArr} row>
|
||||||
|
{children?.slice(0, 3).map((child, index) => {
|
||||||
|
return child.pfp ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: child.pfp }}
|
||||||
|
style={[styles.childrenPfp, { left: index * 19 }]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[styles.childrenPfp, { left: index * 19 }]}
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<Text style={{ color: "white" }}>
|
||||||
|
{child?.firstName?.at(0)}
|
||||||
|
{child?.firstName?.at(1)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{children?.length > 3 && (
|
||||||
|
<View style={[styles.childrenPfp, { left: 3 * 19 }]} center>
|
||||||
|
<Text style={{ color: "white", fontFamily: "Manrope_600SemiBold", fontSize: 12 }}>+{children.length - 3}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.pfp} center>
|
<View style={styles.pfp} center>
|
||||||
<Text style={styles.pfpTxt}>
|
<Text style={styles.pfpTxt}>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { View } from "react-native-ui-lib";
|
|||||||
const RemoveAssigneeBtn = () => {
|
const RemoveAssigneeBtn = () => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.removeBtn} center>
|
<View style={styles.removeBtn} center>
|
||||||
<CloseXIcon />
|
<CloseXIcon width={9} height={9} strokeWidth={2} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import * as Device from 'expo-device';
|
import * as Device from 'expo-device';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import { Platform } from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import {useQueryClient} from "react-query";
|
import {useQueryClient} from "react-query";
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ interface IAuthContext {
|
|||||||
profileType?: ProfileType,
|
profileType?: ProfileType,
|
||||||
profileData?: UserProfile,
|
profileData?: UserProfile,
|
||||||
setProfileData: (profileData: UserProfile) => void,
|
setProfileData: (profileData: UserProfile) => void,
|
||||||
|
setRedirectOverride: (val: boolean) => void,
|
||||||
refreshProfileData: () => Promise<void>
|
refreshProfileData: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,16 +51,16 @@ async function registerForPushNotificationsAsync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Device.isDevice) {
|
if (Device.isDevice) {
|
||||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
const {status: existingStatus} = await Notifications.getPermissionsAsync();
|
||||||
let finalStatus = existingStatus;
|
let finalStatus = existingStatus;
|
||||||
|
|
||||||
if (existingStatus !== 'granted') {
|
if (existingStatus !== 'granted') {
|
||||||
const { status } = await Notifications.requestPermissionsAsync();
|
const {status} = await Notifications.requestPermissionsAsync();
|
||||||
finalStatus = status;
|
finalStatus = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalStatus !== 'granted') {
|
if (finalStatus !== 'granted') {
|
||||||
alert('Failed to get push token for push notification!');
|
// alert('Failed to get push token for push notification!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,15 +73,15 @@ async function registerForPushNotificationsAsync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
|
const token = (await Notifications.getExpoPushTokenAsync({projectId})).data;
|
||||||
console.log('Push Token:', token);
|
console.log('Push Token:', token);
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error getting push token: ${error}`);
|
// alert(`Error getting push token: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Must use a physical device for push notifications');
|
// alert('Must use a physical device for push notifications');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,24 +92,28 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
const [initializing, setInitializing] = useState(true);
|
const [initializing, setInitializing] = useState(true);
|
||||||
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
||||||
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
||||||
|
const [redirectOverride, setRedirectOverride] = useState(false);
|
||||||
|
|
||||||
const {replace} = useRouter();
|
const {replace} = useRouter();
|
||||||
const ready = !initializing;
|
const ready = !initializing;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
||||||
setUser(authUser);
|
if (!redirectOverride) {
|
||||||
|
|
||||||
|
setUser(authUser);
|
||||||
|
|
||||||
if (authUser) {
|
if (authUser) {
|
||||||
await refreshProfileData(authUser);
|
await refreshProfileData(authUser);
|
||||||
const pushToken = await registerForPushNotificationsAsync();
|
const pushToken = await registerForPushNotificationsAsync();
|
||||||
if (pushToken) {
|
if (pushToken) {
|
||||||
await savePushTokenToFirestore(authUser.uid, pushToken);
|
await savePushTokenToFirestore(authUser.uid, pushToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (initializing) setInitializing(false);
|
if (initializing) setInitializing(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
||||||
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}, [initializing]);
|
}, [initializing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ready && user) {
|
if (ready && user && !redirectOverride) {
|
||||||
replace({pathname: "/(auth)/calendar"});
|
replace({pathname: "/(auth)/calendar"});
|
||||||
} else if (ready && !user) {
|
} else if (ready && !user && !redirectOverride) {
|
||||||
replace({pathname: "/(unauth)"});
|
replace({pathname: "/(unauth)"});
|
||||||
}
|
}
|
||||||
}, [user, ready]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
const sub = Notifications.addNotificationReceivedListener(notification => {
|
||||||
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
|
<AuthContext.Provider
|
||||||
|
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => {
|
exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
|
||||||
console.log('Running token refresh job...');
|
console.log('Running token refresh job...');
|
||||||
|
|
||||||
const profilesSnapshot = await db.collection('Profiles').get();
|
const profilesSnapshot = await db.collection('Profiles').get();
|
||||||
@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
|
|||||||
if (profileData.googleAccounts) {
|
if (profileData.googleAccounts) {
|
||||||
try {
|
try {
|
||||||
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
||||||
const googleToken = profileData?.googleAccounts?.[googleEmail];
|
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||||
if (googleToken) {
|
if (googleToken) {
|
||||||
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
||||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
||||||
@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to refresh Google token
|
async function refreshGoogleToken(refreshToken) {
|
||||||
async function refreshGoogleToken(token) {
|
try {
|
||||||
// Assuming you use OAuth2 token refresh flow
|
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
||||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
grant_type: 'refresh_token',
|
||||||
grant_type: 'refresh_token',
|
refresh_token: refreshToken,
|
||||||
refresh_token: token, // Add refresh token stored previously
|
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
|
||||||
client_id: 'YOUR_GOOGLE_CLIENT_ID',
|
});
|
||||||
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data.access_token; // Return new access token
|
return response.data.access_token; // Return the new access token
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Google token:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMicrosoftToken(token) {
|
async function refreshMicrosoftToken(refreshToken) {
|
||||||
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
try {
|
||||||
grant_type: 'refresh_token',
|
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
refresh_token: token, // Add refresh token stored previously
|
grant_type: 'refresh_token',
|
||||||
client_id: 'YOUR_MICROSOFT_CLIENT_ID',
|
refresh_token: refreshToken,
|
||||||
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
|
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
|
||||||
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
|
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() {
|
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 firestore from "@react-native-firebase/firestore";
|
||||||
import storage from "@react-native-firebase/storage";
|
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 * as ImagePicker from "expo-image-picker";
|
||||||
import { Platform } from "react-native";
|
import {Platform} from "react-native";
|
||||||
|
|
||||||
export const useChangeProfilePicture = () => {
|
export const useChangeProfilePicture = (customUserId?: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { user, refreshProfileData } = useAuthContext();
|
const {user, refreshProfileData} = useAuthContext();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["changeProfilePicture"],
|
mutationKey: ["changeProfilePicture"],
|
||||||
@ -38,20 +38,24 @@ export const useChangeProfilePicture = () => {
|
|||||||
const downloadURL = await reference.getDownloadURL();
|
const downloadURL = await reference.getDownloadURL();
|
||||||
console.log("Download URL:", downloadURL);
|
console.log("Download URL:", downloadURL);
|
||||||
|
|
||||||
|
if(!customUserId) {
|
||||||
await firestore()
|
await firestore()
|
||||||
.collection("Profiles")
|
.collection("Profiles")
|
||||||
.doc(user?.uid)
|
.doc(user?.uid)
|
||||||
.update({ pfp: downloadURL });
|
.update({pfp: downloadURL});
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error uploading profile picture:", e.message);
|
console.error("Error uploading profile picture:", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Invalidate queries to refresh profile data
|
// Invalidate queries to refresh profile data
|
||||||
queryClient.invalidateQueries("Profiles");
|
if (!customUserId) {
|
||||||
refreshProfileData();
|
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 = () => {
|
export const useCreateEventsFromProvider = () => {
|
||||||
const {user: currentUser} = useAuthContext();
|
const { user: currentUser } = useAuthContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createEventsFromProvider"],
|
mutationKey: ["createEventsFromProvider"],
|
||||||
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
|
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
|
||||||
try {
|
try {
|
||||||
for (const eventData of eventDataArray) {
|
// Create an array of promises for each event's Firestore read/write operation
|
||||||
|
const promises = eventDataArray.map(async (eventData) => {
|
||||||
console.log("Processing EventData: ", eventData);
|
console.log("Processing EventData: ", eventData);
|
||||||
|
|
||||||
|
// Check if the event already exists
|
||||||
const snapshot = await firestore()
|
const snapshot = await firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.where("id", "==", eventData.id)
|
.where("id", "==", eventData.id)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (snapshot.empty) {
|
if (snapshot.empty) {
|
||||||
await firestore()
|
// Event doesn't exist, so add it
|
||||||
|
return firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.add({...eventData, creatorId: currentUser?.uid});
|
.add({ ...eventData, creatorId: currentUser?.uid });
|
||||||
} else {
|
} else {
|
||||||
console.log("Event already exists, updating...");
|
// Event exists, update it
|
||||||
const docId = snapshot.docs[0].id;
|
const docId = snapshot.docs[0].id;
|
||||||
await firestore()
|
return firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.doc(docId)
|
.doc(docId)
|
||||||
.set({...eventData, creatorId: currentUser?.uid}, {merge: true});
|
.set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// Execute all promises in parallel
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error creating/updating events: ", e);
|
console.error("Error creating/updating events: ", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export const useLoginWithQrCode = () => {
|
export const useLoginWithQrCode = () => {
|
||||||
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["loginWithQrCode"],
|
mutationKey: ["loginWithQrCode"],
|
||||||
mutationFn: async ({userId}: { userId: string }) => {
|
mutationFn: async ({userId}: { userId: string }) => {
|
||||||
try {
|
try {
|
||||||
|
setRedirectOverride(true)
|
||||||
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
||||||
token: string
|
token: string
|
||||||
}>
|
}>
|
||||||
|
|||||||
@ -1,44 +1,47 @@
|
|||||||
import { useMutation } from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import { useSetUserData } from "./useSetUserData";
|
import {useSetUserData} from "./useSetUserData";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import * as Localization from "expo-localization";
|
import * as Localization from "expo-localization";
|
||||||
|
|
||||||
export const useSignUp = () => {
|
export const useSignUp = () => {
|
||||||
const { mutateAsync: setUserData } = useSetUserData();
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
const {mutateAsync: setUserData} = useSetUserData();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["signUp"],
|
mutationKey: ["signUp"],
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}) => {
|
}) => {
|
||||||
await auth()
|
setRedirectOverride(true)
|
||||||
.createUserWithEmailAndPassword(email, password)
|
|
||||||
.then(async (res) => {
|
await auth()
|
||||||
try {
|
.createUserWithEmailAndPassword(email, password)
|
||||||
await setUserData({
|
.then(async (res) => {
|
||||||
newUserData: {
|
try {
|
||||||
userType: ProfileType.PARENT,
|
await setUserData({
|
||||||
firstName: firstName,
|
newUserData: {
|
||||||
lastName: lastName,
|
userType: ProfileType.PARENT,
|
||||||
familyId: uuidv4(),
|
firstName: firstName,
|
||||||
timeZone: Localization.getCalendars()[0].timeZone,
|
lastName: lastName,
|
||||||
},
|
familyId: uuidv4(),
|
||||||
customUser: res.user,
|
timeZone: Localization.getCalendars()[0].timeZone,
|
||||||
});
|
},
|
||||||
} catch (error) {
|
customUser: res.user,
|
||||||
console.error(error);
|
});
|
||||||
}
|
} 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 {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||||
|
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
|
||||||
|
|
||||||
export const useFetchAndSaveGoogleEvents = () => {
|
export const useFetchAndSaveGoogleEvents = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
const {mutateAsync: clearToken} = useClearTokens();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["fetchAndSaveGoogleEvents"],
|
mutationKey: ["fetchAndSaveGoogleEvents"],
|
||||||
@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
|
|||||||
timeMax.toISOString().slice(0, -5) + "Z"
|
timeMax.toISOString().slice(0, -5) + "Z"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(!response.success) {
|
||||||
|
await clearToken({email: email!, provider: "google"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Google Calendar events fetched:", response);
|
console.log("Google Calendar events fetched:", response);
|
||||||
|
|
||||||
const items = response?.map((item) => {
|
const items = response?.googleEvents?.map((item) => {
|
||||||
if (item.allDay) {
|
if (item.allDay) {
|
||||||
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
|
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
|
||||||
item.endDate = item.startDate;
|
item.endDate = item.startDate;
|
||||||
|
|||||||
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";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "Cally";
|
PRODUCT_NAME = Cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -484,7 +484,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "Cally";
|
PRODUCT_NAME = Cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
"expo-auth-session": "^5.5.2",
|
"expo-auth-session": "^5.5.2",
|
||||||
"expo-barcode-scanner": "~13.0.1",
|
"expo-barcode-scanner": "~13.0.1",
|
||||||
"expo-build-properties": "~0.12.4",
|
"expo-build-properties": "~0.12.4",
|
||||||
|
"expo-cached-image": "^51.0.19",
|
||||||
"expo-calendar": "~13.0.5",
|
"expo-calendar": "~13.0.5",
|
||||||
"expo-camera": "~15.0.16",
|
"expo-camera": "~15.0.16",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
|
|||||||
258
yarn.lock
258
yarn.lock
@ -960,6 +960,89 @@
|
|||||||
wrap-ansi "^7.0.0"
|
wrap-ansi "^7.0.0"
|
||||||
ws "^8.12.1"
|
ws "^8.12.1"
|
||||||
|
|
||||||
|
"@expo/cli@0.18.30":
|
||||||
|
version "0.18.30"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.18.30.tgz#0cb4829aa11e98ae350a5c15958b9816e9a1d2f0"
|
||||||
|
integrity sha512-V90TUJh9Ly8stYo8nwqIqNWCsYjE28GlVFWEhAFCUOp99foiQr8HSTpiiX5GIrprcPoWmlGoY+J5fQA29R4lFg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.20.0"
|
||||||
|
"@expo/code-signing-certificates" "0.0.5"
|
||||||
|
"@expo/config" "~9.0.0-beta.0"
|
||||||
|
"@expo/config-plugins" "~8.0.8"
|
||||||
|
"@expo/devcert" "^1.0.0"
|
||||||
|
"@expo/env" "~0.3.0"
|
||||||
|
"@expo/image-utils" "^0.5.0"
|
||||||
|
"@expo/json-file" "^8.3.0"
|
||||||
|
"@expo/metro-config" "0.18.11"
|
||||||
|
"@expo/osascript" "^2.0.31"
|
||||||
|
"@expo/package-manager" "^1.5.0"
|
||||||
|
"@expo/plist" "^0.1.0"
|
||||||
|
"@expo/prebuild-config" "7.0.9"
|
||||||
|
"@expo/rudder-sdk-node" "1.1.1"
|
||||||
|
"@expo/spawn-async" "^1.7.2"
|
||||||
|
"@expo/xcpretty" "^4.3.0"
|
||||||
|
"@react-native/dev-middleware" "0.74.85"
|
||||||
|
"@urql/core" "2.3.6"
|
||||||
|
"@urql/exchange-retry" "0.3.0"
|
||||||
|
accepts "^1.3.8"
|
||||||
|
arg "5.0.2"
|
||||||
|
better-opn "~3.0.2"
|
||||||
|
bplist-creator "0.0.7"
|
||||||
|
bplist-parser "^0.3.1"
|
||||||
|
cacache "^18.0.2"
|
||||||
|
chalk "^4.0.0"
|
||||||
|
ci-info "^3.3.0"
|
||||||
|
connect "^3.7.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
env-editor "^0.4.1"
|
||||||
|
fast-glob "^3.3.2"
|
||||||
|
find-yarn-workspace-root "~2.0.0"
|
||||||
|
form-data "^3.0.1"
|
||||||
|
freeport-async "2.0.0"
|
||||||
|
fs-extra "~8.1.0"
|
||||||
|
getenv "^1.0.0"
|
||||||
|
glob "^7.1.7"
|
||||||
|
graphql "15.8.0"
|
||||||
|
graphql-tag "^2.10.1"
|
||||||
|
https-proxy-agent "^5.0.1"
|
||||||
|
internal-ip "4.3.0"
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
is-wsl "^2.1.1"
|
||||||
|
js-yaml "^3.13.1"
|
||||||
|
json-schema-deref-sync "^0.13.0"
|
||||||
|
lodash.debounce "^4.0.8"
|
||||||
|
md5hex "^1.0.0"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
node-fetch "^2.6.7"
|
||||||
|
node-forge "^1.3.1"
|
||||||
|
npm-package-arg "^7.0.0"
|
||||||
|
open "^8.3.0"
|
||||||
|
ora "3.4.0"
|
||||||
|
picomatch "^3.0.1"
|
||||||
|
pretty-bytes "5.6.0"
|
||||||
|
progress "2.0.3"
|
||||||
|
prompts "^2.3.2"
|
||||||
|
qrcode-terminal "0.11.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
requireg "^0.2.2"
|
||||||
|
resolve "^1.22.2"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
resolve.exports "^2.0.2"
|
||||||
|
semver "^7.6.0"
|
||||||
|
send "^0.18.0"
|
||||||
|
slugify "^1.3.4"
|
||||||
|
source-map-support "~0.5.21"
|
||||||
|
stacktrace-parser "^0.1.10"
|
||||||
|
structured-headers "^0.4.1"
|
||||||
|
tar "^6.0.5"
|
||||||
|
temp-dir "^2.0.0"
|
||||||
|
tempy "^0.7.1"
|
||||||
|
terminal-link "^2.1.1"
|
||||||
|
text-table "^0.2.0"
|
||||||
|
url-join "4.0.0"
|
||||||
|
wrap-ansi "^7.0.0"
|
||||||
|
ws "^8.12.1"
|
||||||
|
|
||||||
"@expo/code-signing-certificates@0.0.5":
|
"@expo/code-signing-certificates@0.0.5":
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz"
|
resolved "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz"
|
||||||
@ -968,6 +1051,48 @@
|
|||||||
node-forge "^1.2.1"
|
node-forge "^1.2.1"
|
||||||
nullthrows "^1.1.1"
|
nullthrows "^1.1.1"
|
||||||
|
|
||||||
|
"@expo/config-plugins@8.0.10":
|
||||||
|
version "8.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-8.0.10.tgz#5cda076f38bc04675cb42d8acdd23d6e460a62de"
|
||||||
|
integrity sha512-KG1fnSKRmsudPU9BWkl59PyE0byrE2HTnqbOrgwr2FAhqh7tfr9nRs6A9oLS/ntpGzmFxccTEcsV0L4apsuxxg==
|
||||||
|
dependencies:
|
||||||
|
"@expo/config-types" "^51.0.3"
|
||||||
|
"@expo/json-file" "~8.3.0"
|
||||||
|
"@expo/plist" "^0.1.0"
|
||||||
|
"@expo/sdk-runtime-versions" "^1.0.0"
|
||||||
|
chalk "^4.1.2"
|
||||||
|
debug "^4.3.1"
|
||||||
|
find-up "~5.0.0"
|
||||||
|
getenv "^1.0.0"
|
||||||
|
glob "7.1.6"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
semver "^7.5.4"
|
||||||
|
slash "^3.0.0"
|
||||||
|
slugify "^1.6.6"
|
||||||
|
xcode "^3.0.1"
|
||||||
|
xml2js "0.6.0"
|
||||||
|
|
||||||
|
"@expo/config-plugins@8.0.9", "@expo/config-plugins@~8.0.0-beta.0", "@expo/config-plugins@~8.0.8":
|
||||||
|
version "8.0.9"
|
||||||
|
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.9.tgz"
|
||||||
|
integrity sha512-dNCG45C7BbDPV9MdWvCbsFtJtVn4w/TJbb5b7Yr6FA8HYIlaaVM0wqUMzTPmGj54iYXw8X/Vge8uCPxg7RWgeA==
|
||||||
|
dependencies:
|
||||||
|
"@expo/config-types" "^51.0.0-unreleased"
|
||||||
|
"@expo/json-file" "~8.3.0"
|
||||||
|
"@expo/plist" "^0.1.0"
|
||||||
|
"@expo/sdk-runtime-versions" "^1.0.0"
|
||||||
|
chalk "^4.1.2"
|
||||||
|
debug "^4.3.1"
|
||||||
|
find-up "~5.0.0"
|
||||||
|
getenv "^1.0.0"
|
||||||
|
glob "7.1.6"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
semver "^7.5.4"
|
||||||
|
slash "^3.0.0"
|
||||||
|
slugify "^1.6.6"
|
||||||
|
xcode "^3.0.1"
|
||||||
|
xml2js "0.6.0"
|
||||||
|
|
||||||
"@expo/config-plugins@~5.0.3":
|
"@expo/config-plugins@~5.0.3":
|
||||||
version "5.0.4"
|
version "5.0.4"
|
||||||
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-5.0.4.tgz"
|
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-5.0.4.tgz"
|
||||||
@ -1020,6 +1145,45 @@
|
|||||||
resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.2.tgz"
|
resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.2.tgz"
|
||||||
integrity sha512-IglkIoiDwJMY01lYkF/ZSBoe/5cR+O3+Gx6fpLFjLfgZGBTdyPkKa1g8NWoWQCk+D3cKL2MDbszT2DyRRB0YqQ==
|
integrity sha512-IglkIoiDwJMY01lYkF/ZSBoe/5cR+O3+Gx6fpLFjLfgZGBTdyPkKa1g8NWoWQCk+D3cKL2MDbszT2DyRRB0YqQ==
|
||||||
|
|
||||||
|
"@expo/config-types@^51.0.3":
|
||||||
|
version "51.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450"
|
||||||
|
integrity sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA==
|
||||||
|
|
||||||
|
"@expo/config@9.0.3", "@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0":
|
||||||
|
version "9.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/@expo/config/-/config-9.0.3.tgz"
|
||||||
|
integrity sha512-eOTNM8eOC8gZNHgenySRlc/lwmYY1NOgvjwA8LHuvPT7/eUwD93zrxu3lPD1Cc/P6C/2BcVdfH4hf0tLmDxnsg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "~7.10.4"
|
||||||
|
"@expo/config-plugins" "~8.0.8"
|
||||||
|
"@expo/config-types" "^51.0.0-unreleased"
|
||||||
|
"@expo/json-file" "^8.3.0"
|
||||||
|
getenv "^1.0.0"
|
||||||
|
glob "7.1.6"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
semver "^7.6.0"
|
||||||
|
slugify "^1.3.4"
|
||||||
|
sucrase "3.34.0"
|
||||||
|
|
||||||
|
"@expo/config@9.0.4":
|
||||||
|
version "9.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.4.tgz#52f0a94edd0e2c36dfb5e284cc1a6d99d9d2af97"
|
||||||
|
integrity sha512-g5ns5u1JSKudHYhjo1zaSfkJ/iZIcWmUmIQptMJZ6ag1C0ShL2sj8qdfU8MmAMuKLOgcIfSaiWlQnm4X3VJVkg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "~7.10.4"
|
||||||
|
"@expo/config-plugins" "~8.0.8"
|
||||||
|
"@expo/config-types" "^51.0.3"
|
||||||
|
"@expo/json-file" "^8.3.0"
|
||||||
|
getenv "^1.0.0"
|
||||||
|
glob "7.1.6"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
semver "^7.6.0"
|
||||||
|
slugify "^1.3.4"
|
||||||
|
sucrase "3.34.0"
|
||||||
|
|
||||||
"@expo/config@~7.0.2":
|
"@expo/config@~7.0.2":
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz"
|
resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz"
|
||||||
@ -1285,6 +1449,23 @@
|
|||||||
semver "^7.6.0"
|
semver "^7.6.0"
|
||||||
xml2js "0.6.0"
|
xml2js "0.6.0"
|
||||||
|
|
||||||
|
"@expo/prebuild-config@7.0.9":
|
||||||
|
version "7.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-7.0.9.tgz#7abd489e18ed6514a0c9cd214eb34c0d5efda799"
|
||||||
|
integrity sha512-9i6Cg7jInpnGEHN0jxnW0P+0BexnePiBzmbUvzSbRXpdXihYUX2AKMu73jgzxn5P1hXOSkzNS7umaY+BZ+aBag==
|
||||||
|
dependencies:
|
||||||
|
"@expo/config" "~9.0.0-beta.0"
|
||||||
|
"@expo/config-plugins" "~8.0.8"
|
||||||
|
"@expo/config-types" "^51.0.3"
|
||||||
|
"@expo/image-utils" "^0.5.0"
|
||||||
|
"@expo/json-file" "^8.3.0"
|
||||||
|
"@react-native/normalize-colors" "0.74.85"
|
||||||
|
debug "^4.3.1"
|
||||||
|
fs-extra "^9.0.0"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
semver "^7.6.0"
|
||||||
|
xml2js "0.6.0"
|
||||||
|
|
||||||
"@expo/rudder-sdk-node@1.1.1":
|
"@expo/rudder-sdk-node@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz"
|
||||||
@ -3592,6 +3773,19 @@ babel-plugin-polyfill-regenerator@^0.6.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-define-polyfill-provider" "^0.6.2"
|
"@babel/helper-define-polyfill-provider" "^0.6.2"
|
||||||
|
|
||||||
|
babel-plugin-react-compiler@0.0.0-experimental-592953e-20240517:
|
||||||
|
version "0.0.0-experimental-592953e-20240517"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-592953e-20240517.tgz#e800fa1550d03573cd5637218dc711f12f642249"
|
||||||
|
integrity sha512-OjG1SVaeQZaJrqkMFJatg8W/MTow8Ak5rx2SI0ETQBO1XvOk/XZGMbltNCPdFJLKghBYoBjC+Y3Ap/Xr7B01mA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/generator" "7.2.0"
|
||||||
|
"@babel/types" "^7.19.0"
|
||||||
|
chalk "4"
|
||||||
|
invariant "^2.2.4"
|
||||||
|
pretty-format "^24"
|
||||||
|
zod "^3.22.4"
|
||||||
|
zod-validation-error "^2.1.0"
|
||||||
|
|
||||||
babel-plugin-react-compiler@^0.0.0-experimental-592953e-20240517:
|
babel-plugin-react-compiler@^0.0.0-experimental-592953e-20240517:
|
||||||
version "0.0.0-experimental-7d62301-20240821"
|
version "0.0.0-experimental-7d62301-20240821"
|
||||||
resolved "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-7d62301-20240821.tgz"
|
resolved "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-7d62301-20240821.tgz"
|
||||||
@ -3659,6 +3853,22 @@ babel-preset-expo@~11.0.14:
|
|||||||
babel-plugin-react-native-web "~0.19.10"
|
babel-plugin-react-native-web "~0.19.10"
|
||||||
react-refresh "^0.14.2"
|
react-refresh "^0.14.2"
|
||||||
|
|
||||||
|
babel-preset-expo@~11.0.15:
|
||||||
|
version "11.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-11.0.15.tgz#f29b1ac1f59f8739f63c80515906186586c24d3c"
|
||||||
|
integrity sha512-rgiMTYwqIPULaO7iZdqyL7aAff9QLOX6OWUtLZBlOrOTreGY1yHah/5+l8MvI6NVc/8Zj5LY4Y5uMSnJIuzTLw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/plugin-proposal-decorators" "^7.12.9"
|
||||||
|
"@babel/plugin-transform-export-namespace-from" "^7.22.11"
|
||||||
|
"@babel/plugin-transform-object-rest-spread" "^7.12.13"
|
||||||
|
"@babel/plugin-transform-parameters" "^7.22.15"
|
||||||
|
"@babel/preset-react" "^7.22.15"
|
||||||
|
"@babel/preset-typescript" "^7.23.0"
|
||||||
|
"@react-native/babel-preset" "0.74.87"
|
||||||
|
babel-plugin-react-compiler "0.0.0-experimental-592953e-20240517"
|
||||||
|
babel-plugin-react-native-web "~0.19.10"
|
||||||
|
react-refresh "^0.14.2"
|
||||||
|
|
||||||
babel-preset-jest@^29.6.3:
|
babel-preset-jest@^29.6.3:
|
||||||
version "29.6.3"
|
version "29.6.3"
|
||||||
resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz"
|
resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz"
|
||||||
@ -5115,6 +5325,13 @@ expo-build-properties@~0.12.4:
|
|||||||
ajv "^8.11.0"
|
ajv "^8.11.0"
|
||||||
semver "^7.6.0"
|
semver "^7.6.0"
|
||||||
|
|
||||||
|
expo-cached-image@^51.0.19:
|
||||||
|
version "51.0.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-cached-image/-/expo-cached-image-51.0.19.tgz#27447d761a4b7414a2e5fee2e25c9436dd6f073e"
|
||||||
|
integrity sha512-HcIKolCKyrYcfimWp64S25Tv8YneUsKV47yJ93L4l4NVA7GJulqSS/fr2jf6B3mzw5rZNDU+eDAf1nzcxavfkg==
|
||||||
|
dependencies:
|
||||||
|
expo "51"
|
||||||
|
|
||||||
expo-calendar@~13.0.5:
|
expo-calendar@~13.0.5:
|
||||||
version "13.0.5"
|
version "13.0.5"
|
||||||
resolved "https://registry.npmjs.org/expo-calendar/-/expo-calendar-13.0.5.tgz"
|
resolved "https://registry.npmjs.org/expo-calendar/-/expo-calendar-13.0.5.tgz"
|
||||||
@ -5259,6 +5476,19 @@ expo-modules-autolinking@1.11.2:
|
|||||||
require-from-string "^2.0.2"
|
require-from-string "^2.0.2"
|
||||||
resolve-from "^5.0.0"
|
resolve-from "^5.0.0"
|
||||||
|
|
||||||
|
expo-modules-autolinking@1.11.3:
|
||||||
|
version "1.11.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.11.3.tgz#bc64d278c04015014bb5802e3cfcd942d7c07168"
|
||||||
|
integrity sha512-oYh8EZEvYF5TYppxEKUTTJmbr8j7eRRnrIxzZtMvxLTXoujThVPMFS/cbnSnf2bFm1lq50TdDNABhmEi7z0ngQ==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.1.0"
|
||||||
|
commander "^7.2.0"
|
||||||
|
fast-glob "^3.2.5"
|
||||||
|
find-up "^5.0.0"
|
||||||
|
fs-extra "^9.1.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
|
||||||
expo-modules-core@1.12.24:
|
expo-modules-core@1.12.24:
|
||||||
version "1.12.24"
|
version "1.12.24"
|
||||||
resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz"
|
resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz"
|
||||||
@ -5266,6 +5496,13 @@ expo-modules-core@1.12.24:
|
|||||||
dependencies:
|
dependencies:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
|
|
||||||
|
expo-modules-core@1.12.26:
|
||||||
|
version "1.12.26"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.26.tgz#86c4087dc6246abfc4d7f5e61097dc8cc4b22262"
|
||||||
|
integrity sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA==
|
||||||
|
dependencies:
|
||||||
|
invariant "^2.2.4"
|
||||||
|
|
||||||
expo-notifications@~0.28.18:
|
expo-notifications@~0.28.18:
|
||||||
version "0.28.18"
|
version "0.28.18"
|
||||||
resolved "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.18.tgz"
|
resolved "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.18.tgz"
|
||||||
@ -5359,6 +5596,27 @@ expo-web-browser@~13.0.0, expo-web-browser@~13.0.3:
|
|||||||
resolved "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-13.0.3.tgz"
|
resolved "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-13.0.3.tgz"
|
||||||
integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ==
|
integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ==
|
||||||
|
|
||||||
|
expo@51:
|
||||||
|
version "51.0.38"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.38.tgz#e4127b230454a34a507cfb9f1a2e4b3855cb0579"
|
||||||
|
integrity sha512-/B9npFkOPmv6WMIhdjQXEY0Z9k/67UZIVkodW8JxGIXwKUZAGHL+z1R5hTtWimpIrvVhyHUFU3f8uhfEKYhHNQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.20.0"
|
||||||
|
"@expo/cli" "0.18.30"
|
||||||
|
"@expo/config" "9.0.4"
|
||||||
|
"@expo/config-plugins" "8.0.10"
|
||||||
|
"@expo/metro-config" "0.18.11"
|
||||||
|
"@expo/vector-icons" "^14.0.3"
|
||||||
|
babel-preset-expo "~11.0.15"
|
||||||
|
expo-asset "~10.0.10"
|
||||||
|
expo-file-system "~17.0.1"
|
||||||
|
expo-font "~12.0.10"
|
||||||
|
expo-keep-awake "~13.0.2"
|
||||||
|
expo-modules-autolinking "1.11.3"
|
||||||
|
expo-modules-core "1.12.26"
|
||||||
|
fbemitter "^3.0.0"
|
||||||
|
whatwg-url-without-unicode "8.0.0-3"
|
||||||
|
|
||||||
expo@~51.0.24:
|
expo@~51.0.24:
|
||||||
version "51.0.34"
|
version "51.0.34"
|
||||||
resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz"
|
resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user