From 72ff1b8098b8c441a30501242af846487db8c7c8 Mon Sep 17 00:00:00 2001 From: 95grooot Date: Sun, 1 Feb 2026 22:48:42 +1100 Subject: [PATCH 1/3] Enable direct communication between guards and employers for shift-related or general messages --- guard_app/src/api/messages.ts | 83 +++++ guard_app/src/navigation/AppNavigator.tsx | 11 +- guard_app/src/screen/MessagesScreen.tsx | 431 +++++++++++++++++++--- 3 files changed, 482 insertions(+), 43 deletions(-) create mode 100644 guard_app/src/api/messages.ts diff --git a/guard_app/src/api/messages.ts b/guard_app/src/api/messages.ts new file mode 100644 index 000000000..990fadbf4 --- /dev/null +++ b/guard_app/src/api/messages.ts @@ -0,0 +1,83 @@ +import http from '../lib/http'; + +export type MessageUser = { + _id?: string; + id?: string; + name?: string; + email?: string; + role?: 'guard' | 'employer' | 'admin'; +}; + +export type MessageDto = { + _id?: string; + sender: MessageUser; + receiver: MessageUser; + content: string; + timestamp: string; + isRead: boolean; +}; + +type InboxResponse = { + data?: { + messages?: MessageDto[]; + }; + messages?: MessageDto[]; +}; + +type SentResponse = { + data?: { + messages?: MessageDto[]; + }; + messages?: MessageDto[]; +}; + +type ConversationResponse = { + data?: { + conversation?: { + participant?: { + id?: string; + _id?: string; + name?: string; + email?: string; + role?: 'guard' | 'employer' | 'admin'; + }; + messages?: MessageDto[]; + }; + }; +}; + +type SendMessageResponse = { + data?: { + messageId?: string; + sender?: MessageUser; + receiver?: MessageUser; + content?: string; + timestamp?: string; + isRead?: boolean; + }; +}; + +export async function getInboxMessages() { + const { data } = await http.get('/messages/inbox'); + return data?.data?.messages ?? data?.messages ?? []; +} + +export async function getSentMessages() { + const { data } = await http.get('/messages/sent'); + return data?.data?.messages ?? data?.messages ?? []; +} + +export async function getConversation(userId: string) { + const { data } = await http.get(`/messages/conversation/${userId}`); + return data?.data?.conversation ?? { participant: undefined, messages: [] }; +} + +export async function sendMessage(payload: { receiverId: string; content: string }) { + const { data } = await http.post('/messages', payload); + return data?.data ?? {}; +} + +export async function markMessageAsRead(messageId: string) { + const { data } = await http.patch(`/messages/${messageId}/read`); + return data; +} diff --git a/guard_app/src/navigation/AppNavigator.tsx b/guard_app/src/navigation/AppNavigator.tsx index 47dc60c19..3279c0209 100644 --- a/guard_app/src/navigation/AppNavigator.tsx +++ b/guard_app/src/navigation/AppNavigator.tsx @@ -16,7 +16,16 @@ export type RootStackParamList = { Signup: undefined; Settings: undefined; EditProfile: undefined; - Messages: undefined; + Messages: + | { + context?: 'shift' | 'general'; + shiftParticipantId?: string; + shiftParticipantName?: string; + shiftTitle?: string; + generalParticipantId?: string; + generalParticipantName?: string; + } + | undefined; Notifications: undefined; ShiftDetails: { shift: any; refresh?: () => void }; diff --git a/guard_app/src/screen/MessagesScreen.tsx b/guard_app/src/screen/MessagesScreen.tsx index 8c6da3203..a57126bb6 100644 --- a/guard_app/src/screen/MessagesScreen.tsx +++ b/guard_app/src/screen/MessagesScreen.tsx @@ -1,6 +1,6 @@ // Put vector-icons first, then React (to satisfy your import/order rule) import { Ionicons } from '@expo/vector-icons'; -import React, { useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { SafeAreaView, View, @@ -11,58 +11,264 @@ import { StyleSheet, KeyboardAvoidingView, Platform, + ActivityIndicator, + Alert, } from 'react-native'; +import { useRoute } from '@react-navigation/native'; +import type { RouteProp } from '@react-navigation/native'; + +import { getMe } from '../api/auth'; +import { + getConversation, + getInboxMessages, + getSentMessages, + sendMessage as sendMessageApi, + type MessageDto, + type MessageUser, +} from '../api/messages'; +import type { RootStackParamList } from '../navigation/AppNavigator'; const NAVY = '#274b93'; +const SLATE = '#111827'; type Message = { id: string; from: 'guard' | 'employer'; + senderName: string; text: string; - time: string; + timestamp: string; + context: 'shift' | 'general'; + shiftTitle?: string; + status?: 'sending' | 'sent' | 'delivered' | 'read'; }; export default function MessagesScreen() { - const [messages, setMessages] = useState([ - { - id: '1', - from: 'employer', - text: 'Hi Alex, can you confirm shift for tomorrow?', - time: '10:00 AM', - }, - { - id: '2', - from: 'guard', - text: 'Yes, I’ll be there at 9 AM sharp.', - time: '10:02 AM', - }, - { - id: '3', - from: 'employer', - text: 'Great, see you then!', - time: '10:05 AM', - }, - ]); + const route = useRoute>(); + const initialContext = + route.params?.context ?? (route.params?.shiftParticipantId ? 'shift' : 'general'); + const shiftTitle = route.params?.shiftTitle ?? 'Shift conversation'; + + const [messagesByContext, setMessagesByContext] = useState<{ + shift: Message[]; + general: Message[]; + }>({ shift: [], general: [] }); const [input, setInput] = useState(''); + const [activeContext, setActiveContext] = useState<'shift' | 'general'>(initialContext); + const [isTyping, setIsTyping] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [currentUser, setCurrentUser] = useState<{ id: string; name?: string; role?: string } | null>( + null, + ); + const [shiftParticipant, setShiftParticipant] = useState<{ + id: string; + name: string; + email?: string; + role?: string; + } | null>(null); + const [generalParticipant, setGeneralParticipant] = useState<{ + id: string; + name: string; + email?: string; + role?: string; + } | null>(null); + + const contextMessages = useMemo( + () => messagesByContext[activeContext], + [messagesByContext, activeContext], + ); + const activeParticipant = activeContext === 'shift' ? shiftParticipant : generalParticipant; + + const getUserId = (user?: MessageUser) => user?._id ?? user?.id; + + const mapDtoToMessage = (dto: MessageDto, context: 'shift' | 'general'): Message => { + const senderId = getUserId(dto.sender); + const isCurrentUser = senderId && senderId === currentUser?.id; + const inferredRole = + dto.sender?.role ?? (isCurrentUser ? currentUser?.role : undefined); + const role = inferredRole === 'employer' ? 'employer' : 'guard'; + return { + id: dto._id ?? `${dto.timestamp}-${senderId ?? 'unknown'}`, + from: role, + senderName: dto.sender?.name ?? dto.sender?.email ?? 'Unknown', + text: dto.content, + timestamp: dto.timestamp, + context, + shiftTitle: context === 'shift' ? shiftTitle : undefined, + status: isCurrentUser ? (dto.isRead ? 'read' : 'sent') : undefined, + }; + }; + + const buildParticipantFromMessage = (msg: MessageDto, meId: string) => { + const senderId = getUserId(msg.sender); + const receiverId = getUserId(msg.receiver); + const isSenderMe = senderId && senderId === meId; + const otherUser = isSenderMe ? msg.receiver : msg.sender; + const otherId = getUserId(otherUser); + if (!otherId) return null; + return { + id: otherId, + name: otherUser?.name ?? otherUser?.email ?? 'Participant', + email: otherUser?.email, + role: otherUser?.role, + }; + }; + + useEffect(() => { + const loadParticipants = async () => { + try { + setLoading(true); + setError(null); + + const me = await getMe(); + const meId = me?._id ?? me?.id; + if (!meId) throw new Error('Unable to determine user'); + setCurrentUser({ id: meId, name: me?.name, role: me?.role }); + + if (route.params?.shiftParticipantId) { + setShiftParticipant({ + id: route.params.shiftParticipantId, + name: route.params.shiftParticipantName ?? 'Shift participant', + }); + } + + if (route.params?.generalParticipantId) { + setGeneralParticipant({ + id: route.params.generalParticipantId, + name: route.params.generalParticipantName ?? 'Conversation', + }); + return; + } + + const [inbox, sent] = await Promise.all([getInboxMessages(), getSentMessages()]); + const combined = [...inbox, ...sent].sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ); + const first = combined[0]; + const participant = first ? buildParticipantFromMessage(first, meId) : null; + if (participant) { + setGeneralParticipant(participant); + } + } catch (e) { + console.error(e); + setError('Failed to load messages'); + } finally { + setLoading(false); + } + }; + + void loadParticipants(); + }, [route.params?.generalParticipantId, route.params?.generalParticipantName, route.params?.shiftParticipantId, route.params?.shiftParticipantName]); + + useEffect(() => { + const loadConversation = async () => { + const participant = activeContext === 'shift' ? shiftParticipant : generalParticipant; + if (!participant?.id) { + setMessagesByContext((prev) => ({ ...prev, [activeContext]: [] })); + return; + } + + try { + setLoading(true); + setError(null); + const conversation = await getConversation(participant.id); + if (conversation?.participant) { + const { id, _id, name, email, role } = conversation.participant; + const normalizedParticipant = { + id: id ?? _id ?? participant.id, + name: name ?? email ?? participant.name, + email, + role, + }; + if (activeContext === 'shift') { + setShiftParticipant(normalizedParticipant); + } else { + setGeneralParticipant(normalizedParticipant); + } + } + const mapped = (conversation?.messages ?? []).map((msg) => mapDtoToMessage(msg, activeContext)); + setMessagesByContext((prev) => ({ ...prev, [activeContext]: mapped })); + } catch (e) { + console.error(e); + setError('Failed to load conversation'); + } finally { + setLoading(false); + } + }; - const sendMessage = () => { + void loadConversation(); + }, [activeContext, generalParticipant?.id, shiftParticipant?.id, currentUser?.id]); + + const formatTime = (iso: string) => + new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + const sendMessage = async () => { if (!input.trim()) return; + const participant = activeContext === 'shift' ? shiftParticipant : generalParticipant; + if (!participant?.id) { + Alert.alert('No recipient', 'Select a conversation before sending.'); + return; + } + + const newId = Date.now().toString(); const newMsg: Message = { - id: Date.now().toString(), - from: 'guard', + id: newId, + from: currentUser?.role === 'employer' ? 'employer' : 'guard', + senderName: currentUser?.name ?? 'You', text: input.trim(), - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + timestamp: new Date().toISOString(), + context: activeContext, + shiftTitle: activeContext === 'shift' ? shiftTitle : undefined, + status: 'sending', }; - setMessages((prev) => [...prev, newMsg]); + setMessagesByContext((prev) => ({ ...prev, [activeContext]: [...prev[activeContext], newMsg] })); setInput(''); + + try { + const sent = await sendMessageApi({ receiverId: participant.id, content: newMsg.text }); + setMessagesByContext((prev) => ({ + ...prev, + [activeContext]: prev[activeContext].map((msg) => + msg.id === newId + ? { + ...msg, + id: sent.messageId ?? msg.id, + timestamp: sent.timestamp ?? msg.timestamp, + status: sent.isRead ? 'read' : 'sent', + } + : msg, + ), + })); + } catch (e) { + console.error(e); + setMessagesByContext((prev) => ({ + ...prev, + [activeContext]: prev[activeContext].filter((msg) => msg.id !== newId), + })); + Alert.alert('Error', 'Failed to send message'); + } }; const renderMessage = ({ item }: { item: Message }) => ( - - {item.text} - {item.time} + + + + {item.senderName} • {item.from === 'guard' ? 'Guard' : 'Employer'} + + + {item.text} + + + + {formatTime(item.timestamp)} + + {item.from === 'guard' && item.status && ( + • {item.status} + )} + + ); @@ -74,25 +280,112 @@ export default function MessagesScreen() { keyboardVerticalOffset={90} > - - Messages + + + Messages + + + setActiveContext('shift')} + > + + Shift + + + setActiveContext('general')} + > + + General + + + + + + {activeContext === 'shift' ? shiftTitle : 'General conversation'} + + + {activeParticipant?.name ? `With ${activeParticipant.name}` : 'No participant selected'} + + + + {error && {error}} + item.id} renderItem={renderMessage} - contentContainerStyle={styles.chat} + contentContainerStyle={[ + styles.chat, + contextMessages.length === 0 && styles.chatEmpty, + ]} + ListEmptyComponent={ + loading ? ( + + + Loading messages… + + ) : ( + + + No messages yet + + Start the conversation to coordinate shifts or share updates. + + + ) + } /> + {isTyping && ( + + + Employer is typing… + + setIsTyping(false)} + > + Dismiss + + + )} + - + @@ -111,20 +404,51 @@ const styles = StyleSheet.create({ backgroundColor: NAVY, paddingVertical: 14, paddingHorizontal: 16, + justifyContent: 'space-between', }, + headerLeft: { flexDirection: 'row', alignItems: 'center' }, headerTitle: { color: '#fff', fontSize: 18, fontWeight: '700', marginLeft: 8, }, + contextToggle: { + flexDirection: 'row', + backgroundColor: 'rgba(255,255,255,0.18)', + borderRadius: 16, + padding: 2, + }, + contextChip: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 14, + }, + contextChipActive: { backgroundColor: '#ffffff' }, + contextChipText: { fontSize: 12, color: '#e5e7eb', fontWeight: '600' }, + contextChipTextActive: { color: NAVY }, + + contextBanner: { + backgroundColor: '#f1f5f9', + paddingHorizontal: 16, + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: '#e5e7eb', + }, + contextBannerText: { fontSize: 14, fontWeight: '700', color: SLATE }, + contextBannerSub: { fontSize: 12, color: '#6b7280', marginTop: 2 }, + errorText: { color: '#b91c1c', paddingHorizontal: 16, paddingTop: 8 }, chat: { padding: 12 }, + chatEmpty: { flexGrow: 1, justifyContent: 'center' }, + placeholder: { alignItems: 'center', paddingHorizontal: 24 }, + placeholderTitle: { marginTop: 8, fontSize: 16, fontWeight: '700', color: SLATE }, + placeholderText: { marginTop: 4, textAlign: 'center', color: '#6b7280' }, + messageRow: { marginBottom: 10 }, bubble: { maxWidth: '75%', padding: 12, - marginBottom: 10, borderRadius: 16, }, bubbleGuard: { @@ -137,8 +461,29 @@ const styles = StyleSheet.create({ backgroundColor: '#e5e7eb', borderTopLeftRadius: 4, }, - msgText: { color: '#fff', fontSize: 15 }, - msgTime: { fontSize: 10, color: '#d1d5db', marginTop: 4, textAlign: 'right' }, + msgSender: { fontSize: 11, color: '#6b7280', marginBottom: 4, fontWeight: '600' }, + msgTextLight: { color: '#fff', fontSize: 15 }, + msgTextDark: { color: SLATE, fontSize: 15 }, + metaRow: { flexDirection: 'row', alignItems: 'center', marginTop: 4 }, + msgTimeLight: { fontSize: 10, color: '#d1d5db' }, + msgTimeDark: { fontSize: 10, color: '#6b7280' }, + msgStatus: { marginLeft: 4, fontSize: 10, color: '#c7d2fe' }, + + typingRow: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingBottom: 8, + }, + typingBubble: { + backgroundColor: '#e5e7eb', + borderRadius: 14, + paddingHorizontal: 12, + paddingVertical: 8, + }, + typingText: { color: SLATE, fontSize: 12, fontWeight: '600' }, + typingToggle: { marginLeft: 8 }, + typingToggleText: { color: NAVY, fontSize: 12, fontWeight: '600' }, inputBar: { flexDirection: 'row', @@ -153,6 +498,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 14, backgroundColor: '#f3f4f6', }, + inputDisabled: { opacity: 0.6 }, sendBtn: { marginLeft: 8, backgroundColor: NAVY, @@ -161,4 +507,5 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, + sendBtnDisabled: { opacity: 0.5 }, }); From 6d0de69a6934ba9e48848ad43799a67aecc16164 Mon Sep 17 00:00:00 2001 From: 95grooot Date: Mon, 2 Feb 2026 01:16:16 +1100 Subject: [PATCH 2/3] =?UTF-8?q?Register=20each=20user=E2=80=99s=20device?= =?UTF-8?q?=20with=20a=20unique=20push=20token=20so=20the=20backend=20can?= =?UTF-8?q?=20send=20notifications.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/vcs.xml | 1 + .idea/workspace.xml | 75 ++++- .../src/controllers/user.controller.js | 43 +++ app-backend/src/models/User.js | 9 + app-backend/src/routes/user.routes.js | 36 +- guard_app/App.tsx | 18 +- guard_app/app.json | 13 +- guard_app/package-lock.json | 315 ++++++++++++++++-- guard_app/package.json | 3 + guard_app/src/api/pushTokens.ts | 10 + guard_app/src/lib/localStorage.ts | 10 + guard_app/src/lib/pushNotifications.ts | 58 ++++ guard_app/src/screen/ShiftDetailsScreen.tsx | 32 +- guard_app/src/screen/loginscreen.tsx | 3 + 14 files changed, 571 insertions(+), 55 deletions(-) create mode 100644 guard_app/src/api/pushTokens.ts create mode 100644 guard_app/src/lib/pushNotifications.ts diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddfb..0b03f47d6 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 41337b1e1..49db28c32 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,17 +5,70 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + - { "lastFilter": { diff --git a/app-backend/src/controllers/user.controller.js b/app-backend/src/controllers/user.controller.js index e737c2edd..bc2847dfd 100644 --- a/app-backend/src/controllers/user.controller.js +++ b/app-backend/src/controllers/user.controller.js @@ -162,3 +162,46 @@ export const updateEmployerProfile = async (req, res) => { res.status(500).json({ message: err.message }); } }; + +/** + * @desc Register or update a push token for the logged-in user + * @route POST /api/v1/users/push-token + * @access Private (all roles) + */ +export const registerPushToken = async (req, res) => { + try { + const { token, platform, deviceId } = req.body; + + if (!token || typeof token !== 'string') { + return res.status(400).json({ message: 'Push token is required.' }); + } + + const user = await User.findById(req.user.id); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const existing = user.pushTokens?.find((item) => item.token === token); + if (existing) { + existing.platform = platform ?? existing.platform; + existing.deviceId = deviceId ?? existing.deviceId; + existing.updatedAt = new Date(); + } else { + user.pushTokens = [ + ...(user.pushTokens ?? []), + { + token, + platform, + deviceId, + updatedAt: new Date(), + }, + ]; + } + + await user.save(); + + return res.status(200).json({ message: 'Push token registered.' }); + } catch (err) { + return res.status(500).json({ message: err.message }); + } +}; diff --git a/app-backend/src/models/User.js b/app-backend/src/models/User.js index 86a7bdde9..b5d57f6fd 100644 --- a/app-backend/src/models/User.js +++ b/app-backend/src/models/User.js @@ -90,6 +90,15 @@ const userSchema = new mongoose.Schema( default: null, }, + pushTokens: [ + { + token: { type: String, required: true }, + platform: { type: String }, + deviceId: { type: String }, + updatedAt: { type: Date, default: Date.now }, + }, + ], + // soft delete fields isDeleted: { type: Boolean, default: false, index: true }, // marks user as deactivated deletedAt: { type: Date, default: null }, // when it was deactivated diff --git a/app-backend/src/routes/user.routes.js b/app-backend/src/routes/user.routes.js index b55aaf8bf..539a7d0dc 100644 --- a/app-backend/src/routes/user.routes.js +++ b/app-backend/src/routes/user.routes.js @@ -17,7 +17,8 @@ import { adminUpdateUserProfile, getAllGuards, listUsers, - deleteUser + deleteUser, + registerPushToken } from '../controllers/user.controller.js'; const router = express.Router(); @@ -80,6 +81,39 @@ router .get(auth, loadUser, getMyProfile) .put(auth, loadUser, updateMyProfile); +/** + * @swagger + * /api/v1/users/push-token: + * post: + * summary: Register a push notification token + * tags: [Users] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - token + * properties: + * token: + * type: string + * platform: + * type: string + * deviceId: + * type: string + * responses: + * 200: + * description: Token registered + * 400: + * description: Validation error + * 401: + * description: Unauthorized + */ +router.post('/push-token', auth, loadUser, registerPushToken); + /** * @swagger * /api/v1/users/profile: diff --git a/guard_app/App.tsx b/guard_app/App.tsx index 4ee48559d..6e8510428 100644 --- a/guard_app/App.tsx +++ b/guard_app/App.tsx @@ -1,10 +1,26 @@ // App.tsx import { NavigationContainer } from '@react-navigation/native'; -import React from 'react'; +import React, { useEffect } from 'react'; import AppNavigator from './src/navigation/AppNavigator'; +import { registerPushTokenIfNeeded, subscribeToPushTokenChanges } from './src/lib/pushNotifications'; export default function App() { + useEffect(() => { + let subscription: { remove: () => void } | null = null; + const register = async () => { + await registerPushTokenIfNeeded(); + subscription = subscribeToPushTokenChanges(async (newToken) => { + await registerPushTokenIfNeeded(newToken); + }); + }; + + void register(); + return () => { + subscription?.remove(); + }; + }, []); + return ( diff --git a/guard_app/app.json b/guard_app/app.json index 5a74f891a..d83e6d9bd 100644 --- a/guard_app/app.json +++ b/guard_app/app.json @@ -1,7 +1,7 @@ { "expo": { "name": "SecureShift-GuardApp", - "slug": "SecureShift-GuardApp", + "slug": "secureshift-guardapp", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -20,10 +20,17 @@ "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" }, - "edgeToEdgeEnabled": true + "edgeToEdgeEnabled": true, + "package": "com.secureshiftguardapp.secureshiftguardapp" }, "web": { "favicon": "./assets/favicon.png" - } + }, + "extra": { + "eas": { + "projectId": "59453bc8-475d-40dd-9f7f-f275fd6d1eea" + } + }, + "owner": "secureshift-guardapp" } } diff --git a/guard_app/package-lock.json b/guard_app/package-lock.json index 91ecc6e20..ff2819479 100644 --- a/guard_app/package-lock.json +++ b/guard_app/package-lock.json @@ -19,9 +19,12 @@ "date-fns": "^4.1.0", "expo": "~53.0.25", "expo-constants": "~17.1.8", + "expo-dev-client": "~5.2.4", + "expo-device": "~7.1.4", "expo-document-picker": "~13.1.6", "expo-image-picker": "~16.1.4", "expo-location": "~18.1.6", + "expo-notifications": "~0.31.5", "expo-status-bar": "~2.2.3", "react": "19.0.0", "react-dom": "19.0.0", @@ -107,6 +110,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2025,6 +2029,7 @@ "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-5.0.5.tgz", "integrity": "sha512-P8UFTi+YsmiD1BmdTdiIQITzDMcZgronsA3RTQ4QKJjHM3bas11oGzLQOnFaIZnlEV8Rrr3m1m+RHxvnpL+t/A==", "license": "MIT", + "peer": true, "peerDependencies": { "react-native": "*" } @@ -2239,6 +2244,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "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", @@ -3033,6 +3044,7 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.17.tgz", "integrity": "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==", "license": "MIT", + "peer": true, "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", @@ -3209,6 +3221,7 @@ "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3279,6 +3292,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -3828,6 +3842,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4126,6 +4141,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", @@ -4152,7 +4180,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" @@ -4369,6 +4396,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", @@ -4437,8 +4470,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/bplist-creator": { "version": "0.1.0", @@ -4501,6 +4533,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -4566,7 +4599,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4598,7 +4630,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", @@ -5131,7 +5162,6 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -5148,7 +5178,6 @@ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "license": "MIT", - "peer": true, "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -5162,7 +5191,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5172,7 +5200,6 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">= 6" }, @@ -5318,7 +5345,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", @@ -5345,7 +5371,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", @@ -5417,7 +5442,6 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -5437,15 +5461,13 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -5461,7 +5483,6 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -5550,7 +5571,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -5805,6 +5825,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5861,6 +5882,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5959,6 +5981,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6397,6 +6420,7 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.25.tgz", "integrity": "sha512-KMaIMAd0vKl2ooiDB9XMKTuRAhSmrLdPOEON8Ck9mPmSrJB1FHBs6gb63d5IJTQAE1jBZLZj0JBg6rqRBOrTjg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.23", @@ -6440,6 +6464,15 @@ } } }, + "node_modules/expo-application": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.1.5.tgz", + "integrity": "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz", @@ -6469,6 +6502,118 @@ "react-native": "*" } }, + "node_modules/expo-dev-client": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.2.4.tgz", + "integrity": "sha512-s/N/nK5LPo0QZJpV4aPijxyrzV4O49S3dN8D2fljqrX2WwFZzWwFO6dX1elPbTmddxumdcpczsdUPY+Ms8g43g==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "5.1.16", + "expo-dev-menu": "6.1.14", + "expo-dev-menu-interface": "1.10.0", + "expo-manifests": "~0.16.6", + "expo-updates-interface": "~1.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.16.tgz", + "integrity": "sha512-tbCske9pvbozaEblyxoyo/97D6od9Ma4yAuyUnXtRET1CKAPKYS+c4fiZ+I3B4qtpZwN3JNFUjG3oateN0y6Hg==", + "license": "MIT", + "dependencies": { + "ajv": "8.11.0", + "expo-dev-menu": "6.1.14", + "expo-manifests": "~0.16.6", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/expo-dev-launcher/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/expo-dev-menu": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.14.tgz", + "integrity": "sha512-yonNMg2GHJZtuisVowdl1iQjZfYP85r1D1IO+ar9D9zlrBPBJhq2XEju52jd1rDmDkmDuEhBSbPNhzIcsBNiPg==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "1.10.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz", + "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.1.4.tgz", + "integrity": "sha512-HS04IiE1Fy0FRjBLurr9e5A6yj3kbmQB+2jCZvbSGpsjBnCLdSk/LCii4f5VFhPIBWJLyYuN5QqJyEAw6BcS4Q==", + "license": "MIT", + "dependencies": { + "ua-parser-js": "^0.7.33" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-device/node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "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-document-picker": { "version": "13.1.6", "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.1.6.tgz", @@ -6493,6 +6638,7 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz", "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==", "license": "MIT", + "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -6522,6 +6668,12 @@ "expo": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, "node_modules/expo-keep-awake": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", @@ -6541,6 +6693,19 @@ "expo": "*" } }, + "node_modules/expo-manifests": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", + "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "license": "MIT", + "dependencies": { + "@expo/config": "~11.0.12", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "2.1.14", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz", @@ -6568,6 +6733,26 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-notifications": { + "version": "0.31.5", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.31.5.tgz", + "integrity": "sha512-HsitfTrSESFDWwaX0Y+6GQlWEooQqZKdGbNTwTPHfp5PNCr02tVPwwya9j1tdg3Awj8/vmfXmSxzNhULfmgJhQ==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.7.6", + "@ide/backoff": "^1.0.0", + "abort-controller": "^3.0.0", + "assert": "^2.0.0", + "badgin": "^1.1.5", + "expo-application": "~6.1.5", + "expo-constants": "~17.1.8" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-status-bar": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.2.3.tgz", @@ -6582,6 +6767,15 @@ "react-native": "*" } }, + "node_modules/expo-updates-interface": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", + "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -6822,7 +7016,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" @@ -7204,7 +7397,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" @@ -7506,6 +7698,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", @@ -7610,7 +7818,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" @@ -7732,7 +7939,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -7773,6 +7979,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", @@ -7835,7 +8057,6 @@ "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", @@ -7931,7 +8152,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" @@ -9074,8 +9294,7 @@ "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/memoize-one": { "version": "5.2.1", @@ -9761,7 +9980,6 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "boolbase": "^1.0.0" }, @@ -9809,11 +10027,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" @@ -9823,7 +10056,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", @@ -10392,7 +10624,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" @@ -10448,6 +10679,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10675,6 +10907,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10715,6 +10948,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -10745,6 +10979,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.6.tgz", "integrity": "sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.79.6", @@ -10824,6 +11059,7 @@ "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.7.1.tgz", "integrity": "sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -10846,6 +11082,7 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz", "integrity": "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -10856,6 +11093,7 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.11.1.tgz", "integrity": "sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw==", "license": "MIT", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.1.7", @@ -11437,7 +11675,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", @@ -11578,7 +11815,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", @@ -12753,6 +12989,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12951,7 +13188,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12975,6 +13211,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", @@ -13172,7 +13421,6 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -13500,6 +13748,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/guard_app/package.json b/guard_app/package.json index 256a8ece0..34b6fb6ae 100644 --- a/guard_app/package.json +++ b/guard_app/package.json @@ -26,9 +26,12 @@ "date-fns": "^4.1.0", "expo": "~53.0.25", "expo-constants": "~17.1.8", + "expo-dev-client": "~5.2.4", + "expo-device": "~7.1.4", "expo-document-picker": "~13.1.6", "expo-image-picker": "~16.1.4", "expo-location": "~18.1.6", + "expo-notifications": "~0.31.5", "expo-status-bar": "~2.2.3", "react": "19.0.0", "react-dom": "19.0.0", diff --git a/guard_app/src/api/pushTokens.ts b/guard_app/src/api/pushTokens.ts new file mode 100644 index 000000000..04e967c74 --- /dev/null +++ b/guard_app/src/api/pushTokens.ts @@ -0,0 +1,10 @@ +import http from '../lib/http'; + +export async function registerPushToken(payload: { + token: string; + platform: string; + deviceId?: string; +}) { + const { data } = await http.post('/users/push-token', payload); + return data; +} diff --git a/guard_app/src/lib/localStorage.ts b/guard_app/src/lib/localStorage.ts index 994b06e76..1ababec8a 100644 --- a/guard_app/src/lib/localStorage.ts +++ b/guard_app/src/lib/localStorage.ts @@ -2,6 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; const TOKEN_KEY = 'auth_token'; const PROFILE_IMAGE_KEY = 'profile_image'; +const PUSH_TOKEN_KEY = 'push_token'; export const LocalStorage = { setToken: async function (token: string): Promise { @@ -22,6 +23,15 @@ export const LocalStorage = { clearProfileImage: async function (): Promise { await AsyncStorage.removeItem(PROFILE_IMAGE_KEY); }, + setPushToken: async function (token: string): Promise { + await AsyncStorage.setItem(PUSH_TOKEN_KEY, token); + }, + getPushToken: async function (): Promise { + return AsyncStorage.getItem(PUSH_TOKEN_KEY); + }, + removePushToken: async function (): Promise { + await AsyncStorage.removeItem(PUSH_TOKEN_KEY); + }, clearAll: async function (): Promise { await AsyncStorage.clear(); }, diff --git a/guard_app/src/lib/pushNotifications.ts b/guard_app/src/lib/pushNotifications.ts new file mode 100644 index 000000000..75279cac6 --- /dev/null +++ b/guard_app/src/lib/pushNotifications.ts @@ -0,0 +1,58 @@ +import * as Notifications from 'expo-notifications'; +import Constants from 'expo-constants'; +import { Platform } from 'react-native'; + +import { registerPushToken } from '../api/pushTokens'; +import { LocalStorage } from './localStorage'; + +const getProjectId = () => + Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId; + +export async function registerPushTokenIfNeeded(tokenOverride?: string): Promise { + const authToken = await LocalStorage.getToken(); + if (!authToken) return; + + const storedToken = await LocalStorage.getPushToken(); + const newToken = tokenOverride ?? (await getExpoPushToken()); + if (!newToken) return; + + if (storedToken === newToken) return; + + await registerPushToken({ + token: newToken, + platform: Platform.OS, + }); + await LocalStorage.setPushToken(newToken); +} + +export async function getExpoPushToken(): Promise { + try { + const permission = await Notifications.getPermissionsAsync(); + let status = permission.status; + + if (status !== 'granted') { + const request = await Notifications.requestPermissionsAsync(); + status = request.status; + } + + if (status !== 'granted') { + return null; + } + + const token = await Notifications.getExpoPushTokenAsync({ + projectId: getProjectId(), + }); + return token.data; + } catch (error) { + console.warn('Failed to get push token', error); + return null; + } +} + +export function subscribeToPushTokenChanges(onToken: (token: string) => void) { + return Notifications.addPushTokenListener((event) => { + if (event?.data) { + onToken(event.data); + } + }); +} diff --git a/guard_app/src/screen/ShiftDetailsScreen.tsx b/guard_app/src/screen/ShiftDetailsScreen.tsx index 9d40da16e..61231c3cf 100644 --- a/guard_app/src/screen/ShiftDetailsScreen.tsx +++ b/guard_app/src/screen/ShiftDetailsScreen.tsx @@ -1,23 +1,21 @@ // src/screen/ShiftDetailsScreen.tsx import { Ionicons } from '@expo/vector-icons'; -import { RouteProp, useRoute } from '@react-navigation/native'; +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import React, { useEffect, useState } from 'react'; import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { checkIn, checkOut } from '../api/attendance'; import LocationVerificationModal from '../components/LocationVerificationModal'; import { getAttendanceForShift, setAttendanceForShift } from '../lib/attendancestore'; +import type { RootStackParamList } from '../navigation/AppNavigator'; import { COLORS } from '../theme/colors'; import { formatDate } from '../utils/date'; import type { ShiftDto } from '../api/shifts'; -// ✅ Keep this local if you don't have a shared RootStackParamList updated -type RootStackParamList = { - ShiftDetails: { shift: ShiftDto; refresh?: () => void }; -}; - type ScreenRouteProp = RouteProp; +type Nav = NativeStackNavigationProp; type AttendanceState = { checkInTime?: string; @@ -26,6 +24,7 @@ type AttendanceState = { export default function ShiftDetailsScreen() { const route = useRoute(); + const navigation = useNavigation