54 Commits

Author SHA1 Message Date
heyethereum
7475215d44 hide email section for defaultUser 2024-08-21 22:06:16 +08:00
heyethereum
a7d4f693fa fix default user sign in without gmail scan 2024-08-19 22:58:59 +08:00
cf7935baae Added two more remarkText for phone and SMS, Fixed text wraping on Full Content Modal 2024-08-17 15:16:30 +08:00
7cce7b66d7 Added useEffect to fetch and log current user and token in QRScannerScreen. 2024-08-17 14:41:35 +08:00
d5217cc59b Added back the banner for emailScreen 2024-08-17 11:41:46 +08:00
6a00ec453d Added hasIpAddress display. Fixed Unknown User name for non-gmail users 2024-08-16 15:31:16 +08:00
b9c382eaa8 Fix email screen layout and show errorText when no emails are found 2024-08-16 12:23:09 +08:00
850675a083 detect if not gmail and setEmpty Messag to login with Gmail account 2024-08-16 12:11:43 +08:00
13dd88cd66 Make QR Tips not polled when on other screens
moved function to useEffect instead on QrScanner Screen
2024-08-16 11:18:56 +08:00
47898d1489 Updated TrackingDetection UI 2024-08-15 22:16:27 +08:00
8e37d9d2b6 Updated hasExecutble icon on ScannedDataBox 2024-08-15 21:27:17 +08:00
10803b9322 edited EmailScreen's Delte Modal formatting 2024-08-15 20:45:54 +08:00
7618532d7a Fixed ClearScanData not being called when closing ScannedData Modal 2024-08-15 16:22:13 +08:00
36be49a740 Updated sliding animation for ScannedDatabox on all 3 screens 2024-08-15 16:04:13 +08:00
ecf174e1d6 Fixed UI bugs for PHONE/SMS/TEXT on EmailScreen and History Screen 2024-08-15 02:39:49 +08:00
dcd3399689 Fixed Security header not clickable 2024-08-15 01:44:48 +08:00
593b57094c Added DeleteAllEmail function 2024-08-15 01:29:11 +08:00
549dc1efde Updated SettingScreen
Adjusted QRScannerscreen when calling settingscreen
2024-08-14 17:13:03 +08:00
170997097f Added QR Tips and fixed fetchUser Email 2024-08-14 13:36:39 +08:00
187fd768e6 Added DeleteEmail button
Fixed email contianer to not be affected by polling

added banner when no internet connections
2024-08-14 11:35:43 +08:00
785e84c4f2 Fixed unclickable area in EmailScreen 2024-08-13 22:37:21 +08:00
22277e3a6d Fixed ScanQrCode from Image with RNQRGenerator 2024-08-13 19:39:55 +08:00
0f18b55aad Restored expo-camera to be used for readQRFromImage, temporary for reference 2024-08-13 19:31:16 +08:00
b347665453 Updated 4 checks on scannedDataBox 2024-08-13 17:09:35 +08:00
d33b97b45b Updated links on settingScreen 2024-08-12 21:07:03 +08:00
5f20256c6e Updated EmailScreen to pass QR-ID to ScannedDataBox 2024-08-12 20:44:10 +08:00
9b81225fb1 added expo-linking to allow SMSTO & EMAILTO 2024-08-12 18:44:24 +08:00
db115f1a58 Fixed the Redirect Logic for UI 2024-08-12 16:18:31 +08:00
d810f3ef8c Update Wifi UI, Pending fix of connect to wifi button 2024-08-12 15:51:40 +08:00
fcc6f76c99 Update scannedDataBox for URL type(minimal viable logic) 2024-08-12 15:28:47 +08:00
fcb710a888 added back icons for Scanned Data box. Pending to verify all dynamic ScannedDataBox views. (Incomplete Testing) 2024-08-12 02:08:34 +08:00
e4cc584924 Refactored with a new Camera Library:(react-native-vision-camera). Need fix scanQRCodeFromImage 2024-08-12 01:14:54 +08:00
bfd79fbf73 With correct package-lock.json 2024-08-11 21:53:53 +08:00
cad9f2c097 added Polling Control with useFocusEffect to prevent GetScannedEmail on other screen 2024-08-11 21:23:03 +08:00
80e38660cd Fixed the EmailScreen Logic and moved banner to middle 2024-08-11 21:13:43 +08:00
01fa62a58f Restored last working pacjage.json - prevent build failure on native_modules.gradle line: 401 2024-08-11 20:24:50 +08:00
202e04e272 Debugging Raw Scanned Barcode Data, added sample images and log to onBarcodeScan 2024-08-11 00:12:48 +08:00
9972de364f Added logic to auto fetch Google Access Token and Refresh Token for EmailScreen 2024-08-10 23:24:04 +08:00
5f974c8d71 updated email screen to be responsive, fixed banner and fetchUserEmail button 2024-08-10 23:15:52 +08:00
b21e270f56 Update Logo and Scanned DataBox Logic 2024-08-10 22:04:05 +08:00
ca4a92f5f0 Updtated EmailScreen for useremail and resycn button, added banner when resync. Added marginTop padding for all screens 2024-08-07 15:54:21 +08:00
f893f3285a ReCommit: Removed header for all screens. Updated QRScanner Screen UI to be responsive to screen size 2024-08-07 12:07:17 +08:00
7cb5cbbe34 updated UI for relative screensize. Handle long payload by truncating the displayes content 2024-08-06 11:38:40 +08:00
6032aebd6a Updated ScannedDataBox UI 2024-08-05 15:14:41 +08:00
20cb565f2f updated getEmail and getScannedEmail endpoints. Added Polling and Rescan Inbox feature for EmailScreen.tsx 2024-08-04 22:17:40 +08:00
32a8f5d30f Working on getEmails.Able to fetch email and load in Flatlist. Pending refresh featurea and to handle status 202 2024-08-04 16:56:35 +08:00
7f7f686ef8 Recommit, without build falire (native_modules.gradle' line: 401, finished with non-zero exit value 1) 2024-08-03 23:07:41 +08:00
heyethereum
804eb6f024 added inteceptor to include diff headers in diff env 2024-08-03 13:47:24 +08:00
heyethereum
1fdfd94baf fix node not loading correct env files 2024-08-03 10:29:35 +08:00
heyethereum
28f6ab3553 merge changes in package.son 2024-08-03 09:13:14 +08:00
278f51e606 Loading sample email on EmailScreen.tsx 2024-07-28 23:18:35 +08:00
48144f88b5 Added Setting button to QRScannerScreen, call SettingScreen as Modal instead 2024-07-28 22:21:24 +08:00
b2ff312537 Re Commit, undo import ofDrawerNavigatore as app could not launch 2024-07-28 21:09:38 +08:00
8f7aa6862b Added Email to bottomCustomTabBar, Added EmailScreen. Pending setting drawer on main page 2024-07-28 21:06:59 +08:00
73 changed files with 5224 additions and 5676 deletions

View File

@@ -1 +1,2 @@
NODE_ENV=development
BASE_URL=http://192.168.1.30:8080 BASE_URL=http://192.168.1.30:8080

View File

@@ -1,2 +1,3 @@
BASE_URL=https://bk5wiynzsi.execute-api.ap-southeast-1.amazonaws.com/api/ NODE_ENV=production
BASE_URL=https://bk5wiynzsi.execute-api.ap-southeast-1.amazonaws.com/api
ENV=production ENV=production

41
.gitignore vendored
View File

@@ -55,3 +55,44 @@ amplifytools.xcconfig
.secret-* .secret-*
**.sample **.sample
#amplify-do-not-edit-end #amplify-do-not-edit-end
# @generated expo-cli sync-b5df6a44d8735348b729920a7406b633cfb74d4c
# The following patterns were generated by expo-cli
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# @end expo-cli

34
App.tsx
View File

@@ -1,21 +1,19 @@
import './gesture-handler'; import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import QRScannerScreen from './screens/QRScannerScreen'; import QRScannerScreen from './screens/QRScannerScreen';
import HistoryScreen from './screens/HistoryScreen'; import HistoryScreen from './screens/HistoryScreen';
import SettingsScreen from './screens/SettingsScreen'; import EmailScreen from './screens/EmailScreen'; // Import the Email screen
import { QRCodeContext } from './types'; import { QRCodeContext } from './types';
import CustomTabBar from './components/CustomTabBar'; import CustomTabBar from './components/CustomTabBar';
import store from './store'; import store from './store';
import { withAuthenticator } from '@aws-amplify/ui-react-native'; import { withAuthenticator } from '@aws-amplify/ui-react-native';
import { Amplify } from 'aws-amplify'; import { Amplify } from 'aws-amplify';
import config from './src/aws-exports'; import config from './src/aws-exports';
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
import { useKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import { View } from 'react-native';
enableScreens(); enableScreens();
@@ -28,26 +26,38 @@ const App: React.FC = () => {
const clearScanData = () => { const clearScanData = () => {
setScannedData(''); setScannedData('');
console.log('ClearScanedDATa');
}; };
useEffect(() => {
deactivateKeepAwake(); // Allow the screen to timeout
}, []);
return ( return (
<Provider store={store}> <Provider store={store}>
<QRCodeContext.Provider value={{ scannedData, setScannedData }}> <QRCodeContext.Provider value={{ scannedData, setScannedData }}>
<NavigationContainer> <NavigationContainer>
<Tab.Navigator <Tab.Navigator
initialRouteName="QRScanner" initialRouteName="QRScanner"
tabBar={(props) => <CustomTabBar {...props} clearScanData={clearScanData} />} tabBar={(props) => <CustomTabBar {...props} clearScanData={clearScanData} />}
> screenOptions={{ headerShown: false }} // turn of header for all screens
>
<Tab.Screen name="History" component={HistoryScreen} /> <Tab.Screen name="History" component={HistoryScreen} />
<Tab.Screen name="QRScanner"> <Tab.Screen name="QRScanner">
{(props) => <QRScannerScreen {...props} clearScanData={clearScanData} />} {(props) => <QRScannerScreen {...props} clearScanData={clearScanData} />}
</Tab.Screen> </Tab.Screen>
<Tab.Screen name="Settings" component={SettingsScreen} /> <Tab.Screen name="Email" component={EmailScreen} />
</Tab.Navigator> </Tab.Navigator>
</NavigationContainer> </NavigationContainer>
</QRCodeContext.Provider> </QRCodeContext.Provider>
</Provider> </Provider>
); );
}; };
export default withAuthenticator(App); export default withAuthenticator(App);

View File

@@ -110,6 +110,7 @@ android {
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
} }
} }
packagingOptions { packagingOptions {

View File

@@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.safeqr.safeqr",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
}

View File

@@ -1,6 +1,3 @@
import android.os.Bundle;
package com.safeqr.safeqr package com.safeqr.safeqr
import android.os.Build import android.os.Build

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -25,6 +25,9 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64 # ./gradlew <task> -PreactNativeArchitectures=x86_64
@@ -54,3 +57,5 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK. # Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false expo.useLegacyPackaging=false
VisionCamera_enableCodeScanner=true

BIN
android/keystore Normal file

Binary file not shown.

View File

@@ -1,116 +1,270 @@
import axios from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
const { API_BASE_URL } = Constants.expoConfig.extra; const { API_BASE_URL, ENVIRONMENT } = Constants.expoConfig.extra;
//const API_BASE_URL = 'http://192.168.1.30:8080/v1/qrcodetypes'; import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth';
const API_URL_DETECT = "/v1/qrcodetypes/detect";
const API_URL_VERIFY_URL = "/v1/qrcodetypes/verifyURL" const API_URL_SCAN = "/v1/qrcodetypes/scan";
const API_URL_VIRUS_TOTAL_CHECK = "/v1/qrcodetypes/virusTotalCheck" const API_URL_GET_QR_DETAILS = "/v1/qrcodetypes/getQRDetails";
const API_URL_CHECK_REDIRECTS = "/v1/qrcodetypes/checkRedirects"
const API_URL_GET_HISTORIES = "/v1/user/getScannedHistories"
const API_URL_DELETE_SCANNED_HISTORY = "/v1/user/deleteScannedHistories" const API_URL_GET_HISTORIES = "/v1/user/getScannedHistories";
const API_URL_GET_BOOKMARKS = "/v1/user/getBookmarks" const API_URL_DELETE_SCANNED_HISTORY = "/v1/user/deleteScannedHistories";
const API_URL_SET_BOOKMARK = "/v1/user/setBookmark" const API_URL_DELETE_ALL_HISTORIES = "/v1/user/deleteAllScannedHistories";
const API_URL_DELETE_BOOKMARK = "/v1/user/deleteBookmark" const API_URL_GET_BOOKMARKS = "/v1/user/getBookmarks";
const API_URL_SET_BOOKMARK = "/v1/user/setBookmark";
const API_URL_DELETE_BOOKMARK = "/v1/user/deleteBookmark";
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({
baseURL: API_BASE_URL,
});
// Request interceptor to set headers based on env
apiClient.interceptors.request.use(
async (config) => {
const token = await fetchIdToken();
const userId = await fetchUserId();
if (ENVIRONMENT === 'production') {
if (!config.headers.Authorization) {
config.headers.Authorization = `Bearer ${token}`;
}
} else {
if (!config.headers['X-USER-ID']) {
config.headers['X-USER-ID'] = userId;
}
}
// Log the X-USER-ID header
console.log('X-USER-ID:', config.headers['X-USER-ID']);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Define a generic function to handle all types of requests // Define a generic function to handle all types of requests
export const apiRequest = async (config) => { export const apiRequest = async (config: AxiosRequestConfig<any>) => {
try { try {
console.log(`API Call - ${config.method.toUpperCase()}:`, config.url, config.data || ''); const methodName = config.method?.toUpperCase() || 'REQUEST';
console.log("ENVIRONMENT:", ENVIRONMENT);
console.log(`API Call - ${methodName}:`, config.url, config.data || '');
console.log(config); console.log(config);
const response = await apiClient(config);
const response = await axios(config); console.log(`API Response for ${methodName}:`, response.data);
console.log('API Response:', response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
const methodName = config.method?.toUpperCase() || 'REQUEST';
if (error.response) { if (error.response) {
// The request was made and the server responded with a status code that falls out of the range of 2xx console.error(`API Error - Response for ${methodName}:`, error.response.data);
console.error('API Error - Response:', error.response.data);
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received console.error(`API Error - No Response for ${methodName}:`, error.request);
console.error('API Error - No Response:', error.request);
} else { } else {
// Something happened in setting up the request that triggered an Error console.error(`API Error - General for ${methodName}:`, error.message);
console.error('API Error - General:', error.message);
} }
throw error; throw error;
} }
}; };
export const detectQRCodeType = async (data) => { // Function to get the token
const fetchIdToken = async () => {
const { tokens } = await fetchAuthSession();
return tokens.idToken.toString();
};
// Function to get the user ID
const fetchUserId = async () => {
const currentUser = await getCurrentUser();
return currentUser.userId;
};
// Function to handle /scan request
export const scanQRCode = async (data: string) => {
return apiRequest({ return apiRequest({
method: 'post', method: 'post',
url: `${API_BASE_URL}${API_URL_DETECT}`, url: `${API_BASE_URL}${API_URL_SCAN}`,
data: { data } data: { data }
}); });
}; };
export const verifyURL = async (data) => { // Function to get QR code details
return apiRequest({ export const getQRCodeDetails = async (qrCodeId: string) => {
method: 'post',
url: `${API_BASE_URL}${API_URL_VERIFY_URL}`,
data: { data }
});
};
export const virusTotalCheck = async (data) => {
return apiRequest({
method: 'post',
url: `${API_BASE_URL}${API_URL_VIRUS_TOTAL_CHECK}`,
data: { data }
});
};
export const checkRedirects = async (data) => {
return apiRequest({
method: 'post',
url: `${API_BASE_URL}${API_URL_CHECK_REDIRECTS}`,
data: { data }
});
};
// GET User's Scanned Histories
export const getScannedHistories = async (userId: string) => {
return apiRequest({ return apiRequest({
method: 'get', method: 'get',
url: `${API_BASE_URL}${API_URL_GET_HISTORIES}`, url: `${API_BASE_URL}${API_URL_GET_QR_DETAILS}`,
headers: { "X-USER-ID": userId }, headers: { 'QR-ID': qrCodeId },
}); });
}; };
// GET All User's Bookmark
export const getAllUserBookmarks = async (userId: string) => { //-----------
// GET User's Scanned Histories
export const getScannedHistories = async () => {
return apiRequest({
method: 'get',
url: `${API_BASE_URL}${API_URL_GET_HISTORIES}`
});
};
// GET All User's Bookmarks
export const getAllUserBookmarks = async () => {
return apiRequest({ return apiRequest({
method: 'get', method: 'get',
url: `${API_BASE_URL}${API_URL_GET_BOOKMARKS}`, url: `${API_BASE_URL}${API_URL_GET_BOOKMARKS}`,
headers: { "X-USER-ID": userId },
}); });
}; };
// Create Bookmark on QR Code // Create Bookmark on QR Code
export const setBookmark = async (userId: string, qrCodeId: string) => { export const setBookmark = async (qrCodeId: string) => {
return apiRequest({ return apiRequest({
method: 'post', method: 'post',
url: `${API_BASE_URL}${API_URL_SET_BOOKMARK}`, url: `${API_BASE_URL}${API_URL_SET_BOOKMARK}`,
headers: { "X-USER-ID": userId}, data: { qrCodeId }
data: { "qrCodeId": qrCodeId }
}); });
}; };
// Delete single bookmark // Delete single bookmark
export const deleteBookmark = async (userId: string, qrCodeId: string) => { export const deleteBookmark = async (qrCodeId: string) => {
return apiRequest({ return apiRequest({
method: 'put', method: 'put',
url: `${API_BASE_URL}${API_URL_DELETE_BOOKMARK}`, url: `${API_BASE_URL}${API_URL_DELETE_BOOKMARK}`,
headers: { "X-USER-ID": userId}, data: { qrCodeId }
data: { "qrCodeId": qrCodeId }
}); });
}; };
// Delete Single Scanned History // Delete Single Scanned History
export const deleteScannedHistory = async (userId: string, qrCodeId: string) => { export const deleteScannedHistory = async (qrCodeId: string) => {
return apiRequest({ return apiRequest({
method: 'put', method: 'put',
url: `${API_BASE_URL}${API_URL_DELETE_SCANNED_HISTORY}`, url: `${API_BASE_URL}${API_URL_DELETE_SCANNED_HISTORY}`,
headers: { "X-USER-ID": userId}, data: { qrCodeId }
data: { "qrCodeId": qrCodeId }
}); });
};
// Function to delete all scanned histories
export const deleteAllScannedHistories = async () => {
return apiRequest({
method: 'put',
url: `${API_BASE_URL}${API_URL_DELETE_ALL_HISTORIES}`,
});
};
// GET already scanned emails from DB
export const getScannedEmails = async () => {
console.log("getScannedEmails function called");
try {
console.log("Making API request to get already scanned emails from the database");
const response = await apiRequest({
method: 'get',
url: `${API_BASE_URL}${API_URL_GET_SCANNED_EMAILS}`
});
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 for getScannedEmails:", error.response.data);
} else if (error.request) {
console.error("Request error, no response received for getScannedEmails:", error.request);
} else {
console.error("Error message for getScannedEmails:", error.message);
}
throw error;
}
};
// Function to start the scanning of inbox in the server
export const getEmails = async (accessToken: string, refreshToken: string) => {
console.log("getEmails function called");
try {
console.log("Making API request to get Gmail emails with accessToken and refreshToken");
const response = await apiRequest({
method: 'get',
url: `${API_BASE_URL}${API_URL_GET_EMAILS}`,
headers: {
accessToken,
refreshToken,
},
});
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 for getEmails:", error.response.data);
} else if (error.request) {
console.error("Request error, no response received for getEmails:", error.request);
} else {
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}`,
});
};
// 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 fetch QR code tips
export const getQRTips = async () => {
try {
const response = await apiRequest({
method: 'get',
url: `${API_BASE_URL}${API_URL_TIPS_GET}`,
});
return response;
} catch (error) {
console.error('Error fetching QR tips:', error);
throw error;
}
};
// Function to delete all emails
export const deleteAllEmails = async () => {
try {
const response = await apiRequest({
method: 'put',
url: `${API_BASE_URL}${API_URL_GMAIL_DELETE_ALL_MESSAGES}`,
});
return response; // Assuming the response contains the message
} catch (error) {
console.error('Error deleting all emails:', error);
throw error;
}
}; };

View File

@@ -1,10 +1,18 @@
import 'dotenv/config'; import 'dotenv/config';
console.log('Current NODE_ENV:', process.env.NODE_ENV);
console.log('Current BASE_URL:', process.env.BASE_URL);
console.log('Current ENVFILE:', process.env.ENVFILE);
export default ({ config }) => { export default ({ config }) => {
return { return {
...config, ...config,
extra: { extra: {
API_BASE_URL: process.env.BASE_URL, API_BASE_URL: process.env.BASE_URL,
ENVIRONMENT: process.env.NODE_ENV,
eas: {
projectId: "88ad983d-5ca3-44e6-bc1b-8a9a941af992"
}
}, },
}; };
}; };

View File

@@ -26,6 +26,17 @@
"projectId": "88ad983d-5ca3-44e6-bc1b-8a9a941af992" "projectId": "88ad983d-5ca3-44e6-bc1b-8a9a941af992"
} }
}, },
"plugins": [
[
"react-native-vision-camera",
{
"cameraPermissionText": "$(PRODUCT_NAME) needs access to your Camera.",
"enableMicrophonePermission": true,
"microphonePermissionText": "$(PRODUCT_NAME) needs access to your Microphone.",
"enableCodeScanner": true
}
]
],
"owner": "piggyinu" "owner": "piggyinu"
} }
} }

BIN
assets/SafeQR_Logo 1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
assets/bakcup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -1,17 +1,15 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons, MaterialIcons } from '@expo/vector-icons';
// Define custom props for CustomTabBar // Define custom props for CustomTabBar
interface CustomTabBarProps extends BottomTabBarProps { interface CustomTabBarProps extends BottomTabBarProps {
clearScanData: () => void; clearScanData: () => void;
} }
// Custom tab bar component with typings // Custom tab bar component with typings
const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, navigation, clearScanData }) => { const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, navigation, clearScanData }) => {
return ( return (
<View style={styles.tabBar}> <View style={styles.tabBar}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
@@ -25,13 +23,13 @@ const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, naviga
const isFocused = state.index === index; const isFocused = state.index === index;
// Event handler for tab press // Event handler for tab press
const onPress = () => { const onPress = () => {
const event = navigation.emit({ const event = navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true canPreventDefault: true,
}); });
if (!isFocused && !event.defaultPrevented) { if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name); navigation.navigate(route.name);
@@ -53,7 +51,9 @@ const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, naviga
}); });
}; };
const iconName = route.name === 'QRScanner' ? 'camera' : route.name === 'History' ? 'time' : 'settings'; // Define the icon for each tab
const iconName =
route.name === 'QRScanner' ? 'camera' : route.name === 'History' ? 'time' : 'settings';
return ( return (
<TouchableOpacity <TouchableOpacity
@@ -66,7 +66,11 @@ const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, naviga
onLongPress={onLongPress} onLongPress={onLongPress}
style={styles.tabButton} style={styles.tabButton}
> >
<Ionicons name={iconName} size={24} color={isFocused ? '#ff69b4' : '#222'} /> {route.name === 'Email' ? (
<MaterialIcons name="email" size={24} color={isFocused ? '#ff69b4' : '#222'} />
) : (
<Ionicons name={iconName} size={24} color={isFocused ? '#ff69b4' : '#222'} />
)}
{/* Check if label is a string before rendering */} {/* Check if label is a string before rendering */}
{typeof label === 'string' ? ( {typeof label === 'string' ? (
<Text style={{ color: isFocused ? '#ff69b4' : '#222' }}> <Text style={{ color: isFocused ? '#ff69b4' : '#222' }}>
@@ -77,13 +81,15 @@ const CustomTabBar: React.FC<CustomTabBarProps> = ({ state, descriptors, naviga
); );
})} })}
<View style={styles.floatingButton}> <View style={styles.floatingButton}>
<TouchableOpacity onPress={() => { <TouchableOpacity
clearScanData(); onPress={() => {
navigation.reset({ clearScanData();
index: 0, navigation.reset({
routes: [{ name: 'QRScanner' }], index: 0,
}); routes: [{ name: 'QRScanner' }],
}}> });
}}
>
<Ionicons name="camera" size={28} color="#fff" /> <Ionicons name="camera" size={28} color="#fff" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>

File diff suppressed because it is too large Load Diff

8
index.js Normal file
View File

@@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

View File

@@ -13,7 +13,7 @@ PODS:
- EXJSONUtils (0.13.1) - EXJSONUtils (0.13.1)
- EXManifests (0.14.3): - EXManifests (0.14.3):
- ExpoModulesCore - ExpoModulesCore
- Expo (51.0.18): - Expo (51.0.24):
- ExpoModulesCore - ExpoModulesCore
- expo-dev-client (4.0.19): - expo-dev-client (4.0.19):
- EXManifests - EXManifests
@@ -224,13 +224,13 @@ PODS:
- Yoga - Yoga
- ExpoAsset (10.0.10): - ExpoAsset (10.0.10):
- ExpoModulesCore - ExpoModulesCore
- ExpoCamera (15.0.13): - ExpoCamera (15.0.14):
- ExpoModulesCore - ExpoModulesCore
- ZXingObjC/OneD - ZXingObjC/OneD
- ZXingObjC/PDF417 - ZXingObjC/PDF417
- ExpoFileSystem (17.0.1): - ExpoFileSystem (17.0.1):
- ExpoModulesCore - ExpoModulesCore
- ExpoFont (12.0.7): - ExpoFont (12.0.9):
- ExpoModulesCore - ExpoModulesCore
- ExpoImageManipulator (12.0.5): - ExpoImageManipulator (12.0.5):
- EXImageLoader - EXImageLoader
@@ -240,7 +240,7 @@ PODS:
- ExpoModulesCore - ExpoModulesCore
- ExpoKeepAwake (13.0.2): - ExpoKeepAwake (13.0.2):
- ExpoModulesCore - ExpoModulesCore
- ExpoModulesCore (1.12.18): - ExpoModulesCore (1.12.20):
- DoubleConversion - DoubleConversion
- glog - glog
- hermes-engine - hermes-engine
@@ -1217,9 +1217,9 @@ PODS:
- React-Core - React-Core
- react-native-netinfo (11.3.1): - react-native-netinfo (11.3.1):
- React-Core - React-Core
- react-native-safe-area-context (4.10.8): - react-native-safe-area-context (4.10.5):
- React-Core - React-Core
- react-native-webview (13.10.4): - react-native-webview (13.8.6):
- DoubleConversion - DoubleConversion
- glog - glog
- hermes-engine - hermes-engine
@@ -1825,19 +1825,19 @@ SPEC CHECKSUMS:
EXImageLoader: ab589d67d6c5f2c33572afea9917304418566334 EXImageLoader: ab589d67d6c5f2c33572afea9917304418566334
EXJSONUtils: 30c17fd9cc364d722c0946a550dfbf1be92ef6a4 EXJSONUtils: 30c17fd9cc364d722c0946a550dfbf1be92ef6a4
EXManifests: c1fab4c3237675e7b0299ea8df0bcb14baca4f42 EXManifests: c1fab4c3237675e7b0299ea8df0bcb14baca4f42
Expo: 56b642d0930789fc847dc7f424d2d599dfe59a5e Expo: 798848eae1daf13363d69790986146b08d0cf92f
expo-dev-client: bcca43a56437123873b32a36ea31568e4212c1ca expo-dev-client: bcca43a56437123873b32a36ea31568e4212c1ca
expo-dev-launcher: ab76344f7a72c7b6bb255280a66202655ecf1965 expo-dev-launcher: ab76344f7a72c7b6bb255280a66202655ecf1965
expo-dev-menu: 9b9886b383163f14821e624f3eb51a2084491aa2 expo-dev-menu: 9b9886b383163f14821e624f3eb51a2084491aa2
expo-dev-menu-interface: be32c09f1e03833050f0ee290dcc86b3ad0e73e4 expo-dev-menu-interface: be32c09f1e03833050f0ee290dcc86b3ad0e73e4
ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875 ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875
ExpoCamera: c5be8a28a769c4dee3a02e017e5b55415d9beb2c ExpoCamera: a5d000b22cd7dfd2c5904ed960e549de42c96da0
ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51 ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51
ExpoFont: 43b69559cef3d773db57c7ae7edd3cb0aa0dc610 ExpoFont: e7f2275c10ca8573c991e007329ad6bf98086485
ExpoImageManipulator: aea99205c66043a00a0af90e345395637b9902fa ExpoImageManipulator: aea99205c66043a00a0af90e345395637b9902fa
ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816 ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08 ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
ExpoModulesCore: 30e1ed4659356cb9a84e0e5ddf1d090d735973c1 ExpoModulesCore: 5440e96a8ee014f4fd88e77264985fd0a65f5f8c
ExpoSharing: 8db05dd85081219f75989a3db2c92fe5e9741033 ExpoSharing: 8db05dd85081219f75989a3db2c92fe5e9741033
EXUpdatesInterface: 996527fd7d1a5d271eb523258d603f8f92038f24 EXUpdatesInterface: 996527fd7d1a5d271eb523258d603f8f92038f24
FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2 FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2
@@ -1871,8 +1871,8 @@ SPEC CHECKSUMS:
React-Mapbuffer: 9f68550e7c6839d01411ac8896aea5c868eff63a React-Mapbuffer: 9f68550e7c6839d01411ac8896aea5c868eff63a
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321 react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371 react-native-safe-area-context: a240ad4b683349e48b1d51fed1611138d1bdad97
react-native-webview: 596fb33d67a3cde5a74bf1f6b4c28d3543477fdd react-native-webview: 05bae3a03a1e4f59568dfc05286c0ebf8954106c
React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb
React-NativeModulesApple: 585d1b78e0597de364d259cb56007052d0bda5e5 React-NativeModulesApple: 585d1b78e0597de364d259cb56007052d0bda5e5
React-perflogger: 7bb9ba49435ff66b666e7966ee10082508a203e8 React-perflogger: 7bb9ba49435ff66b666e7966ee10082508a203e8

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -22,15 +22,12 @@
<rect key="frame" x="0" y="0" width="414" height="736"/> <rect key="frame" x="0" y="0" width="414" height="736"/>
</imageView> </imageView>
</subviews> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="1gX-mQ-vu6"/> <constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="1gX-mQ-vu6"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="6tX-OG-Sck"/> <constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="6tX-OG-Sck"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="ABX-8g-7v4"/> <constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="ABX-8g-7v4"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="jkI-2V-eW5"/> <constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="jkI-2V-eW5"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="2VS-Uz-0LU"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="LhH-Ei-DKo"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="I6l-TP-6fn"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="nbp-HC-eaG"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/> <constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/> <constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/> <constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>

7
metro.config.js Normal file
View File

@@ -0,0 +1,7 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = config;

View File

@@ -6,25 +6,23 @@ import HistoryScreen from '../screens/HistoryScreen';
import SettingsScreen from '../screens/SettingsScreen'; import SettingsScreen from '../screens/SettingsScreen';
import CustomTabBar from '../components/CustomTabBar'; import CustomTabBar from '../components/CustomTabBar';
// Create a bottom tab navigator
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
// Main navigation component
const AppNavigator = () => { const AppNavigator = () => {
return ( return (
// Wrap the navigator in a NavigationContainer to manage the navigation tree
<NavigationContainer> <NavigationContainer>
<Tab.Navigator
{/* Define the tab navigator with custom tab bar and initial route */} initialRouteName="QRScanner"
<Tab.Navigator initialRouteName="QR Scanner" tabBar={props => <CustomTabBar {...props} />}> tabBar={props => <CustomTabBar clearScanData={function (): void {
throw new Error('Function not implemented.');
{/* Define each tab with a name and corresponding component */} } } {...props} />}
>
<Tab.Screen name="History" component={HistoryScreen} /> <Tab.Screen name="History" component={HistoryScreen} />
<Tab.Screen name="QR Scanner" component={QRScannerScreen} /> <Tab.Screen name="QRScanner" component={QRScannerScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} /> <Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator> </Tab.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}; };
export default AppNavigator; export default AppNavigator;

7882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
{ {
"name": "safeqr", "name": "safeqr",
"version": "1.0.0", "version": "1.0.0",
"main": "expo/AppEntry.js",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start --dev-client",
"android:dev": "ENVFILE=.env.development expo run:android", "android:dev": "NODE_ENV=development ENVFILE=.env.development expo run:android",
"android:prod": "ENVFILE=.env.production expo run:android", "android:prod": "NODE_ENV=production ENVFILE=.env.production expo run:android",
"ios:dev": "ENVFILE=.env.development expo run:ios", "ios:dev": "NODE_ENV=development ENVFILE=.env.development expo run:ios",
"ios:prod": "ENVFILE=.env.production expo run:ios", "ios:prod": "NODE_ENV=production ENVFILE=.env.production expo run:ios",
"web": "expo start --web" "web": "expo start --web",
"android": "expo run:android",
"ios": "expo run:ios"
}, },
"dependencies": { "dependencies": {
"@aws-amplify/auth": "^6.3.10", "@aws-amplify/auth": "^6.3.10",
@@ -21,30 +22,42 @@
"@react-navigation/bottom-tabs": "^6.5.20", "@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/drawer": "^6.7.2", "@react-navigation/drawer": "^6.7.2",
"@react-navigation/native": "^6.1.17", "@react-navigation/native": "^6.1.17",
"@react-navigation/stack": "^6.4.1",
"@reduxjs/toolkit": "^2.2.6", "@reduxjs/toolkit": "^2.2.6",
"@zxing/library": "^0.21.2",
"aws-amplify": "^6.4.2", "aws-amplify": "^6.4.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"expo": "~51.0.17", "expo": "~51.0.24",
"expo-camera": "~15.0.13", "expo-barcode-scanner": "~13.0.1",
"expo-dev-client": "~4.0.19", "expo-camera": "~15.0.14",
"expo-dev-client": "~4.0.22",
"expo-image-manipulator": "^12.0.5", "expo-image-manipulator": "^12.0.5",
"expo-image-picker": "~15.0.7", "expo-image-picker": "~15.0.7",
"expo-intent-launcher": "~11.0.1",
"expo-linking": "~6.3.1",
"expo-sharing": "~12.0.1", "expo-sharing": "~12.0.1",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"jpeg-js": "^0.4.4",
"jsqr": "^1.4.0",
"react": "18.2.0", "react": "18.2.0",
"react-native": "^0.74.3", "react-native": "0.74.5",
"react-native-canvas": "^0.1.40",
"react-native-dotenv": "^3.4.11", "react-native-dotenv": "^3.4.11",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.16.1", "react-native-gesture-handler": "~2.16.1",
"react-native-get-random-values": "^1.11.0", "react-native-get-random-values": "^1.11.0",
"react-native-image-picker": "^7.1.2",
"react-native-qrcode-svg": "^6.3.1", "react-native-qrcode-svg": "^6.3.1",
"react-native-reanimated": "~3.10.1", "react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "^4.10.4", "react-native-safe-area-context": "^4.10.5",
"react-native-screens": "3.31.1", "react-native-screens": "3.31.1",
"react-native-svg": "15.2.0", "react-native-svg": "15.2.0",
"react-native-webview": "^13.10.4", "react-native-vision-camera": "^4.5.1",
"react-native-webview": "^13.8.6",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"redux": "^5.0.1" "redux": "^5.0.1",
"rn-qr-generator": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",

View File

@@ -18,7 +18,7 @@ export const toggleBookmark = createAsyncThunk(
'qrCodes/toggleBookmark', 'qrCodes/toggleBookmark',
async ({ userId, qrCode }: { userId: string, qrCode: QRCodeType }, { dispatch, rejectWithValue }) => { async ({ userId, qrCode }: { userId: string, qrCode: QRCodeType }, { dispatch, rejectWithValue }) => {
try { try {
await (qrCode.bookmarked ? deleteBookmark(userId, qrCode.data.id) : setBookmark(userId, qrCode.data.id)); await (qrCode.bookmarked ? deleteBookmark(qrCode.data.id) : setBookmark(qrCode.data.id));
// Dispatch the action to update local state // Dispatch the action to update local state
dispatch(toggleBookmarkInState(qrCode)); dispatch(toggleBookmarkInState(qrCode));
return qrCode; return qrCode;
@@ -33,7 +33,7 @@ export const deleteQRCode = createAsyncThunk(
'qrCodes/deleteQRCode', 'qrCodes/deleteQRCode',
async ({ userId, qrCodeId }: { userId: string, qrCodeId: string }, { dispatch, rejectWithValue }) => { async ({ userId, qrCodeId }: { userId: string, qrCodeId: string }, { dispatch, rejectWithValue }) => {
try { try {
await deleteScannedHistory(userId, qrCodeId); await deleteScannedHistory(qrCodeId);
dispatch(deleteQRCodeInState(qrCodeId)); dispatch(deleteQRCodeInState(qrCodeId));
return qrCodeId; return qrCodeId;
} catch (error) { } catch (error) {

471
screens/EmailScreen.tsx Normal file
View File

@@ -0,0 +1,471 @@
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, deleteEmail } from '../api/qrCodeAPI';
import { fetchAuthSession } from 'aws-amplify/auth';
import { Buffer } from 'buffer';
import ScannedDataBox from '../components/ScannedDataBox';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const EmailScreen: React.FC = () => {
const [selectedMessage, setSelectedMessage] = useState(null);
const [emailData, setEmailData] = useState(null);
const [loading, setLoading] = useState(true);
const [rescanLoading, setRescanLoading] = useState(false);
const [error, setError] = useState(null);
const [userEmail, setUserEmail] = useState('');
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedQrCodeId, setSelectedQrCodeId] = useState(null);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [messageToDelete, setMessageToDelete] = useState(null);
const [emptyMessage, setEmptyMessage] = useState('');
const [bannerOpacity] = useState(new Animated.Value(0));
useEffect(() => {
fetchUserEmail();
}, []);
const fetchUserEmail = async () => {
try {
const userInfo = await getUserInfo();
if (userInfo && userInfo.email) {
setUserEmail(userInfo.email);
if (userInfo.source === 'Google') {
startInboxScanning();
} else {
setEmptyMessage('Please Sign in with Google to scan your Gmails for QR Codes');
setLoading(false);
}
} else {
setEmptyMessage('Please Sign in with Google to scan your Gmails for QR Codes');
setLoading(false);
}
} catch (error) {
setEmptyMessage('Please Sign in with Google to scan your Gmails for QR Codes');
setLoading(false);
}
};
const startInboxScanning = async () => {
setRescanLoading(true);
showBanner(); // Show the scanning banner
try {
const { tokens } = await fetchAuthSession();
const idToken = tokens.idToken.toString();
const parts = idToken.split('.');
const payload = parts[1];
const decodedPayload = Buffer.from(payload, 'base64').toString('utf8');
const parsedPayload = JSON.parse(decodedPayload);
const googleAccessToken = parsedPayload["custom:access_token"];
const googleRefreshToken = parsedPayload["custom:refresh_token"];
if (googleAccessToken && googleRefreshToken) {
await getEmails(googleAccessToken, googleRefreshToken);
setRescanLoading(false);
} else {
setError('Google access token or refresh token missing.');
setRescanLoading(false);
}
} catch (error) {
setError('Error rescanning inbox.');
setRescanLoading(false);
}
};
const startPollingForScannedEmails = useCallback(() => {
const pollingInterval = setInterval(async () => {
try {
const scannedEmails = await getScannedEmails();
if (scannedEmails && scannedEmails.messages && scannedEmails.messages.length > 0) {
setEmailData((prevEmailData) => {
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;
}
});
} else {
setEmptyMessage('No Emails with QR Code');
}
setLoading(false);
} catch (error) {
setEmptyMessage('No Emails with QR Code');
setLoading(false);
}
}, 10000);
return () => clearInterval(pollingInterval);
}, [selectedMessage]);
const handleSelectMessage = (message) => {
setSelectedMessage(selectedMessage === message ? null : message);
};
const refreshScannedEmails = async () => {
try {
const scannedEmails = await getScannedEmails();
setEmailData(scannedEmails);
} catch (error) {
setError('Error refreshing emails.');
}
};
useFocusEffect(
useCallback(() => {
const stopPolling = startPollingForScannedEmails();
return () => {
stopPolling();
};
}, [startPollingForScannedEmails])
);
const handleUrlClick = (id) => {
setSelectedQrCodeId(id);
setIsModalVisible(true);
};
const handleDeleteEmail = async (messageId: string) => {
try {
await deleteEmail(messageId);
setEmailData((prevEmailData) => {
const updatedMessages = prevEmailData.messages.filter((message) => message.messageId !== messageId);
if (updatedMessages.length === 0) {
setEmptyMessage('No Emails with QR Code');
}
return {
...prevEmailData,
messages: updatedMessages,
};
});
} catch (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);
}
};
const showBanner = () => {
Animated.timing(bannerOpacity, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start(() => {
setTimeout(() => {
Animated.timing(bannerOpacity, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}).start();
}, 3000);
});
};
return (
<View style={styles.container}>
{/* Header with Email and Refresh Button */}
<View style={styles.headerContainer}>
<Text style={styles.emailHeader}>Email: {userEmail}</Text>
<TouchableOpacity onPress={refreshScannedEmails} style={styles.refreshButton}>
<Ionicons name="refresh" size={24} color="#ff69b4" />
</TouchableOpacity>
</View>
{/* Loading and Empty Message */}
{loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#ff69b4" />
<Text style={{ color: '#ff69b4' }}>Fetching emails...</Text>
</View>
)}
{!loading && emptyMessage && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{emptyMessage}</Text>
</View>
)}
{/* Email Data */}
{emailData && (
<FlatList
data={emailData.messages}
keyExtractor={(item) => item.messageId}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handleSelectMessage(item)} style={styles.messageContainer}>
<Text style={styles.subject}>{item.subject}</Text>
<Text style={styles.date}>{item.date}</Text>
{selectedMessage?.messageId === item.messageId && (
<View style={styles.emailListContainer}>
<Text style={styles.qrCodeHeader}>Decoded QR Codes:</Text>
{item.decodedContentsDetails?.map((details, index) => (
<View key={index} style={styles.qrCodeContainer}>
<TouchableOpacity onPress={() => handleUrlClick(details.data.id)}>
<Text style={styles.qrCodeLink}>{details.data.contents}</Text>
</TouchableOpacity>
</View>
))}
<View style={styles.dividerHorizontal} />
<TouchableOpacity onPress={() => handleDeletePress(item.messageId)} style={styles.deleteButtonContainer}>
<Text style={styles.deleteButtonText}>Delete this entry</Text>
<Ionicons name="trash-bin" size={24} color="#ff69b4" />
</TouchableOpacity>
</View>
)}
</TouchableOpacity>
)}
/>
)}
{/* Banner when scanning inbox */}
<Animated.View style={[styles.banner, { opacity: bannerOpacity }]} pointerEvents="none">
<Text style={styles.bannerText}>Scanning emails in the background. This may take a while...</Text>
</Animated.View>
{/* Modal for ScannedDataBox */}
<Modal
visible={isModalVisible}
transparent={true}
animationType="slide"
onRequestClose={() => setIsModalVisible(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPressOut={() => setIsModalVisible(false)}
>
<View style={styles.modalContainer}>
<ScannedDataBox qrCodeId={selectedQrCodeId} clearScanData={() => setIsModalVisible(false)} />
</View>
</TouchableOpacity>
</Modal>
{/* Modal to prompt for deleting */}
<Modal
transparent={true}
visible={isDeleteModalVisible}
animationType="fade"
onRequestClose={() => setIsDeleteModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
<Text style={styles.modalTitle}>Are you sure?</Text>
<Text style={styles.modalText}>This will only delete the entry on the app and not the actual email.</Text>
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.modalButton}
onPress={confirmDelete}
>
<Text style={styles.modalButtonText}>Yes, Delete</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.modalButton}
onPress={() => setIsDeleteModalVisible(false)}
>
<Text style={[styles.modalButtonText, { color: '#ff69b4' }]}>No, Keep It</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
banner: {
position: 'absolute',
top: screenHeight * 0.50,
left: screenWidth * 0.1,
right: screenWidth * 0.1,
backgroundColor: '#ff69b4',
paddingVertical: screenHeight * 0.02,
paddingHorizontal: screenWidth * 0.05,
borderRadius: screenWidth * 0.05,
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
},
bannerText: {
color: '#fff',
fontWeight: 'bold',
textAlign: 'center',
fontSize: screenWidth * 0.04,
},
container: {
flex: 1,
backgroundColor: '#f8f0fc',
paddingHorizontal: screenWidth * 0.025,
paddingTop: screenHeight * 0.05,
paddingBottom: screenHeight * 0.1,
},
headerContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: screenHeight * 0.02,
},
emailHeader: {
fontSize: screenWidth * 0.045,
fontWeight: 'bold',
color: '#ff69b4',
},
dividerHorizontal: {
width: '100%',
height: 1,
backgroundColor: '#ddd',
marginVertical: screenWidth * 0.025,
},
emailListContainer: {
flex: 1,
},
refreshButton: {
padding: screenWidth * 0.025,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
color: '#ff69b4',
fontSize: screenWidth * 0.04,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
color: '#ff69b4',
fontSize: screenWidth * 0.04,
fontWeight: 'bold',
textAlign: 'center',
},
rescanIndicator: {
justifyContent: 'center',
alignItems: 'center',
marginVertical: screenHeight * 0.015,
},
rescanText: {
color: '#ff69b4',
fontSize: screenWidth * 0.04,
},
messageContainer: {
backgroundColor: '#fff',
padding: screenWidth * 0.025,
borderRadius: screenWidth * 0.025,
marginBottom: screenHeight * 0.015,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: { width: 0, height: screenHeight * 0.002 },
shadowRadius: screenWidth * 0.025,
elevation: 2,
},
subject: {
fontSize: screenWidth * 0.04,
fontWeight: 'bold',
color: '#000',
},
date: {
fontSize: screenWidth * 0.035,
color: '#555',
},
qrCodeContainer: {
marginBottom: screenHeight * 0.015,
},
qrCodeLink: {
fontSize: screenWidth * 0.035,
color: '#0000ff',
textDecorationLine: 'underline',
marginVertical: screenHeight * 0.005,
},
qrCodeHeader: {
fontSize: screenWidth * 0.04,
fontWeight: 'bold',
color: '#ff69b4',
marginBottom: screenHeight * 0.01,
marginTop: screenHeight * 0.015,
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContainer: {
marginHorizontal: '5%',
borderRadius: screenWidth * 0.025,
backgroundColor: 'white',
padding: screenWidth * 0.025,
elevation: 5,
},
modalTitle: {
fontSize: screenWidth * 0.05,
fontWeight: 'bold',
marginBottom: screenHeight * 0.01,
},
modalText: {
color: '#ff69b4',
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',
},
deleteButtonContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
padding: screenWidth * 0.02,
},
deleteButtonText: {
marginRight: screenWidth * 0.02,
color: '#ff69b4',
fontSize: screenWidth * 0.035,
},
});
export default EmailScreen;

View File

@@ -1,34 +1,37 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, BackHandler, Modal } from 'react-native'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, Modal, ActivityIndicator, Dimensions, Animated } from 'react-native';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import ScannedDataBox from '../components/ScannedDataBox'; import ScannedDataBox from '../components/ScannedDataBox';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { RootState, AppDispatch } from '../store'; import { RootState, AppDispatch } from '../store';
import { QRCodeType } from '../types'; import { QRCodeType } from '../types';
import { toggleBookmark, deleteQRCode, setScannedHistories } from '../reducers/qrCodesReducer'; import { toggleBookmark, deleteQRCode, setScannedHistories } from '../reducers/qrCodesReducer';
import useFetchUserAttributes from '../hooks/useFetchUserAttributes'; import useFetchUserAttributes from '../hooks/useFetchUserAttributes';
import { getScannedHistories } from '../api/qrCodeAPI'; import { getScannedHistories } from '../api/qrCodeAPI';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const HistoryScreen: React.FC = () => { const HistoryScreen: React.FC = () => {
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
const histories = useSelector((state: RootState) => state.qrCodes.histories); const histories = useSelector((state: RootState) => state.qrCodes.histories);
const { userAttributes } = useFetchUserAttributes(); const { userAttributes } = useFetchUserAttributes();
const [showBookmarks, setShowBookmarks] = useState<boolean>(false); const [showBookmarks, setShowBookmarks] = useState<boolean>(false);
const [qrCodeToDelete, setQrCodeToDelete] = useState<string | null>(null); const [qrCodeToDelete, setQrCodeToDelete] = useState<string | null>(null);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState<boolean>(false);
const [historiesLoading, setHistoriesLoading] = useState(false); const [historiesLoading, setHistoriesLoading] = useState(false);
const [historiesError, setHistoriesError] = useState<string | null>(null); const [historiesError, setHistoriesError] = useState<string | null>(null);
const [selectedQrCodeId, setSelectedQrCodeId] = useState<string | null>(null);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false); // Modal for ScannedDataBox
const fetchHistories = useCallback(async () => { const fetchHistories = useCallback(async () => {
if (!userAttributes?.sub) return; if (!userAttributes?.sub) return;
try { try {
setHistoriesLoading(true); setHistoriesLoading(true);
const historiesData = await getScannedHistories(userAttributes.sub); const historiesData = await getScannedHistories();
dispatch(setScannedHistories(historiesData)); dispatch(setScannedHistories(historiesData));
setHistoriesLoading(false);
} catch (error: any) { } catch (error: any) {
setHistoriesError(error.message); setHistoriesError(error.message);
} finally { } finally {
@@ -42,124 +45,118 @@ const HistoryScreen: React.FC = () => {
} }
}, [userAttributes?.sub, fetchHistories]); }, [userAttributes?.sub, fetchHistories]);
const handleDelete = useCallback((qrCodeId: string) => { const handleDeleteQrCode = useCallback((qrCodeId: string) => {
if (userAttributes?.sub) { if (userAttributes?.sub) {
dispatch(deleteQRCode({ userId: userAttributes.sub, qrCodeId })); dispatch(deleteQRCode({ userId: userAttributes.sub, qrCodeId }));
setIsModalVisible(false); setIsDeleteModalVisible(false);
} }
}, [dispatch, userAttributes]); }, [dispatch, userAttributes]);
const [selectedData, setSelectedData] = useState<string | null>(null); const handleSelectQrCodeForView = (item: QRCodeType) => {
const [selectedScanResult, setSelectedScanResult] = useState<any | null>(null); setSelectedQrCodeId(item.data.id || null);
const [selectedType, setSelectedType] = useState<string | null>(null); setIsModalVisible(true); // Show ScannedDataBox Modal
useEffect(() => {
const backAction = () => {
if (selectedData) {
setSelectedData(null);
setSelectedScanResult(null);
setSelectedType(null);
return true;
}
return false;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
return () => backHandler.remove();
}, [selectedData]);
const filteredQrCodes = showBookmarks ? histories.filter(qr => qr.bookmarked) : histories;
const handleItemPress = (item: QRCodeType) => {
// setSelectedData(item.data);
// setSelectedScanResult(item.scanResult);
// setSelectedType(item.type);
//setSelectedData(item.contents);
setSelectedType(item.data.type);
console.log('Selected QR code data:', item);
// console.log('Selected QR code type:', item.type);
}; };
const clearSelectedData = () => { const clearSelectedQrCodeData = () => {
setSelectedData(null); setSelectedQrCodeId(null);
setSelectedScanResult(null); setIsModalVisible(false); // Close ScannedDataBox Modal
setSelectedType(null);
}; };
const filteredQrCodes = showBookmarks
? histories.filter((qr) => qr.bookmarked)
: histories;
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Header for toggling between History and Bookmarks */}
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<TouchableOpacity onPress={() => { setShowBookmarks(false); clearSelectedData(); }}> <TouchableOpacity onPress={() => { setShowBookmarks(false); clearSelectedQrCodeData(); }}>
<Text style={!showBookmarks ? styles.headerTextActive : styles.headerTextInactive}>History</Text> <Text style={!showBookmarks ? styles.headerTextActive : styles.headerTextInactive}>History</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => { setShowBookmarks(true); clearSelectedData(); }}> <TouchableOpacity onPress={() => { setShowBookmarks(true); clearSelectedQrCodeData(); }}>
<Text style={showBookmarks ? styles.headerTextActive : styles.headerTextInactive}>Bookmarks</Text> <Text style={showBookmarks ? styles.headerTextActive : styles.headerTextInactive}>Bookmarks</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Display scanned data details */}
{selectedData && ( {historiesLoading && <ActivityIndicator size="large" color="#ff69b4" />}
<View style={styles.scannedDataBoxContainer}>
<ScannedDataBox data={selectedData} scanResult={selectedScanResult} dataType={selectedType} clearScanData={clearSelectedData} /> {!historiesLoading && filteredQrCodes.length === 0 && (
</View> <Text style={styles.emptyMessage}>
{showBookmarks ? 'No bookmarks available' : 'No history available'}
</Text>
)} )}
{/* List of QR codes */}
<FlatList <FlatList
data={filteredQrCodes} data={filteredQrCodes}
renderItem={({ item }) => { renderItem={({ item }) => (
// console.log('Rendering QR code item:', item); <View style={styles.itemContainer}>
return ( <View style={styles.itemLeft}>
<View style={styles.itemContainer}> <TouchableOpacity onPress={() => handleSelectQrCodeForView(item)} style={styles.itemContent}>
<View style={styles.itemLeft}> <Image source={require('../assets/ScanIcon3.png')} style={styles.scanIcon} />
<TouchableOpacity onPress={() => handleItemPress(item)} style={styles.itemContent}> <View style={styles.textContainer}>
<Image source={require('../assets/ScanIcon3.png')} style={styles.scanIcon} /> <Text style={styles.dataText} numberOfLines={1} ellipsizeMode="tail">{item.data.contents}</Text>
<View style={styles.textContainer}> </View>
<Text style={styles.dataText} numberOfLines={1} ellipsizeMode="tail">{item.data.contents}</Text> </TouchableOpacity>
</View> <Text style={styles.dateText}>{new Date(item.data.createdAt).toLocaleDateString('en-GB', {
</TouchableOpacity> day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'})}
<Text style={styles.dateText}>{new Date(item.data.createdAt).toLocaleDateString('en-GB', { </Text>
day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'})}
</Text>
</View>
<View style={styles.itemRight}>
<TouchableOpacity onPress={() => dispatch(toggleBookmark({ userId: userAttributes.sub, qrCode: item}))}>
<Ionicons name={item.bookmarked ? "bookmark" : "bookmark-outline"} size={24} color={item.bookmarked ? "#2196F3" : "#ff69b4"} />
</TouchableOpacity>
<TouchableOpacity onPress={() => {
setQrCodeToDelete(item.data.id);
setIsModalVisible(true);
}}>
<Ionicons name="close-circle-outline" size={24} color="#ff69b4" />
</TouchableOpacity>
</View>
</View> </View>
); <View style={styles.itemRight}>
}} <TouchableOpacity onPress={() => dispatch(toggleBookmark({ userId: userAttributes.sub, qrCode: item}))}>
keyExtractor={(item, index) => { <Ionicons name={item.bookmarked ? "bookmark" : "bookmark-outline"} size={screenWidth * 0.06} color={item.bookmarked ? "#2196F3" : "#ff69b4"} />
//console.log(item, index); </TouchableOpacity>
<TouchableOpacity onPress={() => {
return index.toString(); setQrCodeToDelete(item.data.id);
}} setIsDeleteModalVisible(true);
}}>
<Ionicons name="trash-outline" size={screenWidth * 0.06} color="#ff69b4" />
</TouchableOpacity>
</View>
</View>
)}
keyExtractor={(item, index) => index.toString()}
contentContainerStyle={styles.flatListContent} contentContainerStyle={styles.flatListContent}
/> />
{/* Modal for delete confirmation */}
{/* Modal for ScannedDataBox */}
<Modal
visible={isModalVisible}
transparent={true}
animationType="slide"
onRequestClose={clearSelectedQrCodeData}
>
{/* The greyspace outside, made clickable to close the modal */}
<TouchableOpacity
style={styles.scannedDataBoxModalOverlay}
activeOpacity={1}
onPressOut={clearSelectedQrCodeData}
>
{/* Ensure ScannedDataBox does not render another modal */}
<View style={styles.scannedDataBoxModalContainer}>
<ScannedDataBox qrCodeId={selectedQrCodeId} clearScanData={clearSelectedQrCodeData} />
</View>
</TouchableOpacity>
</Modal>
{/* Modal to confirm deletion */}
<Modal <Modal
transparent={true} transparent={true}
visible={isModalVisible} visible={isDeleteModalVisible}
animationType="fade" animationType="fade"
onRequestClose={() => setIsModalVisible(false)} onRequestClose={() => setIsDeleteModalVisible(false)}
> >
<View style={styles.modalContainer}> <View style={styles.modalOverlay}>
<View style={styles.modalContent}> <View style={styles.modalContainer}>
<Text style={styles.modalTitle}>Are you sure?</Text> <Text style={styles.modalTitle}>Are you sure?</Text>
<Text style={styles.modalText}>If bookmarked, this will be removed from both History and Bookmarks.</Text> <Text style={styles.modalText}>If bookmarked, this will be removed from both History and Bookmarks.</Text>
<View style={styles.modalButtons}> <View style={styles.modalButtons}>
<TouchableOpacity style={styles.modalButton} onPress={() => handleDelete(qrCodeToDelete!)}> <TouchableOpacity style={styles.modalButton} onPress={() => handleDeleteQrCode(qrCodeToDelete!)}>
<Text style={styles.modalButtonText}>Yes, Delete</Text> <Text style={styles.modalButtonText}>Yes, Delete</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.modalButton} onPress={() => setIsModalVisible(false)}> <TouchableOpacity style={styles.modalButton} onPress={() => setIsDeleteModalVisible(false)}>
<Text style={[styles.modalButtonText, { color: '#ff69b4' }]}>No, Keep It</Text> <Text style={[styles.modalButtonText, { color: '#ff69b4' }]}>No, Keep It</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -175,6 +172,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#f8f0fc', backgroundColor: '#f8f0fc',
padding: 20, padding: 20,
paddingTop: screenHeight * 0.05,
}, },
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -182,12 +180,12 @@ const styles = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
}, },
headerTextActive: { headerTextActive: {
fontSize: 24, fontSize: screenWidth * 0.06,
fontWeight: 'bold', fontWeight: 'bold',
color: '#ff69b4', color: '#ff69b4',
}, },
headerTextInactive: { headerTextInactive: {
fontSize: 24, fontSize: screenWidth * 0.06,
fontWeight: 'bold', fontWeight: 'bold',
color: '#ccc', color: '#ccc',
}, },
@@ -196,9 +194,9 @@ const styles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#ffe6f0', backgroundColor: '#ffe6f0',
padding: 10, padding: screenWidth * 0.025,
borderRadius: 10, borderRadius: screenWidth * 0.025,
marginBottom: 10, marginBottom: screenWidth * 0.025,
}, },
itemLeft: { itemLeft: {
flex: 1, flex: 1,
@@ -217,44 +215,61 @@ const styles = StyleSheet.create({
marginLeft: 0, marginLeft: 0,
}, },
dataText: { dataText: {
fontSize: 12, fontSize: screenWidth * 0.03,
color: '#000', color: '#000',
marginBottom: 7 marginBottom: screenWidth * 0.02,
}, },
dateText: { dateText: {
fontSize: 12, fontSize: screenWidth * 0.03,
color: '#666', color: '#666',
marginLeft: 10, marginLeft: screenWidth * 0.02,
flex: 1 flex: 1,
}, },
scanIcon: { scanIcon: {
width: 40, width: screenWidth * 0.1,
height: 40, height: screenWidth * 0.1,
}, },
flatListContent: { flatListContent: {
paddingBottom: 100, paddingBottom: screenHeight * 0.1,
}, },
modalContainer: { emptyMessage: {
textAlign: 'center',
fontSize: screenWidth * 0.04,
color: '#ff69b4',
marginVertical: screenHeight * 0.02,
},
modalOverlay: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.5)',
}, },
modalOverlayTouchable: {
flex: 1,
justifyContent: 'center',
},
modalContainer: {
marginHorizontal: '5%',
borderRadius: screenWidth * 0.025,
backgroundColor: 'white',
padding: screenWidth * 0.025,
elevation: 5,
},
modalContent: { modalContent: {
width: '80%', width: '80%',
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: 10, borderRadius: screenWidth * 0.025,
padding: 20, padding: screenWidth * 0.05,
alignItems: 'center', alignItems: 'center',
}, },
modalTitle: { modalTitle: {
fontSize: 20, fontSize: screenWidth * 0.05,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 10, marginBottom: screenHeight * 0.01,
}, },
modalText: { modalText: {
fontSize: 16, color: '#ff69b4',
marginBottom: 20, fontSize: screenWidth * 0.04,
marginBottom: screenHeight * 0.02,
textAlign: 'center', textAlign: 'center',
}, },
modalButtons: { modalButtons: {
@@ -265,14 +280,23 @@ const styles = StyleSheet.create({
modalButton: { modalButton: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
padding: 10, padding: screenWidth * 0.025,
}, },
modalButtonText: { modalButtonText: {
fontSize: 16, fontSize: screenWidth * 0.04,
color: '#000', color: '#000',
}, },
scannedDataBoxContainer: { scannedDataBoxModalOverlay: {
marginBottom: 20, flex: 1,
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
scannedDataBoxModalContainer: {
marginHorizontal: '5%',
borderRadius: screenWidth * 0.025,
backgroundColor: 'white',
padding: screenWidth * 0.025,
elevation: 5,
}, },
}); });

View File

@@ -1,305 +1,429 @@
import React, { useState, useEffect, useContext, useCallback } from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, Image } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Image, Dimensions, Modal, Animated } from 'react-native';
import { Camera, CameraView, scanFromURLAsync } from 'expo-camera'; import { Camera, useCameraDevice, useCameraPermission, useCodeScanner } from 'react-native-vision-camera';
import { QRCodeContext } from '../types'; import { Ionicons } from '@expo/vector-icons';
import axios from 'axios'; // For URL calls
import { Ionicons } from '@expo/vector-icons'; // For icons
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import RNQRGenerator from 'rn-qr-generator';
import ScannedDataBox from '../components/ScannedDataBox'; import ScannedDataBox from '../components/ScannedDataBox';
import { useDispatch } from 'react-redux'; import { scanQRCode, getQRTips } from '../api/qrCodeAPI';
import { RootState, AppDispatch } from '../store'; import SettingsScreen from './SettingsScreen';
import { addQRCode } from '../reducers/qrCodesReducer'; // Assuming you have actions defined for Redux import NetInfo from '@react-native-community/netinfo';
import { detectQRCodeType, verifyURL, checkRedirects } from '../api/qrCodeAPI'; // Import utility functions import { useFocusEffect } from '@react-navigation/native';
import { getCurrentUser, fetchAuthSession } from 'aws-amplify/auth';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
// Main Function
const QRScannerScreen: React.FC<{ clearScanData: () => void }> = ({ clearScanData }) => { const QRScannerScreen: React.FC<{ clearScanData: () => void }> = ({ clearScanData }) => {
const navigation = useNavigation(); // call Navigation bar // State management
const dispatch = useDispatch<AppDispatch>(); // Use dispatch for Redux actions const [isSettingsModalVisible, setIsSettingsModalVisible] = useState<boolean>(false);
const [enableTorch, setEnableTorch] = useState<boolean>(false);
const [showSplash, setShowSplash] = useState<boolean>(true); // call splash screen
const qrCodeContext = useContext(QRCodeContext); // From ./types.ts
const { qrCodes, setQrCodes } = qrCodeContext || { qrCodes: [], setQrCodes: () => {} };
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scanned, setScanned] = useState<boolean>(false); const [scanned, setScanned] = useState<boolean>(false);
const [scannedData, setScannedData] = useState<string>(''); // State for QR scanned Data const [qrCodeId, setQRCodeId] = useState<string | null>(null);
const [dataType, setDataType] = useState<string>(''); // State for data type const [isScannedDataBoxVisible, setIsScannedDataBoxVisible] = useState<boolean>(false);
const [enableTorch, setEnableTorch] = useState<boolean>(false); // State for torch const [bannerOpacity] = useState(new Animated.Value(0));
const [cameraVisible, setCameraVisible] = useState<boolean>(true); // State to control camera visibility const [isConnected, setIsConnected] = useState<boolean>(true);
const [qrTip, setQrTip] = useState<string>('Always scan QR codes from trusted sources');
const [scannedDataBoxY] = useState(new Animated.Value(screenHeight)); // Start off-screen
// Add state variables for scan results // Camera permissions and device management
const [secureConnection, setSecureConnection] = useState<boolean | null>(null); const { hasPermission, requestPermission } = useCameraPermission();
const [virusTotalCheck, setVirusTotalCheck] = useState<boolean | null>(null); const device = useCameraDevice('back');
const [redirects, setRedirects] = useState<number | null>(null);
// Request Camera Permission and initialize the app
useEffect(() => { useEffect(() => {
const initializeApp = async () => { const fetchAuthData = async () => {
const { status } = await Camera.requestCameraPermissionsAsync(); try {
setHasPermission(status === 'granted'); const currentUser = await getCurrentUser();
setShowSplash(false); console.log('Current user:', currentUser);
console.log("Camera permissions initialized");
const { tokens } = await fetchAuthSession();
const test = await fetchAuthSession();
console.log('Tokens:', tokens);
console.log("AWS access token: ", tokens.accessToken.toString());
console.log("Test data: ", test);
} catch (error) {
console.error('Error fetching auth data:', error);
}
}; };
initializeApp(); fetchAuthData();
}, []); }, []);
// Clear Scan Data const fetchTips = async () => {
const clearScanDataInternal = () => {
setScannedData('');
setScanned(false);
setDataType('');
console.log("Scan data cleared");
};
// Handle QR Code Payload
const handlePayload = async (payload: string) => {
setScanned(true);
console.log("Scanning Completed. Payload is:", payload);
const type = await detectQRCodeType(payload);
const secureConnectionResult = await verifyURL(payload);
const redirectResult = await checkRedirects(payload);
setSecureConnection(secureConnectionResult.isSecure);
setVirusTotalCheck(!secureConnectionResult.isMalicious); // Assuming you have virusTotalCheck logic integrated here
setRedirects(redirectResult.redirects);
const qrCode = {
data: payload,
type,
scanResult: {
secureConnection: secureConnectionResult.isSecure,
virusTotalCheck: !secureConnectionResult.isMalicious,
redirects: redirectResult.redirects
},
bookmarked: false // by default
};
setScannedData(payload);
console.log("Payload received:", payload);
console.log("Type received from server:", type);
setDataType(type);
dispatch(addQRCode(qrCode)); // Dispatch action to save QR code data
console.log("QR code data added to history");
};
// Send QR Code Data to Backend Server
const sendToAPIServer = async (payload: string): Promise<string> => {
console.log('Sending QR code data to backend:', payload);
try { try {
const response = await axios.post('http://192.168.1.30:8080/v1/qrcodetypes/scan', { const response = await getQRTips();
data: payload, setQrTip(response.tips); // Set the qrTip state to the value of the tips property
}, {
headers: {
'Content-Type': 'application/json',
},
});
console.log('Response from backend:', response.data);
return response.data;
} catch (error) { } catch (error) {
console.error('Error detecting QR code type:', error); console.error('Error fetching QR tips:', error);
return 'UNKNOWN';
} }
}; };
// Toggle Torch (Flashlight) // Only run when the screen is focusd
const toggleTorch = () => { useFocusEffect(
setEnableTorch((prev) => !prev); React.useCallback(() => {
console.log("Torch toggled:", enableTorch ? "off" : "on"); requestPermission(); // Request camera permission when screen is focused
// Initial fetch for QR tips
fetchTips();
// Set interval for fetching QR tips every 6 seconds
const intervalId = setInterval(fetchTips, 6000);
// Subscribe to network state updates
const unsubscribe = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected);
if (!state.isConnected) {
showBanner(); // Show banner if the device goes offline
}
});
return () => {
clearInterval(intervalId); // Clear interval when screen is unfocused
unsubscribe(); // Unsubscribe from network state updates
};
}, [])
);
const showScannedDataBox = () => {
setIsScannedDataBoxVisible(true);
Animated.timing(scannedDataBoxY, {
toValue: 0, // Slide the modal to the top of the screen
duration: 500,
useNativeDriver: true,
}).start();
}; };
// Handle Test Scan const hideScannedDataBox = () => {
const handleTestScan = () => { Animated.timing(scannedDataBoxY, {
handlePayload('TEST123'); toValue: screenHeight, // Move it back off-screen
console.log("Test scan executed"); duration: 500,
useNativeDriver: true,
}).start(() => {
setIsScannedDataBoxVisible(false);
});
}; };
// Read QR Code from Image const clearSelectedQrCodeData = () => {
setQRCodeId(null);
hideScannedDataBox();
setScanned(false); // Reset the scanned state so the camera can scan again
clearScanData(); // Call the clearScanData passed from App.tsx
};
// Show an offline 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 handlePayload = async (payload: string) => {
setScanned(true);
console.info("Decoded QR Code, Payload is: ", payload);
try {
const response = await scanQRCode(payload);
const qrCodeId = response.qrcode.data.id;
setQRCodeId(qrCodeId);
showScannedDataBox(); // Show the ScannedDataBox pop-up with animation
} catch (error) {
console.error("Error scanning QR code:", error);
}
};
// Use the camera to scan QR codes
const codeScanner = useCodeScanner({
codeTypes: ['qr'], // Only scan QR codes
onCodeScanned: (codes) => {
if (!scanned && codes[0]?.value) {
handlePayload(codes[0].value); // Handle the QR code value
}
}
});
// Read QR code from an image
const readQRFromImage = async () => { const readQRFromImage = async () => {
clearScanDataInternal();
console.log("Reading QR code from image"); console.log("Reading QR code from image");
const result = await ImagePicker.launchImageLibraryAsync({ const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: false, // Don't ask user to crop images allowsEditing: false,
quality: 1, quality: 1,
}); });
if (result && result.assets && result.assets.length > 0 && result.assets[0].uri) { // Ensure the uri is not empty if (result && result.assets && result.assets.length > 0 && result.assets[0].uri) {
try { try {
const scannedResult = await scanFromURLAsync(result.assets[0].uri); const detectionResult = await RNQRGenerator.detect({
if (scannedResult && scannedResult[0] && scannedResult[0].data) { uri: result.assets[0].uri,
handlePayload(scannedResult[0].data); });
// Not sure why scannedResult.data is undefined but access as array work, KIV
console.log('QR code data from image:', scannedResult[0].data); const { values } = detectionResult;
if (values.length > 0) {
handlePayload(values[0]); // Handle the first detected QR code value
} else { } else {
setScannedData("No QR Code Found");
//setTimeout(() => setScannedData(""), 4000);
console.log("No QR code found in the selected image"); console.log("No QR code found in the selected image");
Alert.alert('No QR code found in the selected image.');
} }
} catch (error) { } catch (error) {
console.error('Error scanning QR code from image:', error); console.error('Error scanning QR code from image:', error);
Alert.alert('Failed to scan QR code from image.');
} }
} }
}; };
// Clear scan data when screen is focused // Check for camera permissions
useFocusEffect( if (!hasPermission) {
useCallback(() => { return <Text>Requesting camera permission...</Text>;
setCameraVisible(true);
clearScanDataInternal();
console.log("Screen focused, scan data cleared and camera enabled");
return () => {
setCameraVisible(false);
console.log("Screen unfocused, camera disabled");
};
}, [navigation])
);
if (showSplash) {
return (
<View style={styles.splashContainer}>
<ActivityIndicator size="large" color="#ff69b4" />
</View>
);
} }
if (hasPermission === null) { // Wait for the device to be ready
return <Text>Requesting for camera permission</Text>; if (!device) {
} return <Text>Loading camera...</Text>;
if (hasPermission === false) {
return <Text>No access to camera</Text>;
} }
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.banner}> {/* Banner for network connectivity */}
<Text style={styles.headerText}>SafeQR v0.89</Text> <Animated.View style={[styles.banner, { opacity: bannerOpacity }]}>
</View> <Text style={styles.bannerText}>No Internet Connection</Text>
<Text style={styles.welcomeText}>Welcome to SafeQR code Scanner</Text> </Animated.View>
<Text style={styles.titleText}>Welcome to</Text>
<Image source={require('../assets/SafeQR_Logo 1.png')} style={styles.logo} />
<Text style={styles.welcomeText}>Please point the camera at the QR Code</Text>
<View style={styles.cameraContainer}> <View style={styles.cameraContainer}>
{cameraVisible && ( {device && (
<CameraView <Camera
onBarcodeScanned={scanned ? undefined : ({ data }) => handlePayload(data)} style={StyleSheet.absoluteFill}
barcodeScannerSettings={{ barcodeTypes: ['qr', 'pdf417'] }} device={device}
style={styles.camera} isActive={!isSettingsModalVisible && !isScannedDataBoxVisible} // Disable the camera when settings modal or ScannedDataBox is open
enableTorch={enableTorch} torch={enableTorch ? 'on' : 'off'}
/> codeScanner={codeScanner}
/>
)} )}
<TouchableOpacity onPress={toggleTorch} style={styles.flashButton}> {/* Torch Button */}
<Ionicons name="flashlight" size={24} color="#fff" /> <TouchableOpacity
onPress={() => device.hasFlash && setEnableTorch((prev) => !prev)}
style={styles.flashButton}
disabled={!device.hasFlash}
>
<Ionicons
name={device.hasFlash ? 'flashlight' : 'flashlight-outline'}
size={screenWidth * 0.06}
color={device.hasFlash ? "#fff" : "#888"}
/>
</TouchableOpacity> </TouchableOpacity>
{/* <TouchableOpacity onPress={handleTestScan} style={styles.testButton}>
<Ionicons name="bug" size={24} color="#fff" /> {/* Gallery Button */}
</TouchableOpacity> */}
<TouchableOpacity onPress={readQRFromImage} style={styles.galleryButton}> <TouchableOpacity onPress={readQRFromImage} style={styles.galleryButton}>
<Ionicons name="image" size={24} color="#fff" /> <Ionicons name="image" size={screenWidth * 0.06} color="#fff" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{scannedData !== '' && ( {/* QR Code Tips Below Camera Container */}
<View style={styles.scannedDataBox}> <View style={styles.tipsContainer}>
<ScannedDataBox <View style={styles.iconTextRow}>
data={scannedData} <Ionicons name="bulb" size={24} color="red" />
dataType={dataType} <Text style={styles.tipsText}>{qrTip}</Text>
clearScanData={clearScanDataInternal}
scanResult={{
secureConnection,
virusTotalCheck,
redirects
}}
/>
</View> </View>
)} </View>
{/* Scanned Data Box as a modal with sliding animation */}
<Modal
transparent={true}
visible={isScannedDataBoxVisible}
animationType="none"
onRequestClose={clearSelectedQrCodeData} // Call clearSelectedQrCodeData when the modal is requested to close
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={clearSelectedQrCodeData} // Call clearSelectedQrCodeData on press
>
<Animated.View style={[styles.modalContainer, { transform: [{ translateY: scannedDataBoxY }] }]}>
<ScannedDataBox
qrCodeId={qrCodeId!}
clearScanData={clearSelectedQrCodeData} // Close modal and reset the scanned state
/>
</Animated.View>
</TouchableOpacity>
</Modal>
{/* Settings Icon */}
<TouchableOpacity onPress={() => setIsSettingsModalVisible(true)} style={styles.settingsButton}>
<Ionicons name="settings" size={screenWidth * 0.06} color="#000" />
</TouchableOpacity>
{/* Settings Modal */}
<Modal
animationType="slide"
transparent={true}
visible={isSettingsModalVisible}
onRequestClose={() => setIsSettingsModalVisible(false)}
style={styles.settingsModal}
>
<View style={styles.settingsModalContainer}>
<View style={styles.settingsModalContent}>
<SettingsScreen />
<TouchableOpacity onPress={() => setIsSettingsModalVisible(false)} style={styles.closeButton}>
<Text style={styles.closeButtonText}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View> </View>
); );
}; };
// Stylesheet
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f8f0fc', backgroundColor: '#f8f0fc',
padding: 20, padding: 20,
}, },
banner: { titleText: {
alignItems: 'center', textAlign: 'center',
marginBottom: 20, fontSize: 20,
marginTop: screenHeight * 0.05,
color: 'black',
}, },
headerText: { logo: {
fontSize: 24, alignSelf: 'center',
fontWeight: 'bold', width: screenWidth * 0.5,
color: '#ff69b4', height: screenWidth * 0.2,
resizeMode: 'contain',
marginVertical: 10,
}, },
splashContainer: { welcomeText: {
flex: 1, textAlign: 'center',
justifyContent: 'center', fontSize: 15,
alignItems: 'center', marginVertical: 10,
backgroundColor: '#f8f0fc', color: 'black',
height: '100%',
width: '100%',
}, },
cameraContainer: { cameraContainer: {
height: '60%', width: '100%',
height: '45%',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
borderRadius: 10, borderRadius: 10,
overflow: 'hidden', overflow: 'hidden',
alignSelf: 'center',
marginTop: '1%',
}, },
camera: { settingsButton: {
width: '100%', position: 'absolute',
height: '100%', top: screenHeight * 0.05,
right: 20,
}, },
flashButton: { flashButton: {
position: 'absolute', position: 'absolute',
bottom: 20, bottom: screenHeight * 0.025,
left: 100, left: screenWidth * 0.2,
width: 50, width: screenWidth * 0.125,
height: 50, height: screenWidth * 0.125,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#000', backgroundColor: '#000',
borderRadius: 25, borderRadius: screenWidth * 0.0625,
},
testButton: {
position: 'absolute',
bottom: 1,
alignSelf: 'stretch',
backgroundColor: '#000',
padding: 10,
borderRadius: 5,
}, },
galleryButton: { galleryButton: {
position: 'absolute', position: 'absolute',
bottom: 20, bottom: screenHeight * 0.025,
right: 100, right: screenWidth * 0.2,
width: 50, width: screenWidth * 0.125,
height: 50, height: screenWidth * 0.125,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#000', backgroundColor: '#000',
borderRadius: 25, borderRadius: screenWidth * 0.0625,
}, },
scannedDataBox: { modalOverlay: {
flex: 1,
justifyContent: 'center', // Aligns the modal to the bottom by default
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContainer: {
marginHorizontal: '5%',
borderRadius: screenWidth * 0.025,
backgroundColor: 'white',
padding: screenWidth * 0.025,
elevation: 5,
},
settingsModal: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
settingsModalContainer: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#f8f0fc',
},
settingsModalContent: {
flex: 1,
backgroundColor: '#f8f0fc',
padding: screenWidth * 0.05,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
alignItems: 'center',
},
closeButton: {
marginTop: screenHeight * 0.01,
padding: screenWidth * 0.025,
backgroundColor: '#ff69b4',
borderRadius: screenWidth * 0.0125,
},
closeButtonText: {
color: 'white',
fontWeight: 'bold',
},
banner: {
position: 'absolute', position: 'absolute',
top: '10%', top: screenHeight * 0.4,
left: '5%', left: screenWidth * 0.1,
right: '5%', right: screenWidth * 0.1,
zIndex: 2, backgroundColor: '#ff69b4',
paddingVertical: screenHeight * 0.02,
paddingHorizontal: screenWidth * 0.05,
borderRadius: screenWidth * 0.05,
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
}, },
welcomeText: { bannerText: {
color: '#fff',
fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
fontSize: 20, fontSize: screenWidth * 0.04,
marginVertical: 10, },
color: 'black', tipsContainer: {
backgroundColor: '#fff',
padding: 10,
borderRadius: 10,
alignItems: 'center',
marginTop: 10,
},
iconTextRow: {
flexDirection: 'row',
alignItems: 'center',
},
tipsText: {
color: '#f41c87',
fontSize: 16,
textAlign: 'center',
marginLeft: 5,
paddingHorizontal: 10,
}, },
}); });

View File

@@ -1,27 +1,43 @@
import { View, Text, StyleSheet, TouchableOpacity, Linking, Button } from 'react-native'; import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Linking, Alert, Button } from 'react-native';
import { useAuthenticator } from '@aws-amplify/ui-react-native'; import { useAuthenticator } from '@aws-amplify/ui-react-native';
import useFetchUserAttributes from '../hooks/useFetchUserAttributes'; import useFetchUserAttributes from '../hooks/useFetchUserAttributes';
import { fetchAuthSession, getCurrentUser, signInWithRedirect } from 'aws-amplify/auth'; import { fetchAuthSession, getCurrentUser, signInWithRedirect, signOut } from 'aws-amplify/auth';
import { useEffect, useState } from 'react'; import { deleteAllEmails, deleteAllScannedHistories, getUserInfo } from '../api/qrCodeAPI'; // Import the API function
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { createDrawerNavigator } from '@react-navigation/drawer'; import { Ionicons } from '@expo/vector-icons';
function SignOutButton() { function SignOutButton() {
const { signOut } = useAuthenticator(); const { signOut } = useAuthenticator();
return <Button title="Sign Out" onPress={signOut} />; return <Button title="Sign Out" onPress={signOut} />;
} }
const handleSignInWithRedirect = async () => { const handleSignInWithRedirect = async () => {
try { try {
await signInWithRedirect(); await signInWithRedirect();
} catch (error) { } catch (error) {
console.error('Error during sign in:', error); console.log('Error during sign in:', error);
} }
}; };
const SettingsScreen: React.FC = () => { const SettingsScreen: React.FC = () => {
const { userAttributes } = useFetchUserAttributes(); const { userAttributes } = useFetchUserAttributes();
const [googleAccessToken, setGoogleAccessToken] = useState<string | null>(null); const [googleAccessToken, setGoogleAccessToken] = useState<string | null>(null);
const [userEmail, setUserEmail] = useState<string | null>(null);
const [userSource, setUserSource] = useState<string | null>(null);
const fetchUserEmail = async () => {
try {
console.log('fetchUserEmail triggered');
const userInfo = await getUserInfo();
console.log("User Source: ", userInfo.source);
setUserSource(userInfo.source)
setUserEmail(userInfo.email); // Assuming userInfo has an email property
} catch (error) {
console.log('Error fetching user email:', error);
}
};
useEffect(() => { useEffect(() => {
const getGoogleAccessToken = async () => { const getGoogleAccessToken = async () => {
@@ -42,7 +58,6 @@ const SettingsScreen: React.FC = () => {
const parts = idToken.split('.'); const parts = idToken.split('.');
if (parts.length !== 3) { if (parts.length !== 3) {
throw new Error('ID token is not a valid JWT'); throw new Error('ID token is not a valid JWT');
} }
const payload = parts[1]; const payload = parts[1];
@@ -53,8 +68,8 @@ const SettingsScreen: React.FC = () => {
try { try {
parsedPayload = JSON.parse(decodedPayload); parsedPayload = JSON.parse(decodedPayload);
} catch (parseError) { } catch (parseError) {
console.error('Error parsing payload:', parseError); console.log('Error parsing payload:', parseError);
console.error(`Parse error: ${parseError.message}\nPayload: ${decodedPayload}`); console.log(`Parse error: ${parseError.message}\nPayload: ${decodedPayload}`);
return; return;
} }
@@ -70,7 +85,6 @@ const SettingsScreen: React.FC = () => {
second: '2-digit' second: '2-digit'
}; };
if (parsedPayload["custom:access_token"]) { if (parsedPayload["custom:access_token"]) {
console.log('Google Access Token:', parsedPayload["custom:access_token"]); console.log('Google Access Token:', parsedPayload["custom:access_token"]);
console.log('Google Refresh Token: ', parsedPayload["custom:refresh_token"]); console.log('Google Refresh Token: ', parsedPayload["custom:refresh_token"]);
@@ -82,14 +96,13 @@ const SettingsScreen: React.FC = () => {
console.log("date created: ", new Date(1721715837500).toLocaleString('en-US', options)); console.log("date created: ", new Date(1721715837500).toLocaleString('en-US', options));
} else { } else {
console.error('No Google access token found in the payload'); console.log('No Google access token found in the payload');
} }
} else { } else {
console.error('No ID token found in the session'); console.log('No ID token found in the session');
} }
} catch (error) { } catch (error) {
console.error('Error getting Google access token:', error); console.log('Error getting Google access token:', error);
} }
}; };
@@ -97,65 +110,139 @@ const SettingsScreen: React.FC = () => {
getGoogleAccessToken(); getGoogleAccessToken();
} }
}, [userAttributes]); }, [userAttributes]);
useEffect(() => {
fetchUserEmail();
}, []);
const handleLinkPress = (url: string) => { const handleLinkPress = (url: string) => {
Linking.openURL(url); Linking.openURL(url);
}; };
const handleDeleteAllHistories = async () => {
try {
const response = await deleteAllScannedHistories();
Alert.alert('Success', response.message);
} catch (error) {
Alert.alert('Error', 'Failed to delete histories. Please try again.');
}
};
const handleSignOut = () => {
Alert.alert('Confirm Sign Out', 'Are you sure you want to sign out?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Sign Out', onPress: () => signOut() }
]);
};
const handleDeleteAllEmails = async () => {
Alert.alert('Confirm Delete', 'Are you sure you want to delete all emails?', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
onPress: async () => {
try {
const response = await deleteAllEmails();
Alert.alert('Success', response.message);
} catch (error) {
Alert.alert('Error', 'Failed to delete all emails. Please try again.');
}
}
}
]);
};
const userName = userAttributes?.name || (userEmail ? userEmail.split('@')[0] : 'Unknown User');
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.header}>Settings</Text> <Text style={styles.header}>Settings</Text>
<View style={styles.profileSection}>
<Text style={styles.sectionTitle}>Profile</Text> {/* Profile Section */}
<View style={styles.section}>
<View style={styles.profileContainer}>
<Ionicons name="person-circle" size={60} color="#f41c87" style={styles.profileIcon} />
<Text style={styles.userName}>Hello, {userName}</Text>
</View>
{userAttributes ? ( {userAttributes ? (
<View> <TouchableOpacity style={styles.logoutButton} onPress={handleSignOut}>
<Text style={styles.userName}>Hello, {userAttributes?.name}</Text> <Ionicons name="log-out-outline" size={24} color="#fff" style={styles.buttonIcon} />
{googleAccessToken && ( <Text style={styles.logoutButtonText}>Log Out</Text>
<Text>Google Access Token: {googleAccessToken.substring(0, 10)}...</Text> </TouchableOpacity>
)}
<SignOutButton />
</View>
) : ( ) : (
<TouchableOpacity style={styles.loginButton} onPress={handleSignInWithRedirect}> <TouchableOpacity style={styles.loginButton} onPress={handleSignInWithRedirect}>
<Ionicons name="log-in-outline" size={24} color="#fff" style={styles.buttonIcon} />
<Text style={styles.loginButtonText}>Log In</Text> <Text style={styles.loginButtonText}>Log In</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<View style={styles.divider} /> <View style={styles.divider} />
<View style={styles.aboutUsSection}>
{/* Email Section - Only show when userSource is "Google" */}
{userSource === "Google" && (
<>
<View style={styles.section}>
<View style={styles.emailRow}>
<Text style={styles.sectionTitle}>Email: </Text>
<Text style={styles.userEmail}>{userEmail || 'Loading...'}</Text>
</View>
<TouchableOpacity style={styles.deleteAllButton} onPress={handleDeleteAllEmails}>
<Ionicons name="trash-outline" size={24} color="#fff" style={styles.buttonIcon} />
<Text style={styles.deleteAllButtonText}>Delete All Email</Text>
</TouchableOpacity>
</View>
<View style={styles.divider} />
</>
)}
{/* History & Bookmarks Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>History & Bookmarks</Text>
<TouchableOpacity style={styles.deleteAllButton} onPress={handleDeleteAllHistories}>
<Ionicons name="trash-outline" size={24} color="#fff" style={styles.buttonIcon} />
<Text style={styles.deleteAllButtonText}>Delete All History</Text>
</TouchableOpacity>
</View>
<View style={styles.divider} />
{/* About Us Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>About Us</Text> <Text style={styles.sectionTitle}>About Us</Text>
<TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/marketing/')}> <TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/marketing/')}>
<Text style={styles.linkText}>safeqr.github.io/marketing</Text> <Text style={styles.linkText}>safeqr.github.io/marketing</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/privacy-policy')}> <TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/marketing/#/privacy-policy')}>
<Text style={styles.linkText}>Privacy Policy</Text> <Text style={styles.linkText}>Privacy Policy</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/terms-of-service')}> <TouchableOpacity onPress={() => handleLinkPress('https://safeqr.github.io/marketing/#/terms-of-service')}>
<Text style={styles.linkText}>Terms Of Service</Text> <Text style={styles.linkText}>Terms Of Service</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={styles.versionText}>Version 1.2</Text>
<Text style={styles.versionText}>Version 1.0</Text>
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
userName: {
fontSize: 16,
marginBottom: 10,
},
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f8f0fc', backgroundColor: '#f8f0fc',
padding: 20, padding: 10,
width: '100%',
}, },
header: { header: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 'bold',
color: '#ff69b4', color: '#ff69b4',
marginBottom: 20, marginBottom: 20,
textAlign: 'center',
}, },
profileSection: { section: {
marginBottom: 20, marginBottom: 20,
}, },
sectionTitle: { sectionTitle: {
@@ -164,26 +251,85 @@ const styles = StyleSheet.create({
color: '#000', color: '#000',
marginBottom: 10, marginBottom: 10,
}, },
loginButton: { profileContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffe6f0',
padding: 15,
borderRadius: 15,
},
profileIcon: {
marginRight: 15,
},
userName: {
fontSize: 18,
color: '#f41c87',
fontWeight: 'bold',
},
logoutButton: {
backgroundColor: '#ff69b4', backgroundColor: '#ff69b4',
paddingVertical: 8, borderRadius: 25,
paddingVertical: 10,
paddingHorizontal: 20, paddingHorizontal: 20,
borderRadius: 20, alignItems: 'center',
justifyContent: 'center',
alignSelf: 'flex-start',
marginVertical: 10,
flexDirection: 'row',
},
logoutButtonText: {
color: '#fff',
fontSize: 16,
},
loginButton: {
flexDirection: 'row',
backgroundColor: '#ff69b4',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 25,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'flex-start', alignSelf: 'flex-start',
}, },
loginButtonText: { loginButtonText: {
color: '#000', color: '#fff',
fontSize: 16, fontSize: 16,
marginLeft: 10,
},
userEmail: {
fontSize: 16,
color: '#000',
marginBottom: 10,
},
deleteAllButton: {
flexDirection: 'row',
backgroundColor: '#f41c87',
borderRadius: 25,
paddingVertical: 10,
paddingHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'flex-start',
marginVertical: 10,
},
deleteAllButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
marginLeft: 10,
},
buttonIcon: {
marginRight: 10,
}, },
divider: { divider: {
height: 1, height: 1,
backgroundColor: '#ccc', backgroundColor: '#ccc',
marginVertical: 20, marginVertical: 20,
}, },
aboutUsSection: { emailRow: {
marginBottom: 20, flexDirection: 'row',
alignItems: 'center', // Align the text vertically in the center
marginBottom: 10, // Add some spacing below the row
}, },
linkText: { linkText: {
fontSize: 16, fontSize: 16,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
temp/Sample QR/EmailTO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
temp/Sample QR/Phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
temp/Sample QR/SMSTO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
temp/Sample QR/Text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
temp/Sample QR/WIFI_WEP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
temp/Sample QR/WIFI_WPA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
temp/Sample QR/whatsapp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB