diff --git a/backend/app.py b/backend/app.py index 287c29e5..2c1062a4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -256,47 +256,52 @@ def get_recent_activity(): {"$unwind": {"path": "$log_info", "preserveNullAndEmptyArrays": True}}, {"$sort": {"timestamp": pymongo.DESCENDING}} ] - + recent_activity = list(detection.aggregate(pipeline)) formatted_activity = [] for activity in recent_activity: # Get log info if it exists log_info = activity.get("log_info", {}) - + # Determine if it's phishing based on either detection details or log verdict is_phishing = activity.get("details") == "Phishing" or log_info.get("verdict") == "Phishing" - + # Format date for display timestamp = activity.get("timestamp") formatted_time = time_ago(timestamp) if timestamp else "Unknown" - + # Get probability and ensure it's a valid float probability = 0.0 if log_info.get("probability") is not None: probability = float(log_info.get("probability")) elif activity.get("ensemble_score") is not None: probability = float(activity.get("ensemble_score")) - + # Print debug info - print(f"Debug - ID: {activity.get('_id')}, Probability: {probability}, Type: {type(probability)}") - + # print(f"Debug - ID: {activity.get('_id')}, Probability: {probability}, Type: {type(probability)}") # Keep if needed + formatted_activity.append({ - "id": str(activity["_id"]), + # "id": str(activity["_id"]), # This is Detection _id, maybe rename or keep separate? "detect_id": activity.get("detect_id", "N/A"), + "log_id": str(log_info.get("_id")) if log_info else None, # ✅ Add the Log's _id here "title": "Phishing Detected" if is_phishing else "Safe Link Verified", "link": f"{activity.get('url', 'Unknown URL')} - {activity.get('metadata', {}).get('source', 'Scan')}", "time": formatted_time, "icon": "suspicious-icon" if is_phishing else "safe-icon", - "severity": activity.get("severity", "Medium"), - "probability": probability, # Now guaranteed to be a float + "severity": activity.get("severity", "Medium"), # Use severity from Detection if available + "probability": probability, "platform": log_info.get("platform", "Web"), - "recommended_action": "Block URL" if is_phishing else "Allow URL", + "recommended_action": log_info.get("recommended_action", "N/A") if log_info else ("Block URL" if is_phishing else "Allow URL"), # Use log action if available # Additional fields needed for modal "url": activity.get("url", "Unknown URL"), "date_scanned": timestamp }) + # Filter out entries where log_id is None if you only want entries that have a corresponding log + formatted_activity = [item for item in formatted_activity if item["log_id"] is not None] + + return jsonify({"recent_activity": formatted_activity}) except Exception as e: @@ -306,7 +311,6 @@ def get_recent_activity(): - # ✅ **GET - Fetch Severity Counts** @app.route("/severity-counts", methods=["GET"]) def get_severity_counts(): diff --git a/frontend/app/Navigation.js b/frontend/app/Navigation.js index af0fb89d..1f4a2a01 100644 --- a/frontend/app/Navigation.js +++ b/frontend/app/Navigation.js @@ -2,22 +2,21 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { - View, - Text, - TouchableOpacity, - Switch, - StyleSheet, - Image, - AppState +View, +Text, +TouchableOpacity, +Switch, +StyleSheet, +Image, +AppState } from "react-native"; // **** Import useRef **** -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef } from "react"; import GradientScreen from "./screens/components/GradientScreen"; import TopBar from "./screens/components/TopBar"; import NotificationToast from "./screens/components/NotificationToast"; import { requestNotificationPermissions, setupNotificationListeners, scheduleNotification } from "./services/NotificationService"; import config from "./config"; - //Screens import Home from "./screens/tabScreens/Home"; import Analytics from "./screens/tabScreens/Analytics"; @@ -30,393 +29,375 @@ import ForgotPassword from "./screens/ForgotPassword"; import Reports from "./screens/reportsPage/Reports"; import CreateReport from "./screens/reportsPage/CreateReport"; import EditReport from "./screens/reportsPage/EditReport"; - - //Icons import { Entypo } from "@expo/vector-icons"; import Octicons from "@expo/vector-icons/Octicons"; import FontAwesome6 from "@expo/vector-icons/FontAwesome6"; import Ionicons from "@expo/vector-icons/Ionicons"; - - const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); - // Create a global navigation variable to store the navigation prop let globalNavigation = null; - //Bottom Tab Bar Component // TabGroup component function TabGroup({ navigation, hasUnreadNotifications, onNotificationRead }) { - // Store the navigation prop globally when this component mounts - useEffect(() => { - globalNavigation = navigation; - }, [navigation]); - - const [isDarkMode, setDarkMode] = useState(false); - - const handleToggleDarkMode = () => { - setDarkMode((prevMode) => !prevMode); - }; - - return ( - - ({ - tabBarIcon: ({ color }) => { - let iconName; - let IconLibrary; - - if (route.name === "Home") { - iconName = "home"; - IconLibrary = Entypo; - } else if (route.name === "Analytics") { - iconName = "graph"; - IconLibrary = Octicons; - } else if (route.name === "Logs") { - iconName = "list"; - IconLibrary = FontAwesome6; - } else if (route.name === "Settings") { - iconName = "settings-sharp"; - IconLibrary = Ionicons; - } - - return ( - - - - ); - }, - tabBarLabel: ({ focused }) => - focused ? ( - - {route.name} - - ) : null, - - tabBarActiveTintColor: isDarkMode ? "#00A757" : "#3AED97", - tabBarInactiveTintColor: isDarkMode ? "#AAAAAA" : "#218555", - tabBarStyle: { - backgroundColor: isDarkMode ? "#FFFFFF" : "#000000", - height: 56, - borderTopWidth: 0, - }, - })} - > - - {() => ( - { +globalNavigation = navigation; +}, [navigation]); +const [isDarkMode, setDarkMode] = useState(false); +const handleToggleDarkMode = () => { +setDarkMode((prevMode) => !prevMode); +}; +return ( + + ({ +tabBarIcon: ({ color }) => { +let iconName; +let IconLibrary; +if (route.name === "Home") { + iconName = "home"; + IconLibrary = Entypo; + } else if (route.name === "Analytics") { + iconName = "graph"; + IconLibrary = Octicons; + } else if (route.name === "Logs") { + iconName = "list"; + IconLibrary = FontAwesome6; + } else if (route.name === "Settings") { + iconName = "settings-sharp"; + IconLibrary = Ionicons; + } + + return ( + + + + ); + }, + tabBarLabel: ({ focused }) => + focused ? ( + + {route.name} + + ) : null, + + tabBarActiveTintColor: isDarkMode ? "#00A757" : "#3AED97", + tabBarInactiveTintColor: isDarkMode ? "#AAAAAA" : "#218555", + tabBarStyle: { + backgroundColor: isDarkMode ? "#FFFFFF" : "#000000", + height: 56, + borderTopWidth: 0, + }, + })} + > + + {() => ( + - } - > - - - )} - - - - {() => ( - + } + > + + + )} + + + + {() => ( + - } - > - - - )} - - - - {() => ( - + } + > + + + )} + + + + {() => ( + - } - > - - - )} - - - - {() => ( - + } + > + + + )} + + + + {() => ( + - } - > - - - )} - - - - ); -} + hasUnreadNotifications={hasUnreadNotifications} + onNotificationRead={onNotificationRead} + /> + } + > + + + )} + + + +); +} // Main Stack Navigator function MainStack({ hasUnreadNotifications, onNotificationRead }) { - return ( - - - - - - {props => ( - - )} - - - - - - - ); +return ( + + + + + +{props => ( + +)} + + + + + + +); } - export default function Navigation() { - const [inAppNotification, setInAppNotification] = useState(null); - const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); - const appState = useRef(AppState.currentState); - // **** Add useRef to track the last notified ID **** - const lastNotifiedIdRef = useRef(null); - - // Set up notifications on app start - useEffect(() => { - // Request permissions - requestNotificationPermissions(); - - // Set up listeners for system notifications - const cleanupListeners = setupNotificationListeners( - (notification) => { - // Handle received notification - console.log('Notification received', notification); - }, - (response) => { - // Handle notification response (user tap) - const data = response.notification.request.content.data; - navigateToNotificationsScreen(data); - } - ); - - // Set up polling for new notifications - const checkInterval = setInterval(checkForNewNotifications, 30000); // Every 30 seconds - checkForNewNotifications(); // Check immediately on mount - - // Clean up on unmount - return () => { - cleanupListeners(); - clearInterval(checkInterval); - }; - }, []); +const [inAppNotification, setInAppNotification] = useState(null); +const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); +const appState = useRef(AppState.currentState); +// **** Add useRef to track the last notified ID **** +const lastNotifiedIdRef = useRef(null); +// Set up notifications on app start +useEffect(() => { +// Request permissions +requestNotificationPermissions(); +// Set up listeners for system notifications +const cleanupListeners = setupNotificationListeners( + (notification) => { + // Handle received notification + console.log('Notification received', notification); + }, + (response) => { + // Handle notification response (user tap) + const data = response.notification.request.content.data; + navigateToNotificationsScreen(data); + } +); + +// Set up polling for new notifications +const checkInterval = setInterval(checkForNewNotifications, 30000); // Every 30 seconds +checkForNewNotifications(); // Check immediately on mount + +// Clean up on unmount +return () => { + cleanupListeners(); + clearInterval(checkInterval); +}; + +}, []); +// Monitor app state to determine notification type +useEffect(() => { +const subscription = AppState.addEventListener('change', nextAppState => { +appState.current = nextAppState; +}); +return () => { + subscription.remove(); +}; + +}, []); +// Function to check for new notifications +const checkForNewNotifications = async () => { +try { +// Check if config and BASE_URL exist +if (!config || !config.BASE_URL) { +console.error('BASE_URL is not defined in config'); +return; +} +const response = await fetch(`${config.BASE_URL}/logs`); + // **** Added error handling for non-ok responses **** + if (!response.ok) { + console.error(`Error fetching logs: ${response.status} ${response.statusText}`); + // Optionally handle specific statuses like 404, 500 etc. + return; + } + const data = await response.json(); + + // **** Changed check to ensure data exists and is an array **** + if (data && Array.isArray(data) && data.length > 0) { + // Get the latest notification (assuming API returns sorted newest first) + const latestNotification = data[0]; + console.log(`[Check] Latest log ID from API: ${latestNotification.id}`); // Debug log + handleNewNotification(latestNotification); + } else { + // console.log("[Check] No logs found or invalid data format from API."); // Optional debug log + } +} catch (error) { + console.error('Error during checkForNewNotifications fetch:', error); +} - // Monitor app state to determine notification type - useEffect(() => { - const subscription = AppState.addEventListener('change', nextAppState => { - appState.current = nextAppState; +}; +// Handle a new notification +const handleNewNotification = (notification) => { +// **** Ensure notification and its ID exist **** +if (!notification || !notification.id) { +console.warn("handleNewNotification received invalid notification data:", notification); +return; +} +// Determine if link is malicious based on icon or other property +const isMalicious = notification.icon === "suspicious-icon"; +const title = isMalicious ? + "Warning: Malicious Link Detected" : + "Safe Link Verified"; +const body = notification.link || ""; // Use link as body + +// Set unread notifications flag (might need refinement later if you track read status globally) +setHasUnreadNotifications(true); + +// **** Core Logic: Check if the ID is new **** +if (notification.id !== lastNotifiedIdRef.current) { + console.log(`[New Notification] ID ${notification.id} is different from last notified ID ${lastNotifiedIdRef.current}. Showing toast.`); // Debug log + if (appState.current === 'active') { + // App is in foreground, show in-app notification + setInAppNotification({ + id: notification.id, // Use the actual ID from the log + link: notification.link, // Use link from log data + icon: notification.icon || (isMalicious ? "suspicious-icon" : "safe-icon"), + // Add any other properties your NotificationToast needs from the notification object }); - - return () => { - subscription.remove(); - }; - }, []); - - // Function to check for new notifications - const checkForNewNotifications = async () => { - try { - // Check if config and BASE_URL exist - if (!config || !config.BASE_URL) { - console.error('BASE_URL is not defined in config'); - return; - } - - const response = await fetch(`${config.BASE_URL}/logs`); - // **** Added error handling for non-ok responses **** - if (!response.ok) { - console.error(`Error fetching logs: ${response.status} ${response.statusText}`); - // Optionally handle specific statuses like 404, 500 etc. - return; - } - const data = await response.json(); - - // **** Changed check to ensure data exists and is an array **** - if (data && Array.isArray(data) && data.length > 0) { - // Get the latest notification (assuming API returns sorted newest first) - const latestNotification = data[0]; - console.log(`[Check] Latest log ID from API: ${latestNotification.id}`); // Debug log - handleNewNotification(latestNotification); - } else { - // console.log("[Check] No logs found or invalid data format from API."); // Optional debug log - } - } catch (error) { - console.error('Error during checkForNewNotifications fetch:', error); - } - }; - - // Handle a new notification - const handleNewNotification = (notification) => { - // **** Ensure notification and its ID exist **** - if (!notification || !notification.id) { - console.warn("handleNewNotification received invalid notification data:", notification); - return; - } - - // Determine if link is malicious based on icon or other property - const isMalicious = notification.icon === "suspicious-icon"; - const title = isMalicious ? - "Warning: Malicious Link Detected" : - "Safe Link Verified"; - const body = notification.link || ""; // Use link as body - - // Set unread notifications flag (might need refinement later if you track read status globally) - setHasUnreadNotifications(true); - - // **** Core Logic: Check if the ID is new **** - if (notification.id !== lastNotifiedIdRef.current) { - console.log(`[New Notification] ID ${notification.id} is different from last notified ID ${lastNotifiedIdRef.current}. Showing toast.`); // Debug log - if (appState.current === 'active') { - // App is in foreground, show in-app notification - setInAppNotification({ - id: notification.id, // Use the actual ID from the log - link: notification.link, // Use link from log data - icon: notification.icon || (isMalicious ? "suspicious-icon" : "safe-icon"), - // Add any other properties your NotificationToast needs from the notification object - }); - // **** Update the last notified ID **** - lastNotifiedIdRef.current = notification.id; - } else { - // App is in background, show system notification - console.log(`[New Notification] App not active. Scheduling system notification for ID ${notification.id}.`); // Debug log - scheduleNotification(title, body, notification); - // **** Update the last notified ID even for background notifications **** - // This prevents showing an in-app toast for the same item when the app returns to foreground - lastNotifiedIdRef.current = notification.id; - } - } else { - console.log(`[New Notification] ID ${notification.id} is the SAME as last notified ID ${lastNotifiedIdRef.current}. Skipping toast.`); // Debug log - } - }; - - // Function to navigate to notifications screen using the global navigation variable - const navigateToNotificationsScreen = (data) => { - if (globalNavigation) { - globalNavigation.navigate('Notifications', { - isDarkMode: false, // Pass relevant props if needed - onToggleDarkMode: () => {}, - ...data // Pass any data needed by the Notifications screen - }); - // Clear unread notifications when navigating to the Notifications screen - setHasUnreadNotifications(false); - } else { - console.warn("Cannot navigate: globalNavigation is not set."); - } - }; - - // Simply return the MainStack and NotificationToast - return ( - <> - setHasUnreadNotifications(false)} - /> - - {/* Notification Toast */} - {inAppNotification && ( - { // Renamed param for clarity - console.log("Toast pressed. Navigating with data:", notificationData); // Debug log - if (globalNavigation) { - // Pass the specific notification data needed by the screen - globalNavigation.navigate('Notifications', { - notificationIdToHighlight: notificationData.id, // Example: pass ID to highlight - // Pass other relevant props if Notifications screen needs them - }); - setInAppNotification(null); // Dismiss toast after navigation - setHasUnreadNotifications(false); // Mark as read - } else { - console.warn("Cannot navigate from toast press: globalNavigation not set."); - } - }} - onDismiss={() => { - console.log("Toast dismissed (timeout or manual)."); // Debug log - setInAppNotification(null); - }} - /> - )} - - ); + // **** Update the last notified ID **** + lastNotifiedIdRef.current = notification.id; + } else { + // App is in background, show system notification + console.log(`[New Notification] App not active. Scheduling system notification for ID ${notification.id}.`); // Debug log + scheduleNotification(title, body, notification); + // **** Update the last notified ID even for background notifications **** + // This prevents showing an in-app toast for the same item when the app returns to foreground + lastNotifiedIdRef.current = notification.id; + } +} else { + console.log(`[New Notification] ID ${notification.id} is the SAME as last notified ID ${lastNotifiedIdRef.current}. Skipping toast.`); // Debug log } +}; +// Function to navigate to notifications screen using the global navigation variable +const navigateToNotificationsScreen = (data) => { +if (globalNavigation) { +globalNavigation.navigate('Notifications', { +isDarkMode: false, // Pass relevant props if needed +onToggleDarkMode: () => {}, +...data // Pass any data needed by the Notifications screen +}); +// Clear unread notifications when navigating to the Notifications screen +setHasUnreadNotifications(false); +} else { +console.warn("Cannot navigate: globalNavigation is not set."); +} +}; +// Simply return the MainStack and NotificationToast +return ( +<> + setHasUnreadNotifications(false)} +/> +{/* Notification Toast */} + {inAppNotification && ( + { // Renamed param for clarity + console.log("Toast pressed. Navigating with data:", notificationData); // Debug log + if (globalNavigation) { + // Pass the specific notification data needed by the screen + globalNavigation.navigate('Notifications', { + notificationIdToHighlight: notificationData.id, // Example: pass ID to highlight + // Pass other relevant props if Notifications screen needs them + }); + setInAppNotification(null); // Dismiss toast after navigation + setHasUnreadNotifications(false); // Mark as read + } else { + console.warn("Cannot navigate from toast press: globalNavigation not set."); + } + }} + onDismiss={() => { + console.log("Toast dismissed (timeout or manual)."); // Debug log + setInAppNotification(null); + }} + /> + )} + + +); +} // Styles (Keep existing styles) const styles = StyleSheet.create({ - topBar: { - height: 60, - backgroundColor: "transparent", - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingHorizontal: 16, - }, - logo: { - width: 120, - height: 30, - resizeMode: "contain", - }, - topBarIcons: { - flexDirection: "row", - alignItems: "center", - gap: 10, - }, +topBar: { +height: 60, +backgroundColor: "transparent", +flexDirection: "row", +justifyContent: "space-between", +alignItems: "center", +paddingHorizontal: 16, +}, +logo: { +width: 120, +height: 30, +resizeMode: "contain", +}, +topBarIcons: { +flexDirection: "row", +alignItems: "center", +gap: 10, +}, }); \ No newline at end of file diff --git a/frontend/app/screens/components/DetailsModal.js b/frontend/app/screens/components/DetailsModal.js index 602ca8aa..af4db9f1 100644 --- a/frontend/app/screens/components/DetailsModal.js +++ b/frontend/app/screens/components/DetailsModal.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, Linking } from 'react'; +import React, { useEffect, useState } from 'react'; import { Modal, View, @@ -7,79 +7,35 @@ import { StyleSheet, Dimensions, ActivityIndicator, - Image, - Alert + Alert, + Linking } from 'react-native'; -import Icon from 'react-native-vector-icons/MaterialIcons'; // Ensure you have react-native-vector-icons installed -const iconMap = { - "suspicious": require("../../../assets/images/suspicious-icon.png"), - "safe": require("../../../assets/images/safe-icon.png"), -}; - -const severityColors = { - "low": "#31EE9A", - "medium": "#FFC107", - "high": "#FF8C00", - "critical": "#FF0000" -}; - - -const handleUpdate = async () => { - if (!logDetails?.log_id) return; - - try { - const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - severity: "High", - probability: 90, - platform: "Web", - recommended_action: "Review URL", - }), - }); - - const data = await response.json(); - if (data.error) { - console.error("Error updating log:", data.error); - } else { - console.log("Log updated successfully:", data); - onClose(); - } - } catch (error) { - console.error("Error updating log:", error); - } -}; +// Removed the external handleDelete and handleUpdate as we'll integrate the logic +// const handleUpdate = async (logDetails, onClose, config) => { ... }; +// const handleDelete = async (logDetails, onClose, config, onDelete) => { ... }; -const handleDelete = async () => { - if (!logDetails?.log_id) return; - try { - const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, { - method: "DELETE", - }); - - const data = await response.json(); - if (data.error) { - console.error("Error deleting log:", data.error); - } else { - console.log("Log deleted successfully:", data); - onClose(); - } - } catch (error) { - console.error("Error deleting log:", error); - } +const severityColors = { + "low": "#31EE9A", + "medium": "#FFC107", + "high": "#FF8C00", + "critical": "#FF0000" }; - -const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, onDeletePress }) => { +// Renamed onDelete prop to onLogDeleted for clarity +const DetailsModal = ({ visible, onClose, logDetails, loading, config, onLogDeleted }) => { const [screenDimensions, setScreenDimensions] = useState(Dimensions.get('window')); const [editedLog, setEditedLog] = useState({}); + const [isDeleting, setIsDeleting] = useState(false); // State to track deletion in progress + const [deleteError, setDeleteError] = useState(null); // State for deletion error messages useEffect(() => { if (logDetails) { setEditedLog(logDetails); + // Reset delete states when new log details are loaded + setIsDeleting(false); + setDeleteError(null); } }, [logDetails]); @@ -87,13 +43,11 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on const updateDimensions = () => { setScreenDimensions(Dimensions.get('window')); }; - - Dimensions.addEventListener('change', updateDimensions); + const dimensionChangeSubscription = Dimensions.addEventListener('change', updateDimensions); return () => { - if (Dimensions.removeEventListener) { - Dimensions.removeEventListener('change', updateDimensions); - } + // Clean up the event listener subscription + dimensionChangeSubscription.remove(); }; }, []); @@ -109,6 +63,58 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on } }; + // Function to execute the actual delete API call + const executeDelete = async () => { + if (!logDetails?.log_id) { + setDeleteError("Cannot delete: Log ID is missing."); + Alert.alert("Deletion Failed", "Log ID is missing."); // Also show alert for critical error + return; + } + + setIsDeleting(true); // Start loading state + setDeleteError(null); // Clear any previous error + + try { + // Your backend delete endpoint structure + const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, { + method: "DELETE", + // Add any necessary headers here (e.g., authorization) + }); + + if (!response.ok) { + // Attempt to read error message from the response body + let errorData = "Failed to delete log."; + try { + const jsonResponse = await response.json(); + errorData = jsonResponse.error || jsonResponse.message || errorData; + } catch (e) { + // If parsing fails, use a generic message + console.warn("Could not parse error response body:", e); + } + throw new Error(errorData); + } + + console.log("Log deleted successfully:", logDetails.log_id); + + // Notify the parent component that this log was deleted + if (onLogDeleted) { + // Pass the ID of the log that was deleted back to the parent + onLogDeleted(logDetails.log_id); + } + + // Close the modal ONLY after successful deletion + onClose(); + + } catch (error) { + console.error("Error deleting log:", error); + setDeleteError(error.message || "Failed to delete log."); // Set state for UI display + Alert.alert("Deletion Failed", error.message || "Failed to delete log."); // Also show an alert for immediate user feedback + } finally { + setIsDeleting(false); // End loading state + } + }; + + // Handle Delete Press with confirmation const handleDeletePress = () => { Alert.alert("Confirm Delete", "Are you sure you want to delete this log?", [ @@ -116,20 +122,11 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on { text: "Delete", style: "destructive", - onPress: () => { - if (onDelete && editedLog?.id) { - onDelete(editedLog.id); // Trigger the delete function - handleClosePress(); // Optionally close the modal after deletion - } - }, + onPress: executeDelete, // Call the function that performs the delete }, ]); }; - - const isSafe = editedLog?.recommended_action === "Allow URL"; - const iconSource = isSafe ? iconMap.safe : iconMap.suspicious; - const probability = editedLog?.probability ? Math.round(editedLog.probability) : "N/A"; const severity = editedLog?.severity ? editedLog.severity.toLowerCase() : "unknown"; const severityColor = severityColors[severity] || "#FFFFFF"; @@ -145,25 +142,41 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on true} + onResponderRelease={() => {}} > - - {loading ? ( - - ) : logDetails ? ( - <> - - {/* Dynamic Icon */} - - + {/* Initial Loading state (e.g., fetching details) */} + {loading && ( + + + Loading Details... + )} + + {/* Deletion Loading state */} + {isDeleting && ( + + + Deleting... + + )} + + {/* Error Display for initial load */} + {!loading && !logDetails && ( + Error loading details. + )} + {/* Main Content (show if not loading and logDetails exist) */} + {!loading && logDetails && ( + <> {/* URL Display */} { @@ -177,6 +190,7 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on }); } }} + disabled={isDeleting} // Disable link while deleting > {logDetails.url || "Unknown URL"} @@ -215,29 +229,35 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on + {/* Display Deletion Error Message */} + {deleteError && ( + {deleteError} + )} + + {/* Buttons Row (Delete before Close) */} {/* Delete Button */} - - Delete + {isDeleting ? 'Deleting...' : 'Delete'} {/* Close Button */} - Close - ) : ( - Error loading details. )} @@ -259,32 +279,38 @@ const styles = StyleSheet.create({ padding: 20, alignItems: 'center', }, - headerIcons: { - flexDirection: 'row', - position: 'absolute', - right: 15, - top: 15, - }, - iconButton: { - marginLeft: 10, - }, - iconContainer: { - marginBottom: 15, - alignItems: 'center', - }, - icon: { - width: 70, - height: 70, + loadingContainer: { + justifyContent: 'center', + alignItems: 'center', + padding: 20, }, + loadingText: { + marginTop: 10, + color: '#31EE9A', + fontSize: 16, + }, + deletingContainer: { + position: 'absolute', // Position over content + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.7)', // Semi-transparent overlay + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + zIndex: 1, // Ensure it's on top + }, + deletingText: { + marginTop: 10, + color: '#FFFFFF', + fontSize: 16, + }, urlText: { fontSize: 18, fontWeight: '600', color: '#FFFFFF', textAlign: 'center', - }, - urlLabel: { - fontSize: 14, - color: '#AAAAAA', marginBottom: 20, }, analysisContainer: { @@ -311,33 +337,30 @@ const styles = StyleSheet.create({ fontWeight: '500', textAlign: 'right', }, - actionButton: { - backgroundColor: '#31EE9A', - paddingVertical: 12, - paddingHorizontal: 30, - borderRadius: 15, - marginTop: 10, - width: '100%', - alignItems: 'center', - }, actionButtonText: { color: '#000000', fontSize: 16, fontWeight: '600', }, - errorText: { + errorText: { // Style for initial loading error color: 'red', fontSize: 16, fontWeight: 'bold', textAlign: 'center', }, + deleteErrorText: { // Style specifically for deletion error + color: '#FF4C4C', // Use a red color + fontSize: 14, + marginTop: 10, + textAlign: 'center', + fontWeight: 'bold', + }, buttonRow: { flexDirection: 'row', justifyContent: 'space-between', width: '100%', marginTop: 20, }, - sideButton: { flex: 1, backgroundColor: '#31EE9A', @@ -345,8 +368,7 @@ const styles = StyleSheet.create({ marginHorizontal: 5, borderRadius: 15, alignItems: 'center', - }, + }, }); -export default DetailsModal; - \ No newline at end of file +export default DetailsModal; \ No newline at end of file diff --git a/frontend/app/screens/tabScreens/Logs.js b/frontend/app/screens/tabScreens/Logs.js index e434601d..c8eee226 100644 --- a/frontend/app/screens/tabScreens/Logs.js +++ b/frontend/app/screens/tabScreens/Logs.js @@ -1,14 +1,14 @@ import React, { useState, useCallback } from "react"; import { - StyleSheet, - SafeAreaView, - Text, - View, - TextInput, - TouchableOpacity, - FlatList, - ActivityIndicator, - Alert, +StyleSheet, +SafeAreaView, +Text, +View, +TextInput, +TouchableOpacity, +FlatList, +ActivityIndicator, +Alert, } from "react-native"; import { useSharedValue } from "react-native-reanimated"; import Icon from "react-native-vector-icons/MaterialIcons"; @@ -20,373 +20,345 @@ import config from "../../config"; import DetailsModal from '../components/DetailsModal'; const iconMap = { - "suspicious-icon": require("../../../assets/images/suspicious-icon.png"), - "safe-icon": require("../../../assets/images/safe-icon.png"), +"suspicious-icon": require("../../../assets/images/suspicious-icon.png"), +"safe-icon": require("../../../assets/images/safe-icon.png"), }; export default function Logs({ route }) { - const [url, setUrl] = useState(""); - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); // For initial/filter fetch - const [searching, setSearching] = useState(false); // For search input delay feedback - const viewableItems = useSharedValue([]); - const [activeFilter, setActiveFilter] = useState("recent"); - const [modalVisible, setModalVisible] = useState(false); - const [selectedLog, setSelectedLog] = useState(null); - const [logLoading, setLogLoading] = useState(false); // For modal details fetch +const [url, setUrl] = useState(""); +const [logs, setLogs] = useState([]); +const [loading, setLoading] = useState(true); // For initial/filter fetch +const [searching, setSearching] = useState(false); // For search input delay feedback +const viewableItems = useSharedValue([]); +const [activeFilter, setActiveFilter] = useState("recent"); +const [modalVisible, setModalVisible] = useState(false); +const [selectedLog, setSelectedLog] = useState(null); +const [logLoading, setLogLoading] = useState(false); // For modal details fetch - const filterOptions = [ - { id: 'recent', label: 'Recent' }, - { id: 'safe', label: 'Safe' }, - { id: 'phishing', label: 'Phishing' }, - { id: 'low', label: 'Low' }, - { id: 'medium', label: 'Medium' }, - { id: 'high', label: 'High' }, - { id: 'critical', label: 'Critical' } - ]; +const filterOptions = [ +{ id: 'recent', label: 'Recent' }, +{ id: 'safe', label: 'Safe' }, +{ id: 'phishing', label: 'Phishing' }, +{ id: 'low', label: 'Low' }, +{ id: 'medium', label: 'Medium' }, +{ id: 'high', label: 'High' }, +{ id: 'critical', label: 'Critical' } +]; - // --- Modal Logic --- - const showModal = async (logId) => { - console.log(`[Logs.js] showModal function started for ID: ${logId}`); - if (!logId) { - console.error("[Logs.js] showModal called with invalid logId:", logId); - return; - } - setLogLoading(true); - // Set modal visible slightly earlier maybe? So container appears while loading - // setModalVisible(true); // Option: Make visible before fetch starts - try { - const response = await fetch(`${config.BASE_URL}/logs/${logId}`); - console.log('[Logs.js] Fetch response status:', response.status); - const data = await response.json(); - console.log('[Logs.js] Fetched data:', JSON.stringify(data, null, 2)); - - if (data.error || !response.ok) { - console.error("[Logs.js] Error fetching log details from API:", data.error || `Status ${response.status}`); - Alert.alert("Error", "Could not load log details."); // Show error to user - setModalVisible(false); // Hide modal on error - setSelectedLog(null); - } else { - console.log("[Logs.js] Fetch successful. Setting selected log and making modal visible."); - setSelectedLog(data); - console.log("[Logs.js] Calling setModalVisible(true)"); - setModalVisible(true); // Make visible *after* data is ready - } - } catch (error) { - console.error("[Logs.js] Error DURING fetch operation:", error); - Alert.alert("Error", "An error occurred while fetching details."); // Show error to user - setModalVisible(false); // Hide modal on error - setSelectedLog(null); - } finally { - console.log("[Logs.js] Setting log loading to false."); - setLogLoading(false); - } - }; +// --- Modal Logic --- +const showModal = async (logId) => { +console.log(`[Logs.js] showModal function started for ID: ${logId}`); +if (!logId) { + console.error("[Logs.js] showModal called with invalid logId:", logId); + return; +} +setLogLoading(true); - const closeModal = () => { - // Modify the existing log to show current state BEFORE changing it - console.log(`[Logs.js] closeModal function called. Current state before closing - modalVisible: ${modalVisible}, logLoading: ${logLoading}`); // <-- MODIFY THIS LOG +try { + const response = await fetch(`${config.BASE_URL}/logs/${logId}`); + console.log('[Logs.js] Fetch response status:', response.status); + const data = await response.json(); + console.log('[Logs.js] Fetched data:', JSON.stringify(data, null, 2)); - setModalVisible(false); - setSelectedLog(null); // Set selected log to null too - - // Add a log IMMEDIATELY after setting state to confirm the call was made - // Note: The state might not update *immediately* in the console here due to batching, - // but seeing this log confirms setModalVisible(false) was executed. - console.log(`[Logs.js] setModalVisible(false) EXECUTED.`); // <-- ADD THIS LOG - }; - - // --- Fetching and Filtering Logic --- (Keep as is) - const handleFilterChange = (filter) => { - setActiveFilter(filter); - console.log("Active Filter:", filter); - fetchLogs(filter); - }; - - const fetchLogs = async (filterType = activeFilter) => { - if (!searching) { - setLoading(true); - } - try { - const response = await fetch(`${config.BASE_URL}/logs?filter=${filterType}`); - const data = await response.json(); - setLogs(data); - } catch (error) { - console.error("Error fetching logs:", error); - Alert.alert("Error", "Could not fetch logs."); - } finally { - setLoading(false); - // setSearching(false); // Optional reset - } - }; - - useFocusEffect( - useCallback(() => { - fetchLogs(activeFilter); - }, [activeFilter]) - ); + if (data.error || !response.ok) { + console.error("[Logs.js] Error fetching log details from API:", data.error || `Status ${response.status}`); + Alert.alert("Error", "Could not load log details."); // Show error to user + setModalVisible(false); // Hide modal on error + setSelectedLog(null); + } else { + console.log("[Logs.js] Fetch successful. Setting selected log and making modal visible."); + setSelectedLog(data); + console.log("[Logs.js] Calling setModalVisible(true)"); + setModalVisible(true); // Make visible *after* data is ready + } +} catch (error) { + console.error("[Logs.js] Error DURING fetch operation:", error); + Alert.alert("Error", "An error occurred while fetching details."); // Show error to user + setModalVisible(false); // Hide modal on error + setSelectedLog(null); +} finally { + console.log("[Logs.js] Setting log loading to false."); + setLogLoading(false); +} +}; - useFocusEffect( - useCallback(() => { - if (route?.params?.logId) { - showModal(route.params.logId); - // Optional: Clear the param after showing the modal to prevent re-showing on focus gain - // navigation.setParams({ logId: undefined }); // Requires navigation prop - } - }, [route?.params?.logId]) - ); +const closeModal = () => { +// Modify the existing log to show current state BEFORE changing it +console.log(`[Logs.js] closeModal function called. Current state before closing - modalVisible: ${modalVisible}, logLoading: ${logLoading}`); +setModalVisible(false); +setSelectedLog(null); // Set selected log to null too - const viewabilityConfig = { - itemVisiblePercentThreshold: 100, - }; +// Add a log IMMEDIATELY after setting state to confirm the call was made +console.log(`[Logs.js] setModalVisible(false) EXECUTED.`); +}; - // --- Client-side filtering --- (Keep as is) - const getFilteredLogs = () => { - const categoryFilteredLogs = logs.filter(log => { - switch (activeFilter) { - case "recent": return true; - case "safe": return log.status?.toLowerCase().includes("safe") || log.status?.toLowerCase().includes("safe"); - case "phishing": return log.status?.toLowerCase().includes("phishing") || log.status?.toLowerCase().includes("phishing"); - case "low": return log.severity?.toLowerCase() === "low"; - case "medium": return log.severity?.toLowerCase() === "medium"; - case "high": return log.severity?.toLowerCase() === "high"; - case "critical": return log.severity?.toLowerCase() === "critical"; - default: return true; - } - }); +// --- Updated Fetching and Filtering Logic --- +const handleFilterChange = (filter) => { + if (activeFilter !== filter) { + console.log("Active Filter changing to:", filter); + setActiveFilter(filter); // Update active filter + fetchLogs(filter, url); // Fetch logs immediately with the new filter + } +}; - if (!url || url.trim() === "") { - return categoryFilteredLogs; +const fetchLogs = async (filterType = activeFilter, searchQuery = "") => { + console.log(`[Logs.js] Fetching logs with filter: ${filterType}, search: "${searchQuery}"`); + + if (!searching) { + setLoading(true); + } + + try { + // Construct query parameters + let queryParams = `filter=${encodeURIComponent(filterType)}`; + if (searchQuery && searchQuery.trim() !== "") { + queryParams += `&search=${encodeURIComponent(searchQuery)}`; } - const searchTerm = url.toLowerCase().trim(); - return categoryFilteredLogs.filter(log => - Object.values(log).some(value => - typeof value === "string" && value.toLowerCase().includes(searchTerm) - ) - ); - }; - - // --- Handle Search Input --- (Keep as is) - const handleInputChange = (text) => { - setUrl(text); - if (text.trim() !== "") { - setSearching(true); - // Debounce might be better here, but basic timeout for example: - const timer = setTimeout(() => { - setSearching(false); - }, 500); // Shorter delay for search feedback - // Need useRef to clear timer properly if implementing debounce - } else { - setSearching(false); - } - }; + + const response = await fetch(`${config.BASE_URL}/logs?${queryParams}`); + + if (!response.ok) { + throw new Error(`HTTP status ${response.status}`); + } + + const data = await response.json(); + console.log(`[Logs.js] Fetched ${data.length} logs for filter "${filterType}"`); + setLogs(data); + } catch (error) { + console.error("[Logs.js] Error fetching logs:", error); + Alert.alert("Error", `Could not fetch logs: ${error.message}`); + } finally { + setLoading(false); + } +}; - const displayedLogs = getFilteredLogs(); +useFocusEffect( + useCallback(() => { + fetchLogs(activeFilter, url); + }, [activeFilter]) +); - // --- Delete Logic --- - const handleDeleteLog = async (logId) => { // Make the function async - if (!logId) { - console.error("[Logs.js] handleDeleteLog called with invalid ID:", logId); - Alert.alert("Error", "Cannot delete log: Invalid ID."); - return; +useFocusEffect( + useCallback(() => { + if (route?.params?.logId) { + showModal(route.params.logId); } + }, [route?.params?.logId]) +); - console.log(`[Logs.js] Attempting to delete log with ID: ${logId}`); +const viewabilityConfig = { + itemVisiblePercentThreshold: 100, +}; - // Optional: Add a loading indicator state specifically for deletion if needed - // setDeleting(true); +// --- Handle Search Input --- +const handleInputChange = (text) => { + setUrl(text); + if (text.trim() !== "") { + setSearching(true); + // Debounce for search feedback + const timer = setTimeout(() => { + fetchLogs(activeFilter, text); // Search using API with current filter and search text + setSearching(false); + }, 500); + + // Need useRef to clear timer properly in a complete implementation + return () => clearTimeout(timer); + } else { + setSearching(false); + fetchLogs(activeFilter, ""); // Clear search, fetch with current filter only + } +}; - try { - const response = await fetch(`${config.BASE_URL}/logs/${logId}`, { - method: 'DELETE', - headers: { - // Add any necessary headers like Authorization if your API requires them - 'Content-Type': 'application/json', - }, - }); +// Handle search button press +const handleSearch = () => { + fetchLogs(activeFilter, url); +}; - console.log(`[Logs.js] DELETE response status for ID ${logId}:`, response.status); +// --- Delete Logic --- +const handleDeleteLog = async (logId) => { +if (!logId) { + console.error("[Logs.js] handleDeleteLog called with invalid ID:", logId); + Alert.alert("Error", "Cannot delete log: Invalid ID."); + return; +} +console.log(`[Logs.js] Attempting to delete log with ID: ${logId}`); - // Check if the deletion was successful (status code 200 OK) - if (response.ok) { - // If successful, THEN remove the log from the local state - setLogs((prevLogs) => prevLogs.filter(log => log.id !== logId)); - console.log(`[Logs.js] Log ${logId} successfully deleted from state.`); - Alert.alert("Success", "Log deleted successfully."); // Optional success feedback - // The modal closes itself via handleClosePress called in DetailsModal.js after onDelete - } else { - // Handle errors (e.g., log not found, server error) - const errorData = await response.json().catch(() => ({})); // Try to parse JSON error, default to empty object - console.error(`[Logs.js] Failed to delete log ${logId}. Status: ${response.status}`, errorData); - Alert.alert( - "Deletion Failed", - errorData.error || `Could not delete log (Status: ${response.status}). Please try again.` - ); - } - } catch (error) { - // Handle network errors or other unexpected issues - console.error(`[Logs.js] Error during fetch DELETE operation for ID ${logId}:`, error); - Alert.alert("Error", "An error occurred while trying to delete the log. Check your connection."); - } finally { - // Optional: Stop delete-specific loading indicator - // setDeleting(false); - } - }; - +try { + const response = await fetch(`${config.BASE_URL}/logs/${logId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + console.log(`[Logs.js] DELETE response status for ID ${logId}:`, response.status); - // --- Render --- - console.log(`[Logs.js] Rendering component. Current modalVisible state: ${modalVisible}, Current logLoading state: ${logLoading}`); + if (response.ok) { + setLogs((prevLogs) => prevLogs.filter(log => log.id !== logId)); + console.log(`[Logs.js] Log ${logId} successfully deleted from state.`); + Alert.alert("Success", "Log deleted successfully."); + } else { + const errorData = await response.json().catch(() => ({})); + console.error(`[Logs.js] Failed to delete log ${logId}. Status: ${response.status}`, errorData); + Alert.alert( + "Deletion Failed", + errorData.error || `Could not delete log (Status: ${response.status}). Please try again.` + ); + } +} catch (error) { + console.error(`[Logs.js] Error during fetch DELETE operation for ID ${logId}:`, error); + Alert.alert("Error", "An error occurred while trying to delete the log. Check your connection."); +} +}; - return ( - - {/* --- Search Section --- */} - - Search: - - - - - - - +// --- Render --- +console.log(`[Logs.js] Rendering component. Current modalVisible state: ${modalVisible}, Current logLoading state: ${logLoading}`); +return ( + +{/* --- Search Section --- */} + +Search: + + + + + + + - {/* --- Filter Section --- */} - - - +{/* --- Filter Section --- */} + + + - {/* --- Content Area (Spinner or List) --- */} - - {loading ? ( - - - Loading logs... - - ) : searching ? ( - - - {/* Searching... */} - - ) : displayedLogs.length === 0 ? ( - - No logs found for "{activeFilter}" filter{url ? ` matching "${url}"` : ""}. - - ) : ( - item.id.toString()} - renderItem={({ item }) => ( - { - console.log(`[Logs.js] TouchableOpacity onPress - Calling showModal for ID: ${item.id}`); - showModal(item.id); - }} - > - - - )} - showsVerticalScrollIndicator={false} - contentContainerStyle={styles.listContent} - onViewableItemsChanged={({ viewableItems: vItems }) => { - viewableItems.value = vItems.map((vItem) => vItem.item.id); +{/* --- Content Area (Spinner or List) --- */} + + {loading ? ( + + + Loading logs... + + ) : searching ? ( + + + + ) : logs.length === 0 ? ( + + No logs found for "{activeFilter}" filter{url ? ` matching "${url}"` : ""}. + + ) : ( + item.id.toString()} + renderItem={({ item }) => ( + { + console.log(`[Logs.js] TouchableOpacity onPress - Calling showModal for ID: ${item.id}`); + showModal(item.id); + }} + > + - )} - + + )} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContent} + onViewableItemsChanged={({ viewableItems: vItems }) => { + viewableItems.value = vItems.map((vItem) => vItem.item.id); + }} + viewabilityConfig={viewabilityConfig} + /> + )} + - {/* --- Modal --- */} - {/* Add the debug log before rendering */} - {console.log(`[Logs.js] Preparing to render DetailsModal. Props being passed - visible: ${modalVisible || logLoading}, logLoading: ${logLoading}`)} +{/* --- Modal --- */} +{console.log(`[Logs.js] Preparing to render DetailsModal. Props being passed - visible: ${modalVisible || logLoading}, logLoading: ${logLoading}`)} - - - ); + + +); } // --- Styles --- (Keep original Logs.js styles) const styles = StyleSheet.create({ - container: { - flex: 1, - paddingHorizontal: 20, - paddingTop: 20, - paddingBottom: 0, - backgroundColor: '#121212', // Example background color - }, - searchSection: { - marginBottom: 15, - }, - filterSection: { - marginBottom: 15, - }, - contentArea: { - flex: 1, - }, - text: { - color: "#31EE9A", - fontSize: 12, - marginBottom: 5, - }, - inputWrapper: { - position: "relative", - }, - textInput: { - fontSize: 14, - height: 45, - borderRadius: 12, - color: "#000000", - backgroundColor: "#3AED97", - paddingRight: 45, - paddingLeft: 15, - }, - iconWrapper: { - position: "absolute", - right: 15, - top: "50%", - transform: [{ translateY: -12 }], - }, - centeredIndicator: { - flex: 1, - justifyContent: "center", - alignItems: "center", - paddingBottom: 50, - }, - loadingText: { - marginTop: 10, - color: "#31EE9A", - fontSize: 16, - }, - noResultsText: { - color: "#AAAAAA", // Lighter grey - fontSize: 16, - textAlign: 'center', - paddingHorizontal: 20, // Add some padding if text is long - }, - listContent: { - paddingBottom: 20, - }, +container: { +flex: 1, +paddingHorizontal: 20, +paddingTop: 20, +paddingBottom: 0, +backgroundColor: '#121212', // Example background color +}, +searchSection: { +marginBottom: 15, +}, +filterSection: { +marginBottom: 15, +}, +contentArea: { +flex: 1, +}, +text: { +color: "#31EE9A", +fontSize: 12, +marginBottom: 5, +}, +inputWrapper: { +position: "relative", +}, +textInput: { +fontSize: 14, +height: 45, +borderRadius: 12, +color: "#000000", +backgroundColor: "#3AED97", +paddingRight: 45, +paddingLeft: 15, +}, +iconWrapper: { +position: "absolute", +right: 15, +top: "50%", +transform: [{ translateY: -12 }], +}, +centeredIndicator: { +flex: 1, +justifyContent: "center", +alignItems: "center", +paddingBottom: 50, +}, +loadingText: { +marginTop: 10, +color: "#31EE9A", +fontSize: 16, +}, +noResultsText: { +color: "#AAAAAA", // Lighter grey +fontSize: 16, +textAlign: 'center', +paddingHorizontal: 20, // Add some padding if text is long +}, +listContent: { +paddingBottom: 20, +}, }); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e053d707..77f304ac 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -489,6 +489,7 @@ "react-native-helmet-async": "^2.0.4", "react-native-is-edge-to-edge": "^1.1.6", "react-native-linear-gradient": "^2.8.3", + "react-native-modal": "^14.0.0-rc.1", "react-native-pager-view": "6.5.1", "react-native-paper": "^5.13.1", "react-native-reanimated": "~3.16.1", @@ -12933,6 +12934,15 @@ } } }, + "node_modules/react-native-animatable": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz", + "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + } + }, "node_modules/react-native-background-gradient": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/react-native-background-gradient/-/react-native-background-gradient-0.0.2.tgz", @@ -13017,6 +13027,19 @@ "react-native": "*" } }, + "node_modules/react-native-modal": { + "version": "14.0.0-rc.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz", + "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==", + "license": "MIT", + "dependencies": { + "react-native-animatable": "1.4.0" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.70.0" + } + }, "node_modules/react-native-pager-view": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.5.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index eaa3085e..613f1153 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -483,6 +483,7 @@ "react-native-helmet-async": "^2.0.4", "react-native-is-edge-to-edge": "^1.1.6", "react-native-linear-gradient": "^2.8.3", + "react-native-modal": "^14.0.0-rc.1", "react-native-pager-view": "6.5.1", "react-native-paper": "^5.13.1", "react-native-reanimated": "~3.16.1", diff --git a/package-lock.json b/package-lock.json index 7074d9c0..f5ac707e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-native": "0.76.3", "react-native-gesture-handler": "^2.24.0", "react-native-linear-gradient": "^2.8.3", + "react-native-modal": "^14.0.0-rc.1", "react-native-svg": "^15.10.0", "tailwindcss": "^3.4.16" }, @@ -5297,6 +5298,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/axios": { "version": "1.7.9", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", @@ -10571,6 +10587,18 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", @@ -10616,18 +10644,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "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", @@ -11610,6 +11626,15 @@ } } }, + "node_modules/react-native-animatable": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz", + "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + } + }, "node_modules/react-native-css-interop": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz", @@ -11678,6 +11703,19 @@ "react-native": "*" } }, + "node_modules/react-native-modal": { + "version": "14.0.0-rc.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz", + "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==", + "license": "MIT", + "dependencies": { + "react-native-animatable": "1.4.0" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.70.0" + } + }, "node_modules/react-native-reanimated": { "version": "3.16.3", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz", @@ -12147,6 +12185,23 @@ } ] }, + "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", diff --git a/package.json b/package.json index 880d751a..075c7ebb 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-native-async-storage/async-storage": "1.23.1", "axios": "^1.7.9", "expo": "~52.0.11", + "expo-notifications": "~0.29.13", "expo-status-bar": "~2.0.0", "express": "^4.21.2", "nativewind": "^4.1.23", @@ -20,9 +21,9 @@ "react-native": "0.76.3", "react-native-gesture-handler": "^2.24.0", "react-native-linear-gradient": "^2.8.3", + "react-native-modal": "^14.0.0-rc.1", "react-native-svg": "^15.10.0", - "tailwindcss": "^3.4.16", - "expo-notifications": "~0.29.13" + "tailwindcss": "^3.4.16" }, "devDependencies": { "@babel/core": "^7.20.0",