diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx
index 662daa9..2edaa4e 100644
--- a/app/(auth)/_layout.tsx
+++ b/app/(auth)/_layout.tsx
@@ -23,8 +23,9 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
-import {useAtom, useSetAtom} from "jotai";
+import { useAtom, useSetAtom } from "jotai";
import {
+ isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
@@ -32,6 +33,7 @@ import {
export default function TabLayout() {
const { mutateAsync: signOut } = useSignOut();
+ const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex);
@@ -79,6 +81,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
+ setIsFamilyView(false);
}}
icon={}
/>
@@ -91,6 +94,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
+ setIsFamilyView(false);
}}
icon={}
/>
@@ -118,6 +122,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
+ setIsFamilyView(false);
}}
icon={}
/>
@@ -130,6 +135,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
+ setIsFamilyView(false);
}}
icon={}
/>
@@ -142,6 +148,7 @@ export default function TabLayout() {
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
+ setIsFamilyView(false);
}}
label={"Manage Settings"}
labelStyle={styles.label}
diff --git a/app/(unauth)/cal_sync.tsx b/app/(unauth)/cal_sync.tsx
new file mode 100644
index 0000000..0e9f15b
--- /dev/null
+++ b/app/(unauth)/cal_sync.tsx
@@ -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 (
+
+
+
+
+ Let's get started!
+
+
+ Add your calendar below to sync events to your Cally calendar
+
+
+
+
+ {!profileData?.googleAccounts && (
+
+
+
+
+
+ setRedirectOverride(false)}
+ marginT-50
+ labelStyle={{
+ fontFamily: "PlusJakartaSans_600SemiBold",
+ fontSize: 16,
+ }}
+ style={{height: 50}}
+ backgroundColor="#fd1775"
+ />
+
+
+
+ )
+}
+
+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,
+ },
+});
diff --git a/app/(unauth)/onboarding_flow/index.tsx b/app/(unauth)/connect.tsx
similarity index 80%
rename from app/(unauth)/onboarding_flow/index.tsx
rename to app/(unauth)/connect.tsx
index 8d09129..7abd451 100644
--- a/app/(unauth)/onboarding_flow/index.tsx
+++ b/app/(unauth)/connect.tsx
@@ -1,5 +1,5 @@
import Entry from "@/components/pages/main/Entry";
export default function Screen() {
- return ;
+ return ;
}
diff --git a/app/(unauth)/get_started.tsx b/app/(unauth)/get_started.tsx
new file mode 100644
index 0000000..f5788f2
--- /dev/null
+++ b/app/(unauth)/get_started.tsx
@@ -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(null);
+ const [showCameraDialog, setShowCameraDialog] = useState(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 (
+
+
+
+
+ Get started with Cally
+
+
+
+
+
+ }
+ onPress={handleOpenQrCodeDialog}
+ style={{height: 50}}
+ color={Colors.black}
+ backgroundColor={Colors.white}
+ />
+ {/* GOOGLE LOGIN HERE */}
+
+
+
+
+
+ or
+
+
+
+
+
+ router.push("/(unauth)/sign_up")}
+ style={{height: 50, borderStyle: "solid", borderColor: "#E2E2E2", borderWidth: 2}}
+ color={Colors.black}
+ backgroundColor={"transparent"}
+ />
+
+
+
+
+
+
+
+
+ Already have an account?
+
+
+ router.push("/(unauth)/sign_in")}
+ labelStyle={[
+ {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 16,
+ color: "#919191",
+ },
+ {fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
+ ]}
+ />
+
+
+
+ {/* Legacy, move into separate component */}
+ {/* Camera Dialog */}
+
+
+
+ {isLoading && (
+
+ )}
+
+ )
+}
diff --git a/app/(unauth)/index.tsx b/app/(unauth)/index.tsx
index 6bd604c..25d0cd0 100644
--- a/app/(unauth)/index.tsx
+++ b/app/(unauth)/index.tsx
@@ -1,10 +1,44 @@
-import Entry from "@/components/pages/main/Entry";
import {SafeAreaView} from "react-native-safe-area-context";
+import {Button, Image, Text, View} from "react-native-ui-lib";
+import React from "react";
+import {useRouter} from "expo-router";
export default function Screen() {
+ const router = useRouter()
+
return (
-
-
+
+
+
+
+
+
+
+
+ Welcome to Cally
+
+
+ Lightening Mental Loads,
+ One Family at a Time
+
+
+
+
+
+
+ router.push("/(unauth)/get_started")}
+ style={{height: 50}}
+ backgroundColor="#fd1775"
+ />
+
+
)
-}
\ No newline at end of file
+}
diff --git a/app/(unauth)/onboarding_flow/_layout.tsx b/app/(unauth)/onboarding_flow/_layout.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/app/(unauth)/reset_password.tsx b/app/(unauth)/reset_password.tsx
new file mode 100644
index 0000000..ce263f1
--- /dev/null
+++ b/app/(unauth)/reset_password.tsx
@@ -0,0 +1,6 @@
+import React from "react";
+import {ResetPasswordPage} from "@/components/pages/main/ResetPasswordPage";
+
+export default function Screen() {
+ return
+}
diff --git a/app/(unauth)/sign_in.tsx b/app/(unauth)/sign_in.tsx
new file mode 100644
index 0000000..454fbef
--- /dev/null
+++ b/app/(unauth)/sign_in.tsx
@@ -0,0 +1,6 @@
+import SignInPage from "@/components/pages/main/SignInPage";
+import React from "react";
+
+export default function Screen() {
+ return
+}
diff --git a/app/(unauth)/sign_up.tsx b/app/(unauth)/sign_up.tsx
new file mode 100644
index 0000000..d653f37
--- /dev/null
+++ b/app/(unauth)/sign_up.tsx
@@ -0,0 +1,8 @@
+import React from "react";
+import SignUpPage from "@/components/pages/main/SignUpPage";
+
+export default function Screen() {
+ return (
+
+ )
+}
diff --git a/assets/svgs/CloseXIcon.tsx b/assets/svgs/CloseXIcon.tsx
index b072267..b040fe6 100644
--- a/assets/svgs/CloseXIcon.tsx
+++ b/assets/svgs/CloseXIcon.tsx
@@ -5,13 +5,14 @@ const CloseXIcon: React.FC = (props) => (
width={15}
height={15}
fill="none"
+ viewBox="0 0 15 15"
{...props}
>
diff --git a/assets/svgs/PlusIcon.tsx b/assets/svgs/PlusIcon.tsx
index 5e9af64..7acb9ad 100644
--- a/assets/svgs/PlusIcon.tsx
+++ b/assets/svgs/PlusIcon.tsx
@@ -4,6 +4,7 @@ const PlusIcon = (props: SvgProps) => (
+ );
};
const styles = StyleSheet.create({
- textfield: {
- backgroundColor: "white",
- marginVertical: 10,
- padding: 30,
- height: 45,
- borderRadius: 50,
- fontFamily: "PlusJakartaSans_300Light",
- },
- jakartaLight: {
- fontFamily: "PlusJakartaSans_300Light",
- fontSize: 16,
- color: "#484848",
- },
- jakartaMedium: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 16,
- color: "#919191",
- textDecorationLine: "underline",
- },
+ textfield: {
+ backgroundColor: "white",
+ marginVertical: 10,
+ padding: 30,
+ height: 45,
+ borderRadius: 50,
+ fontFamily: "PlusJakartaSans_300Light",
+ },
+ jakartaLight: {
+ fontFamily: "PlusJakartaSans_300Light",
+ fontSize: 16,
+ color: "#484848",
+ },
+ jakartaMedium: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 16,
+ color: "#919191",
+ textDecorationLine: "underline",
+ },
});
export default SignInPage;
diff --git a/components/pages/main/SignUpPage.tsx b/components/pages/main/SignUpPage.tsx
index 361dbb6..32f83b1 100644
--- a/components/pages/main/SignUpPage.tsx
+++ b/components/pages/main/SignUpPage.tsx
@@ -3,6 +3,9 @@ import {
Button,
ButtonSize,
Checkbox,
+ Colors,
+ KeyboardAwareScrollView,
+ LoaderScreen,
Text,
TextField,
TextFieldRef,
@@ -10,16 +13,15 @@ import {
View,
} from "react-native-ui-lib";
import {useSignUp} from "@/hooks/firebase/useSignUp";
-import {StyleSheet} from "react-native";
+import {KeyboardAvoidingView, StyleSheet} from "react-native";
import {AntDesign} from "@expo/vector-icons";
+import KeyboardManager from "react-native-keyboard-manager";
+import {SafeAreaView} from "react-native-safe-area-context";
+import {useRouter} from "expo-router";
-const SignUpPage = ({
- setTab,
- }: {
- setTab: React.Dispatch<
- React.SetStateAction<"register" | "login" | "reset-password">
- >;
-}) => {
+KeyboardManager.setEnableAutoToolbar(true);
+
+const SignUpPage = () => {
const [email, setEmail] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
@@ -28,154 +30,196 @@ const SignUpPage = ({
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const [allowFaceID, setAllowFaceID] = useState(false);
const [acceptTerms, setAcceptTerms] = useState(false);
- const {mutateAsync: signUp} = useSignUp();
+ const {mutateAsync: signUp, isLoading} = useSignUp();
const lnameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
+ const router = useRouter()
+
const handleSignUp = async () => {
await signUp({email, password, firstName, lastName});
+ router.replace("/(unauth)/cal_sync")
};
return (
-
- Get started with Cally
-
- Please enter your details.
-
- {
- lnameRef.current?.focus();
- }}
- blurOnSubmit={false}
- />
- {
- emailRef.current?.focus();
- }}
- blurOnSubmit={false}
- />
- {
- passwordRef.current?.focus();
- }}
- blurOnSubmit={false}
- />
-
- setIsPasswordVisible(!isPasswordVisible)}
- >
-
-
- }
- />
-
-
-
- {
- setAllowFaceID(value);
- }}
- />
-
- Allow FaceID for login in future
-
-
-
- setAcceptTerms(value)}
- />
-
-
- I accept the
+
+
+
+
+
+ Get started with Cally
-
-
- {" "}
- terms and conditions
+
+ Please enter your details.
+
+
+
+
+ {
+ lnameRef.current?.focus();
+ }}
+ blurOnSubmit={false}
+ accessibilityLabel="First name input"
+ accessibilityHint="Enter your first name"
+ accessible
+ returnKeyType="next"
+ textContentType="givenName"
+ importantForAccessibility="yes"
+ />
+ {
+ emailRef.current?.focus();
+ }}
+ blurOnSubmit={false}
+ accessibilityLabel="Last name input"
+ accessibilityHint="Enter your last name"
+ accessible
+ returnKeyType="next"
+ textContentType="familyName"
+ importantForAccessibility="yes"
+ />
+ {
+ passwordRef.current?.focus();
+ }}
+ />
+
+
+ setIsPasswordVisible(!isPasswordVisible)}
+ >
+
+
+ }
+ />
+
+
+
+
+
+
+ {
+ setAllowFaceID(value);
+ }}
+ />
+
+ Allow FaceID for login in future
-
- and
-
-
- {" "}
- privacy policy
+
+
+ setAcceptTerms(value)}
+ />
+
+
+ I accept the
+
+
+
+ {" "}
+ terms and conditions
+
+
+ and
+
+
+ {" "}
+ privacy policy
+
+
+
+
+
+
+
+
+
+
+
+
+ Already have an account?
-
+
+ router.replace("/(unauth)/sign_in")}
+ />
+
-
-
-
-
-
-
- Already have an account?
-
+
- setTab("login")}
- />
-
-
-
+ {isLoading && (
+
+ )}
+
);
};
@@ -192,8 +236,6 @@ const styles = StyleSheet.create({
fontSize: 13,
color: "#919191",
},
- //mora da se izmeni kako treba
- bottomView: {marginTop: "auto", marginBottom: 30, marginTop: "auto"},
jakartaLight: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 13,
diff --git a/components/pages/settings/CalendarSettingsPage.tsx b/components/pages/settings/CalendarSettingsPage.tsx
index 809d877..46ff557 100644
--- a/components/pages/settings/CalendarSettingsPage.tsx
+++ b/components/pages/settings/CalendarSettingsPage.tsx
@@ -1,851 +1,547 @@
-import { AntDesign, Ionicons } from "@expo/vector-icons";
-import React, { useCallback, useEffect, useState } from "react";
-import { Button, Checkbox, Text, View } from "react-native-ui-lib";
-import { ActivityIndicator, Alert, ScrollView, StyleSheet } from "react-native";
-import { TouchableOpacity } from "react-native-gesture-handler";
-import { useAuthContext } from "@/contexts/AuthContext";
-import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
+import {AntDesign, Ionicons} from "@expo/vector-icons";
+import React, {useCallback, useState} from "react";
+import {Button, Checkbox, Text, View} from "react-native-ui-lib";
+import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
+import {TouchableOpacity} from "react-native-gesture-handler";
+import {useAuthContext} from "@/contexts/AuthContext";
+import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce";
import AppleIcon from "@/assets/svgs/AppleIcon";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
import OutlookIcon from "@/assets/svgs/OutlookIcon";
-import * as AuthSession from "expo-auth-session";
-import * as Google from "expo-auth-session/providers/google";
-import * as WebBrowser from "expo-web-browser";
-import { UserProfile } from "@firebase/auth";
-import { useFetchAndSaveGoogleEvents } from "@/hooks/useFetchAndSaveGoogleEvents";
-import { useFetchAndSaveOutlookEvents } from "@/hooks/useFetchAndSaveOutlookEvents";
-import { useFetchAndSaveAppleEvents } from "@/hooks/useFetchAndSaveAppleEvents";
-import * as AppleAuthentication from "expo-apple-authentication";
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
-import { colorMap } from "@/constants/colorMap";
-import { useAtom } from "jotai";
-import { settingsPageIndex } from "../calendar/atoms";
+import {colorMap} from "@/constants/colorMap";
+import {useSetAtom} from "jotai";
+import {settingsPageIndex} from "../calendar/atoms";
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
+import {useClearTokens} from "@/hooks/firebase/useClearTokens";
+import {useCalSync} from "@/hooks/useCalSync";
+import Feather from "@expo/vector-icons/Feather";
-const googleConfig = {
- androidClientId:
- "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
- iosClientId:
- "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
- webClientId:
- "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
- scopes: [
- "email",
- "profile",
- "https://www.googleapis.com/auth/calendar.events.owned",
- ],
-};
-
-const microsoftConfig = {
- clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
- redirectUri: AuthSession.makeRedirectUri({ path: "settings" }),
- scopes: [
- "openid",
- "profile",
- "email",
- "offline_access",
- "Calendars.ReadWrite",
- "User.Read",
- ],
- authorizationEndpoint:
- "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
- tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
-};
const CalendarSettingsPage = () => {
- const { profileData } = useAuthContext();
- const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
- const [firstDayOfWeek, setFirstDayOfWeek] = useState(
- profileData?.firstDayOfWeek ??
- ExpoLocalization.getCalendars()[0].firstWeekday === 1
- ? "Mondays"
- : "Sundays"
- );
- const [isModalVisible, setModalVisible] = useState(false);
- const [selectedService, setSelectedService] = useState<
- "google" | "outlook" | "apple"
- >("google");
- const [selectedEmail, setSelectedEmail] = useState("");
+ const {profileData} = useAuthContext();
+ const setPageIndex = useSetAtom(settingsPageIndex);
- const showConfirmationDialog = (
- serviceName: "google" | "outlook" | "apple",
- email: string
- ) => {
- setSelectedService(serviceName);
- setSelectedEmail(email);
- setModalVisible(true);
- };
-
- const handleConfirm = () => {
- clearToken(selectedService, selectedEmail);
- setModalVisible(false);
- };
-
- const handleCancel = () => {
- setModalVisible(false);
- };
-
- const [selectedColor, setSelectedColor] = useState(
- profileData?.eventColor ?? colorMap.pink
- );
- const [previousSelectedColor, setPreviousSelectedColor] = useState(
- profileData?.eventColor ?? colorMap.pink
- );
-
- 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 = response.authentication?.accessToken;
-
- const userInfoResponse = await fetch(
- "https://www.googleapis.com/oauth2/v3/userinfo",
- {
- headers: { Authorization: `Bearer ${accessToken}` },
- }
- );
-
- const userInfo = await userInfoResponse.json();
- const googleMail = userInfo.email;
-
- let googleAccounts = profileData?.googleAccounts;
- const updatedGoogleAccounts = googleAccounts
- ? { ...googleAccounts, [googleMail]: accessToken }
- : { [googleMail]: accessToken };
-
- await updateUserData({
- newUserData: { googleAccounts: updatedGoogleAccounts },
- });
-
- await fetchAndSaveGoogleEvents({
- token: accessToken,
- email: googleMail,
- });
- }
- } catch (error) {
- console.error("Error during Google sign-in:", error);
- }
- };
-
- const handleMicrosoftSignIn = async () => {
- try {
- console.log("Starting Microsoft sign-in...");
-
- const authRequest = new AuthSession.AuthRequest({
- clientId: microsoftConfig.clientId,
- scopes: microsoftConfig.scopes,
- redirectUri: microsoftConfig.redirectUri,
- responseType: AuthSession.ResponseType.Code,
- usePKCE: true, // Enable PKCE
- });
-
- console.log("Auth request created:", authRequest);
-
- const authResult = await authRequest.promptAsync({
- authorizationEndpoint: microsoftConfig.authorizationEndpoint,
- });
-
- console.log("Auth result:", authResult);
-
- if (authResult.type === "success" && authResult.params?.code) {
- const code = authResult.params.code;
- console.log("Authorization code received:", code);
-
- // Exchange authorization code for tokens
- const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: `client_id=${
- microsoftConfig.clientId
- }&redirect_uri=${encodeURIComponent(
- microsoftConfig.redirectUri
- )}&grant_type=authorization_code&code=${code}&code_verifier=${
- authRequest.codeVerifier
- }&scope=${encodeURIComponent(
- "https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
- )}`,
- });
-
- console.log("Token response status:", tokenResponse.status);
-
- if (!tokenResponse.ok) {
- const errorText = await tokenResponse.text();
- console.error("Token exchange failed:", errorText);
- return;
- }
-
- const tokenData = await tokenResponse.json();
- console.log("Token data received:", tokenData);
-
- if (tokenData?.access_token) {
- console.log("Access token received, fetching user info...");
-
- // Fetch user info from Microsoft Graph API to get the email
- const userInfoResponse = await fetch(
- "https://graph.microsoft.com/v1.0/me",
- {
- headers: {
- Authorization: `Bearer ${tokenData.access_token}`,
- },
- }
- );
-
- const userInfo = await userInfoResponse.json();
- console.log("User info received:", userInfo);
-
- if (userInfo.error) {
- console.error("Error fetching user info:", userInfo.error);
- } else {
- const outlookMail = userInfo.mail || userInfo.userPrincipalName;
-
- let microsoftAccounts = profileData?.microsoftAccounts;
- const updatedMicrosoftAccounts = microsoftAccounts
- ? { ...microsoftAccounts, [outlookMail]: tokenData.access_token }
- : { [outlookMail]: tokenData.access_token };
-
- // Update user data with Microsoft token and email
- await updateUserData({
- newUserData: { microsoftAccounts: updatedMicrosoftAccounts },
- });
-
- await fetchAndSaveOutlookEvents(
- tokenData.access_token,
- outlookMail
- );
- console.log("User data updated successfully.");
- }
- }
- } else {
- console.warn("Authentication was not successful:", authResult);
- }
- } catch (error) {
- console.error("Error during Microsoft sign-in:", error);
- }
- };
-
- const handleAppleSignIn = async () => {
- try {
- console.log("Starting Apple Sign-in...");
-
- const credential = await AppleAuthentication.signInAsync({
- requestedScopes: [
- AppleAuthentication.AppleAuthenticationScope.EMAIL,
- AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
- ],
- });
-
- console.log("Apple sign-in result:", credential);
-
- const appleToken = credential.identityToken;
- const appleMail = credential.email;
-
- if (appleToken) {
- console.log("Apple ID token received. Fetch user info if needed...");
-
- await updateUserData({
- newUserData: { appleToken, appleMail },
- });
-
- console.log("User data updated with Apple ID token.");
- await fetchAndSaveAppleEvents({ token: appleToken, email: appleMail! });
- } else {
- console.warn(
- "Apple authentication was not successful or email was hidden."
- );
- }
- } catch (error) {
- console.error("Error during Apple Sign-in:", error);
- }
- };
-
- const debouncedUpdateUserData = useCallback(
- debounce(async (color: string) => {
- try {
- await updateUserData({
- newUserData: {
- eventColor: color,
- },
- });
- } catch (error) {
- console.error("Failed to update color:", error);
- setSelectedColor(previousSelectedColor);
- }
- }, 500),
- []
- );
-
- const debouncedUpdateFirstDayOfWeek = useCallback(
- debounce(async (firstDayOfWeek: string) => {
- try {
- await updateUserData({
- newUserData: {
- firstDayOfWeek,
- },
- });
- } catch (error) {
- console.error("Failed to update first day of week:", error);
- }
- }, 500),
- []
- );
-
- const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
- setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
- debouncedUpdateFirstDayOfWeek(
- firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"
+ const [firstDayOfWeek, setFirstDayOfWeek] = useState(
+ profileData?.firstDayOfWeek ??
+ ExpoLocalization.getCalendars()[0].firstWeekday === 1
+ ? "Mondays"
+ : "Sundays"
);
- };
+ const [isModalVisible, setModalVisible] = useState(false);
+ const [selectedService, setSelectedService] = useState<
+ "google" | "outlook" | "apple"
+ >("google");
+ const [selectedEmail, setSelectedEmail] = useState("");
- const handleChangeColor = (color: string) => {
- setPreviousSelectedColor(selectedColor);
- setSelectedColor(color);
- debouncedUpdateUserData(color);
- };
+ const showConfirmationDialog = (
+ serviceName: "google" | "outlook" | "apple",
+ email: string
+ ) => {
+ setSelectedService(serviceName);
+ setSelectedEmail(email);
+ setModalVisible(true);
+ };
- const clearToken = async (
- provider: "google" | "outlook" | "apple",
- email: string
- ) => {
- const newUserData: Partial = {};
- 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 });
- };
+ const handleConfirm = async () => {
+ await clearToken({email: selectedEmail, provider: selectedService});
+ setModalVisible(false);
+ };
- let isConnectedToGoogle = false;
- if (profileData?.googleAccounts) {
- Object.values(profileData?.googleAccounts).forEach((item) => {
- if (item !== null) {
- isConnectedToGoogle = true;
- return;
- }
- });
- }
+ const handleCancel = () => {
+ setModalVisible(false);
+ };
- let isConnectedToMicrosoft = false;
- const microsoftAccounts = profileData?.microsoftAccounts;
- if (microsoftAccounts) {
- Object.values(profileData?.microsoftAccounts).forEach((item) => {
- if (item !== null) {
- isConnectedToMicrosoft = true;
- return;
- }
- });
- }
+ const [selectedColor, setSelectedColor] = useState(
+ profileData?.eventColor ?? colorMap.pink
+ );
+ const [previousSelectedColor, setPreviousSelectedColor] = useState(
+ profileData?.eventColor ?? colorMap.pink
+ );
- let isConnectedToApple = false;
- if (profileData?.appleAccounts) {
- Object.values(profileData?.appleAccounts).forEach((item) => {
- if (item !== null) {
- isConnectedToApple = true;
- return;
- }
- });
- }
+ const {mutateAsync: updateUserData} = useUpdateUserData();
+ const {mutateAsync: clearToken} = useClearTokens();
- return (
-
- setPageIndex(0)}>
-
-
-
- Return to main settings
-
-
-
-
- Calendar settings
-
-
- Event Color Preference
-
-
- handleChangeColor(colorMap.pink)}>
-
- {selectedColor == colorMap.pink && (
-
- )}
-
-
- handleChangeColor(colorMap.orange)}
- >
-
- {selectedColor == colorMap.orange && (
-
- )}
-
-
- handleChangeColor(colorMap.green)}>
-
- {selectedColor == colorMap.green && (
-
- )}
-
-
- handleChangeColor(colorMap.teal)}>
-
- {selectedColor == colorMap.teal && (
-
- )}
-
-
- handleChangeColor(colorMap.purple)}
- >
-
- {selectedColor == colorMap.purple && (
-
- )}
-
-
-
-
-
- Weekly Start Date
-
- handleChangeFirstDayOfWeek("Sundays")}
- />
-
- Sundays
-
- {" "}
- (default)
-
-
-
-
- handleChangeFirstDayOfWeek("Mondays")}
- />
-
- Mondays
-
-
-
-
- Add Calendar
-
+ const {
+ isSyncingGoogle,
+ isSyncingOutlook,
+ isConnectedToGoogle,
+ isConnectedToMicrosoft,
+ isConnectedToApple,
+ handleAppleSignIn,
+ isSyncingApple,
+ handleMicrosoftSignIn,
+ fetchAndSaveOutlookEvents,
+ fetchAndSaveGoogleEvents,
+ handleStartGoogleSignIn,
+ fetchAndSaveAppleEvents
+ } = useCalSync()
- promptAsync()}
- label={"Connect Google"}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- {profileData?.googleAccounts
- ? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
- const googleToken = profileData?.googleAccounts?.[googleMail];
- return (
- googleToken && (
- {
- showConfirmationDialog("google", googleMail);
- }}
- label={`Disconnect ${googleMail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- )
- );
- })
- : null}
+ const debouncedUpdateUserData = useCallback(
+ debounce(async (color: string) => {
+ try {
+ await updateUserData({
+ newUserData: {
+ eventColor: color,
+ },
+ });
+ } catch (error) {
+ console.error("Failed to update color:", error);
+ setSelectedColor(previousSelectedColor);
+ }
+ }, 500),
+ []
+ );
- handleAppleSignIn()}
- label={"Connect Apple"}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- {profileData?.appleAccounts
- ? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
- const appleToken = profileData?.appleAccounts?.[appleEmail];
- return (
- appleToken && (
- showConfirmationDialog("apple", appleEmail)}
- label={`Disconnect ${appleEmail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- )
- );
- })
- : null}
+ const debouncedUpdateFirstDayOfWeek = useCallback(
+ debounce(async (firstDayOfWeek: string) => {
+ try {
+ await updateUserData({
+ newUserData: {
+ firstDayOfWeek,
+ },
+ });
+ } catch (error) {
+ console.error("Failed to update first day of week:", error);
+ }
+ }, 500),
+ []
+ );
- handleMicrosoftSignIn()}
- label={"Connect Outlook"}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- {profileData?.microsoftAccounts
- ? Object.keys(profileData?.microsoftAccounts)?.map(
- (microsoftEmail) => {
- const microsoftToken =
- profileData?.microsoftAccounts?.[microsoftEmail];
- return (
- microsoftToken && (
- {
- showConfirmationDialog("outlook", microsoftEmail);
- }}
- label={`Disconnect ${microsoftEmail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{
- numberOfLines: 2,
- }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
+ const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
+ setFirstDayOfWeek(firstDayOfWeek);
+ debouncedUpdateFirstDayOfWeek(firstDayOfWeek);
+ };
+
+ const handleChangeColor = (color: string) => {
+ setPreviousSelectedColor(selectedColor);
+ setSelectedColor(color);
+ debouncedUpdateUserData(color);
+ };
+
+ return (
+
+ setPageIndex(0)}>
+
+
- )
- );
- }
- )
- : null}
-
- {(isConnectedToGoogle ||
- isConnectedToMicrosoft ||
- isConnectedToApple) && (
- <>
-
- Connected Calendars
-
-
-
-
- {profileData?.googleAccounts &&
- Object.keys(profileData?.googleAccounts)?.map(
- (googleEmail) => {
- const googleToken =
- profileData?.googleAccounts?.[googleEmail];
- return (
- googleToken && (
-
- fetchAndSaveGoogleEvents({
- token: googleToken,
- email: googleEmail,
- })
- }
- >
-
-
- fetchAndSaveGoogleEvents({
- token: googleToken,
- email: googleEmail,
- })
- }
- label={`Sync ${googleEmail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{ numberOfLines: 3 }}
- iconSource={() => (
-
-
-
+
+ Return to main settings
+
+
+
+
+ Calendar settings
+
+
+ Event Color Preference
+
+
+ handleChangeColor(colorMap.pink)}>
+
+ {selectedColor == colorMap.pink && (
+
)}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
-
- {isSyncingGoogle ? (
-
- ) : (
-
- )}
-
- )
- );
- }
- )}
-
- {profileData?.appleAccounts &&
- Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
- const appleToken = profileData?.appleAccounts?.[appleEmail];
- return (
- appleToken && (
-
- fetchAndSaveAppleEvents({
- email: appleEmail,
- token: appleToken,
- })
- }
- >
-
-
- fetchAndSaveAppleEvents({
- email: appleEmail,
- token: appleToken,
- })
- }
- label={`Sync ${appleEmail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{ numberOfLines: 3 }}
- iconSource={() => (
-
-
-
- )}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- {isSyncingApple ? (
-
- ) : (
-
- )}
-
- )
- );
- })}
-
- {profileData?.microsoftAccounts &&
- Object.keys(profileData?.microsoftAccounts)?.map(
- (microsoftEmail) => {
- const microsoftToken =
- profileData?.microsoftAccounts?.[microsoftEmail];
- return (
- microsoftToken && (
-
- fetchAndSaveOutlookEvents({
- token: microsoftToken,
- email: microsoftEmail,
- })
- }
- >
-
-
- fetchAndSaveOutlookEvents({
- token: microsoftToken,
- email: microsoftEmail,
- })
- }
- label={`Sync ${microsoftEmail}`}
- labelStyle={styles.addCalLbl}
- labelProps={{ numberOfLines: 3 }}
- iconSource={() => (
-
-
-
+ handleChangeColor(colorMap.orange)}
+ >
+
+ {selectedColor == colorMap.orange && (
+
)}
- style={styles.addCalBtn}
- color="black"
- text70BL
- />
- {isSyncingOutlook ? (
-
- ) : (
-
- )}
-
- )
- );
- }
- )}
-
+
+ handleChangeColor(colorMap.green)}>
+
+ {selectedColor == colorMap.green && (
+
+ )}
+
+
+ handleChangeColor(colorMap.teal)}>
+
+ {selectedColor == colorMap.teal && (
+
+ )}
+
+
+ handleChangeColor(colorMap.purple)}
+ >
+
+ {selectedColor == colorMap.purple && (
+
+ )}
+
+
+
+
+
+ Weekly Start Date
+
+ handleChangeFirstDayOfWeek("Sundays")}
+ />
+
+ Sundays
+
+ {" "}
+ (default)
+
+
+
+
+ handleChangeFirstDayOfWeek("Mondays")}
+ />
+
+ Mondays
+
+
+
+
+ Add Calendar
+
+
+ handleStartGoogleSignIn()}
+ label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
+ labelStyle={styles.addCalLbl}
+ labelProps={{
+ numberOfLines: 2,
+ }}
+ iconSource={() => (
+
+
+
+ )}
+ style={styles.addCalBtn}
+ color="black"
+ text70BL
+ />
+
+ {!profileData?.appleAccounts && (
+ handleAppleSignIn()}
+ label={"Connect Apple"}
+ labelStyle={styles.addCalLbl}
+ labelProps={{
+ numberOfLines: 2,
+ }}
+ iconSource={() => (
+
+
+
+ )}
+ style={styles.addCalBtn}
+ color="black"
+ text70BL
+ />
+ )}
+
+ handleMicrosoftSignIn()}
+ label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
+ labelStyle={styles.addCalLbl}
+ labelProps={{
+ numberOfLines: 2,
+ }}
+ iconSource={() => (
+
+
+
+ )}
+ style={styles.addCalBtn}
+ color="black"
+ text70BL
+ />
+
+ {(isConnectedToGoogle ||
+ isConnectedToMicrosoft ||
+ isConnectedToApple) && (
+ <>
+
+ Connected Calendars
+
+
+
+
+ {profileData?.googleAccounts &&
+ Object.keys(profileData?.googleAccounts)?.map(
+ (googleEmail) => {
+ const googleToken =
+ profileData?.googleAccounts?.[googleEmail]?.accessToken;
+ return (
+ googleToken && (
+
+
+ {isSyncingGoogle ? (
+
+
+
+ ) : (
+
+
+ fetchAndSaveGoogleEvents({
+ token: googleToken,
+ email: googleEmail,
+ })
+ }
+ iconSource={() => }
+ />
+
+ )}
+
+
+
+
+ {googleEmail}
+
+ showConfirmationDialog("google", googleEmail)
+ }
+ iconSource={() => }
+ />
+
+
+ )
+ );
+ }
+ )}
+
+ {profileData?.appleAccounts &&
+ Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
+ console.log(profileData?.appleAccounts)
+
+ const appleToken = profileData?.appleAccounts?.[appleEmail];
+ return (
+ appleToken && (
+
+
+
+
+
+
+ {appleEmail}
+
+ {isSyncingApple ? (
+
+
+
+ ) : (
+
+
+ fetchAndSaveAppleEvents({
+ email: appleEmail,
+ token: appleToken,
+ })
+ }
+ iconSource={() => }
+ />
+
+ )}
+ showConfirmationDialog("apple", appleEmail)}
+ iconSource={() => }
+ />
+
+
+ )
+ );
+ })}
+
+ {profileData?.microsoftAccounts &&
+ Object.keys(profileData?.microsoftAccounts)?.map(
+ (microsoftEmail) => {
+ const microsoftToken =
+ profileData?.microsoftAccounts?.[microsoftEmail];
+ return (
+ microsoftToken && (
+
+
+ {isSyncingOutlook ? (
+
+
+
+ ) : (
+
+
+ fetchAndSaveOutlookEvents({
+ token: microsoftToken,
+ email: microsoftEmail,
+ })
+ }
+ iconSource={() => }
+ />
+
+ )}
+
+
+
+
+ {microsoftEmail}
+
+ showConfirmationDialog("outlook", microsoftEmail)
+ }
+ iconSource={() => }
+ />
+
+
+ )
+ );
+ }
+ )}
+
+
+ >
+ )}
- >
- )}
-
-
-
- );
+
+
+ );
};
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,
- },
+ 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: "70%",
+ textAlign: "left",
+ lineHeight: 20,
+ overflow: "hidden",
+ },
+ subTitle: {
+ fontFamily: "Manrope_600SemiBold",
+ fontSize: 18,
+ },
+ cardTitle: {
+ fontFamily: "Manrope_500Medium",
+ fontSize: 15,
+ },
});
export default CalendarSettingsPage;
diff --git a/components/pages/settings/SettingsPage.tsx b/components/pages/settings/SettingsPage.tsx
index b999785..cbe0b6c 100644
--- a/components/pages/settings/SettingsPage.tsx
+++ b/components/pages/settings/SettingsPage.tsx
@@ -1,6 +1,6 @@
import { Button, Text, View } from "react-native-ui-lib";
import React, { useState } from "react";
-import { StyleSheet } from "react-native";
+import {Linking, StyleSheet} from "react-native";
import { Octicons } from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings";
@@ -21,11 +21,23 @@ const pageIndex = {
policy: 4,
};
+const PRIVACY_POLICY_URL = 'https://callyapp.com';
+
+
const SettingsPage = () => {
const { profileData } = useAuthContext();
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const isntParent = profileData?.userType !== ProfileType.PARENT;
+ const openPrivacyPolicy = async () => {
+ const supported = await Linking.canOpenURL(PRIVACY_POLICY_URL);
+ if (supported) {
+ await Linking.openURL(PRIVACY_POLICY_URL);
+ } else {
+ console.log("Don't know how to open this URL:", PRIVACY_POLICY_URL);
+ }
+ };
+
return (
{pageIndex == 0 && (
@@ -73,7 +85,8 @@ const SettingsPage = () => {
}}
/>
{
color="#ff9900"
style={{ marginRight: 10 }}
/>
-
+
To-Do Reward Settings
@@ -95,6 +108,7 @@ const SettingsPage = () => {
diff --git a/components/pages/settings/UserSettings.tsx b/components/pages/settings/UserSettings.tsx
index a017924..bb908dd 100644
--- a/components/pages/settings/UserSettings.tsx
+++ b/components/pages/settings/UserSettings.tsx
@@ -1,115 +1,131 @@
-import { Text, TouchableOpacity, View } from "react-native-ui-lib";
-import React, { useState } from "react";
-import { Ionicons } from "@expo/vector-icons";
-import { ScrollView, StyleSheet } from "react-native";
+import {FloatingButton, Text, TouchableOpacity, View,} from "react-native-ui-lib";
+import React, {useState} from "react";
+import {Ionicons} from "@expo/vector-icons";
+import {ScrollView, StyleSheet} from "react-native";
import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup";
-import { useAtom } from "jotai";
-import { settingsPageIndex, userSettingsView } from "../calendar/atoms";
-import { AuthContextProvider } from "@/contexts/AuthContext";
+import {useAtom, useSetAtom} from "jotai";
+import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
+import PlusIcon from "@/assets/svgs/PlusIcon";
const UserSettings = () => {
- const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
- const [userView, setUserView] = useAtom(userSettingsView);
- return (
-
-
-
- {
- setPageIndex(0);
- setUserView(true);
- }}
- >
-
-
-
- Return to main settings
-
-
-
-
-
- User Management
-
-
- setUserView(true)}
- centerV
- centerH
- style={userView == true ? styles.btnSelected : styles.btnNot}
- >
-
-
- My Profile
-
-
-
- setUserView(false)}
- centerV
- centerH
- style={userView == false ? styles.btnSelected : styles.btnNot}
- >
-
-
- My Group
-
-
-
-
- {userView && }
- {!userView && }
-
-
+ const setPageIndex = useSetAtom(settingsPageIndex);
+ const [userView, setUserView] = useAtom(userSettingsView);
+ const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
- {!userView && (
-
- selview
-
- )}
-
-
- );
+ return (
+
+
+ {
+ setPageIndex(0);
+ setUserView(true);
+ }}
+ >
+
+
+
+ Return to main settings
+
+
+
+
+
+ User Management
+
+
+ setUserView(true)}
+ centerV
+ centerH
+ style={userView == true ? styles.btnSelected : styles.btnNot}
+ >
+
+
+ My Profile
+
+
+
+ setUserView(false)}
+ centerV
+ centerH
+ style={userView == false ? styles.btnSelected : styles.btnNot}
+ >
+
+
+ My Group
+
+
+
+
+ {userView && }
+ {!userView && }
+
+
+ {!userView && (
+ ,
+ onPress: () => setOnNewUserClick(true),
+ style: styles.bottomButton,
+ labelStyle: {fontFamily: "Manrope_600SemiBold", fontSize: 15},
+ }}
+ />
+ )}
+
+ );
};
const styles = StyleSheet.create({
- buttonSwitch: {
- borderRadius: 50,
- width: "100%",
- backgroundColor: "#ebebeb",
- height: 45,
- },
- btnSelected: {
- backgroundColor: "#05a8b6",
- height: "100%",
- width: "50%",
- borderRadius: 50,
- },
- btnTxt: {
- fontFamily: "Manrope_500Medium",
- fontSize: 15,
- },
- btnNot: {
- height: "100%",
- width: "50%",
- borderRadius: 50,
- },
- title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
+ bottomButton: {
+ position: "absolute",
+ bottom: 15,
+ marginHorizontal: 28,
+ width: 337,
+ backgroundColor: "#e8156c",
+ height: 53.26,
+ },
+ buttonSwitch: {
+ borderRadius: 50,
+ width: "100%",
+ backgroundColor: "#ebebeb",
+ height: 45,
+ },
+ btnSelected: {
+ backgroundColor: "#05a8b6",
+ height: "100%",
+ width: "50%",
+ borderRadius: 50,
+ },
+ btnTxt: {
+ fontFamily: "Manrope_500Medium",
+ fontSize: 15,
+ },
+ btnNot: {
+ height: "100%",
+ width: "50%",
+ borderRadius: 50,
+ },
+ title: {fontFamily: "Manrope_600SemiBold", fontSize: 18},
});
export default UserSettings;
diff --git a/components/pages/settings/user_components/DeleteProfileDialogs.tsx b/components/pages/settings/user_components/DeleteProfileDialogs.tsx
new file mode 100644
index 0000000..64b8efa
--- /dev/null
+++ b/components/pages/settings/user_components/DeleteProfileDialogs.tsx
@@ -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 = ({
+ visible,
+ onDismiss,
+ onFirstYes,
+ onConfirm,
+}) => {
+ const [confirmationDialog, setConfirmationDialog] = useState(false);
+ return (
+ <>
+
+
+ >
+ );
+};
+
+// 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;
diff --git a/components/pages/settings/user_settings_views/MyGroup.tsx b/components/pages/settings/user_settings_views/MyGroup.tsx
index b03494a..c1d6867 100644
--- a/components/pages/settings/user_settings_views/MyGroup.tsx
+++ b/components/pages/settings/user_settings_views/MyGroup.tsx
@@ -1,594 +1,618 @@
import {
- Avatar,
- Button,
- Card,
- Colors,
- Dialog,
- FloatingButton,
- KeyboardAwareScrollView,
- PanningProvider,
- Picker,
- Text,
- TextField,
- TextFieldRef,
- TouchableOpacity,
- View,
+ Avatar,
+ Button,
+ Card,
+ Colors,
+ Dialog, Image,
+ KeyboardAwareScrollView,
+ PanningProvider,
+ Picker,
+ Text,
+ TextField,
+ TextFieldRef,
+ TouchableOpacity,
+ View,
} from "react-native-ui-lib";
-import React, { useEffect, useRef, useState } from "react";
-import { ImageBackground, ScrollView, StyleSheet } from "react-native";
-import { PickerSingleValue } from "react-native-ui-lib/src/components/picker/types";
-import { useCreateSubUser } from "@/hooks/firebase/useCreateSubUser";
-import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
-import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
+import React, {useEffect, useRef, useState} from "react";
+import {ImageBackground, Platform, StyleSheet} from "react-native";
+import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
+import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
+import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
+import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
-import { uuidv4 } from "@firebase/util";
+import {uuidv4} from "@firebase/util";
import QRIcon from "@/assets/svgs/QRIcon";
import EmailIcon from "@/assets/svgs/EmailIcon";
import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import Ionicons from "@expo/vector-icons/Ionicons";
-import KeyboardManager, { PreviousNextView } from "react-native-keyboard-manager";
+import KeyboardManager, {PreviousNextView,} from "react-native-keyboard-manager";
+import {ScrollView} from "react-native-gesture-handler";
+import {useUploadProfilePicture} from "@/hooks/useUploadProfilePicture";
+import {ImagePickerAsset} from "expo-image-picker";
-const MyGroup = () => {
- const [showAddUserDialog, setShowAddUserDialog] = useState(false);
- const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
- const [selectedStatus, setSelectedStatus] = useState<
- string | PickerSingleValue
- >(ProfileType.CHILD);
- const [firstName, setFirstName] = useState("");
- const [lastName, setLastName] = useState("");
- const [email, setEmail] = useState("");
+type MyGroupProps = {
+ onNewUserClick: boolean;
+ setOnNewUserClick: React.Dispatch>;
+};
- const lNameRef = useRef(null);
- const emailRef = useRef(null);
+const MyGroup: React.FC = ({onNewUserClick, setOnNewUserClick}) => {
+ const [showAddUserDialog, setShowAddUserDialog] = useState(false);
+ const [selectedStatus, setSelectedStatus] = useState<
+ string | PickerSingleValue
+ >(ProfileType.CHILD);
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [email, setEmail] = useState("");
- const [showQRCodeDialog, setShowQRCodeDialog] = useState(
- false
- );
+ const [newUserId, setNewUserId] = useState("")
- const { mutateAsync: createSubUser, isLoading, isError } = useCreateSubUser();
- const { data: familyMembers } = useGetFamilyMembers(true);
- const { user } = useAuthContext();
+ const lNameRef = useRef(null);
+ const emailRef = useRef(null);
- const parents =
- familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
- const children =
- familyMembers?.filter((x) => x.userType === ProfileType.CHILD) ?? [];
- const caregivers =
- familyMembers?.filter((x) => x.userType === ProfileType.CAREGIVER) ?? [];
- const familyDevices =
- familyMembers?.filter((x) => x.userType === ProfileType.FAMILY_DEVICE) ??
- [];
+ const [showQRCodeDialog, setShowQRCodeDialog] = useState(
+ false
+ );
- const handleCreateSubUser = async () => {
- if (
- !firstName ||
- (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName)
- ) {
- console.error("First name and last name are required");
- return;
- }
+ const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
+ const {data: familyMembers} = useGetFamilyMembers(true);
+ const {user} = useAuthContext();
+ const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
- // if (selectedStatus !== ProfileType.FAMILY_DEVICE && !email) {
- // console.error("Email is required for non-family device users");
- // return;
- // }
+ const parents =
+ familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
+ const children =
+ familyMembers?.filter((x) => x.userType === ProfileType.CHILD) ?? [];
+ const caregivers =
+ familyMembers?.filter((x) => x.userType === ProfileType.CAREGIVER) ?? [];
+ const familyDevices =
+ familyMembers?.filter((x) => x.userType === ProfileType.FAMILY_DEVICE) ??
+ [];
- if (email && !email.includes("@")) {
- console.error("Invalid email address");
- return;
- }
+ const handleCreateSubUser = async () => {
+ if (
+ !firstName ||
+ (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName)
+ ) {
+ console.error("First name and last name are required");
+ return;
+ }
- const res = await createSubUser({
- firstName,
- lastName: selectedStatus === ProfileType.FAMILY_DEVICE ? "" : lastName,
- email: email || `placeholder_${uuidv4().split("-")[0]}@family.device`,
- password: uuidv4(),
- userType: selectedStatus as ProfileType,
- });
- console.log(res);
+ // if (selectedStatus !== ProfileType.FAMILY_DEVICE && !email) {
+ // console.error("Email is required for non-family device users");
+ // return;
+ // }
- if (!isError) {
- setShowNewUserInfoDialog(false);
+ if (email && !email.includes("@")) {
+ console.error("Invalid email address");
+ return;
+ }
- if (res?.data?.userId) {
- setTimeout(() => {
- setShowQRCodeDialog(res.data.userId);
- }, 500);
- }
- }
- };
+ const res = await createSubUser({
+ firstName,
+ lastName: selectedStatus === ProfileType.FAMILY_DEVICE ? "" : lastName,
+ email: email || `placeholder_${uuidv4().split("-")[0]}@family.device`,
+ password: uuidv4(),
+ userType: selectedStatus as ProfileType,
+ });
+ console.log(res);
- useEffect(() => {
- KeyboardManager.setEnableAutoToolbar(true);
- },[])
+ if (!isError) {
+ setOnNewUserClick(false);
- useEffect(() => {
- setFirstName("");
- setLastName("");
- setEmail("");
- }, []);
+ if (res?.data?.userId) {
+ if (profileImageAsset) {
+ await changeProfilePicture(profileImageAsset)
+ setShowQRCodeDialog(res.data.userId);
+ } else {
+ setTimeout(() => {
+ setShowQRCodeDialog(res.data.userId);
+ }, 500);
+ }
+ handleClearImage()
+ }
+ }
+ };
- // @ts-ignore
- return (
-
-
-
- {!parents.length && !children.length && !caregivers.length && (
-
- {isLoading ? "Loading...." : "No user devices added"}
-
- )}
+ useEffect(() => {
+ if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
+ }, []);
- {(!!parents.length || !!children.length) && (
- <>
-
- Family
-
- {[...parents, ...children]?.map((member, index) => (
+ useEffect(() => {
+ setFirstName("");
+ setLastName("");
+ setEmail("");
+ }, []);
+
+ // @ts-ignore
+ return (
+
+
+
+ {!parents.length && !children.length && !caregivers.length && (
+
+ {isLoading ? "Loading...." : "No user devices added"}
+
+ )}
+
+ {(!!parents.length || !!children.length) && (
+
+
+ Family
+
+ {[...parents, ...children]?.map((member, index) => (
+
+ {member.pfp ? (
+
+ ) : (
+
+ )}
+
+
+ {member.firstName} {member.lastName}
+
+
+
+
+
+ {member.userType === ProfileType.PARENT
+ ? `Admin${member.uid === user?.uid ? " (You)" : ""}`
+ : "Child"}
+
+ setShowQRCodeDialog(val)}
+ showQRCodeDialog={showQRCodeDialog === member?.uid}
+ userId={member?.uid!}
+ />
+
+
+ ))}
+
+ )}
+
+ {!!caregivers.length && (
+
+
+ Caregivers
+
+ {caregivers?.map((member) => (
+
+
+
+
+ {member.firstName} {member.lastName}
+
+
+ Caregiver
+
+
+
+
+
+ setShowQRCodeDialog(val)}
+ showQRCodeDialog={showQRCodeDialog === member?.uid}
+ userId={member?.uid!}
+ />
+
+ ))}
+
+ )}
+
+ {!!familyDevices.length && (
+ <>
+
+ Family Devices
+
+ {familyDevices?.map((member, index) => (
+
+
+
+ {member.firstName}
+
+ Family Device
+
+
+
+
+
+ setShowQRCodeDialog(val)}
+ showQRCodeDialog={showQRCodeDialog === member?.uid}
+ userId={member?.uid!}
+ />
+
+ ))}
+ >
+ )}
+
+
+
+
-
-
- setShowNewUserInfoDialog(true),
- style: styles.bottomButton,
- }}
- />
-
-
-
-
+ {
+ setShowAddUserDialog(false);
+ setTimeout(() => {
+ setShowNewUserInfoDialog(true);
+ }, 500);
+ }}
>
-
-
- }
- >
-
-
-
-
-
-
+
+
+ Enter email address
+
+
-
- {selectedStatus === ProfileType.FAMILY_DEVICE
- ? "Device Name"
- : "First Name"}
-
- {
- lNameRef.current?.focus();
- }}
- blurOnSubmit={false}
- returnKeyType="next"
- />
+ setShowAddUserDialog(false)}
+ center
+ marginT-30
+ >
+ Return to user settings
+
+
+
- {selectedStatus !== ProfileType.FAMILY_DEVICE && (
- <>
- Last Name
- {
- emailRef.current?.focus();
- }}
- blurOnSubmit={false}
- returnKeyType="next"
- />
- >
- )}
+
-
- );
+ {pfpUri ? (
+
+
+ Clear user photo
+
+
+ ) : (
+
+
+ Upload User Profile Photo
+
+
+ )}
+
+
+
+ Member Status
+
+ setSelectedStatus(item)}
+ showSearch
+ floatingPlaceholder
+ style={styles.inViewPicker}
+ trailingAccessory={
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+ {selectedStatus === ProfileType.FAMILY_DEVICE
+ ? "Device Name"
+ : "First Name"}
+
+ {
+ lNameRef.current?.focus();
+ }}
+ blurOnSubmit={false}
+ returnKeyType="next"
+ />
+
+ {selectedStatus !== ProfileType.FAMILY_DEVICE && (
+ <>
+ Last Name
+ {
+ emailRef.current?.focus();
+ }}
+ blurOnSubmit={false}
+ returnKeyType="next"
+ />
+ >
+ )}
+
+ {selectedStatus !== ProfileType.FAMILY_DEVICE && (
+ <>
+ Email Address (Optional)
+
+ >
+ )}
+
+ }
+ onPress={handleCreateSubUser}
+ />
+
+
+
+
+
+ );
};
const styles = StyleSheet.create({
- dialogBtn: {
- height: 47,
- width: 279,
- },
- dialogTitle: { fontFamily: "Manrope_600SemiBold", fontSize: 22 },
- dialogBackBtn: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 15,
- color: "#a7a7a7",
- },
- card: {
- marginVertical: 15,
- backgroundColor: "white",
- width: "100%",
- borderRadius: 12,
- paddingHorizontal: 21,
- paddingVertical: 20,
- },
- bottomButton: {
- position: "absolute",
- bottom: 80,
- width: "100%",
- },
- familyCard: {
- marginBottom: 10,
- borderRadius: 10,
- backgroundColor: Colors.white,
- width: "100%",
- },
- inputField: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 13,
- color: "#565656",
- borderRadius: 50,
- paddingVertical: 12,
- paddingHorizontal: 16,
- backgroundColor: Colors.grey80,
- marginBottom: 16,
- borderColor: Colors.grey50,
- borderWidth: 1,
- height: 40,
- },
- picker: {
- borderRadius: 50,
- paddingVertical: 12,
- paddingHorizontal: 16,
- backgroundColor: Colors.grey80,
- marginBottom: 16,
- borderColor: Colors.grey50,
- borderWidth: 1,
- marginTop: -20,
- height: 40,
- zIndex: 10,
- },
- viewPicker: {
- borderRadius: 50,
- backgroundColor: Colors.grey80,
- marginBottom: 16,
- borderColor: Colors.grey50,
- borderWidth: 1,
- marginTop: 0,
- height: 40,
- zIndex: 10,
- },
- inViewPicker: {
- borderRadius: 50,
- paddingVertical: 12,
- paddingHorizontal: 16,
- marginBottom: 16,
- marginTop: -20,
- height: 40,
- zIndex: 10,
- },
- label: {
- marginBottom: 5,
- fontSize: 12,
- color: Colors.grey40,
- },
- dialogCard: {
- borderRadius: 10,
- gap: 10,
- },
- subTit: {
- fontFamily: "Manrope_500Medium",
- fontSize: 15,
- },
- dialogBtnLbl: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 15,
- color: "white",
- },
- divider: { height: 0.7, backgroundColor: "#e6e6e6", width: "100%" },
- jakarta12: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 12,
- color: "#a1a1a1",
- },
- jakarta13: {
- fontFamily: "PlusJakartaSans_500Medium",
- fontSize: 13,
- },
- pfp: { aspectRatio: 1, width: 37.03, borderRadius: 10.56 },
- userType: {
- fontFamily: "Manrope_500Medium",
- fontSize: 12,
- color: "#858585",
- },
- name: {
- fontFamily: "Manrope_600SemiBold",
- fontSize: 16,
- },
+ dialogBtn: {
+ height: 47,
+ width: 279,
+ },
+ dialogTitle: {fontFamily: "Manrope_600SemiBold", fontSize: 22},
+ dialogBackBtn: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 15,
+ color: "#a7a7a7",
+ },
+ card: {
+ marginVertical: 15,
+ backgroundColor: "white",
+ width: "100%",
+ borderRadius: 12,
+ paddingHorizontal: 21,
+ paddingVertical: 20,
+ },
+ bottomButton: {
+ position: "absolute",
+ bottom: 50,
+ backgroundColor: "#e8156c",
+ width: "100%",
+ },
+ familyCard: {
+ marginBottom: 10,
+ borderRadius: 10,
+ backgroundColor: Colors.white,
+ width: "100%",
+ },
+ inputField: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 13,
+ color: "#565656",
+ borderRadius: 50,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ backgroundColor: Colors.grey80,
+ marginBottom: 16,
+ borderColor: Colors.grey50,
+ borderWidth: 1,
+ height: 40,
+ },
+ picker: {
+ borderRadius: 50,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ backgroundColor: Colors.grey80,
+ marginBottom: 16,
+ borderColor: Colors.grey50,
+ borderWidth: 1,
+ marginTop: -20,
+ height: 40,
+ zIndex: 10,
+ },
+ viewPicker: {
+ borderRadius: 50,
+ backgroundColor: Colors.grey80,
+ marginBottom: 16,
+ borderColor: Colors.grey50,
+ borderWidth: 1,
+ marginTop: 0,
+ height: 40,
+ zIndex: 10,
+ },
+ inViewPicker: {
+ borderRadius: 50,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ marginBottom: 16,
+ marginTop: -20,
+ height: 40,
+ zIndex: 10,
+ },
+ label: {
+ marginBottom: 5,
+ fontSize: 12,
+ color: Colors.grey40,
+ },
+ dialogCard: {
+ borderRadius: 10,
+ gap: 10,
+ },
+ subTit: {
+ fontFamily: "Manrope_500Medium",
+ fontSize: 15,
+ },
+ dialogBtnLbl: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 15,
+ color: "white",
+ },
+ divider: {height: 0.7, backgroundColor: "#e6e6e6", width: "100%"},
+ jakarta12: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 12,
+ color: "#a1a1a1",
+ },
+ jakarta13: {
+ fontFamily: "PlusJakartaSans_500Medium",
+ fontSize: 13,
+ },
+ pfp: {aspectRatio: 1, width: 37.03, borderRadius: 10.56},
+ userType: {
+ fontFamily: "Manrope_500Medium",
+ fontSize: 12,
+ color: "#858585",
+ },
+ name: {
+ fontFamily: "Manrope_600SemiBold",
+ fontSize: 16,
+ },
});
export default MyGroup;
diff --git a/components/pages/settings/user_settings_views/MyProfile.tsx b/components/pages/settings/user_settings_views/MyProfile.tsx
index 869c619..a183b3a 100644
--- a/components/pages/settings/user_settings_views/MyProfile.tsx
+++ b/components/pages/settings/user_settings_views/MyProfile.tsx
@@ -3,6 +3,7 @@ import { StyleSheet, TouchableOpacity } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import * as ImagePicker from "expo-image-picker";
import {
+ Button,
Colors,
Image,
Picker,
@@ -18,6 +19,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
import { colorMap } from "@/constants/colorMap";
+import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
const MyProfile = () => {
const { user, profileData } = useAuthContext();
@@ -32,6 +34,15 @@ const MyProfile = () => {
string | ImagePicker.ImagePickerAsset | null
>(profileData?.pfp || null);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+
+ const handleHideDeleteDialog = () => {
+ setShowDeleteDialog(false);
+ };
+ const handleShowDeleteDialog = () => {
+ setShowDeleteDialog(true);
+ };
+
const { mutateAsync: updateUserData } = useUpdateUserData();
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
const isFirstRender = useRef(true);
@@ -48,13 +59,12 @@ const MyProfile = () => {
return;
}
debouncedUserDataUpdate();
- }, [timeZone, lastName, firstName, profileImage]);
+ }, [timeZone, lastName, firstName]);
useEffect(() => {
if (profileData) {
setFirstName(profileData.firstName || "");
setLastName(profileData.lastName || "");
- // setProfileImage(profileData.pfp || null);
setTimeZone(
profileData.timeZone || Localization.getCalendars()[0].timeZone!
);
@@ -78,7 +88,7 @@ const MyProfile = () => {
if (!result.canceled) {
setProfileImage(result.assets[0].uri);
- changeProfilePicture(result.assets[0]);
+ await changeProfilePicture(result.assets[0]);
}
};
@@ -93,7 +103,7 @@ const MyProfile = () => {
: profileImage;
return (
-
+
Your Profile
@@ -205,6 +215,22 @@ const MyProfile = () => {
+
+ {
+ setShowDeleteDialog(false);
+ }}
+ visible={showDeleteDialog}
+ onDismiss={handleHideDeleteDialog}
+ onConfirm={() => {console.log('delete account here')}}
+ />
);
};
diff --git a/components/pages/settings/user_settings_views/UserMenu.tsx b/components/pages/settings/user_settings_views/UserMenu.tsx
index 2e001e6..6b6cfd5 100644
--- a/components/pages/settings/user_settings_views/UserMenu.tsx
+++ b/components/pages/settings/user_settings_views/UserMenu.tsx
@@ -32,7 +32,10 @@ const UserMenu = ({
panDirection={PanningDirectionsEnum.DOWN}
>
- Scan this QR Code to Login:
+ Ask your family to download the app
+ and then scan the
+ QR Code to join the family group:
+
{
>
setIsVisible(!isVisible)}
diff --git a/components/pages/todos/ToDoItem.tsx b/components/pages/todos/ToDoItem.tsx
index 55bb8c3..c97bb90 100644
--- a/components/pages/todos/ToDoItem.tsx
+++ b/components/pages/todos/ToDoItem.tsx
@@ -1,12 +1,13 @@
import {
View,
Text,
- Checkbox,
TouchableOpacity,
Dialog,
Button,
ButtonSize,
+ Checkbox,
} from "react-native-ui-lib";
+
import React, { useState } from "react";
import { useToDosContext } from "@/contexts/ToDosContext";
import { Ionicons } from "@expo/vector-icons";
@@ -60,15 +61,14 @@ const ToDoItem = (props: {
return (
{visible && (
@@ -84,6 +84,7 @@ const ToDoItem = (props: {
style={{
textDecorationLine: props.item.done ? "line-through" : "none",
fontFamily: "Manrope_500Medium",
+ color: props.item.done? "#a09f9f": "black",
fontSize: 15,
}}
onPress={() => {
@@ -96,6 +97,7 @@ const ToDoItem = (props: {
value={props.item.done}
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
style={styles.checked}
+ size={26.64}
borderRadius={50}
color="#fd1575"
onValueChange={(value) => {
diff --git a/components/pages/todos/ToDosPage.tsx b/components/pages/todos/ToDosPage.tsx
index 590a901..6c88672 100644
--- a/components/pages/todos/ToDosPage.tsx
+++ b/components/pages/todos/ToDosPage.tsx
@@ -42,6 +42,7 @@ const ToDosPage = () => {
message="Here are your To Do's"
isWelcome={true}
link={profileData?.userType == ProfileType.PARENT && pageLink}
+ isToDos={true}
/>
{profileData?.userType == ProfileType.CHILD && (
diff --git a/components/pages/todos/family-chores/FamilyChoresProgress.tsx b/components/pages/todos/family-chores/FamilyChoresProgress.tsx
index 8e2bcb1..66570fe 100644
--- a/components/pages/todos/family-chores/FamilyChoresProgress.tsx
+++ b/components/pages/todos/family-chores/FamilyChoresProgress.tsx
@@ -3,6 +3,7 @@ import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import FamilyChart from "./FamilyChart";
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
+import { Ionicons } from "@expo/vector-icons";
const FamilyChoresProgress = ({
setPageIndex,
@@ -12,7 +13,20 @@ const FamilyChoresProgress = ({
return (
setPageIndex(0)}>
- Back to ToDos
+
+
+
+ Return to To Do's
+
+
diff --git a/components/pages/todos/user-chores/UserChoresProgress.tsx b/components/pages/todos/user-chores/UserChoresProgress.tsx
index b4a4fbd..f4cec19 100644
--- a/components/pages/todos/user-chores/UserChoresProgress.tsx
+++ b/components/pages/todos/user-chores/UserChoresProgress.tsx
@@ -30,8 +30,21 @@ const UserChoresProgress = ({
showsHorizontalScrollIndicator={false}
>
setPageIndex(0)}>
- Back to ToDos
-
+
+
+
+ Return to To Do's
+
+
+
Your To Do's Progress Report
diff --git a/components/shared/HeaderTemplate.tsx b/components/shared/HeaderTemplate.tsx
index b55d97d..3d47487 100644
--- a/components/shared/HeaderTemplate.tsx
+++ b/components/shared/HeaderTemplate.tsx
@@ -1,18 +1,37 @@
import { Image, Text, View } from "react-native-ui-lib";
-import React from "react";
-import { useAuthContext } from "@/contexts/AuthContext";
+import React, { useEffect, useState } from "react";
+import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap";
+import { useAtom } from "jotai";
+import { isFamilyViewAtom } from "../pages/calendar/atoms";
+import { useGetChildrenByParentId } from "@/hooks/firebase/useGetChildrenByParentId";
+import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
+import { UserProfile } from "@/hooks/firebase/types/profileTypes";
+import { child } from "@react-native-firebase/storage";
+import CachedImage from 'expo-cached-image'
const HeaderTemplate = (props: {
message: string;
isWelcome: boolean;
children?: React.ReactNode;
link?: React.ReactNode;
+ isCalendar?: boolean;
+ isToDos?: boolean;
+ isBrainDump?: boolean;
+ isGroceries?: boolean;
}) => {
const { user, profileData } = useAuthContext();
- const headerHeight: number = 72;
+ const { data: members } = useGetFamilyMembers();
+ const [children, setChildren] = useState([]);
+ const [isFamilyView] = useAtom(isFamilyViewAtom);
+
+ const headerHeight: number =
+ (props.isCalendar && 65.54) ||
+ (props.isToDos && 84) ||
+ (props.isGroceries && 72.09) ||
+ 65.54;
const styles = StyleSheet.create({
pfp: {
@@ -26,14 +45,71 @@ const HeaderTemplate = (props: {
pfpTxt: {
fontFamily: "Manrope_500Medium",
fontSize: 30,
- color: 'white',
+ color: "white",
+ },
+ childrenPfpArr: {
+ width: 65.54,
+ position: "absolute",
+ bottom: -12.44,
+ left: (children.length > 3 && -9) || 0,
+ height: 27.32,
+ },
+ childrenPfp: {
+ aspectRatio: 1,
+ width: 27.32,
+ backgroundColor: "#fd1575",
+ borderRadius: 50,
+ position: "absolute",
+ borderWidth: 2,
+ borderColor: "#f2f2f2",
+ },
+ bottomMarg: {
+ marginBottom: isFamilyView ? 30 : 15,
},
});
+ useEffect(() => {
+ if (members) {
+ const childrenMembers = members.filter(
+ (member) => member.userType === ProfileType.CHILD
+ );
+ setChildren(childrenMembers);
+ }
+ }, []);
+
return (
-
+
{profileData?.pfp ? (
-
+
+
+ {isFamilyView && props.isCalendar && (
+
+ {children?.slice(0, 3).map((child, index) => {
+ return child.pfp ? (
+
+ ) : (
+
+
+ {child?.firstName?.at(0)}
+ {child?.firstName?.at(1)}
+
+
+ );
+ })}
+ {children?.length > 3 && (
+
+ +{children.length - 3}
+
+ )}
+
+ )}
+
) : (
diff --git a/components/shared/RemoveAssigneeBtn.tsx b/components/shared/RemoveAssigneeBtn.tsx
index 9416009..3384949 100644
--- a/components/shared/RemoveAssigneeBtn.tsx
+++ b/components/shared/RemoveAssigneeBtn.tsx
@@ -6,7 +6,7 @@ import { View } from "react-native-ui-lib";
const RemoveAssigneeBtn = () => {
return (
-
+
);
};
diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx
index e6f28cc..8483f67 100644
--- a/contexts/AuthContext.tsx
+++ b/contexts/AuthContext.tsx
@@ -8,7 +8,7 @@ import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
-import { Platform } from 'react-native';
+import {Platform} from 'react-native';
import {useQueryClient} from "react-query";
@@ -24,6 +24,7 @@ interface IAuthContext {
profileType?: ProfileType,
profileData?: UserProfile,
setProfileData: (profileData: UserProfile) => void,
+ setRedirectOverride: (val: boolean) => void,
refreshProfileData: () => Promise
}
@@ -50,16 +51,16 @@ async function registerForPushNotificationsAsync() {
}
if (Device.isDevice) {
- const { status: existingStatus } = await Notifications.getPermissionsAsync();
+ const {status: existingStatus} = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
- const { status } = await Notifications.requestPermissionsAsync();
+ const {status} = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
- alert('Failed to get push token for push notification!');
+ // alert('Failed to get push token for push notification!');
return;
}
@@ -72,15 +73,15 @@ async function registerForPushNotificationsAsync() {
}
try {
- const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
+ const token = (await Notifications.getExpoPushTokenAsync({projectId})).data;
console.log('Push Token:', token);
return token;
} catch (error) {
- alert(`Error getting push token: ${error}`);
+ // alert(`Error getting push token: ${error}`);
throw error;
}
} else {
- alert('Must use a physical device for push notifications');
+ // alert('Must use a physical device for push notifications');
}
}
@@ -91,24 +92,28 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
const [initializing, setInitializing] = useState(true);
const [profileType, setProfileType] = useState(undefined);
const [profileData, setProfileData] = useState(undefined);
+ const [redirectOverride, setRedirectOverride] = useState(false);
+
const {replace} = useRouter();
const ready = !initializing;
const queryClient = useQueryClient();
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
- setUser(authUser);
+ if (!redirectOverride) {
+ setUser(authUser);
- if (authUser) {
- await refreshProfileData(authUser);
- const pushToken = await registerForPushNotificationsAsync();
- if (pushToken) {
- await savePushTokenToFirestore(authUser.uid, pushToken);
+ if (authUser) {
+ await refreshProfileData(authUser);
+ const pushToken = await registerForPushNotificationsAsync();
+ if (pushToken) {
+ await savePushTokenToFirestore(authUser.uid, pushToken);
+ }
}
- }
- if (initializing) setInitializing(false);
+ if (initializing) setInitializing(false);
+ }
};
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
@@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}, [initializing]);
useEffect(() => {
- if (ready && user) {
+ if (ready && user && !redirectOverride) {
replace({pathname: "/(auth)/calendar"});
- } else if (ready && !user) {
+ } else if (ready && !user && !redirectOverride) {
replace({pathname: "/(unauth)"});
}
- }, [user, ready]);
+ }, [user, ready, redirectOverride]);
useEffect(() => {
const sub = Notifications.addNotificationReceivedListener(notification => {
@@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
}
return (
-
+
{children}
);
diff --git a/firebase/functions/index.js b/firebase/functions/index.js
index 9091619..704507b 100644
--- a/firebase/functions/index.js
+++ b/firebase/functions/index.js
@@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
}
});
-exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => {
+exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
console.log('Running token refresh job...');
const profilesSnapshot = await db.collection('Profiles').get();
@@ -192,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleAccounts) {
try {
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
- const googleToken = profileData?.googleAccounts?.[googleEmail];
+ const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
if (googleToken) {
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
@@ -239,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
return null;
});
-// Function to refresh Google token
-async function refreshGoogleToken(token) {
- // Assuming you use OAuth2 token refresh flow
- const response = await axios.post('https://oauth2.googleapis.com/token', {
- grant_type: 'refresh_token',
- refresh_token: token, // Add refresh token stored previously
- client_id: 'YOUR_GOOGLE_CLIENT_ID',
- client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
- });
+async function refreshGoogleToken(refreshToken) {
+ try {
+ const response = await axios.post('https://oauth2.googleapis.com/token', {
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
+ });
- return response.data.access_token; // Return new access token
+ return response.data.access_token; // Return the new access token
+ } catch (error) {
+ console.error("Error refreshing Google token:", error);
+ throw error;
+ }
}
-async function refreshMicrosoftToken(token) {
- const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
- grant_type: 'refresh_token',
- refresh_token: token, // Add refresh token stored previously
- client_id: 'YOUR_MICROSOFT_CLIENT_ID',
- client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
- scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
- });
+async function refreshMicrosoftToken(refreshToken) {
+ try {
+ const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
+ scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
+ });
- return response.data.access_token; // Return new access token
+ return response.data.access_token; // Return the new access token
+ } catch (error) {
+ console.error("Error refreshing Microsoft token:", error);
+ throw error;
+ }
}
async function getPushTokensForEvent() {
diff --git a/hooks/firebase/useChangeProfilePicture.ts b/hooks/firebase/useChangeProfilePicture.ts
index 4f0b97e..6dff508 100644
--- a/hooks/firebase/useChangeProfilePicture.ts
+++ b/hooks/firebase/useChangeProfilePicture.ts
@@ -1,13 +1,13 @@
-import { useMutation, useQueryClient } from "react-query";
+import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
import storage from "@react-native-firebase/storage";
-import { useAuthContext } from "@/contexts/AuthContext";
+import {useAuthContext} from "@/contexts/AuthContext";
import * as ImagePicker from "expo-image-picker";
-import { Platform } from "react-native";
+import {Platform} from "react-native";
-export const useChangeProfilePicture = () => {
+export const useChangeProfilePicture = (customUserId?: string) => {
const queryClient = useQueryClient();
- const { user, refreshProfileData } = useAuthContext();
+ const {user, refreshProfileData} = useAuthContext();
return useMutation({
mutationKey: ["changeProfilePicture"],
@@ -38,20 +38,24 @@ export const useChangeProfilePicture = () => {
const downloadURL = await reference.getDownloadURL();
console.log("Download URL:", downloadURL);
+ if(!customUserId) {
await firestore()
.collection("Profiles")
.doc(user?.uid)
- .update({ pfp: downloadURL });
+ .update({pfp: downloadURL});
+ }
} catch (e) {
- console.error("Error uploading profile picture:", e.message);
+ console.error("Error uploading profile picture:", e);
throw e;
}
},
onSuccess: () => {
// Invalidate queries to refresh profile data
- queryClient.invalidateQueries("Profiles");
- refreshProfileData();
+ if (!customUserId) {
+ queryClient.invalidateQueries("Profiles");
+ refreshProfileData();
+ }
},
});
};
\ No newline at end of file
diff --git a/hooks/firebase/useClearTokens.ts b/hooks/firebase/useClearTokens.ts
new file mode 100644
index 0000000..35fd305
--- /dev/null
+++ b/hooks/firebase/useClearTokens.ts
@@ -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 = {};
+ 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});
+ },
+ })
+}
\ No newline at end of file
diff --git a/hooks/firebase/useCreateEvent.ts b/hooks/firebase/useCreateEvent.ts
index 5d5ae7d..2b56346 100644
--- a/hooks/firebase/useCreateEvent.ts
+++ b/hooks/firebase/useCreateEvent.ts
@@ -45,34 +45,41 @@ export const useCreateEvent = () => {
}
export const useCreateEventsFromProvider = () => {
- const {user: currentUser} = useAuthContext();
+ const { user: currentUser } = useAuthContext();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createEventsFromProvider"],
mutationFn: async (eventDataArray: Partial[]) => {
try {
- for (const eventData of eventDataArray) {
+ // Create an array of promises for each event's Firestore read/write operation
+ const promises = eventDataArray.map(async (eventData) => {
console.log("Processing EventData: ", eventData);
+ // Check if the event already exists
const snapshot = await firestore()
.collection("Events")
.where("id", "==", eventData.id)
.get();
if (snapshot.empty) {
- await firestore()
+ // Event doesn't exist, so add it
+ return firestore()
.collection("Events")
- .add({...eventData, creatorId: currentUser?.uid});
+ .add({ ...eventData, creatorId: currentUser?.uid });
} else {
- console.log("Event already exists, updating...");
+ // Event exists, update it
const docId = snapshot.docs[0].id;
- await firestore()
+ return firestore()
.collection("Events")
.doc(docId)
- .set({...eventData, creatorId: currentUser?.uid}, {merge: true});
+ .set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
}
- }
+ });
+
+ // Execute all promises in parallel
+ await Promise.all(promises);
+
} catch (e) {
console.error("Error creating/updating events: ", e);
}
diff --git a/hooks/firebase/useLoginWithQrCode.ts b/hooks/firebase/useLoginWithQrCode.ts
index 13cc44b..d28c456 100644
--- a/hooks/firebase/useLoginWithQrCode.ts
+++ b/hooks/firebase/useLoginWithQrCode.ts
@@ -1,12 +1,16 @@
import {useMutation} from "react-query";
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
import auth from "@react-native-firebase/auth";
+import {useAuthContext} from "@/contexts/AuthContext";
export const useLoginWithQrCode = () => {
+ const {setRedirectOverride} = useAuthContext()
+
return useMutation({
mutationKey: ["loginWithQrCode"],
mutationFn: async ({userId}: { userId: string }) => {
try {
+ setRedirectOverride(true)
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
token: string
}>
diff --git a/hooks/firebase/useSignUp.ts b/hooks/firebase/useSignUp.ts
index 2464cd0..4326297 100644
--- a/hooks/firebase/useSignUp.ts
+++ b/hooks/firebase/useSignUp.ts
@@ -1,44 +1,47 @@
-import { useMutation } from "react-query";
+import {useMutation} from "react-query";
import auth from "@react-native-firebase/auth";
-import { ProfileType } from "@/contexts/AuthContext";
-import { useSetUserData } from "./useSetUserData";
+import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
+import {useSetUserData} from "./useSetUserData";
import {uuidv4} from "@firebase/util";
import * as Localization from "expo-localization";
export const useSignUp = () => {
- const { mutateAsync: setUserData } = useSetUserData();
+ const {setRedirectOverride} = useAuthContext()
+ const {mutateAsync: setUserData} = useSetUserData();
- return useMutation({
- mutationKey: ["signUp"],
- mutationFn: async ({
- email,
- password,
- firstName,
- lastName,
- }: {
- email: string;
- password: string;
- firstName: string;
- lastName: string;
- }) => {
- await auth()
- .createUserWithEmailAndPassword(email, password)
- .then(async (res) => {
- try {
- await setUserData({
- newUserData: {
- userType: ProfileType.PARENT,
- firstName: firstName,
- lastName: lastName,
- familyId: uuidv4(),
- timeZone: Localization.getCalendars()[0].timeZone,
- },
- customUser: res.user,
- });
- } catch (error) {
- console.error(error);
- }
- });
- },
- });
+ return useMutation({
+ mutationKey: ["signUp"],
+ mutationFn: async ({
+ email,
+ password,
+ firstName,
+ lastName,
+ }: {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName: string;
+ }) => {
+ setRedirectOverride(true)
+
+ await auth()
+ .createUserWithEmailAndPassword(email, password)
+ .then(async (res) => {
+ try {
+ await setUserData({
+ newUserData: {
+ userType: ProfileType.PARENT,
+ firstName: firstName,
+ lastName: lastName,
+ familyId: uuidv4(),
+ timeZone: Localization.getCalendars()[0].timeZone,
+ },
+ customUser: res.user,
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ },
+ });
};
diff --git a/hooks/useCalSync.ts b/hooks/useCalSync.ts
new file mode 100644
index 0000000..76c2dff
--- /dev/null
+++ b/hooks/useCalSync.ts
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/hooks/useFetchAndSaveGoogleEvents.ts b/hooks/useFetchAndSaveGoogleEvents.ts
index d2bd62b..b96c0f9 100644
--- a/hooks/useFetchAndSaveGoogleEvents.ts
+++ b/hooks/useFetchAndSaveGoogleEvents.ts
@@ -2,11 +2,13 @@ import {useMutation, useQueryClient} from "react-query";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
+import {useClearTokens} from "@/hooks/firebase/useClearTokens";
export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient()
const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
+ const {mutateAsync: clearToken} = useClearTokens();
return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"],
@@ -26,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
timeMax.toISOString().slice(0, -5) + "Z"
);
+ if(!response.success) {
+ await clearToken({email: email!, provider: "google"})
+ return
+ }
+
console.log("Google Calendar events fetched:", response);
- const items = response?.map((item) => {
+ const items = response?.googleEvents?.map((item) => {
if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
item.endDate = item.startDate;
diff --git a/hooks/useUploadProfilePicture.ts b/hooks/useUploadProfilePicture.ts
new file mode 100644
index 0000000..5c452b6
--- /dev/null
+++ b/hooks/useUploadProfilePicture.ts
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/ios/cally.xcodeproj/project.pbxproj b/ios/cally.xcodeproj/project.pbxproj
index ef5c283..ca73519 100644
--- a/ios/cally.xcodeproj/project.pbxproj
+++ b/ios/cally.xcodeproj/project.pbxproj
@@ -450,7 +450,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
- PRODUCT_NAME = "Cally";
+ PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -484,7 +484,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
- PRODUCT_NAME = "Cally";
+ PRODUCT_NAME = Cally;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
diff --git a/package.json b/package.json
index c968392..72e6b30 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"expo-auth-session": "^5.5.2",
"expo-barcode-scanner": "~13.0.1",
"expo-build-properties": "~0.12.4",
+ "expo-cached-image": "^51.0.19",
"expo-calendar": "~13.0.5",
"expo-camera": "~15.0.16",
"expo-constants": "~16.0.2",
diff --git a/yarn.lock b/yarn.lock
index 3393e35..8e28313 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -960,6 +960,89 @@
wrap-ansi "^7.0.0"
ws "^8.12.1"
+"@expo/cli@0.18.30":
+ version "0.18.30"
+ resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.18.30.tgz#0cb4829aa11e98ae350a5c15958b9816e9a1d2f0"
+ integrity sha512-V90TUJh9Ly8stYo8nwqIqNWCsYjE28GlVFWEhAFCUOp99foiQr8HSTpiiX5GIrprcPoWmlGoY+J5fQA29R4lFg==
+ dependencies:
+ "@babel/runtime" "^7.20.0"
+ "@expo/code-signing-certificates" "0.0.5"
+ "@expo/config" "~9.0.0-beta.0"
+ "@expo/config-plugins" "~8.0.8"
+ "@expo/devcert" "^1.0.0"
+ "@expo/env" "~0.3.0"
+ "@expo/image-utils" "^0.5.0"
+ "@expo/json-file" "^8.3.0"
+ "@expo/metro-config" "0.18.11"
+ "@expo/osascript" "^2.0.31"
+ "@expo/package-manager" "^1.5.0"
+ "@expo/plist" "^0.1.0"
+ "@expo/prebuild-config" "7.0.9"
+ "@expo/rudder-sdk-node" "1.1.1"
+ "@expo/spawn-async" "^1.7.2"
+ "@expo/xcpretty" "^4.3.0"
+ "@react-native/dev-middleware" "0.74.85"
+ "@urql/core" "2.3.6"
+ "@urql/exchange-retry" "0.3.0"
+ accepts "^1.3.8"
+ arg "5.0.2"
+ better-opn "~3.0.2"
+ bplist-creator "0.0.7"
+ bplist-parser "^0.3.1"
+ cacache "^18.0.2"
+ chalk "^4.0.0"
+ ci-info "^3.3.0"
+ connect "^3.7.0"
+ debug "^4.3.4"
+ env-editor "^0.4.1"
+ fast-glob "^3.3.2"
+ find-yarn-workspace-root "~2.0.0"
+ form-data "^3.0.1"
+ freeport-async "2.0.0"
+ fs-extra "~8.1.0"
+ getenv "^1.0.0"
+ glob "^7.1.7"
+ graphql "15.8.0"
+ graphql-tag "^2.10.1"
+ https-proxy-agent "^5.0.1"
+ internal-ip "4.3.0"
+ is-docker "^2.0.0"
+ is-wsl "^2.1.1"
+ js-yaml "^3.13.1"
+ json-schema-deref-sync "^0.13.0"
+ lodash.debounce "^4.0.8"
+ md5hex "^1.0.0"
+ minimatch "^3.0.4"
+ node-fetch "^2.6.7"
+ node-forge "^1.3.1"
+ npm-package-arg "^7.0.0"
+ open "^8.3.0"
+ ora "3.4.0"
+ picomatch "^3.0.1"
+ pretty-bytes "5.6.0"
+ progress "2.0.3"
+ prompts "^2.3.2"
+ qrcode-terminal "0.11.0"
+ require-from-string "^2.0.2"
+ requireg "^0.2.2"
+ resolve "^1.22.2"
+ resolve-from "^5.0.0"
+ resolve.exports "^2.0.2"
+ semver "^7.6.0"
+ send "^0.18.0"
+ slugify "^1.3.4"
+ source-map-support "~0.5.21"
+ stacktrace-parser "^0.1.10"
+ structured-headers "^0.4.1"
+ tar "^6.0.5"
+ temp-dir "^2.0.0"
+ tempy "^0.7.1"
+ terminal-link "^2.1.1"
+ text-table "^0.2.0"
+ url-join "4.0.0"
+ wrap-ansi "^7.0.0"
+ ws "^8.12.1"
+
"@expo/code-signing-certificates@0.0.5":
version "0.0.5"
resolved "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz"
@@ -968,6 +1051,48 @@
node-forge "^1.2.1"
nullthrows "^1.1.1"
+"@expo/config-plugins@8.0.10":
+ version "8.0.10"
+ resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-8.0.10.tgz#5cda076f38bc04675cb42d8acdd23d6e460a62de"
+ integrity sha512-KG1fnSKRmsudPU9BWkl59PyE0byrE2HTnqbOrgwr2FAhqh7tfr9nRs6A9oLS/ntpGzmFxccTEcsV0L4apsuxxg==
+ dependencies:
+ "@expo/config-types" "^51.0.3"
+ "@expo/json-file" "~8.3.0"
+ "@expo/plist" "^0.1.0"
+ "@expo/sdk-runtime-versions" "^1.0.0"
+ chalk "^4.1.2"
+ debug "^4.3.1"
+ find-up "~5.0.0"
+ getenv "^1.0.0"
+ glob "7.1.6"
+ resolve-from "^5.0.0"
+ semver "^7.5.4"
+ slash "^3.0.0"
+ slugify "^1.6.6"
+ xcode "^3.0.1"
+ xml2js "0.6.0"
+
+"@expo/config-plugins@8.0.9", "@expo/config-plugins@~8.0.0-beta.0", "@expo/config-plugins@~8.0.8":
+ version "8.0.9"
+ resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.9.tgz"
+ integrity sha512-dNCG45C7BbDPV9MdWvCbsFtJtVn4w/TJbb5b7Yr6FA8HYIlaaVM0wqUMzTPmGj54iYXw8X/Vge8uCPxg7RWgeA==
+ dependencies:
+ "@expo/config-types" "^51.0.0-unreleased"
+ "@expo/json-file" "~8.3.0"
+ "@expo/plist" "^0.1.0"
+ "@expo/sdk-runtime-versions" "^1.0.0"
+ chalk "^4.1.2"
+ debug "^4.3.1"
+ find-up "~5.0.0"
+ getenv "^1.0.0"
+ glob "7.1.6"
+ resolve-from "^5.0.0"
+ semver "^7.5.4"
+ slash "^3.0.0"
+ slugify "^1.6.6"
+ xcode "^3.0.1"
+ xml2js "0.6.0"
+
"@expo/config-plugins@~5.0.3":
version "5.0.4"
resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-5.0.4.tgz"
@@ -1020,6 +1145,45 @@
resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.2.tgz"
integrity sha512-IglkIoiDwJMY01lYkF/ZSBoe/5cR+O3+Gx6fpLFjLfgZGBTdyPkKa1g8NWoWQCk+D3cKL2MDbszT2DyRRB0YqQ==
+"@expo/config-types@^51.0.3":
+ version "51.0.3"
+ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450"
+ integrity sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA==
+
+"@expo/config@9.0.3", "@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0":
+ version "9.0.3"
+ resolved "https://registry.npmjs.org/@expo/config/-/config-9.0.3.tgz"
+ integrity sha512-eOTNM8eOC8gZNHgenySRlc/lwmYY1NOgvjwA8LHuvPT7/eUwD93zrxu3lPD1Cc/P6C/2BcVdfH4hf0tLmDxnsg==
+ dependencies:
+ "@babel/code-frame" "~7.10.4"
+ "@expo/config-plugins" "~8.0.8"
+ "@expo/config-types" "^51.0.0-unreleased"
+ "@expo/json-file" "^8.3.0"
+ getenv "^1.0.0"
+ glob "7.1.6"
+ require-from-string "^2.0.2"
+ resolve-from "^5.0.0"
+ semver "^7.6.0"
+ slugify "^1.3.4"
+ sucrase "3.34.0"
+
+"@expo/config@9.0.4":
+ version "9.0.4"
+ resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.4.tgz#52f0a94edd0e2c36dfb5e284cc1a6d99d9d2af97"
+ integrity sha512-g5ns5u1JSKudHYhjo1zaSfkJ/iZIcWmUmIQptMJZ6ag1C0ShL2sj8qdfU8MmAMuKLOgcIfSaiWlQnm4X3VJVkg==
+ dependencies:
+ "@babel/code-frame" "~7.10.4"
+ "@expo/config-plugins" "~8.0.8"
+ "@expo/config-types" "^51.0.3"
+ "@expo/json-file" "^8.3.0"
+ getenv "^1.0.0"
+ glob "7.1.6"
+ require-from-string "^2.0.2"
+ resolve-from "^5.0.0"
+ semver "^7.6.0"
+ slugify "^1.3.4"
+ sucrase "3.34.0"
+
"@expo/config@~7.0.2":
version "7.0.3"
resolved "https://registry.npmjs.org/@expo/config/-/config-7.0.3.tgz"
@@ -1285,6 +1449,23 @@
semver "^7.6.0"
xml2js "0.6.0"
+"@expo/prebuild-config@7.0.9":
+ version "7.0.9"
+ resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-7.0.9.tgz#7abd489e18ed6514a0c9cd214eb34c0d5efda799"
+ integrity sha512-9i6Cg7jInpnGEHN0jxnW0P+0BexnePiBzmbUvzSbRXpdXihYUX2AKMu73jgzxn5P1hXOSkzNS7umaY+BZ+aBag==
+ dependencies:
+ "@expo/config" "~9.0.0-beta.0"
+ "@expo/config-plugins" "~8.0.8"
+ "@expo/config-types" "^51.0.3"
+ "@expo/image-utils" "^0.5.0"
+ "@expo/json-file" "^8.3.0"
+ "@react-native/normalize-colors" "0.74.85"
+ debug "^4.3.1"
+ fs-extra "^9.0.0"
+ resolve-from "^5.0.0"
+ semver "^7.6.0"
+ xml2js "0.6.0"
+
"@expo/rudder-sdk-node@1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz"
@@ -3592,6 +3773,19 @@ babel-plugin-polyfill-regenerator@^0.6.1:
dependencies:
"@babel/helper-define-polyfill-provider" "^0.6.2"
+babel-plugin-react-compiler@0.0.0-experimental-592953e-20240517:
+ version "0.0.0-experimental-592953e-20240517"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-592953e-20240517.tgz#e800fa1550d03573cd5637218dc711f12f642249"
+ integrity sha512-OjG1SVaeQZaJrqkMFJatg8W/MTow8Ak5rx2SI0ETQBO1XvOk/XZGMbltNCPdFJLKghBYoBjC+Y3Ap/Xr7B01mA==
+ dependencies:
+ "@babel/generator" "7.2.0"
+ "@babel/types" "^7.19.0"
+ chalk "4"
+ invariant "^2.2.4"
+ pretty-format "^24"
+ zod "^3.22.4"
+ zod-validation-error "^2.1.0"
+
babel-plugin-react-compiler@^0.0.0-experimental-592953e-20240517:
version "0.0.0-experimental-7d62301-20240821"
resolved "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-7d62301-20240821.tgz"
@@ -3659,6 +3853,22 @@ babel-preset-expo@~11.0.14:
babel-plugin-react-native-web "~0.19.10"
react-refresh "^0.14.2"
+babel-preset-expo@~11.0.15:
+ version "11.0.15"
+ resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-11.0.15.tgz#f29b1ac1f59f8739f63c80515906186586c24d3c"
+ integrity sha512-rgiMTYwqIPULaO7iZdqyL7aAff9QLOX6OWUtLZBlOrOTreGY1yHah/5+l8MvI6NVc/8Zj5LY4Y5uMSnJIuzTLw==
+ dependencies:
+ "@babel/plugin-proposal-decorators" "^7.12.9"
+ "@babel/plugin-transform-export-namespace-from" "^7.22.11"
+ "@babel/plugin-transform-object-rest-spread" "^7.12.13"
+ "@babel/plugin-transform-parameters" "^7.22.15"
+ "@babel/preset-react" "^7.22.15"
+ "@babel/preset-typescript" "^7.23.0"
+ "@react-native/babel-preset" "0.74.87"
+ babel-plugin-react-compiler "0.0.0-experimental-592953e-20240517"
+ babel-plugin-react-native-web "~0.19.10"
+ react-refresh "^0.14.2"
+
babel-preset-jest@^29.6.3:
version "29.6.3"
resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz"
@@ -5115,6 +5325,13 @@ expo-build-properties@~0.12.4:
ajv "^8.11.0"
semver "^7.6.0"
+expo-cached-image@^51.0.19:
+ version "51.0.19"
+ resolved "https://registry.yarnpkg.com/expo-cached-image/-/expo-cached-image-51.0.19.tgz#27447d761a4b7414a2e5fee2e25c9436dd6f073e"
+ integrity sha512-HcIKolCKyrYcfimWp64S25Tv8YneUsKV47yJ93L4l4NVA7GJulqSS/fr2jf6B3mzw5rZNDU+eDAf1nzcxavfkg==
+ dependencies:
+ expo "51"
+
expo-calendar@~13.0.5:
version "13.0.5"
resolved "https://registry.npmjs.org/expo-calendar/-/expo-calendar-13.0.5.tgz"
@@ -5259,6 +5476,19 @@ expo-modules-autolinking@1.11.2:
require-from-string "^2.0.2"
resolve-from "^5.0.0"
+expo-modules-autolinking@1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.11.3.tgz#bc64d278c04015014bb5802e3cfcd942d7c07168"
+ integrity sha512-oYh8EZEvYF5TYppxEKUTTJmbr8j7eRRnrIxzZtMvxLTXoujThVPMFS/cbnSnf2bFm1lq50TdDNABhmEi7z0ngQ==
+ dependencies:
+ chalk "^4.1.0"
+ commander "^7.2.0"
+ fast-glob "^3.2.5"
+ find-up "^5.0.0"
+ fs-extra "^9.1.0"
+ require-from-string "^2.0.2"
+ resolve-from "^5.0.0"
+
expo-modules-core@1.12.24:
version "1.12.24"
resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz"
@@ -5266,6 +5496,13 @@ expo-modules-core@1.12.24:
dependencies:
invariant "^2.2.4"
+expo-modules-core@1.12.26:
+ version "1.12.26"
+ resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.26.tgz#86c4087dc6246abfc4d7f5e61097dc8cc4b22262"
+ integrity sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA==
+ dependencies:
+ invariant "^2.2.4"
+
expo-notifications@~0.28.18:
version "0.28.18"
resolved "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.18.tgz"
@@ -5359,6 +5596,27 @@ expo-web-browser@~13.0.0, expo-web-browser@~13.0.3:
resolved "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-13.0.3.tgz"
integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ==
+expo@51:
+ version "51.0.38"
+ resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.38.tgz#e4127b230454a34a507cfb9f1a2e4b3855cb0579"
+ integrity sha512-/B9npFkOPmv6WMIhdjQXEY0Z9k/67UZIVkodW8JxGIXwKUZAGHL+z1R5hTtWimpIrvVhyHUFU3f8uhfEKYhHNQ==
+ dependencies:
+ "@babel/runtime" "^7.20.0"
+ "@expo/cli" "0.18.30"
+ "@expo/config" "9.0.4"
+ "@expo/config-plugins" "8.0.10"
+ "@expo/metro-config" "0.18.11"
+ "@expo/vector-icons" "^14.0.3"
+ babel-preset-expo "~11.0.15"
+ expo-asset "~10.0.10"
+ expo-file-system "~17.0.1"
+ expo-font "~12.0.10"
+ expo-keep-awake "~13.0.2"
+ expo-modules-autolinking "1.11.3"
+ expo-modules-core "1.12.26"
+ fbemitter "^3.0.0"
+ whatwg-url-without-unicode "8.0.0-3"
+
expo@~51.0.24:
version "51.0.34"
resolved "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz"