diff --git a/App.tsx b/App.tsx index b9bccbb..97c506b 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Provider } from 'react-redux'; @@ -12,6 +12,8 @@ import { withAuthenticator } from '@aws-amplify/ui-react-native'; import { Amplify } from 'aws-amplify'; import config from './src/aws-exports'; import { enableScreens } from 'react-native-screens'; +import { useKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; +import { View } from 'react-native'; enableScreens(); @@ -26,6 +28,11 @@ const App: React.FC = () => { setScannedData(''); }; + useEffect(() => { + deactivateKeepAwake(); // Allow the screen to timeout + }, []); + + return ( diff --git a/api/qrCodeAPI.tsx b/api/qrCodeAPI.tsx index ad4bdee..923136d 100644 --- a/api/qrCodeAPI.tsx +++ b/api/qrCodeAPI.tsx @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosRequestConfig } from 'axios'; import Constants from 'expo-constants'; const { API_BASE_URL, ENVIRONMENT } = Constants.expoConfig.extra; import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth'; @@ -20,6 +20,10 @@ const API_URL_GET_EMAILS = "/v1/gmail/getEmails"; const API_URL_GET_SCANNED_EMAILS = "/v1/gmail/getScannedEmails"; const API_URL_GET_USER = "/v1/user/getUser"; +const API_URL_GMAIL_DELETE_MESSAGE = "/v1/gmail/deleteMessage"; +const API_URL_GMAIL_DELETE_ALL_MESSAGES = "/v1/gmail/deleteAllMessages"; + +const API_URL_TIPS_GET = "/v1/tips/getTips"; // Create an Axios instance const apiClient = axios.create({ @@ -53,21 +57,23 @@ apiClient.interceptors.request.use( ); // Define a generic function to handle all types of requests -export const apiRequest = async (config) => { +export const apiRequest = async (config: AxiosRequestConfig) => { try { + const methodName = config.method?.toUpperCase() || 'REQUEST'; console.log("ENVIRONMENT:", ENVIRONMENT); - console.log(`API Call - ${config.method.toUpperCase()}:`, config.url, config.data || ''); + console.log(`API Call - ${methodName}:`, config.url, config.data || ''); console.log(config); const response = await apiClient(config); - console.log('API Response:', response.data); + console.log(`API Response for ${methodName}:`, response.data); return response.data; } catch (error) { + const methodName = config.method?.toUpperCase() || 'REQUEST'; if (error.response) { - console.error('API Error - Response:', error.response.data); + console.error(`API Error - Response for ${methodName}:`, error.response.data); } else if (error.request) { - console.error('API Error - No Response:', error.request); + console.error(`API Error - No Response for ${methodName}:`, error.request); } else { - console.error('API Error - General:', error.message); + console.error(`API Error - General for ${methodName}:`, error.message); } throw error; } @@ -86,7 +92,7 @@ const fetchUserId = async () => { }; // Function to handle /scan request -export const scanQRCode = async (data) => { +export const scanQRCode = async (data: string) => { return apiRequest({ method: 'post', url: `${API_BASE_URL}${API_URL_SCAN}`, @@ -167,24 +173,24 @@ export const getScannedEmails = async () => { url: `${API_BASE_URL}${API_URL_GET_SCANNED_EMAILS}` }); - console.log("API Response:", response); + console.log("API Response for getScannedEmails:", response); return response; } catch (error) { console.error("Error during getScannedEmails API call:", error); if (error.response) { - console.error("Response error data:", error.response.data); + console.error("Response error data for getScannedEmails:", error.response.data); } else if (error.request) { - console.error("Request error, no response received:", error.request); + console.error("Request error, no response received for getScannedEmails:", error.request); } else { - console.error("Error message:", error.message); + console.error("Error message for getScannedEmails:", error.message); } throw error; } }; -// Function to get start the scanning of inbox in server +// Function to start the scanning of inbox in the server export const getEmails = async (accessToken: string, refreshToken: string) => { console.log("getEmails function called"); @@ -199,28 +205,39 @@ export const getEmails = async (accessToken: string, refreshToken: string) => { }, }); - console.log("API Response:", response); + console.log("API Response for getEmails:", response); return response; } catch (error) { console.error("Error during getEmails API call:", error); if (error.response) { - console.error("Response error data:", error.response.data); + console.error("Response error data for getEmails:", error.response.data); } else if (error.request) { - console.error("Request error, no response received:", error.request); + console.error("Request error, no response received for getEmails:", error.request); } else { - console.error("Error message:", error.message); + console.error("Error message for getEmails:", error.message); } throw error; } }; - // Get user information export const getUserInfo = async () => { return apiRequest({ method: 'get', url: `${API_BASE_URL}${API_URL_GET_USER}`, }); -}; \ No newline at end of file +}; + +// Function to delete an email +export const deleteEmail = async (messageId: string) => { + const response = await apiRequest({ + method: 'put', + url: `${API_BASE_URL}${API_URL_GMAIL_DELETE_MESSAGE}`, + data: { messageId }, + }); + return response; +}; + +// Function to delete all emails diff --git a/components/ScannedDataBox.tsx b/components/ScannedDataBox.tsx index 49928b9..249f6e5 100644 --- a/components/ScannedDataBox.tsx +++ b/components/ScannedDataBox.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, Image, TouchableOpacity, Modal, ActivityIndicator, ScrollView, Dimensions, Clipboard, Platform } from 'react-native'; +import { View, Text, StyleSheet, Image, TouchableOpacity, Modal, ActivityIndicator, ScrollView, Dimensions, Clipboard, Platform, Animated } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import { Ionicons, MaterialCommunityIcons, SimpleLineIcons } from '@expo/vector-icons'; import { getQRCodeDetails } from '../api/qrCodeAPI'; @@ -22,6 +22,9 @@ const ScannedDataBox: React.FC = ({ qrCodeId, clearScanData const [qrDetails, setQrDetails] = useState(null); const [isWebViewVisible, setIsWebViewVisible] = useState(false); const [webViewUrl, setWebViewUrl] = useState(''); + const [error, setError] = useState(null); // State to store error message + const [bannerOpacity] = useState(new Animated.Value(0)); // State for banner opacity + @@ -34,6 +37,7 @@ const ScannedDataBox: React.FC = ({ qrCodeId, clearScanData console.log('details for scannedDataBOX:', details); } catch (error) { console.error('Error fetching QR details:', error); + showBanner(); // Show the error banner } }; @@ -50,6 +54,24 @@ const ScannedDataBox: React.FC = ({ qrCodeId, clearScanData ); } + // Function to show the error banner + const showBanner = () => { + Animated.timing(bannerOpacity, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }).start(() => { + setTimeout(() => { + Animated.timing(bannerOpacity, { + toValue: 0, + duration: 500, + useNativeDriver: true, + }).start(); + }, 3000); + }); + }; + + const data = qrDetails.data || {}; const details = qrDetails.details || {}; diff --git a/screens/EmailScreen.tsx b/screens/EmailScreen.tsx index 6f98398..555e5e4 100644 --- a/screens/EmailScreen.tsx +++ b/screens/EmailScreen.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, TouchableOpacity, FlatList, StyleSheet, ActivityIndicator, Alert, Animated, Dimensions, Modal } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; -import { getEmails, getScannedEmails, getUserInfo } from '../api/qrCodeAPI'; +import { getEmails, getScannedEmails, getUserInfo, deleteEmail } from '../api/qrCodeAPI'; import { fetchAuthSession } from 'aws-amplify/auth'; import { Buffer } from 'buffer'; import ScannedDataBox from '../components/ScannedDataBox'; @@ -19,10 +19,14 @@ const EmailScreen: React.FC = () => { const [bannerOpacity] = useState(new Animated.Value(0)); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedQrCodeId, setSelectedQrCodeId] = useState(null); + const [errorBannerOpacity] = useState(new Animated.Value(0)); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const [messageToDelete, setMessageToDelete] = useState(null); - useEffect(() => { - fetchUserEmail(); - }, []); + // Start scanning inbox only once when the component mounts +useEffect(() => { + startInboxScanning(); +}, []); // Function to fetch user email const fetchUserEmail = async () => { @@ -70,7 +74,7 @@ const EmailScreen: React.FC = () => { } }; - // Function to show the banner + // Function to show the scan email in background banner const showBanner = () => { Animated.timing(bannerOpacity, { toValue: 1, @@ -87,26 +91,62 @@ const EmailScreen: React.FC = () => { }); }; + const showErrorBanner = () => { + Animated.timing(errorBannerOpacity, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }).start(() => { + setTimeout(() => { + Animated.timing(errorBannerOpacity, { + toValue: 0, + duration: 500, + useNativeDriver: true, + }).start(); + }, 3000); + }); + }; + // Function to poll for scanned emails const startPollingForScannedEmails = useCallback(() => { const pollingInterval = setInterval(async () => { try { const scannedEmails = await getScannedEmails(); if (scannedEmails) { - setEmailData(scannedEmails); + setEmailData((prevEmailData) => { + // Preserve the selected message if it's still in the new list + const selectedMessageExists = prevEmailData?.messages.some( + (message) => message.messageId === selectedMessage?.messageId + ); + + if (selectedMessageExists) { + return { + ...scannedEmails, + messages: scannedEmails.messages.map((message) => ({ + ...message, + isSelected: message.messageId === selectedMessage?.messageId, + })), + }; + } else { + setSelectedMessage(null); + return scannedEmails; + } + }); setLoading(false); } } catch (error) { console.error('Error fetching scanned emails:', error); setError('Error fetching emails.'); setLoading(false); + showErrorBanner(); } }, 10000); // Poll every 10 seconds return () => clearInterval(pollingInterval); // Cleanup the interval - }, []); + }, [selectedMessage]); const handleSelectMessage = (message) => { + // Toggle selection of the message without affecting polling or scanning setSelectedMessage(selectedMessage === message ? null : message); }; @@ -122,11 +162,12 @@ const EmailScreen: React.FC = () => { useFocusEffect( useCallback(() => { - startInboxScanning(); + // Start polling for scanned emails when the screen gains focus const stopPolling = startPollingForScannedEmails(); - + return () => { - stopPolling(); // Stop polling when the screen is not in focus + // Stop polling when the screen loses focus + stopPolling(); }; }, [startPollingForScannedEmails]) ); @@ -137,6 +178,33 @@ const EmailScreen: React.FC = () => { setIsModalVisible(true); }; + const handleDeleteEmail = async (messageId: string) => { + try { + await deleteEmail(messageId); // Call the API to delete the email + setEmailData((prevEmailData) => ({ + ...prevEmailData, + messages: prevEmailData.messages.filter((message) => message.messageId !== messageId), + })); + console.log('Email deleted successfully'); + } catch (error) { + console.error('Error deleting email:', error); + Alert.alert('Error', 'Failed to delete email. Please try again.'); + } + }; + + const handleDeletePress = (messageId: string) => { + setMessageToDelete(messageId); + setIsDeleteModalVisible(true); + }; + + const confirmDelete = async () => { + if (messageToDelete) { + await handleDeleteEmail(messageToDelete); + setIsDeleteModalVisible(false); + setMessageToDelete(null); // Clear the selected message after deletion + } + }; + return ( {loading && ( @@ -170,7 +238,7 @@ const EmailScreen: React.FC = () => { handleSelectMessage(item)} style={styles.messageContainer}> {item.subject} {item.date} - {selectedMessage === item && ( + {selectedMessage?.messageId === item.messageId && ( Decoded QR Codes: {item.decodedContentsDetails?.map((details, index) => ( @@ -180,18 +248,26 @@ const EmailScreen: React.FC = () => { ))} + + handleDeletePress(item.messageId)} style={styles.deleteButtonContainer}> + Delete this entry + + )} )} /> - )} + Scanning emails in the background. This may take a while... + + Error fetching scanned emails + {/* Modal for ScannedDataBox */} { animationType="slide" onRequestClose={() => setIsModalVisible(false)} > - {/* The greyspace outside , made clickable to close the modl */} + {/* The greyspace outside, made clickable to close the modal */} { > {/* Ensure ScannedDataBox does not render another modal */} setIsModalVisible(false)} /> - - + {/* Modal to prompt for deleting */} + setIsDeleteModalVisible(false)} + > + + + Are you sure? + This will only delete the entry on the app and not the actual email. + + + Yes, Delete + + setIsDeleteModalVisible(false)} + > + No, Keep It + + + + + - ); }; @@ -237,6 +338,12 @@ const styles = StyleSheet.create({ fontWeight: 'bold', color: '#ff69b4', }, + dividerHorizontal: { + width: '100%', + height: 1, + backgroundColor: '#ddd', + marginVertical: screenWidth * 0.025, + }, emailListContainer: { flex: 1, }, @@ -319,18 +426,25 @@ const styles = StyleSheet.create({ justifyContent: 'center', zIndex: 10, // Ensure it appears above other elements }, + errorBanner: { + position: 'absolute', + top: screenHeight * 0.4, // Adjusts the banner to appear in the middle of the screen + left: screenWidth * 0.1, // Adjust these values to center the banner as needed + right: screenWidth * 0.1, + backgroundColor: '#ff69b4', + paddingVertical: screenHeight * 0.02, // Adjust the height of the banner + paddingHorizontal: screenWidth * 0.05, + borderRadius: screenWidth * 0.05, + alignItems: 'center', + justifyContent: 'center', + zIndex: 10, // Ensure it appears above other elements + }, bannerText: { color: '#fff', fontWeight: 'bold', textAlign: 'center', fontSize: screenWidth * 0.04, }, - modalContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', // dark overlay - }, innerModalContainer: { backgroundColor: '#ffe6f0', // pink box color padding: screenWidth * 0.05, @@ -342,7 +456,55 @@ const styles = StyleSheet.create({ top: screenWidth * 0.02, right: screenWidth * 0.02, zIndex: 1, // Ensure it is above other content - } + }, + deleteButtonContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + padding: screenWidth * 0.02, + }, + deleteButtonText: { + marginRight: screenWidth * 0.02, + color: '#ff69b4', + fontSize: screenWidth * 0.035, + }, + modalContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalContent: { + width: '80%', + backgroundColor: 'white', + borderRadius: screenWidth * 0.025, + padding: screenWidth * 0.05, + alignItems: 'center', + }, + modalTitle: { + fontSize: screenWidth * 0.05, + fontWeight: 'bold', + marginBottom: screenHeight * 0.01, + }, + modalText: { + fontSize: screenWidth * 0.04, + marginBottom: screenHeight * 0.02, + textAlign: 'center', + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + }, + modalButton: { + flex: 1, + alignItems: 'center', + padding: screenWidth * 0.025, + }, + modalButtonText: { + fontSize: screenWidth * 0.04, + color: '#000', + }, }); export default EmailScreen; diff --git a/screens/QRScannerScreen.tsx b/screens/QRScannerScreen.tsx index abf000b..3303c7e 100644 --- a/screens/QRScannerScreen.tsx +++ b/screens/QRScannerScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Image, Dimensions, Modal } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, Image, Dimensions, Modal, Animated } from 'react-native'; import { Camera, useCameraDevice, useCameraPermission, useCodeScanner } from 'react-native-vision-camera'; import { Ionicons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; @@ -7,6 +7,7 @@ import RNQRGenerator from 'rn-qr-generator'; import ScannedDataBox from '../components/ScannedDataBox'; import { scanQRCode } from '../api/qrCodeAPI'; import SettingsScreen from './SettingsScreen'; +import NetInfo from '@react-native-community/netinfo'; const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); @@ -16,14 +17,42 @@ const QRScannerScreen: React.FC = () => { const [scanned, setScanned] = useState(false); const [qrCodeId, setQRCodeId] = useState(null); // State for QR code ID const [isScannedDataBoxVisible, setIsScannedDataBoxVisible] = useState(false); // State for ScannedDataBox visibility - + const [bannerOpacity] = useState(new Animated.Value(0)); // Initialize bannerOpacity as an Animated.Value const { hasPermission, requestPermission } = useCameraPermission(); const device = useCameraDevice('back'); + const [isConnected, setIsConnected] = useState(true); // State for network connection useEffect(() => { requestPermission(); + + // Subscribe to network state updates + const unsubscribe = NetInfo.addEventListener(state => { + setIsConnected(state.isConnected); + if (!state.isConnected) { + showBanner(); // Show the banner when the device is offline + } + }); + + // Unsubscribe when component unmounts + return () => unsubscribe(); }, []); + const showBanner = () => { + Animated.timing(bannerOpacity, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }).start(() => { + setTimeout(() => { + Animated.timing(bannerOpacity, { + toValue: 0, + duration: 500, + useNativeDriver: true, + }).start(); + }, 3000); + }); + }; + const handlePayload = async (payload: string) => { setScanned(true); console.info("Decoded QR Code, Payload is: ", payload); @@ -88,6 +117,11 @@ const QRScannerScreen: React.FC = () => { return ( + {/* Banner for network connectivity */} + + No Internet Connection + + Welcome to Please point the camera at the QR Code @@ -270,6 +304,25 @@ const styles = StyleSheet.create({ color: 'white', fontWeight: 'bold', }, + banner: { + position: 'absolute', + top: screenHeight * 0.4, // Adjusts the banner to appear in the middle of the screen + left: screenWidth * 0.1, // Adjust these values to center the banner as needed + right: screenWidth * 0.1, + backgroundColor: '#ff69b4', + paddingVertical: screenHeight * 0.02, // Adjust the height of the banner + paddingHorizontal: screenWidth * 0.05, + borderRadius: screenWidth * 0.05, + alignItems: 'center', + justifyContent: 'center', + zIndex: 10, // Ensure it appears above other elements + }, + bannerText: { + color: '#fff', + fontWeight: 'bold', + textAlign: 'center', + fontSize: screenWidth * 0.04, + } }); export default QRScannerScreen;