From 051837968acdbd3af6d1255b7ed55d9288303a0d Mon Sep 17 00:00:00 2001 From: Matt DiMeglio Date: Mon, 21 Apr 2025 11:41:08 -0400 Subject: [PATCH] Add Customizable Notifications & Add Global Variables --- app/_layout.jsx | 59 ++ app/_layout.tsx | 26 - app/incidents.jsx | 656 +++++++++--------- contexts/GlobalVariablesContext.js | 35 + contexts/index.js | 2 + hooks/index.js | 5 +- hooks/useCallFeed/useCallFeed.jsx | 183 +---- hooks/useGlobalVariablesContext/index.js | 1 + .../useGlobalVariablesContext.js | 4 + hooks/useNotifications/index.js | 1 + hooks/useNotifications/useNotifications.jsx | 115 +++ .../useWebSocketContext.js | 2 +- package-lock.json | 421 +++++++++++ package.json | 2 + 14 files changed, 1008 insertions(+), 504 deletions(-) create mode 100644 app/_layout.jsx delete mode 100644 app/_layout.tsx create mode 100644 contexts/GlobalVariablesContext.js create mode 100644 contexts/index.js create mode 100644 hooks/useGlobalVariablesContext/index.js create mode 100644 hooks/useGlobalVariablesContext/useGlobalVariablesContext.js create mode 100644 hooks/useNotifications/index.js create mode 100644 hooks/useNotifications/useNotifications.jsx diff --git a/app/_layout.jsx b/app/_layout.jsx new file mode 100644 index 0000000..02305f1 --- /dev/null +++ b/app/_layout.jsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from 'react'; +import { router, Stack } from 'expo-router'; +import { GlobalVariablesProvider, WebSocketProvider } from '../contexts'; +import { useNotifications, useWebSocketContext } from '@/hooks'; + +export const unstable_settings = { + initialRouteName: 'login', +}; + +function AppTest() { + const [ oldMessage, setOldMessage ] = useState(1); + const { lastMessage } = useWebSocketContext(); + const { schedulePushNotification } = useNotifications(); + + useEffect(() => { + router.replace('/login'); + }, []); + + const parseAddress = (data) => { + const { Address } = data; + const { StreetAddress, AddressApartment, Town, State } = Address; + const response = `${StreetAddress}${AddressApartment ? ` - ${AddressApartment}` : ''} ${Town}, ${State}` + return response; + } + + useEffect(() => { + if (lastMessage) { + const parsedMessage = JSON?.parse(lastMessage); + if (parsedMessage?.data && new Date(oldMessage)?.getTime() < new Date(parsedMessage?.timestamp)?.getTime()) { + setOldMessage(new Date(parsedMessage?.timestamp)?.getTime()); + schedulePushNotification( + `${parsedMessage?.data?.Response?.ServiceName} - ${parsedMessage?.data?.Incident?.IncNatureCode}`, + `${parsedMessage?.data?.Incident?.IncNature}\n${parseAddress(parsedMessage?.data)}`, + parsedMessage?.data + ); + } + } + }, [lastMessage]); + + return ( + + + + ) +} + +export default function App() { + return ( + + + + + + ); +} \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx deleted file mode 100644 index c723c1f..0000000 --- a/app/_layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useEffect } from 'react'; -import { router, Stack } from 'expo-router'; -import { WebSocketProvider } from '../contexts/WebSocketContext'; - -export const unstable_settings = { - initialRouteName: 'login', -}; - -export default function App() { - - useEffect(() => { - router.replace('/login'); - }, []); - - return ( - - - - - - ); -} \ No newline at end of file diff --git a/app/incidents.jsx b/app/incidents.jsx index c0cc707..936852b 100644 --- a/app/incidents.jsx +++ b/app/incidents.jsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import styled from 'styled-components'; -import { useCallFeed } from '../hooks/useCallFeed'; +import { useCallFeed } from '@/hooks'; import { router } from 'expo-router'; import { Platform, Linking, View, ScrollView, Text, TouchableOpacity } from 'react-native'; import { StatusBar } from 'expo-status-bar'; @@ -16,343 +16,343 @@ import ActionSheet from 'react-native-actions-sheet'; const DepartmentActionSheet = styled(ActionSheet)``; export default function Incidents() { - const actionSheetRef = useRef(null); - const callFeed = useCallFeed(); + const actionSheetRef = useRef(null); + const callFeed = useCallFeed(); - const { - departments, - callIconMap, - callDetails, - callColorSelector, - formatCallTimePast, - formatCallDateTime - } = callFeed; + const { + departments, + callIconMap, + callDetails, + callColorSelector, + formatCallTimePast, + formatCallDateTime + } = callFeed; - const sortedAndFilteredCalls = callDetails - .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) - .filter((item, index, self) => { - return index === self.findIndex(i => { - return `${i?.data?.Incident?.IncID}${i?.data?.Response?.ServiceName}` === `${item?.data?.Incident?.IncID}${item?.data?.Response?.ServiceName}` - }); - }); + const sortedAndFilteredCalls = callDetails + .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) + .filter((item, index, self) => { + return index === self.findIndex(i => { + return `${i?.data?.Incident?.IncID}${i?.data?.Response?.ServiceName}` === `${item?.data?.Incident?.IncID}${item?.data?.Response?.ServiceName}` + }); + }); - const { - departmentTypeMap, - accountDetails, - deptList, - setDeptList, - selectedDepartment, - setSelectedDepartment, - updateSelectedDepartment, - selectedDepartmentColorPicker, - } = departments; + const { + departmentTypeMap, + accountDetails, + deptList, + setDeptList, + selectedDepartment, + setSelectedDepartment, + updateSelectedDepartment, + selectedDepartmentColorPicker, + } = departments; - return ( - - - + + + { + actionSheetRef.current?.show(); + }} + > + + {selectedDepartment?.deptAbv} + + + + + + + + + {sortedAndFilteredCalls?.length ? ( + sortedAndFilteredCalls?.map((callItem, index) => { + const { data: call, timestamp } = callItem; + const { Incident, Address, Response } = call; + const { + ServiceNumber, + IncDate, + IncNature, + IncNatureCode, + IncNatureCodeDesc, + Status, + } = Incident; + const { + StreetAddress, + AddressApartment, + Town, + State, + LocationName, + } = Address; + const { + ServiceName + } = Response; + const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency; + return ( + { + router.push({ + pathname: '/call', + params: { + callDetails: btoa(JSON.stringify(call)) + } + }) + }} > - + + { - actionSheetRef.current?.show(); - }} - > - + {formatCallDateTime(IncDate)} + + {formatCallTimePast(IncDate)} + {Status === 'CLOSED' ? ( + + ) : null} + + + + + + {LocationName ? ( + + {`${LocationName}`} + + ) : ( + + + {`${StreetAddress}`} + + {AddressApartment ? ( + + {` - ${AddressApartment}`} + + ) : null } + + {` ${Town}, ${State}`} + + + )} + + {`${IncNature}`} + + + + {`${IncNatureCodeDesc}`} + + + + + + + + + + + + Service: {ServiceName} + + + {`Incident #: ${ServiceNumber}`} + + + + + + ) + })) : ( + There are no Calls + ) } + + + + + + + + {deptList?.map((item) => { + return ( + + { + actionSheetRef.current?.hide(); + return updateSelectedDepartment( + selectedDepartment?.deptId, + item?.deptId + ) }} > - {selectedDepartment?.deptAbv} - - - - - - - - - {sortedAndFilteredCalls?.length ? ( - sortedAndFilteredCalls?.map((callItem, index) => { - const { data: call, timestamp } = callItem; - const { Incident, Address, Response } = call; - const { - ServiceNumber, - IncDate, - IncNature, - IncNatureCode, - IncNatureCodeDesc, - Status, - } = Incident; - const { - StreetAddress, - AddressApartment, - Town, - State, - LocationName, - } = Address; - const { - ServiceName - } = Response; - const SelectedIcon = callIconMap[IncNature] || AccidentAndEmergency; - return ( - { - router.push({ - pathname: '/call', - params: { - callDetails: btoa(JSON.stringify(call)) - } - }) - }} - > - + - - - {formatCallDateTime(IncDate)} - - {formatCallTimePast(IncDate)} - {Status === 'CLOSED' ? ( - - ) : null} - - - - - - {LocationName ? ( - - {`${LocationName}`} - - ) : ( - - - {`${StreetAddress}`} - - {AddressApartment ? ( - - {` - ${AddressApartment}`} - - ) : null } - - {` ${Town}, ${State}`} - - - )} - - {`${IncNature}`} - - - - {`${IncNatureCodeDesc}`} - - - - - - - - - - - - Service: {ServiceName} - - - {`Incident #: ${ServiceNumber}`} - - - - - - ) - })) : ( - There are no Calls - ) } - - - - - - - - {deptList?.map((item) => { - return ( - - { - actionSheetRef.current?.hide(); - return updateSelectedDepartment( - selectedDepartment?.deptId, - item?.deptId - ) - }} - > - - - {item?.dept} - - {item?.primary ? : null} - - {`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`} - + {item?.dept} + + {item?.primary ? : null} - ); - })} - - - - ) + {`${item?.deptAbv} - ${departmentTypeMap[item?.type]}`} + + + ); + })} + + + + ) } \ No newline at end of file diff --git a/contexts/GlobalVariablesContext.js b/contexts/GlobalVariablesContext.js new file mode 100644 index 0000000..0660093 --- /dev/null +++ b/contexts/GlobalVariablesContext.js @@ -0,0 +1,35 @@ +import React, { createContext, useEffect, useState } from 'react'; +import { AppState } from 'react-native'; + +export const GlobalVariablesContext = createContext(null); + +export const GlobalVariablesProvider = ({ children }) => { + const [appState, setAppState] = useState(AppState.currentState); + + useEffect(() => { + const handleAppStateChange = (nextAppState) => { + if (appState.match(/active/) && nextAppState.match(/inactive|background/)) { + console.log('App is in background or inactive'); + } else if (appState.match(/inactive|background/) && nextAppState === 'active') { + console.log('App is in the foreground'); + } + setAppState(nextAppState); + }; + const subscription = AppState.addEventListener('change', handleAppStateChange); + + return () => { + subscription.remove(); + }; + }, [appState]); + + const exportedVariables = { + appState, + setAppState + } + + return ( + + {children} + + ) +}; \ No newline at end of file diff --git a/contexts/index.js b/contexts/index.js new file mode 100644 index 0000000..3cffedd --- /dev/null +++ b/contexts/index.js @@ -0,0 +1,2 @@ +export { GlobalVariablesProvider, GlobalVariablesContext } from './GlobalVariablesContext'; +export { WebSocketProvider, WebSocketContext } from './WebSocketContext'; \ No newline at end of file diff --git a/hooks/index.js b/hooks/index.js index efebedc..9f85db3 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -1,2 +1,5 @@ export { useCallFeed } from './useCallFeed'; -export { useDepartments } from './useDepartments'; \ No newline at end of file +export { useDepartments } from './useDepartments'; +export { useGlobalVariablesContext } from './useGlobalVariablesContext'; +export { useNotifications } from './useNotifications'; +export { useWebSocketContext } from './useWebSocketContext'; \ No newline at end of file diff --git a/hooks/useCallFeed/useCallFeed.jsx b/hooks/useCallFeed/useCallFeed.jsx index 3d50f1d..ec96fa6 100644 --- a/hooks/useCallFeed/useCallFeed.jsx +++ b/hooks/useCallFeed/useCallFeed.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useDepartments } from '../useDepartments'; import { C, Cardiology, Cpr, FourByFour } from "healthicons-react-native/dist/outline"; -import { useWebSocketContext } from '../useWebSocketContext'; +import { useNotifications, useWebSocketContext } from '@/hooks'; const callIconMap = { "CHEST PAIN|HEART PROBLEMS": Cardiology, @@ -37,119 +37,6 @@ const callIconMap = { // Bacteria - Sick // RuralClinic - Medical Facility Response -const callDetails = { - "Incident": { - "IncID": 75, - "IncNumber": 6873, - "JurisdictionNumber": 3, - "ServiceNumber": 42, - "ServiceID": 45, - "IncDate": "2024-09-25T01:01:01.55", - // "IncNature": "CHEST PAIN|HEART PROBLEMS", - "IncNature": "MOTOR VEHICLE COLLISION (MVC)", - // "IncNature": "CARDIAC ARREST|DEATH", - "IncNatureCode": "ALS", - "IncNatureCodeDesc": "ALS PRIORITY (ALS)", - "Notes": "570, 16:30> 311 Responding\n580, 16:25> Call Dispatched", - "Status": "OPEN", - "Origin": "911" - }, - "Address": { - "StreetAddress": "275 E Main St", - "AddressApartment": "IFO", - "Town": "Bridgeport", - "State": "CT", - "ZipCode": "06608", - "Latitude": 41.178435683035445, - "Longitude": -73.18194442701176, - "County": "Fairfield", - "Intersection1": "E MAIN ST", - "Intersection2": "STRATFORD AVE", - "LocationName": "Chipotle Mexican Grill", - "WeatherCondition": "Foggy" - }, - "Person": { - "Name": "John Doe", - "Age": 19, - "Gender": "Female", - "Statement": "BLOOD PRESSURE 56/41 - IN AND OUT OF CON", - "Conscious": "No", - "Breathing": "Yes", - "CallBackNumber": "(223) 456-7890" - }, - "Response": { - "IncID": 75, - "ResponseID": 75, - "ServiceID": 45, - "ServiceName": "DARIEN EMS", - "Units": [ - { - "Unit": 311, - "Department": 'Darien EMS', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:02:02.55", - "OnScene": "2024-09-25T01:10:10.55", - "Transporting": "2024-09-25T01:25:01.55", - "InService": "2024-09-25T02:00:01.55", - }, - { - "Unit": 315, - "Department": 'Darien EMS Supv', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:03:03.15", - "OnScene": "2024-09-25T01:11:11.55", - "Transporting": null, - "InService": "2024-09-25T02:10:01.55", - }, - { - "Unit": 310, - "Department": 'Darien EMS', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:01:01.55", - "OnScene": "2024-09-25T01:06:06.55", - "Transporting": "2024-09-25T01:25:01.55", - "InService": "2024-09-25T02:15:01.55", - }, - { - "Unit": 'NHT20', - "Department": 'Noroton Heights Fire Department', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:08:08.55", - "OnScene": "2024-09-25T01:12:12.55", - "Transporting": null, - "InService": "2024-09-25T01:01:01.55", - }, - { - "Unit": 'NFDE31', - "Department": 'Noroton Fire Department', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:07:07.55", - "OnScene": "2024-09-25T01:14:14.55", - "Transporting": null, - "InService": "2024-09-25T01:01:01.55", - }, - { - "Unit": 'DFDT43', - "Department": 'Darien Fire Department', - "Dispatched": "2024-09-25T01:01:01.55", - "Responding": "2024-09-25T01:10:10.55", - "OnScene": "2024-09-25T01:18:18.55", - "Transporting": null, - "InService": "2024-09-25T01:01:01.55", - }, - { - "Unit": 1514, - "Department": 'Greenwich EMS', - "Dispatched": "2024-09-25T01:15:15.55", - "Responding": "2024-09-25T01:30:30.55", - "OnScene": "2024-09-25T01:45:45.55", - "Transporting": "2024-09-25T02:05:15.55", - "InService": "2024-09-25T02:25:01.55", - }, - ] - } -} - const formatCallTimePast = (callValue) => { const initDate = new Date(callValue); const currentTime = new Date(); @@ -173,20 +60,20 @@ const formatCallTimePast = (callValue) => { } const formatCallDateTime = (callValue) => { - const initDate = new Date(callValue); - if (initDate) { - const formattedDate = `${initDate.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })}`; - const hours = initDate.getHours().toString().padStart(2, '0'); - const minutes = initDate.getMinutes().toString().padStart(2, '0'); - const formattedTime = `${hours}:${minutes}`; + const initDate = new Date(callValue); + if (initDate) { + const formattedDate = `${initDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })}`; + const hours = initDate.getHours().toString().padStart(2, '0'); + const minutes = initDate.getMinutes().toString().padStart(2, '0'); + const formattedTime = `${hours}:${minutes}`; - return `${formattedDate} - ${formattedTime}`; - } - return 'Date Unavailable'; + return `${formattedDate} - ${formattedTime}`; + } + return 'Date Unavailable'; } export const useCallFeed = () => { @@ -195,34 +82,34 @@ export const useCallFeed = () => { const [allCalls, setAllCalls] = useState([]); const { CallThemes } = departments?.accountDetails; const { - CriticalCallList, - HighCallList, - MediumCallList, - LowCallList, + CriticalCallList, + HighCallList, + MediumCallList, + LowCallList, } = CallThemes; useEffect(() => { - if (lastMessage) { - const parsedMessage = JSON?.parse(lastMessage); - if (parsedMessage?.data) { - setAllCalls([...allCalls, parsedMessage]); - } + if (lastMessage) { + const parsedMessage = JSON?.parse(lastMessage); + if (parsedMessage?.data) { + setAllCalls([...allCalls, parsedMessage]); } + } }, [lastMessage]); const callColorSelector = (callAcuity, cardiacArrestCall, status) => { - if (status === 'CLOSED') { - return '#0000CD'; - } else if (CriticalCallList.includes(cardiacArrestCall)) { - return '#8B0000'; - } else if (HighCallList.includes(callAcuity)) { - return "#FF0000"; - } else if (MediumCallList.includes(callAcuity)) { - return "#FF8C00"; - } else if (LowCallList.includes(callAcuity)) { - return "#228B22"; - } - return 'grey'; + if (status === 'CLOSED') { + return '#0000CD'; + } else if (CriticalCallList.includes(cardiacArrestCall)) { + return '#8B0000'; + } else if (HighCallList.includes(callAcuity)) { + return "#FF0000"; + } else if (MediumCallList.includes(callAcuity)) { + return "#FF8C00"; + } else if (LowCallList.includes(callAcuity)) { + return "#228B22"; + } + return 'grey'; }; return { diff --git a/hooks/useGlobalVariablesContext/index.js b/hooks/useGlobalVariablesContext/index.js new file mode 100644 index 0000000..01e5aab --- /dev/null +++ b/hooks/useGlobalVariablesContext/index.js @@ -0,0 +1 @@ +export { useGlobalVariablesContext } from './useGlobalVariablesContext'; \ No newline at end of file diff --git a/hooks/useGlobalVariablesContext/useGlobalVariablesContext.js b/hooks/useGlobalVariablesContext/useGlobalVariablesContext.js new file mode 100644 index 0000000..57389a7 --- /dev/null +++ b/hooks/useGlobalVariablesContext/useGlobalVariablesContext.js @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { GlobalVariablesContext } from '../../contexts'; + +export const useGlobalVariablesContext = () => useContext(GlobalVariablesContext); \ No newline at end of file diff --git a/hooks/useNotifications/index.js b/hooks/useNotifications/index.js new file mode 100644 index 0000000..719f729 --- /dev/null +++ b/hooks/useNotifications/index.js @@ -0,0 +1 @@ +export { useNotifications } from './useNotifications'; \ No newline at end of file diff --git a/hooks/useNotifications/useNotifications.jsx b/hooks/useNotifications/useNotifications.jsx new file mode 100644 index 0000000..80521fe --- /dev/null +++ b/hooks/useNotifications/useNotifications.jsx @@ -0,0 +1,115 @@ + +import { useState, useEffect, useRef } from 'react'; +import { Platform } from 'react-native'; +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import Constants from 'expo-constants'; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: false, + shouldSetBadge: false, + }), +}); + +const schedulePushNotification = async (title, body, dataObj) => { + await Notifications.scheduleNotificationAsync({ + content: { + title, + body, + data: { data: dataObj }, + }, + trigger: { + type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, + seconds: 2, + }, + }); +} + +const registerForPushNotificationsAsync = async () => { + let token; + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('myNotificationChannel', { + name: 'A channel is needed for the permissions prompt to appear', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } + + if (Device.isDevice) { + const { status: existingStatus } = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + if (finalStatus !== 'granted') { + alert('Failed to get push token for push notification!'); + return; + } + // Learn more about projectId: + // https://docs.expo.dev/push-notifications/push-notifications-setup/#configure-projectid + // EAS projectId is used here. + try { + const projectId = + Constants?.expoConfig?.extra?.eas?.projectId ?? Constants?.easConfig?.projectId; + if (!projectId) { + throw new Error('Project ID not found'); + } + token = ( + await Notifications.getExpoPushTokenAsync({ + projectId, + }) + ).data; + console.log(token); + } catch (e) { + token = `${e}`; + } + } else { + alert('Must use physical device for Push Notifications'); + } + + return token; +} + +export const useNotifications = () => { + const [expoPushToken, setExpoPushToken] = useState(''); + const [channels, setChannels] = useState([]); + const [notification, setNotification] = useState(undefined); + const notificationListener = useRef(); + const responseListener = useRef(); + + useEffect(() => { + registerForPushNotificationsAsync().then(token => token && setExpoPushToken(token)); + + if (Platform.OS === 'android') { + Notifications.getNotificationChannelsAsync().then(value => setChannels(value ?? [])); + } + notificationListener.current = Notifications.addNotificationReceivedListener(notification => { + setNotification(notification); + }); + + responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { + console.log(response); + }); + + return () => { + notificationListener.current && + Notifications.removeNotificationSubscription(notificationListener.current); + responseListener.current && + Notifications.removeNotificationSubscription(responseListener.current); + }; + }, []); + + return ({ + schedulePushNotification: async (title, body, dataObj) => { + await schedulePushNotification(title, body, dataObj); + }, + expoPushToken, + expoNotificationChannels: channels, + expoNotification: notification, + }); +} \ No newline at end of file diff --git a/hooks/useWebSocketContext/useWebSocketContext.js b/hooks/useWebSocketContext/useWebSocketContext.js index 375861d..c3b7bb1 100644 --- a/hooks/useWebSocketContext/useWebSocketContext.js +++ b/hooks/useWebSocketContext/useWebSocketContext.js @@ -1,4 +1,4 @@ import { useContext } from 'react'; -import { WebSocketContext } from '../../contexts/WebSocketContext'; +import { WebSocketContext } from '../../contexts'; export const useWebSocketContext = () => useContext(WebSocketContext); diff --git a/package-lock.json b/package-lock.json index 03a3962..b441616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@react-navigation/native": "^6.0.2", "expo": "^52.0.46", "expo-constants": "~17.0.8", + "expo-device": "^7.0.3", "expo-font": "~13.0.4", "expo-linking": "~7.0.5", + "expo-notifications": "^0.29.14", "expo-router": "~4.0.20", "expo-splash-screen": "~0.29.24", "expo-status-bar": "~2.0.1", @@ -2908,6 +2910,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@ide/backoff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", + "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4750,6 +4758,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/ast-types": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", @@ -4783,6 +4804,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -5009,6 +5045,12 @@ "@babel/core": "^7.0.0" } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5253,6 +5295,24 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5266,6 +5326,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -6080,6 +6156,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -6089,6 +6182,23 @@ "node": ">=8" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", @@ -6777,6 +6887,15 @@ } } }, + "node_modules/expo-application": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.0.2.tgz", + "integrity": "sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.5.tgz", @@ -6808,6 +6927,44 @@ "react-native": "*" } }, + "node_modules/expo-device": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.3.tgz", + "integrity": "sha512-uNGhDYmpDj/3GySWZmRiYSt52Phdim11p0pXfgpCq/nMks0+UPZwl3D0vin5N8/gpVe5yzb13GYuFxiVoDyniw==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/expo-file-system": { "version": "18.0.12", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz", @@ -6922,6 +7079,26 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-notifications": { + "version": "0.29.14", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.29.14.tgz", + "integrity": "sha512-AVduNx9mKOgcAqBfrXS1OHC9VAQZrDQLbVbcorMjPDGXW7m0Q5Q+BG6FYM/saVviF2eO8fhQRsTT40yYv5/bhQ==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.6.5", + "@ide/backoff": "^1.0.0", + "abort-controller": "^3.0.0", + "assert": "^2.0.0", + "badgin": "^1.1.5", + "expo-application": "~6.0.2", + "expo-constants": "~17.0.8" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-router": { "version": "4.0.20", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-4.0.20.tgz", @@ -7466,6 +7643,21 @@ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -7809,6 +8001,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -8179,6 +8383,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8191,6 +8411,18 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8258,6 +8490,24 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8270,6 +8520,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8316,6 +8582,24 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -8325,6 +8609,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -11020,6 +11319,51 @@ "node": ">=0.10.0" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -11519,6 +11863,15 @@ "node": ">=4.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -12587,6 +12940,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12851,6 +13221,23 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "license": "MIT" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -14141,6 +14528,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -14409,6 +14809,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wonka": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", diff --git a/package.json b/package.json index e2cdae2..5c95b20 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "@react-navigation/native": "^6.0.2", "expo": "^52.0.46", "expo-constants": "~17.0.8", + "expo-device": "^7.0.3", "expo-font": "~13.0.4", "expo-linking": "~7.0.5", + "expo-notifications": "^0.29.14", "expo-router": "~4.0.20", "expo-splash-screen": "~0.29.24", "expo-status-bar": "~2.0.1",