Apple calendar sync, timezone, first day of week additions

This commit is contained in:
Milan Paunovic
2024-10-19 22:56:55 +02:00
parent 00b6225a1c
commit 3653400a92
25 changed files with 1154 additions and 689 deletions

View File

@ -1,10 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
@ -22,7 +24,7 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/bdb8c57b-25bb-4d36-b3b8-5b09c5092f52"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">Cally - Family Planner</string>
<string name="app_name">Cally.</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>

View File

@ -1,4 +1,4 @@
rootProject.name = 'Cally - Family Planner'
rootProject.name = 'Cally.'
dependencyResolutionManagement {
versionCatalogs {

View File

@ -1,6 +1,6 @@
{
"expo": {
"name": "Cally - Family Planner",
"name": "Cally.",
"slug": "cally",
"version": "1.0.0",
"orientation": "portrait",
@ -16,7 +16,8 @@
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "23"
"buildNumber": "23",
"usesAppleSignIn": true
},
"android": {
"adaptiveIcon": {
@ -63,7 +64,17 @@
"defaultChannel": "default"
}
],
"expo-font"
[
"expo-calendar",
{
"calendarPermission": "The app needs to access your calendar."
}
],
[
"expo-apple-authentication"
],
"expo-font",
"expo-localization"
],
"experiments": {
"typedRoutes": true

View File

@ -0,0 +1,41 @@
import * as Calendar from 'expo-calendar';
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
try {
const {status} = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') {
throw new Error("Calendar permission not granted");
}
const defaultCalendarSource = await Calendar.getDefaultCalendarAsync();
if (!defaultCalendarSource) {
throw new Error("No calendar found");
}
const events = await Calendar.getEventsAsync(
[defaultCalendarSource.id],
startDate,
endDate
);
return events.map((event) => {
let isAllDay = event.allDay || false;
const startDateTime = new Date(event.startDate);
const endDateTime = new Date(event.endDate);
return {
id: event.id,
title: event.title,
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
});
} catch (error) {
console.error("Error fetching iPhone Calendar events: ", error);
throw error;
}
}

View File

@ -1,4 +1,4 @@
export async function fetchGoogleCalendarEvents(token, email, startDate, endDate) {
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
console.log(token);
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
@ -45,6 +45,7 @@ export async function fetchGoogleCalendarEvents(token, email, startDate, endDate
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
googleEvents.push(googleEvent);

View File

@ -1,4 +1,4 @@
export async function fetchMicrosoftCalendarEvents(token, email, startDate, endDate) {
export async function fetchMicrosoftCalendarEvents(token, email, familyId, startDate, endDate) {
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/calendar/calendarView?startDateTime=${startDate}&endDateTime=${endDate}`,
{
@ -34,6 +34,7 @@ export async function fetchMicrosoftCalendarEvents(token, email, startDate, endD
startDate: startDateTime,
endDate: endDateTime,
allDay: item.isAllDay,
familyId,
email
};

View File

@ -10,6 +10,7 @@ import {
selectedDateAtom,
selectedNewEventDateAtom
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
interface EventCalendarProps {
calendarHeight: number;
@ -17,6 +18,7 @@ interface EventCalendarProps {
export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight}) => {
const {data: events} = useGetEvents();
const {profileData} = useAuthContext()
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom)
const mode = useAtomValue(modeAtom)
const setEditVisible = useSetAtom(editVisibleAtom)
@ -35,6 +37,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight
setEditVisible(true);
setEventForEdit(event);
}}
weekStartsOn={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}

View File

@ -20,7 +20,7 @@ export const InnerCalendar = () => {
return (
<>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30}}
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 60}}
ref={calendarContainerRef}
onLayout={onLayout}
>

View File

@ -1,12 +1,9 @@
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 {ScrollView, StyleSheet} from "react-native";
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import {colorMap} from "@/contexts/SettingsContext";
import {TouchableOpacity} from "react-native-gesture-handler";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce";
@ -17,6 +14,11 @@ 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";
const googleConfig = {
androidClientId:
@ -50,8 +52,8 @@ const microsoftConfig = {
const CalendarSettingsPage = (props: {
setSelectedPage: (page: number) => void;
}) => {
const [startDate, setStartDate] = useState<boolean>(false);
const {profileData} = useAuthContext();
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(profileData?.firstDayOfWeek ?? ExpoLocalization.getCalendars()[0].firstWeekday === 1 ? "Mondays" : "Sundays");
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
@ -60,8 +62,11 @@ const CalendarSettingsPage = (props: {
profileData?.eventColor ?? colorMap.pink
);
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
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);
@ -70,49 +75,6 @@ const CalendarSettingsPage = (props: {
signInWithGoogle();
}, [response]);
const fetchAndSaveGoogleEvents = async (token?: string, email?: string) => {
console.log("Fetching Google Calendar events...");
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
console.log("Token: ", token ?? profileData?.googleToken)
fetchGoogleCalendarEvents(
token ?? profileData?.googleToken,
email ?? profileData?.googleMail,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then(async (response) => {
console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => {
if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0))
item.endDate = item.startDate
}
return item;
}) || [];
await createEventsFromProvider(items);
}).catch((error) => {
console.error("Error fetching Google Calendar events:", error);
});
};
const fetchAndSaveMicrosoftEvents = async (token?: string, email?: string) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));
console.log("Token: ", token ?? profileData?.microsoftToken)
fetchMicrosoftCalendarEvents(
token ?? profileData?.microsoftToken,
email ?? profileData?.outlookMail,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then(async (response) => {
console.log(response);
const items = response ?? [];
await createEventsFromProvider(items);
});
};
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
@ -214,7 +176,7 @@ const CalendarSettingsPage = (props: {
newUserData: {microsoftToken: tokenData.access_token, outlookMail: outlookMail},
});
await fetchAndSaveMicrosoftEvents(tokenData.access_token, outlookMail)
await fetchAndSaveOutlookEvents(tokenData.access_token, outlookMail)
console.log("User data updated successfully.");
}
}
@ -226,6 +188,39 @@ const CalendarSettingsPage = (props: {
}
};
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...");
// Example: Store user token and email
await updateUserData({
newUserData: {appleToken, appleMail},
});
console.log("User data updated with Apple ID token.");
} 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 {
@ -242,6 +237,26 @@ const CalendarSettingsPage = (props: {
[]
);
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 = () => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
}
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
@ -333,10 +348,10 @@ const CalendarSettingsPage = (props: {
<Text style={styles.cardTitle}>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={startDate}
value={firstDayOfWeek === "Sundays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(true)}
onValueChange={() => handleChangeFirstDayOfWeek("Sundays")}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
@ -348,10 +363,10 @@ const CalendarSettingsPage = (props: {
</View>
<View row marginV-5>
<Checkbox
value={!startDate}
value={firstDayOfWeek === "Mondays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(false)}
onValueChange={() => handleChangeFirstDayOfWeek("Mondays")}
/>
<Text text70 marginL-8>
Mondays
@ -379,7 +394,8 @@ const CalendarSettingsPage = (props: {
text70BL
/>
<Button
label="Connect Apple"
onPress={() => !profileData?.appleToken ? handleAppleSignIn() : clearToken("google")}
label={profileData?.appleToken ? `Disconnect ${profileData.appleMail}` : "Connect Apple"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2
@ -416,40 +432,93 @@ const CalendarSettingsPage = (props: {
Connected Calendars
</Text>
<View style={styles.card}>
<View style={styles.noPaddingCard}>
<View style={{marginTop: 20}}>
{!!profileData?.googleMail && (
<Button
onPress={() => fetchAndSaveGoogleEvents()}
label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 2}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<TouchableOpacity
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingGoogle}
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingGoogle ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.appleMail && (
<TouchableOpacity>
<View row paddingR-20 center>
<Button
disabled={isSyncingApple}
onPress={() => fetchAndSaveAppleEvents(undefined, undefined)}
label={`Sync ${profileData?.appleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingApple ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.outlookMail && (
<Button
onPress={() => fetchAndSaveMicrosoftEvents()}
label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 2}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<TouchableOpacity
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingOutlook}
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingOutlook ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
</View>
</View>
@ -480,6 +549,12 @@ const styles = StyleSheet.create({
marginTop: 20,
borderRadius: 12,
},
noPaddingCard: {
backgroundColor: "white",
width: "100%",
marginTop: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",

File diff suppressed because it is too large Load Diff

View File

@ -1,122 +1,207 @@
import { Text, TextField, View } from "react-native-ui-lib";
import React, { useState } from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import {Colors, Picker, Text, TextField, View} from "react-native-ui-lib";
import React, {useEffect, useRef, useState} from "react";
import {ImageBackground, StyleSheet} from "react-native";
import {ScrollView} from "react-native-gesture-handler";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import Ionicons from "@expo/vector-icons/Ionicons";
import * as tz from 'tzdata';
import * as Localization from 'expo-localization';
import debounce from "debounce";
const MyProfile = () => {
const { user, profileData } = useAuthContext();
const {user, profileData} = useAuthContext();
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const [timeZone, setTimeZone] = useState<string>(profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone);
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const { mutateAsync: updateUserData } = useUpdateUserData();
return (
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
const {mutateAsync: updateUserData} = useUpdateUserData();
const isFirstRender = useRef(true);
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
await updateUserData({ newUserData: { firstName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
await updateUserData({ newUserData: { lastName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text text80 marginT-20 marginB-7 style={styles.label}>
Time Zone
</Text>
<TextField text70 placeholder="Time Zone" style={styles.txtBox} />
</View>
</ScrollView>
);
const handleUpdateUserData = async () => {
await updateUserData({newUserData: {firstName, lastName, timeZone}});
}
const debouncedUserDataUpdate = debounce(handleUpdateUserData, 500);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
debouncedUserDataUpdate();
}, [timeZone, lastName, firstName]);
return (
<ScrollView style={{paddingBottom: 100, flex: 1}}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
editable={false}
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text style={styles.jakarta12}>Time Zone</Text>
<View style={styles.viewPicker}>
<Picker
// editable={!isLoading}
value={timeZone}
onChange={(item) => {
setTimeZone(item as string)
}}
showSearch
floatingPlaceholder
style={styles.inViewPicker}
trailingAccessory={
<View style={{
justifyContent: "center",
alignItems: "center",
height: "100%",
marginTop: -38,
paddingRight: 15
}}>
<Ionicons name={"chevron-down"} style={{alignSelf: "center"}} size={20}
color={"#000000"}/>
</View>
}
>
{timeZoneItems}
</Picker>
</View>
</View>
</ScrollView>
);
};
const timeZoneItems = Object.keys(tz.zones).sort().map((zone) => (
<Picker.Item key={zone} label={zone.replace("/", " / ").replace("_", " ")} value={zone}/>
));
const styles = StyleSheet.create({
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfp: {
aspectRatio: 1,
width: 65.54,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1"
},
photoSet:{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07
}
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfp: {
aspectRatio: 1,
width: 65.54,
backgroundColor: "green",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1"
},
photoSet: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07
},
jakarta12: {
paddingVertical: 10,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1",
},
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,
},
});
export default MyProfile;

View File

@ -24,6 +24,8 @@ export interface UserProfile {
googleMail?: string | null;
outlookMail?: string | null;
appleMail?: string | null;
timeZone?: string | null;
firstDayOfWeek?: string | null;
}
export interface ParentProfile extends UserProfile {

View File

@ -6,7 +6,7 @@ import {useAtomValue} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
export const useGetEvents = () => {
const { user, profileData } = useAuthContext();
const {user, profileData} = useAuthContext();
const isFamilyView = useAtomValue(isFamilyViewAtom)
return useQuery({
@ -43,5 +43,7 @@ export const useGetEvents = () => {
};
}));
},
staleTime: Infinity,
cacheTime: Infinity
});
};

View File

@ -3,6 +3,7 @@ import auth from "@react-native-firebase/auth";
import { ProfileType } 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();
@ -30,6 +31,7 @@ export const useSignUp = () => {
firstName: firstName,
lastName: lastName,
familyId: uuidv4(),
timeZone: Localization.getCalendars()[0].timeZone,
},
customUser: res.user,
});

View File

@ -1,11 +1,11 @@
import firestore from "@react-native-firebase/firestore";
import { FirebaseAuthTypes } from "@react-native-firebase/auth";
import { useMutation, useQueryClient } from "react-query";
import { useAuthContext } from "@/contexts/AuthContext";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
import {useMutation, useQueryClient} from "react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
export const useUpdateUserData = () => {
const { user: currentUser, refreshProfileData } = useAuthContext();
const {user: currentUser, refreshProfileData} = useAuthContext();
const queryClient = useQueryClient();
return useMutation({
@ -17,7 +17,7 @@ export const useUpdateUserData = () => {
newUserData: Partial<UserProfile>;
customUser?: FirebaseAuthTypes.User;
}) => {
console.log("Mutation function called with data:", { newUserData, customUser });
console.log("Mutation function called with data:", {newUserData, customUser});
const user = currentUser ?? customUser;

View File

@ -0,0 +1,32 @@
import {useMutation} from "react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
export const useFetchAndSaveAppleEvents = () => {
const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({
mutationKey: ["fetchAndSaveAppleEvents"],
mutationFn: async (token?: string, email?: string) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
try {
const response = await fetchiPhoneCalendarEvents(
profileData?.familyId!,
email,
timeMin,
timeMax
);
console.log(response);
const items = response ?? [];
await createEventsFromProvider(items);
} catch (error) {
console.error("Error fetching and saving Apple Calendar events: ", error);
throw error;
}
},
});
};

View File

@ -0,0 +1,45 @@
import {useMutation} from "react-query";
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
export const useFetchAndSaveGoogleEvents = () => {
const {profileData} = useAuthContext();
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"],
mutationFn: async (token?: string, email?: string) => {
console.log("Fetching Google Calendar events...");
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
console.log("Token: ", token ?? profileData?.googleToken);
try {
const response = await fetchGoogleCalendarEvents(
token ?? profileData?.googleToken,
email ?? profileData?.googleMail,
profileData?.familyId,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
);
console.log("Google Calendar events fetched:", response);
const items = response?.map((item) => {
if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
item.endDate = item.startDate;
}
return item;
}) || [];
await createEventsFromProvider(items);
} catch (error) {
console.error("Error fetching Google Calendar events:", error);
throw error; // Ensure errors are propagated to the mutation
}
},
});
};

View File

@ -0,0 +1,36 @@
import { useMutation } from "react-query";
import { useAuthContext } from "@/contexts/AuthContext";
import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent";
import { fetchMicrosoftCalendarEvents } from "@/calendar-integration/microsoft-calendar-utils";
export const useFetchAndSaveOutlookEvents = () => {
const { profileData } = useAuthContext();
const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider();
return useMutation({
mutationKey: ["fetchAndSaveOutlookEvents"],
mutationFn: async (token?: string, email?: string) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));
console.log("Token: ", token ?? profileData?.microsoftToken);
try {
const response = await fetchMicrosoftCalendarEvents(
token ?? profileData?.microsoftToken,
email ?? profileData?.outlookMail,
profileData?.familyId,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
);
console.log(response);
const items = response ?? [];
await createEventsFromProvider(items);
} catch (error) {
console.error("Error fetching and saving Outlook events: ", error);
throw error;
}
},
});
};

View File

@ -1190,8 +1190,12 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- ExpoAppleAuthentication (6.4.2):
- ExpoModulesCore
- ExpoAsset (10.0.10):
- ExpoModulesCore
- ExpoCalendar (13.0.5):
- ExpoModulesCore
- ExpoCamera (15.0.16):
- ExpoModulesCore
- ZXingObjC/OneD
@ -1210,6 +1214,8 @@ PODS:
- ExpoModulesCore
- ExpoKeepAwake (13.0.2):
- ExpoModulesCore
- ExpoLocalization (15.0.3):
- ExpoModulesCore
- ExpoModulesCore (1.12.24):
- DoubleConversion
- glog
@ -2802,7 +2808,9 @@ DEPENDENCIES:
- expo-dev-launcher (from `../node_modules/expo-dev-launcher`)
- expo-dev-menu (from `../node_modules/expo-dev-menu`)
- expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`)
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoCalendar (from `../node_modules/expo-calendar/ios`)
- ExpoCamera (from `../node_modules/expo-camera/ios`)
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
- ExpoDevice (from `../node_modules/expo-device/ios`)
@ -2811,6 +2819,7 @@ DEPENDENCIES:
- ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLocalization (from `../node_modules/expo-localization/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
@ -2956,8 +2965,12 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-dev-menu"
expo-dev-menu-interface:
:path: "../node_modules/expo-dev-menu-interface/ios"
ExpoAppleAuthentication:
:path: "../node_modules/expo-apple-authentication/ios"
ExpoAsset:
:path: "../node_modules/expo-asset/ios"
ExpoCalendar:
:path: "../node_modules/expo-calendar/ios"
ExpoCamera:
:path: "../node_modules/expo-camera/ios"
ExpoCrypto:
@ -2974,6 +2987,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoLocalization:
:path: "../node_modules/expo-localization/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core"
ExpoSystemUI:
@ -3142,7 +3157,9 @@ SPEC CHECKSUMS:
expo-dev-launcher: fe4f2c0a0aa627449eeaec5f9f11e04090f97c40
expo-dev-menu: 5b14897ecce3a8cf9e9cf9109344c2c192a3766a
expo-dev-menu-interface: be32c09f1e03833050f0ee290dcc86b3ad0e73e4
ExpoAppleAuthentication: 265219fa0ba1110872079f55f56686b9737b0065
ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875
ExpoCalendar: 135beb39ea3795f854a4ea287a49f74c9203ce51
ExpoCamera: 929be541d1c1319fcf32f9f5d9df8b97804346b5
ExpoCrypto: 156078f266bf28f80ecf5e2a9c3a0d6ffce07a1c
ExpoDevice: fc94f0e42ecdfd897e7590f2874fc64dfa7e9b1c
@ -3151,6 +3168,7 @@ SPEC CHECKSUMS:
ExpoHead: fcb28a68ed4ba28f177394d2dfb8a0a8824cd103
ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
ExpoLocalization: f04eeec2e35bed01ab61c72ee1768ec04d093d01
ExpoModulesCore: db3e31e694684f08223d713e89f7648c6d3e04d0
ExpoSystemUI: d4f065a016cae6721b324eb659cdee4d4cf0cb26
ExpoWebBrowser: 7595ccac6938eb65b076385fd23d035db9ecdc8e

View File

@ -178,7 +178,9 @@
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = MV9C3PHV87;
LastSwiftMigration = 1250;
ProvisioningStyle = Automatic;
};
};
};
@ -302,6 +304,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseAuth/FirebaseAuth_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
@ -337,6 +340,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseAuth_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
@ -421,7 +425,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = cally/cally.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = MV9C3PHV87;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@ -441,7 +448,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = CallyFamilyPlanner;
PRODUCT_NAME = "Cally";
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -457,7 +464,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = cally/cally.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = MV9C3PHV87;
INFOPLIST_FILE = cally/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = (
@ -472,7 +482,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = CallyFamilyPlanner;
PRODUCT_NAME = "Cally";
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@ -4,10 +4,12 @@
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Cally - Family Planner</string>
<string>Cally.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -55,12 +57,20 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>The app needs to access your calendar.</string>
<key>NSCalendarsUsageDescription</key>
<string>The app needs to access your calendar.</string>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>NSRemindersFullAccessUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your reminders</string>
<key>NSRemindersUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your reminders</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
@ -92,6 +102,12 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>

View File

@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>

View File

@ -44,9 +44,11 @@
"debounce": "^2.1.1",
"expo": "~51.0.24",
"expo-app-loading": "^2.1.1",
"expo-apple-authentication": "~6.4.2",
"expo-auth-session": "^5.5.2",
"expo-barcode-scanner": "~13.0.1",
"expo-build-properties": "~0.12.4",
"expo-calendar": "~13.0.5",
"expo-camera": "~15.0.16",
"expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.27",
@ -54,6 +56,7 @@
"expo-font": "~12.0.10",
"expo-image-picker": "~15.0.7",
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
"expo-notifications": "~0.28.18",
"expo-router": "~3.5.20",
"expo-splash-screen": "~0.27.5",
@ -84,7 +87,9 @@
"react-native-toast-message": "^2.2.1",
"react-native-ui-lib": "^7.27.0",
"react-native-web": "~0.19.10",
"react-query": "^3.39.3"
"react-query": "^3.39.3",
"timezonecomplete": "^5.13.1",
"tzdata": "^1.0.42"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@ -5001,6 +5001,11 @@ expo-app-loading@^2.1.1:
dependencies:
expo-splash-screen "~0.17.0"
expo-apple-authentication@~6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/expo-apple-authentication/-/expo-apple-authentication-6.4.2.tgz#1c2ea4fcbd1de5736483dccd370cdd6b8e3de15d"
integrity sha512-X4u1n3Ql1hOpztXHbKNq4I1l4+Ff82gC6RmEeW43Eht7VE6E8PrQBpYKw+JJv8osrCJt7R5O1PZwed6WLN5oig==
expo-application@~5.9.0:
version "5.9.1"
resolved "https://registry.npmjs.org/expo-application/-/expo-application-5.9.1.tgz"
@ -5042,6 +5047,11 @@ expo-build-properties@~0.12.4:
ajv "^8.11.0"
semver "^7.6.0"
expo-calendar@~13.0.5:
version "13.0.5"
resolved "https://registry.yarnpkg.com/expo-calendar/-/expo-calendar-13.0.5.tgz#cdc85978cb59d99d6fc9a4fcd2c33d56cc7c8008"
integrity sha512-Wkk7eHvlyhWz2csxU6guYA2HFcLUfYpmlsdMy4a6bneBmFqIZG/ldnLKq/lcQ+BCrfI3fOULt3aNdF6SlZtLlw==
expo-camera@~15.0.16:
version "15.0.16"
resolved "https://registry.npmjs.org/expo-camera/-/expo-camera-15.0.16.tgz"
@ -5153,6 +5163,13 @@ expo-linking@~6.3.0, expo-linking@~6.3.1:
expo-constants "~16.0.0"
invariant "^2.2.4"
expo-localization@~15.0.3:
version "15.0.3"
resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-15.0.3.tgz#772c89b3ab9c925b7eca6911a11ca33980c2b674"
integrity sha512-IfcmlKuKRlowR9qIzL0e+nGHBeNoF7l2GQaOJstc7HZiPjNJ4J1R4D53ZNf483dt7JSkTRJBihdTadOtOEjRdg==
dependencies:
rtl-detect "^1.0.2"
expo-manifests@~0.14.0:
version "0.14.3"
resolved "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.14.3.tgz"
@ -9276,6 +9293,11 @@ rimraf@~2.6.2:
dependencies:
glob "^7.1.3"
rtl-detect@^1.0.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6"
integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@ -10124,6 +10146,13 @@ through@2:
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
timezonecomplete@^5.13.1:
version "5.13.1"
resolved "https://registry.yarnpkg.com/timezonecomplete/-/timezonecomplete-5.13.1.tgz#72c05e82b33013bacc7a38e5d554eafc7914b31f"
integrity sha512-41o3TTExXQ03jQML12Tk64b5TYeEy968Umq5vya+08sFWCcYw5fNqrHubD1vj/JGN1NYhFFBCS09rOL3b7nM2w==
dependencies:
tzdata "1.0.25"
tinycolor2@^1.4.1, tinycolor2@^1.4.2:
version "1.6.0"
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz"
@ -10313,6 +10342,16 @@ typical@^2.6.0:
resolved "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz"
integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==
tzdata@1.0.25:
version "1.0.25"
resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.25.tgz#e8839033c05761e04ef552242e779777becb13d0"
integrity sha512-yAZ/Tv/tBFIPHJGYrOexxW8Swyjszt7rDhIjnIPSqLaP8mzrr3T7D0w4cxQBtToXnQrlFqkEU0stGC/auz0JcQ==
tzdata@^1.0.42:
version "1.0.42"
resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.42.tgz#4f278809b50c50e9c865e44969aa7e746b165638"
integrity sha512-hVA4V8g27yz1YB4Ty4UliwJlWrFOoFrFBYFMd9rKUlRlaF+9Fl3gyzxF/+MQOtCH50pPE+XZ/bYOYkRnBDscVQ==
ua-parser-js@^0.7.33:
version "0.7.39"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz"