Merge pull request #3 from Fhj0607/notifications

Notifications + AsyncStorage
This commit is contained in:
Teodor Salvesen
2026-04-22 21:00:48 +02:00
committed by GitHub
20 changed files with 580 additions and 61 deletions

View File

@@ -1,12 +1,40 @@
import { supabase } from "@/lib/supabase";
import { Session } from "@supabase/supabase-js";
import { Redirect, Tabs } from "expo-router";
import * as Notifications from 'expo-notifications';
import { Redirect, router, Tabs } from "expo-router";
import { useEffect, useState } from "react";
function UseNotificationObserver() {
useEffect(() => {
function redirect(notification: Notifications.Notification) {
const aId = notification.request.content.data?.aId;
if (typeof aId === 'string') {
router.push({pathname: "/assignment/viewDetailsAssignment", params: { aId }});
}
}
const response = Notifications.getLastNotificationResponse();
if (response?.notification) {
redirect(response.notification);
}
const subscription = Notifications.addNotificationResponseReceivedListener(response => {
redirect(response.notification);
});
return () => {
subscription.remove();
};
}, []);
}
export default function TabLayout() {
const [session, SetSession] = useState<Session | null>(null)
const [loading, SetLoading] = useState(true);
UseNotificationObserver();
useEffect(() => {
const loadSession = async () => {
const { data } = await supabase.auth.getSession();
@@ -38,18 +66,6 @@ export default function TabLayout() {
<Tabs.Screen name="assignments" options={{title: "Assignments"}} />
<Tabs.Screen name="subjects" options={{title: "Subjects"}} />
<Tabs.Screen name="timer" options={{title: "Timer"}} />
<Tabs.Screen name="subject/createSubject" options={{ href: null }} />
<Tabs.Screen name="subject/editSubject" options={{ href: null }} />
<Tabs.Screen name="subject/viewDetailsSubject" options={{ href: null }} />
<Tabs.Screen name="assignment/createAssignment" options={{ href: null }} />
<Tabs.Screen name="assignment/editAssignment" options={{ href: null }} />
<Tabs.Screen name="assignment/viewDetailsAssignment" options={{ href: null }} />
<Tabs.Screen name="task/createTask" options={{ href: null }} />
<Tabs.Screen name="task/editTask" options={{ href: null }} />
<Tabs.Screen name="task/viewDetailsTask" options={{ href: null }} />
</Tabs>
);
}

View File

@@ -1,9 +1,34 @@
import { defaultStyles } from "@/constants/defaultStyles";
import { RegisterForLocalNotificationsAsync } from '@/lib/notifications';
import { supabase } from "@/lib/supabase";
import { Session } from '@supabase/supabase-js';
import { Stack } from "expo-router";
import { useEffect, useState } from 'react';
import { Button, Text, View } from "react-native";
export default function HomeScreen() {
const [session, SetSession] = useState<Session | null>(null);
useEffect(() => {
supabase.auth
.getSession()
.then(({ data }) => SetSession(data.session ?? null));
const { data: sub } = supabase.auth.onAuthStateChange(
(_event, newSession) => {
SetSession(newSession);
}
);
return () => sub.subscription.unsubscribe();
}, []);
useEffect(() => {
if (session) {
RegisterForLocalNotificationsAsync();
}
}, [session])
return (
<View style={defaultStyles.container}>
<Stack.Screen

View File

@@ -1,6 +1,15 @@
import { Stack } from "expo-router";
import '../global.css';
import "../global.css";
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="subject" options={{ headerShown: false }} />
<Stack.Screen name="assignment" options={{ headerShown: false }} />
<Stack.Screen name="task" options={{ headerShown: false }} />
<Stack.Screen name="createUser" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
</Stack>
);
}

View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function AssignmentLayout() {
return (
<Stack>
<Stack.Screen name="createAssignment" options={{ title: "Create Assignment" }} />
<Stack.Screen name="editAssignment" options={{ title: "Edit Assignment" }} />
<Stack.Screen name="viewDetailsAssignment" options={{ title: "Assignment Details" }} />
</Stack>
);
}

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import * as AsyncStorage from '@/lib/asyncStorage';
import { supabase } from '@/lib/supabase';
import * as Notifications from 'expo-notifications';
import { router, Stack, useLocalSearchParams } from 'expo-router';
import { useState } from 'react';
import {
@@ -18,37 +20,62 @@ import {
export default function CreateAssignment() {
const sId = (useLocalSearchParams().sId as string) ?? null;
const [title, SetTitle] = useState('');
const [description, SetDescription] = useState('');
const [deadline, SetDeadline] = useState('');
const [isCompleted, SetIsCompleted] = useState(false);
const [isSaving, SetIsSaving] = useState(false);
const ScheduleDeadlineReminder = async (aId: string, title: string, deadline: string) => {
const dl = new Date(deadline);
if (isNaN(dl.getTime())) return null;
const deadlineReminder = new Date(dl.getTime() - 24 * 60 * 60 * 1000);
if (deadlineReminder <= new Date()) return null;
const nId = await Notifications.scheduleNotificationAsync({
content: {
title: 'Assignment deadline coming up',
body: `${title} is due in 24 hours.`,
data: { aId },
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DATE,
date: deadlineReminder,
},
});
return nId;
}
const CreateAssignment = async () => {
if (title.trim() === '') {
Alert.alert('Title is required!');
return;
}
const { data, error: userError } = await supabase.auth.getUser();
const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !data.user) {
if (userError || !userData.user) {
router.replace('../createUser');
return;
}
SetIsSaving(true);
const { error: dbError } = await supabase.from('assignments').insert({
const { data: assignmentData, error: dbError } = await supabase.from('assignments').insert({
title: title.trim(),
description: description.trim(),
deadline: deadline.trim(),
isCompleted,
lastChanged: new Date().toISOString(),
uId: data.user.id,
uId: userData.user.id,
sId,
});
})
.select()
.single();
if (dbError) {
SetIsSaving(false);
@@ -58,6 +85,14 @@ export default function CreateAssignment() {
Alert.alert('Assignment successfully created!');
if (!isCompleted && assignmentData) {
const nId = await ScheduleDeadlineReminder(assignmentData.aId, assignmentData.title, assignmentData.deadline);
if (nId) {
await AsyncStorage.SaveAssignmentNotificationId(assignmentData.aId, nId);
}
}
SetTitle('');
SetDescription('');
SetDeadline('');

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { GetAssignmentNotificationId, RemoveAssignmentNotificationId, SaveAssignmentNotificationId } from '@/lib/asyncStorage';
import { supabase } from '@/lib/supabase';
import * as Notifications from 'expo-notifications';
import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useState } from 'react';
import { ActivityIndicator, Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native';
@@ -20,6 +22,39 @@ export default function EditAssignment() {
const [assignment, SetAssignment] = useState<Assignment | null>(null)
const [isSaving, SetIsSaving] = useState(false);
const ScheduleDeadlineReminder = async (aId: string, title: string, deadline: string) => {
const dl = new Date(deadline);
if (isNaN(dl.getTime())) return null;
const deadlineReminder = new Date(dl.getTime() - 24 * 60 * 60 * 1000);
if (deadlineReminder <= new Date()) return null;
const nId = await Notifications.scheduleNotificationAsync({
content: {
title: 'Assignment deadline coming up',
body: `${title} is due in 24 hours.`,
data: { aId },
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DATE,
date: deadlineReminder,
},
});
return nId;
}
const CancelDeadlineReminder = async (aId: string) => {
const nId = await GetAssignmentNotificationId(aId);
if (!nId) return;
await Notifications.cancelScheduledNotificationAsync(nId);
await RemoveAssignmentNotificationId(aId);
}
const GetAssignment = async (aId: string) => {
const { data, error } = await supabase.from("assignments").select("*").eq("aId", aId).single();
@@ -47,24 +82,27 @@ export default function EditAssignment() {
return;
}
const { data, error: userError } = await supabase.auth.getUser();
const { data: userData, error: userError } = await supabase.auth.getUser();
if(userError || !data.user) {
if(userError || !userData.user) {
router.replace("../createUser");
return;
}
SetIsSaving(true);
const { error: dbError } = await supabase.from("assignments").update({
const { data: assignmentData, error: dbError } = await supabase.from("assignments").update({
title: assignment.title,
description: assignment.description,
deadline: assignment.deadline,
isCompleted: assignment.isCompleted,
lastChanged: new Date().toISOString(),
uId: data.user.id,
uId: userData.user.id,
sId: assignment.sId,
}).eq("aId", aId);
})
.eq("aId", aId)
.select()
.single();
SetIsSaving(false);
@@ -75,6 +113,18 @@ export default function EditAssignment() {
Alert.alert("Assignment successfully edited!");
if (assignmentData) {
await CancelDeadlineReminder(assignmentData.aId);
if (!assignmentData.isCompleted) {
const nId = await ScheduleDeadlineReminder(assignmentData.aId, assignmentData.title, assignmentData.deadline);
if (nId) {
await SaveAssignmentNotificationId(assignmentData.aId, nId);
}
}
}
router.back();
}

11
app/subject/_layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function SubjectLayout() {
return (
<Stack>
<Stack.Screen name="createSubject" options={{ title: "Create Subject" }} />
<Stack.Screen name="editSubject" options={{ title: "Edit Subject" }} />
<Stack.Screen name="viewDetailsSubject" options={{ title: "Subject Details" }} />
</Stack>
);
}

11
app/task/_layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function TaskLayout() {
return (
<Stack>
<Stack.Screen name="createTask" options={{ title: "Create Task" }} />
<Stack.Screen name="editTask" options={{ title: "Edit Task" }} />
<Stack.Screen name="viewDetailsTask" options={{ title: "Task Details" }} />
</Stack>
);
}

15
lib/asyncStorage.ts Normal file
View File

@@ -0,0 +1,15 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
const notificationKey = (aId: string) => `assignment_notification_${aId}`;
export async function SaveAssignmentNotificationId(aId: string, notificationId: string) {
await AsyncStorage.setItem(notificationKey(aId), notificationId);
}
export async function GetAssignmentNotificationId(aId: string) {
return await AsyncStorage.getItem(notificationKey(aId));
}
export async function RemoveAssignmentNotificationId(aId: string) {
await AsyncStorage.removeItem(notificationKey(aId));
}

39
lib/notifications.ts Normal file
View File

@@ -0,0 +1,39 @@
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
function HandleRegistrationError(errorMessage: string) {
alert(errorMessage);
throw new Error(errorMessage);
}
export async function RegisterForLocalNotificationsAsync() {
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX
});
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
HandleRegistrationError('Permission not granted for local notifications');
return;
}
}

View File

@@ -0,0 +1,173 @@
## #Overview
Implemented a full local notification system for assignment deadlines using Expo Notifications, integrated with Supabase-backed assignment data.
---
## #CoreFeatures
### #LocalNotifications
* Integrated `expo-notifications` for scheduling local device notifications
* Configured notification handler for:
* banner display
* sound
* badge updates
* Notifications trigger even when:
* app is in background
* app is fully closed
---
### #DeadlineReminderLogic
* Implemented reminder scheduling based on assignment deadlines
* Default behavior:
* notify **24 hours before deadline**
* Prevented invalid scheduling:
* skip if reminder time is in the past
* validate deadline input before scheduling
---
### #AssignmentIntegration
* Notifications tied directly to assignment lifecycle:
#### On Create:
* Insert assignment into Supabase
* Retrieve inserted assignment (`aId`)
* Schedule reminder if not completed
#### On Edit:
* Cancel existing scheduled notification
* Update assignment in Supabase
* Schedule new reminder if still active
#### On Delete:
* Cancel scheduled notification
* Remove stored notification reference
---
### #NotificationPersistence
* Stored notification IDs locally using AsyncStorage
* Structure:
* `assignmentId → notificationId`
* Enables:
* precise cancellation
* avoiding duplicate notifications
---
### #NotificationCancellation
* Implemented cancellation flow using:
* `Notifications.cancelScheduledNotificationAsync(notificationId)`
* Ensures:
* no duplicate reminders on edit
* no orphan notifications after deletion
---
### #NotificationRouting
* Implemented navigation on notification tap
* Uses:
* `Notifications.addNotificationResponseReceivedListener`
* `Notifications.getLastNotificationResponse()`
#### Behavior:
* Works when:
* app is open
* app is in background
* app is launched from notification
#### Routing:
* Extract `aId` from `notification.content.data`
* Navigate using Expo Router:
```ts
router.push({
pathname: "/assignment/viewDetailsAssignment",
params: { aId }
});
```
---
### #AuthIntegration
* Notification observer only runs when user session exists
* Prevents routing into protected screens when unauthenticated
---
## #ArchitectureDecisions
### #LocalVsBackend
* Chose **local notifications only**
* No backend push notifications used
* Rationale:
* single-user reminders
* simpler implementation
* no need for push tokens or server logic
---
### #DataSeparation
* Supabase:
* stores assignment data (source of truth)
* Device (AsyncStorage):
* stores notification IDs (device-specific state)
---
### #RoutingApproach
* Used existing static route:
* `/assignment/viewDetailsAssignment`
* Passed `aId` via params instead of dynamic route `[aId].tsx`
* Keeps current structure intact
---
## #Summary
A complete local notification system has been implemented with:
* deadline-based scheduling
* lifecycle-aware updates (create/edit/delete)
* duplicate prevention
* device-level persistence
* deep-link style navigation on tap
This provides a solid, production-ready foundation for assignment reminders within the app.
Interesting sources:
https://docs.expo.dev/versions/latest/sdk/async-storage/
https://docs.expo.dev/versions/latest/sdk/securestore/
https://docs.expo.dev/versions/latest/sdk/notifications/

178
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -20,6 +21,7 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-notifications": "~0.32.16",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
@@ -2309,6 +2311,12 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"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/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -2837,6 +2845,18 @@
}
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -4423,6 +4443,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/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -4443,7 +4476,6 @@
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
@@ -4664,6 +4696,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",
@@ -4880,7 +4918,6 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
"integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -4899,7 +4936,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4913,7 +4949,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -5473,7 +5508,6 @@
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -5500,7 +5534,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
@@ -5604,7 +5637,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -5733,7 +5765,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5780,7 +5811,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -6326,6 +6356,15 @@
}
}
},
"node_modules/expo-application": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
"integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-asset": {
"version": "12.0.12",
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
@@ -6458,6 +6497,26 @@
"react-native": "*"
}
},
"node_modules/expo-notifications": {
"version": "0.32.16",
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
"integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
"license": "MIT",
"dependencies": {
"@expo/image-utils": "^0.8.8",
"@ide/backoff": "^1.0.0",
"abort-controller": "^3.0.0",
"assert": "^2.0.0",
"badgin": "^1.1.5",
"expo-application": "~7.0.8",
"expo-constants": "~18.0.13"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-router": {
"version": "6.0.23",
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz",
@@ -7221,7 +7280,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
@@ -7330,7 +7388,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7358,7 +7415,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -7401,7 +7457,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -7550,7 +7605,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7591,7 +7645,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -7620,7 +7673,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7633,7 +7685,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -7898,6 +7949,22 @@
"loose-envify": "^1.0.0"
}
},
"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-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -8014,7 +8081,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8126,7 +8192,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
@@ -8167,6 +8232,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -8206,11 +8287,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -8293,7 +8382,6 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
@@ -9246,7 +9334,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9258,6 +9345,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -9916,11 +10015,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9930,7 +10044,6 @@
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
@@ -10483,7 +10596,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11706,7 +11818,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -11856,7 +11967,6 @@
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -13155,6 +13265,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/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13548,7 +13671,6 @@
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -22,6 +23,7 @@
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-notifications": "~0.32.16",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",