Compare commits
47 Commits
feature-lo
...
fix-defaul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7475215d44 | ||
|
|
a7d4f693fa | ||
| cf7935baae | |||
| 7cce7b66d7 | |||
| d5217cc59b | |||
| 6a00ec453d | |||
| b9c382eaa8 | |||
| 850675a083 | |||
| 13dd88cd66 | |||
| 47898d1489 | |||
| 8e37d9d2b6 | |||
| 10803b9322 | |||
| 7618532d7a | |||
| 36be49a740 | |||
| ecf174e1d6 | |||
| dcd3399689 | |||
| 593b57094c | |||
| 549dc1efde | |||
| 170997097f | |||
| 187fd768e6 | |||
| 785e84c4f2 | |||
| 22277e3a6d | |||
| 0f18b55aad | |||
| b347665453 | |||
| d33b97b45b | |||
| 5f20256c6e | |||
| 9b81225fb1 | |||
| db115f1a58 | |||
| d810f3ef8c | |||
| fcc6f76c99 | |||
| fcb710a888 | |||
| e4cc584924 | |||
| bfd79fbf73 | |||
| cad9f2c097 | |||
| 80e38660cd | |||
| 01fa62a58f | |||
| 202e04e272 | |||
| 9972de364f | |||
| 5f974c8d71 | |||
| b21e270f56 | |||
| ca4a92f5f0 | |||
| f893f3285a | |||
| 7cb5cbbe34 | |||
| 6032aebd6a | |||
| 20cb565f2f | |||
| 32a8f5d30f | |||
| 7f7f686ef8 |
16
App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } 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';
|
||||||
@@ -12,6 +12,8 @@ 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();
|
||||||
|
|
||||||
@@ -24,8 +26,14 @@ 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 }}>
|
||||||
@@ -33,6 +41,7 @@ const App: React.FC = () => {
|
|||||||
<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">
|
||||||
@@ -40,6 +49,11 @@ const App: React.FC = () => {
|
|||||||
</Tab.Screen>
|
</Tab.Screen>
|
||||||
<Tab.Screen name="Email" component={EmailScreen} />
|
<Tab.Screen name="Email" component={EmailScreen} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</QRCodeContext.Provider>
|
</QRCodeContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
20
android/app/release/output-metadata.json
Normal 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"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 189 KiB |
@@ -57,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
@@ -1,19 +1,29 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
const { API_BASE_URL, ENVIRONMENT } = Constants.expoConfig.extra;
|
const { API_BASE_URL, ENVIRONMENT } = Constants.expoConfig.extra;
|
||||||
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth';
|
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth';
|
||||||
//const API_BASE_URL = 'https://localhost:8443';
|
|
||||||
|
|
||||||
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_GET_SCANNED_GMAILS = "/v1/gmail/getEmails"
|
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
|
// Create an Axios instance
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
@@ -36,6 +46,9 @@ apiClient.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the X-USER-ID header
|
||||||
|
console.log('X-USER-ID:', config.headers['X-USER-ID']);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -44,26 +57,23 @@ apiClient.interceptors.request.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 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 {
|
||||||
|
const methodName = config.method?.toUpperCase() || 'REQUEST';
|
||||||
console.log("ENVIRONMENT:", ENVIRONMENT);
|
console.log("ENVIRONMENT:", ENVIRONMENT);
|
||||||
|
console.log(`API Call - ${methodName}:`, config.url, config.data || '');
|
||||||
console.log(`API Call - ${config.method.toUpperCase()}:`, config.url, config.data || '');
|
|
||||||
console.log(config);
|
console.log(config);
|
||||||
const response = await apiClient(config);
|
const response = await apiClient(config);
|
||||||
console.log('API Response:', response.data);
|
console.log(`API Response for ${methodName}:`, 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;
|
||||||
}
|
}
|
||||||
@@ -81,37 +91,26 @@ const fetchUserId = async () => {
|
|||||||
return currentUser.userId;
|
return currentUser.userId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const detectQRCodeType = async (data) => {
|
// 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
|
||||||
|
export const getQRCodeDetails = async (qrCodeId: string) => {
|
||||||
return apiRequest({
|
return apiRequest({
|
||||||
method: 'post',
|
method: 'get',
|
||||||
url: `${API_BASE_URL}${API_URL_VERIFY_URL}`,
|
url: `${API_BASE_URL}${API_URL_GET_QR_DETAILS}`,
|
||||||
data: { data }
|
headers: { 'QR-ID': qrCodeId },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
// GET User's Scanned Histories
|
||||||
export const getScannedHistories = async () => {
|
export const getScannedHistories = async () => {
|
||||||
return apiRequest({
|
return apiRequest({
|
||||||
@@ -119,7 +118,8 @@ export const getScannedHistories = async () => {
|
|||||||
url: `${API_BASE_URL}${API_URL_GET_HISTORIES}`
|
url: `${API_BASE_URL}${API_URL_GET_HISTORIES}`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// GET All User's Bookmark
|
|
||||||
|
// GET All User's Bookmarks
|
||||||
export const getAllUserBookmarks = async () => {
|
export const getAllUserBookmarks = async () => {
|
||||||
return apiRequest({
|
return apiRequest({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -132,7 +132,7 @@ 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}`,
|
||||||
data: { "qrCodeId": qrCodeId }
|
data: { qrCodeId }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ 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}`,
|
||||||
data: { "qrCodeId": qrCodeId }
|
data: { qrCodeId }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,14 +150,121 @@ 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}`,
|
||||||
data: { "qrCodeId": qrCodeId }
|
data: { qrCodeId }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// GET Scan user's GMAILS
|
// Function to delete all scanned histories
|
||||||
export const getScannedGmails = async () => {
|
export const deleteAllScannedHistories = async () => {
|
||||||
return apiRequest({
|
return apiRequest({
|
||||||
method: 'get',
|
method: 'put',
|
||||||
url: `${API_BASE_URL}${API_URL_GET_SCANNED_GMAILS}`
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,10 @@ export default ({ config }) => {
|
|||||||
...config,
|
...config,
|
||||||
extra: {
|
extra: {
|
||||||
API_BASE_URL: process.env.BASE_URL,
|
API_BASE_URL: process.env.BASE_URL,
|
||||||
ENVIRONMENT: process.env.NODE_ENV
|
ENVIRONMENT: process.env.NODE_ENV,
|
||||||
|
eas: {
|
||||||
|
projectId: "88ad983d-5ca3-44e6-bc1b-8a9a941af992"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
11
app.json
@@ -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
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/bakcup.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 305 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 227 KiB |
@@ -6,19 +6,17 @@ 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="QRScanner" component={QRScannerScreen} />
|
<Tab.Screen name="QRScanner" component={QRScannerScreen} />
|
||||||
<Tab.Screen name="Settings" component={SettingsScreen} />
|
<Tab.Screen name="Settings" component={SettingsScreen} />
|
||||||
|
|||||||
7803
package-lock.json
generated
17
package.json
@@ -24,29 +24,40 @@
|
|||||||
"@react-navigation/native": "^6.1.17",
|
"@react-navigation/native": "^6.1.17",
|
||||||
"@react-navigation/stack": "^6.4.1",
|
"@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.24",
|
"expo": "~51.0.24",
|
||||||
|
"expo-barcode-scanner": "~13.0.1",
|
||||||
"expo-camera": "~15.0.14",
|
"expo-camera": "~15.0.14",
|
||||||
"expo-dev-client": "~4.0.19",
|
"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.5",
|
"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-vision-camera": "^4.5.1",
|
||||||
"react-native-webview": "^13.8.6",
|
"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",
|
||||||
|
|||||||
@@ -1,240 +1,222 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from 'react-native';
|
import { View, Text, TouchableOpacity, FlatList, StyleSheet, ActivityIndicator, Alert, Animated, Dimensions, Modal } from 'react-native';
|
||||||
const emailData = {
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
emailAddress: 'user@example.com',
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
messages: [
|
import { getEmails, getScannedEmails, getUserInfo, deleteEmail } from '../api/qrCodeAPI';
|
||||||
{
|
import { fetchAuthSession } from 'aws-amplify/auth';
|
||||||
messageId: '190f239a0b0ccd52',
|
import { Buffer } from 'buffer';
|
||||||
subject: 'Fwd: Inaugural newsletter for EEE alumni, Singapore Polytechnic',
|
import ScannedDataBox from '../components/ScannedDataBox';
|
||||||
historyId: '29512',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:25:44 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: 'ii_190f2388c4123a1f8ce',
|
|
||||||
attachmentId: 'ANGjdJ_o4rREokfsZGp6r4aq9eZwPDCmatH7kcFkHsmKftrFGXBkPCvvdAgr8_oyYMcovpwJP8rzNvImcql0BAYOPCzdCBIeKoX7qddlA51CdsRfkRD7J56JMWbxiwb6qgZkDSDVp3zgEmiPhbk52Cm_QtlSZ8fsmMqAR_jDW9LbdxSSPY-mQJc3pMSgwHG69_BzBvBKXvW3-sJHHt1uL8J1cVo3yrTQ5X_SG3xkyv7eYKu9iFZ65mz70sW2PIooDmZroBRMfXgXnfK0vhp0kFb3vDcKDl8unN9YjRIrRdCov_7thsCIn_ahhFMCd4gPoLMnsva3czuVNbQz7Ze-Uihk3RLc6FuoVzV8BxmEjuYMApxPDm17ztm6FG9i_TADSQFE5e4ZxtJycCA3lYr0',
|
|
||||||
decodedContent: [
|
|
||||||
'https://icrm.sp.edu.sg/alumni/',
|
|
||||||
'https://industry.sp.edu.sg/span/career-opportunities/'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f2386f85c3a36',
|
|
||||||
subject: 'Fwd: Counting Down - ITE Alumni Association 25th Anniversary Beach Cleanup',
|
|
||||||
historyId: '29518',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:24:26 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://www.itealumni.org.sg/massemail/20221021/beach_clean_up_qrcode.png',
|
|
||||||
decodedContent: [
|
|
||||||
'https://me-qr.com/YHuVUWd'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f233d7ca4d8ac',
|
|
||||||
subject: 'Fwd: You’ve unlocked the Business Profile Rewards Programme!',
|
|
||||||
historyId: '29571',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:19:25 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://image.mkt.grab.com/lib/fe3a15707564067f751c78/m/55/8a4a3d09-136f-45df-b093-cc7ba5ae5a58.png',
|
|
||||||
decodedContent: [
|
|
||||||
'https://grab.onelink.me/2695613898?pid=EDM&c=ALL_NA_PAX_GFB_ALL_REG__BusinessRidesChallenge_NA&is_retargeting=true&af_dp=grab%3A%2F%2Fopen%3FscreenType%3DREWARDSWEB%26screen%3Dchallenge&af_force_deeplink=true&af_sub5=edm&af_ad=&af_web_dp=https%3A%2F%2Fwww.grab.com%2Fdownload%2F'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f232a1a55c1dd',
|
|
||||||
subject: 'Fwd: [UPDATE!] EEE Alumni Homecoming 2024 is now on 2 Aug, Friday',
|
|
||||||
historyId: '29574',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:18:05 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://file-au.clickdimensions.com/spedusg-a7v6v/files/eeealumnihomecoming2024on2aug.jpg',
|
|
||||||
decodedContent: [
|
|
||||||
'https://scanned.page/6645b492afa32'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f231cb59cb122',
|
|
||||||
subject: 'Fwd: Latest Job Opportunities (posted in Aug) with Attractive Salaries for ITE Graduates - follow us on IG, FB & TG!',
|
|
||||||
historyId: '29580',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:17:11 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: 'ii_190f2310981c2203e801',
|
|
||||||
attachmentId: 'ANGjdJ9da1OrxMtqkRbiPAvHbJIYMIBzHAbZlVSaSsJKbg3Q3HSSlO1xA5xb1Q8PAbzqh7QjIJ4gIdqo9vTiuQanpkcv7Suhj4KhPFbL2woGSy6w4G_Ihbqw-A2V06oPPO74AZWlDISzDH9bxbs2fjCcaGqFJykXNfYVy4Tfy7ZKbHIC1w12YAcK_xooDPEl48UU5Z0XHhlpWGTnAfMOUvGBkUF28MHFyPwhMYcaQgJ8GajQtm5LsXkiavj4JBU-27oPB9eupTMLzKZXKZY7kzaxLwfI_adCu7HxPQudQoPaVHL_sUHxohm8Tl3mJV0T4sVyzL4bemLEQBVXQfsp55mZspyk6ujOkKe7YHboI_EyNsvcRHKlezO6_uovYI-alBuLSJ2gEIXjViU_63tn',
|
|
||||||
decodedContent: [
|
|
||||||
'https://sites.google.com/view/itecareerservices/home',
|
|
||||||
'https://www.instagram.com/itealumni/',
|
|
||||||
'https://www.facebook.com/ITE-Alumni-Network-152816488099453'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f2315e39caf67',
|
|
||||||
subject: 'Fwd: Invite to SEIT Security Summit 2021',
|
|
||||||
historyId: '29596',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:16:44 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://mcusercontent.com/75a08b98bc328a8dcf78d9606/images/09ba74eb-b05f-a564-df79-53e4cdc6f53c.jpg',
|
|
||||||
decodedContent: [
|
|
||||||
'https://form.gov.sg/6101fe896bd2f30011aaf4c6'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f2309ae35c6d2',
|
|
||||||
subject: 'Fwd: Many Exciting, Good Paying Job Opportunities in our Job Portal for ITE Graduates',
|
|
||||||
historyId: '29607',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:15:53 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: 'ii_190f22744b62d7326d2',
|
|
||||||
attachmentId: 'ANGjdJ-M5yXJcDRyejnSdcpr0JipKrq1PL1c2gpXiMd6jC6EI0oa4VKe9QJf_oArNRb4ae2IvhZO7OE7BOj_GqcLYS29qz4sUCTQvXA2Z4ydsLOgBkhMyHyFIL9KGLW4YZ3eDVHDPifGnAeNZuRgLTsF7E8Td3sFvfcITcI1SEM04eqpALEHPJmpKh7Er5hJv4ciLlfd2yA0Z9-wRHT7174rjAIypjy606W5mdYdV9B7Y63IwUz8sckrs-BmMsWPooUKA2qrz_vDatb3EVQzmRZreAJaUwIhMUv30JB5x3SS9NrKyDmoj_oU04iVXUWAjUE1SOFbN2Bk2nALoSVgAHwXdvOPvp17iI8vgp0VXKyocHvkNOqpsdKbfsRDR9yVhImqs4s83lAOH9X1rp9V',
|
|
||||||
decodedContent: [
|
|
||||||
'https://www.instagram.com/itealumni/',
|
|
||||||
'https://www.facebook.com/ITEAlumni'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cid: 'ii_190f22744b62c91aec1',
|
|
||||||
attachmentId: 'ANGjdJ_2XGOHh1AS3Vtx02mFNCIKP-DpSz8sChaNxzeFaviwLe5zBsLpxJ8vv4WYrYElyy7S0xxjy5q3CWWlHGI8zgkZCF95zPlEvJ8f_bWswlQ6tIanXM3b_tk6Jh_e35X99t1PjfRQFDck3vWEfHtMuM3brPkaNP2Ely4bjGO9bthU58TRpcHq3sdnhlrK2If0xLygyS9mw22og17RVGd8vI39QzVSF7xY8JnI-OTJbiRRZ4es3lFLStf3DpjSHIRqeja8Ps2oEduCLl4MQZ7Mhy0df585QC-eJvk6UUX20q2JfQPX6Up7xuR9sxD0GLFeJhseF98Y4QN3K2v6YAhch-LFp3_p-StNQfLafI2bOfFAiv9J8FflTHEIPOICLkRvM_2lRYK4cQajgROp',
|
|
||||||
decodedContent: [
|
|
||||||
'https://uspur.e2i.com.sg/ITE',
|
|
||||||
'https://sites.google.com/view/bookyourecg',
|
|
||||||
'https://sites.google.com/view/ecg-ce/home',
|
|
||||||
'https://sites.google.com/view/virtualchallenge/book-ecg-appointment'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f2119dd09ee0e',
|
|
||||||
subject: 'Fwd: Exclusive insights for Risk and Compliance Managers - don\'t miss out!',
|
|
||||||
historyId: '29628',
|
|
||||||
date: 'Sat, 27 Jul 2024 10:42:27 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://go.ibf.org.sg/l/892591/2023-10-09/g98t7/892591/1696907378NZU0nrZd/qr_code__21_.png',
|
|
||||||
decodedContent: [
|
|
||||||
'https://us02web.zoom.us/webinar/register/WN_qHoX3DXUS-y_qP3eWt2jXQ'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190e95f5626b2a39',
|
|
||||||
subject: 'Test Test',
|
|
||||||
historyId: '28144',
|
|
||||||
date: 'Thu, 25 Jul 2024 18:10:57 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: '190e95f3d0096ff49b41',
|
|
||||||
attachmentId: 'ANGjdJ9dE5MqEQkNaX1ssbR9jTwRttD28jj5b3-A8iGS_v8QNm6s5HNqknpTeVQP75fCTrLdwbbvnCI120H1Boo_Qls42YqL470MHAw_d_QPXF10TWUNhpOVpTBPXntHwxD4tFoxHfvH47OdZVE-LuuYbJUsLu9BzYM6F3t2uBfsimbOhTkmgHHW8LOcnX1ZjM5Q53rsbD_K7UaBfKgWaaaDq4U4qg4ztLscRe00zLGkOPxgETTf2SRPm4x5fHVfoszj1xQRjSbTpdkbAnslWH5HYgDWP3854wBObQGfsyve6LfARBMNH7Xez0OAX8k',
|
|
||||||
decodedContent: [
|
|
||||||
'0002010102111531260207020052044611190861600 26860011SG.COM.NETS01231198500065G99123123590002111119086160003089086160110012990891A6686851800007SG.SGQR01122007073092D9020701.00110306330029040201050242060400000708202307175204000053037025802SG5922BENDEMEER PRAWN NOODLE6009Singapore63040AEB'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f22775c9d2a35',
|
|
||||||
subject: 'Fwd: Monthly Update - Highlights from July',
|
|
||||||
historyId: '29700',
|
|
||||||
date: 'Sat, 27 Jul 2024 11:00:00 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: 'ii_190f22664b8f23245d3',
|
|
||||||
attachmentId: 'ANGjdJ-5y7TR8dckE9jKe5o9xV7zLQ2Eq4U4LPcfhj29Nx8aR0N8bJqKm3OvSy0hP6cOvH9RbO1vR7Sf8Q3_G6QhsaB8Y1xYTQpv4QY8ZTloGOaBbhMyHyKGM9KHLW5YZ3eDVHDPifGnAeNZuRgLTsF7E8Td3sFvfcITcI1SEM04eqpALEHPJmpKh7Er5hJv4ciLlfd2yA0Z9-wRHT7174rjAIypjy606W5mdYdV9B7Y63IwUz8sckrs-BmMsWPooUKA2qrz_vDatb3EVQzmRZreAJaUwIhMUv30JB5x3SS9NrKyDmoj_oU04iVXUWAjUE1SOFbN2Bk2nALoSVgAHwXdvOPvp17iI8vgp0VXKyocHvkNOqpsdKbfsRDR9yVhImqs4s83lAOH9X1rp9V',
|
|
||||||
decodedContent: [
|
|
||||||
'https://www.example.com/newsletter/july',
|
|
||||||
'https://www.example.com/events/upcoming'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f22887e0d8b27',
|
|
||||||
subject: 'Fwd: Invitation to Join the Annual Tech Summit 2024',
|
|
||||||
historyId: '29702',
|
|
||||||
date: 'Sat, 27 Jul 2024 10:45:30 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/images/tech_summit_invite.png',
|
|
||||||
decodedContent: [
|
|
||||||
'https://example.com/register/techsummit2024'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f229a4b1d2c89',
|
|
||||||
subject: 'Fwd: Important Updates on Your Account',
|
|
||||||
historyId: '29704',
|
|
||||||
date: 'Sat, 27 Jul 2024 10:30:15 +0800',
|
|
||||||
qrCodeByContentId: [
|
|
||||||
{
|
|
||||||
cid: 'ii_190f229e89df3a5d0',
|
|
||||||
attachmentId: 'ANGjdJ9-M5yXJcDRyejnSdcpr0JipKrq1PL1c2gpXiMd6jC6EI0oa4VKe9QJf_oArNRb4ae2IvhZO7OE7BOj_GqcLYS29qz4sUCTQvXA2Z4ydsLOgBkhMyHyFIL9KGLW4YZ3eDVHDPifGnAeNZuRgLTsF7E8Td3sFvfcITcI1SEM04eqpALEHPJmpKh7Er5hJv4ciLlfd2yA0Z9-wRHT7174rjAIypjy606W5mdYdV9B7Y63IwUz8sckrs-BmMsWPooUKA2qrz_vDatb3EVQzmRZreAJaUwIhMUv30JB5x3SS9NrKyDmoj_oU04iVXUWAjUE1SOFbN2Bk2nALoSVgAHwXdvOPvp17iI8vgp0VXKyocHvkNOqpsdKbfsRDR9yVhImqs4s83lAOH9X1rp9V',
|
|
||||||
decodedContent: [
|
|
||||||
'https://www.example.com/account/updates'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '190f230bb4a5e7d2',
|
|
||||||
subject: 'Fwd: Your Subscription is About to Expire!',
|
|
||||||
historyId: '29710',
|
|
||||||
date: 'Sat, 27 Jul 2024 10:15:45 +0800',
|
|
||||||
qrCodeByURL: [
|
|
||||||
{
|
|
||||||
url: 'https://example.com/images/subscription_expire.png',
|
|
||||||
decodedContent: [
|
|
||||||
'https://example.com/renew/subscription'
|
|
||||||
],
|
|
||||||
totalQRCodeFound: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||||
|
|
||||||
const EmailScreen: React.FC = () => {
|
const EmailScreen: React.FC = () => {
|
||||||
const [selectedMessage, setSelectedMessage] = useState(null);
|
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) => {
|
const handleSelectMessage = (message) => {
|
||||||
setSelectedMessage(selectedMessage === message ? null : 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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.header}>{emailData.emailAddress}</Text>
|
{/* Header with Email and Refresh Button */}
|
||||||
<Text style={styles.emailAddress}>--- </Text>
|
<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
|
<FlatList
|
||||||
data={emailData.messages}
|
data={emailData.messages}
|
||||||
keyExtractor={(item) => item.messageId}
|
keyExtractor={(item) => item.messageId}
|
||||||
@@ -242,104 +224,247 @@ const EmailScreen: React.FC = () => {
|
|||||||
<TouchableOpacity onPress={() => handleSelectMessage(item)} style={styles.messageContainer}>
|
<TouchableOpacity onPress={() => handleSelectMessage(item)} style={styles.messageContainer}>
|
||||||
<Text style={styles.subject}>{item.subject}</Text>
|
<Text style={styles.subject}>{item.subject}</Text>
|
||||||
<Text style={styles.date}>{item.date}</Text>
|
<Text style={styles.date}>{item.date}</Text>
|
||||||
{selectedMessage === item && (
|
{selectedMessage?.messageId === item.messageId && (
|
||||||
<View style={styles.emailDetailContainer}>
|
<View style={styles.emailListContainer}>
|
||||||
{item.qrCodeByContentId && (
|
<Text style={styles.qrCodeHeader}>Decoded QR Codes:</Text>
|
||||||
<View>
|
{item.decodedContentsDetails?.map((details, index) => (
|
||||||
<Text style={styles.qrCodeHeader}>QR Codes by Content ID:</Text>
|
|
||||||
{item.qrCodeByContentId.map((qrCode, index) => (
|
|
||||||
<View key={index} style={styles.qrCodeContainer}>
|
<View key={index} style={styles.qrCodeContainer}>
|
||||||
{qrCode.decodedContent.map((url, i) => (
|
<TouchableOpacity onPress={() => handleUrlClick(details.data.id)}>
|
||||||
<Text key={i} style={styles.qrCodeLink}>{url}</Text>
|
<Text style={styles.qrCodeLink}>{details.data.contents}</Text>
|
||||||
))}
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
<View style={styles.dividerHorizontal} />
|
||||||
)}
|
<TouchableOpacity onPress={() => handleDeletePress(item.messageId)} style={styles.deleteButtonContainer}>
|
||||||
{item.qrCodeByURL && (
|
<Text style={styles.deleteButtonText}>Delete this entry</Text>
|
||||||
<View>
|
<Ionicons name="trash-bin" size={24} color="#ff69b4" />
|
||||||
<Text style={styles.qrCodeHeader}>QR Codes by URL:</Text>
|
</TouchableOpacity>
|
||||||
{item.qrCodeByURL.map((qrCode, index) => (
|
|
||||||
<View key={index} style={styles.qrCodeContainer}>
|
|
||||||
<Text style={styles.qrCodeLink}>{qrCode.url}</Text>
|
|
||||||
{qrCode.decodedContent.map((url, i) => (
|
|
||||||
<Text key={i} style={styles.qrCodeLink}>{url}</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
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: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f8f0fc',
|
backgroundColor: '#f8f0fc',
|
||||||
padding: 10,
|
paddingHorizontal: screenWidth * 0.025,
|
||||||
paddingBottom: 90, // Add this line to give space for the nav bar
|
paddingTop: screenHeight * 0.05,
|
||||||
|
paddingBottom: screenHeight * 0.1,
|
||||||
},
|
},
|
||||||
header: {
|
headerContainer: {
|
||||||
fontSize: 24,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: screenHeight * 0.02,
|
||||||
|
},
|
||||||
|
emailHeader: {
|
||||||
|
fontSize: screenWidth * 0.045,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#ff69b4',
|
color: '#ff69b4',
|
||||||
marginBottom: 10,
|
},
|
||||||
|
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',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
emailAddress: {
|
rescanIndicator: {
|
||||||
fontSize: 18,
|
justifyContent: 'center',
|
||||||
color: '#ff69b4',
|
alignItems: 'center',
|
||||||
marginBottom: 10,
|
marginVertical: screenHeight * 0.015,
|
||||||
},
|
},
|
||||||
emailContainer: {
|
rescanText: {
|
||||||
paddingBottom: 120, // Add padding to prevent overlap with custom navbar
|
color: '#ff69b4',
|
||||||
|
fontSize: screenWidth * 0.04,
|
||||||
},
|
},
|
||||||
messageContainer: {
|
messageContainer: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
padding: 10,
|
padding: screenWidth * 0.025,
|
||||||
borderRadius: 10,
|
borderRadius: screenWidth * 0.025,
|
||||||
marginBottom: 10,
|
marginBottom: screenHeight * 0.015,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: screenHeight * 0.002 },
|
||||||
shadowRadius: 5,
|
shadowRadius: screenWidth * 0.025,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
subject: {
|
subject: {
|
||||||
fontSize: 16,
|
fontSize: screenWidth * 0.04,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#000',
|
color: '#000',
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
fontSize: 14,
|
fontSize: screenWidth * 0.035,
|
||||||
color: '#555',
|
color: '#555',
|
||||||
marginBottom: 5,
|
|
||||||
},
|
|
||||||
emailDetailContainer: {
|
|
||||||
backgroundColor: '#f8f0fc',
|
|
||||||
borderRadius: 10,
|
|
||||||
marginTop: 10,
|
|
||||||
padding: 10,
|
|
||||||
},
|
|
||||||
qrCodeHeader: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#ff69b4',
|
|
||||||
marginBottom: 5,
|
|
||||||
},
|
},
|
||||||
qrCodeContainer: {
|
qrCodeContainer: {
|
||||||
marginBottom: 5,
|
marginBottom: screenHeight * 0.015,
|
||||||
},
|
},
|
||||||
qrCodeLink: {
|
qrCodeLink: {
|
||||||
fontSize: 14,
|
fontSize: screenWidth * 0.035,
|
||||||
color: '#0000ff',
|
color: '#0000ff',
|
||||||
textDecorationLine: 'underline',
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
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;
|
||||||
@@ -27,8 +32,6 @@ const HistoryScreen: React.FC = () => {
|
|||||||
setHistoriesLoading(true);
|
setHistoriesLoading(true);
|
||||||
const historiesData = await getScannedHistories();
|
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,78 +45,53 @@ 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);
|
const clearSelectedQrCodeData = () => {
|
||||||
|
setSelectedQrCodeId(null);
|
||||||
return () => backHandler.remove();
|
setIsModalVisible(false); // Close ScannedDataBox Modal
|
||||||
}, [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 filteredQrCodes = showBookmarks
|
||||||
|
? histories.filter((qr) => qr.bookmarked)
|
||||||
|
: histories;
|
||||||
|
|
||||||
const clearSelectedData = () => {
|
|
||||||
setSelectedData(null);
|
|
||||||
setSelectedScanResult(null);
|
|
||||||
setSelectedType(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
return (
|
|
||||||
<View style={styles.itemContainer}>
|
<View style={styles.itemContainer}>
|
||||||
<View style={styles.itemLeft}>
|
<View style={styles.itemLeft}>
|
||||||
<TouchableOpacity onPress={() => handleItemPress(item)} style={styles.itemContent}>
|
<TouchableOpacity onPress={() => handleSelectQrCodeForView(item)} style={styles.itemContent}>
|
||||||
<Image source={require('../assets/ScanIcon3.png')} style={styles.scanIcon} />
|
<Image source={require('../assets/ScanIcon3.png')} style={styles.scanIcon} />
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
<Text style={styles.dataText} numberOfLines={1} ellipsizeMode="tail">{item.data.contents}</Text>
|
<Text style={styles.dataText} numberOfLines={1} ellipsizeMode="tail">{item.data.contents}</Text>
|
||||||
@@ -125,41 +103,60 @@ const HistoryScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.itemRight}>
|
<View style={styles.itemRight}>
|
||||||
<TouchableOpacity onPress={() => dispatch(toggleBookmark({ userId: userAttributes.sub, qrCode: item}))}>
|
<TouchableOpacity onPress={() => dispatch(toggleBookmark({ userId: userAttributes.sub, qrCode: item}))}>
|
||||||
<Ionicons name={item.bookmarked ? "bookmark" : "bookmark-outline"} size={24} color={item.bookmarked ? "#2196F3" : "#ff69b4"} />
|
<Ionicons name={item.bookmarked ? "bookmark" : "bookmark-outline"} size={screenWidth * 0.06} color={item.bookmarked ? "#2196F3" : "#ff69b4"} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
setQrCodeToDelete(item.data.id);
|
setQrCodeToDelete(item.data.id);
|
||||||
setIsModalVisible(true);
|
setIsDeleteModalVisible(true);
|
||||||
}}>
|
}}>
|
||||||
<Ionicons name="close-circle-outline" size={24} color="#ff69b4" />
|
<Ionicons name="trash-outline" size={screenWidth * 0.06} color="#ff69b4" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
)}
|
||||||
}}
|
keyExtractor={(item, index) => index.toString()}
|
||||||
keyExtractor={(item, index) => {
|
|
||||||
//console.log(item, index);
|
|
||||||
|
|
||||||
return 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.modalOverlay}>
|
||||||
<View style={styles.modalContainer}>
|
<View style={styles.modalContainer}>
|
||||||
<View style={styles.modalContent}>
|
|
||||||
<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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,219 +1,269 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, Modal } 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 SettingsScreen from './SettingsScreen'; // Import the Settings screen
|
import { getCurrentUser, fetchAuthSession } from 'aws-amplify/auth';
|
||||||
|
|
||||||
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||||
|
|
||||||
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 [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 [scannedData, setScannedData] = useState<string>(''); // State for QR scanned Data
|
|
||||||
const [dataType, setDataType] = useState<string>(''); // State for data type
|
|
||||||
const [enableTorch, setEnableTorch] = useState<boolean>(false); // State for torch
|
|
||||||
const [cameraVisible, setCameraVisible] = useState<boolean>(true); // State to control camera visibility
|
|
||||||
|
|
||||||
// State to control the visibility of the modal
|
|
||||||
const [isSettingsModalVisible, setIsSettingsModalVisible] = useState<boolean>(false);
|
const [isSettingsModalVisible, setIsSettingsModalVisible] = useState<boolean>(false);
|
||||||
|
const [enableTorch, setEnableTorch] = useState<boolean>(false);
|
||||||
|
const [scanned, setScanned] = useState<boolean>(false);
|
||||||
|
const [qrCodeId, setQRCodeId] = useState<string | null>(null);
|
||||||
|
const [isScannedDataBoxVisible, setIsScannedDataBoxVisible] = useState<boolean>(false);
|
||||||
|
const [bannerOpacity] = useState(new Animated.Value(0));
|
||||||
|
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 = () => {
|
try {
|
||||||
setScannedData('');
|
const response = await getQRTips();
|
||||||
setScanned(false);
|
setQrTip(response.tips); // Set the qrTip state to the value of the tips property
|
||||||
setDataType('');
|
} catch (error) {
|
||||||
console.log("Scan data cleared");
|
console.error('Error fetching QR tips:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only run when the screen is focusd
|
||||||
|
useFocusEffect(
|
||||||
|
React.useCallback(() => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideScannedDataBox = () => {
|
||||||
|
Animated.timing(scannedDataBoxY, {
|
||||||
|
toValue: screenHeight, // Move it back off-screen
|
||||||
|
duration: 500,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start(() => {
|
||||||
|
setIsScannedDataBoxVisible(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
const handlePayload = async (payload: string) => {
|
||||||
setScanned(true);
|
setScanned(true);
|
||||||
console.log("Scanning Completed. Payload is:", payload);
|
console.info("Decoded QR Code, 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");
|
|
||||||
};
|
|
||||||
|
|
||||||
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 scanQRCode(payload);
|
||||||
data: payload,
|
const qrCodeId = response.qrcode.data.id;
|
||||||
}, {
|
setQRCodeId(qrCodeId);
|
||||||
headers: {
|
showScannedDataBox(); // Show the ScannedDataBox pop-up with animation
|
||||||
'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 scanning QR code:", error);
|
||||||
return 'UNKNOWN';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTorch = () => {
|
// Use the camera to scan QR codes
|
||||||
setEnableTorch((prev) => !prev);
|
const codeScanner = useCodeScanner({
|
||||||
console.log("Torch toggled:", enableTorch ? "off" : "on");
|
codeTypes: ['qr'], // Only scan QR codes
|
||||||
};
|
onCodeScanned: (codes) => {
|
||||||
|
if (!scanned && codes[0]?.value) {
|
||||||
const handleTestScan = () => {
|
handlePayload(codes[0].value); // Handle the QR code value
|
||||||
handlePayload('TEST123');
|
}
|
||||||
console.log("Test scan executed");
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
// 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);
|
});
|
||||||
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");
|
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(
|
// Check for camera permissions
|
||||||
useCallback(() => {
|
if (!hasPermission) {
|
||||||
setCameraVisible(true);
|
return <Text>Requesting camera permission...</Text>;
|
||||||
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}>
|
||||||
<Text style={styles.headerText}>SafeQR v0.89</Text>
|
{/* Banner for network connectivity */}
|
||||||
<Text style={styles.welcomeText}>Welcome to SafeQR code Scanner</Text>
|
<Animated.View style={[styles.banner, { opacity: bannerOpacity }]}>
|
||||||
|
<Text style={styles.bannerText}>No Internet Connection</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>
|
||||||
|
|
||||||
|
{/* Gallery Button */}
|
||||||
<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 */}
|
{/* Settings Icon */}
|
||||||
<TouchableOpacity onPress={() => setIsSettingsModalVisible(true)} style={styles.settingsButton}>
|
<TouchableOpacity onPress={() => setIsSettingsModalVisible(true)} style={styles.settingsButton}>
|
||||||
<Ionicons name="settings" size={24} color="#000" />
|
<Ionicons name="settings" size={screenWidth * 0.06} color="#000" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Settings Modal */}
|
{/* Settings Modal */}
|
||||||
@@ -222,9 +272,10 @@ const QRScannerScreen: React.FC<{ clearScanData: () => void }> = ({ clearScanDat
|
|||||||
transparent={true}
|
transparent={true}
|
||||||
visible={isSettingsModalVisible}
|
visible={isSettingsModalVisible}
|
||||||
onRequestClose={() => setIsSettingsModalVisible(false)}
|
onRequestClose={() => setIsSettingsModalVisible(false)}
|
||||||
|
style={styles.settingsModal}
|
||||||
>
|
>
|
||||||
<View style={styles.modalContainer}>
|
<View style={styles.settingsModalContainer}>
|
||||||
<View style={styles.modalContent}>
|
<View style={styles.settingsModalContent}>
|
||||||
<SettingsScreen />
|
<SettingsScreen />
|
||||||
<TouchableOpacity onPress={() => setIsSettingsModalVisible(false)} style={styles.closeButton}>
|
<TouchableOpacity onPress={() => setIsSettingsModalVisible(false)} style={styles.closeButton}>
|
||||||
<Text style={styles.closeButtonText}>Close</Text>
|
<Text style={styles.closeButtonText}>Close</Text>
|
||||||
@@ -236,103 +287,144 @@ const QRScannerScreen: React.FC<{ clearScanData: () => void }> = ({ clearScanDat
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stylesheet
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f8f0fc',
|
backgroundColor: '#f8f0fc',
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
headerText: {
|
titleText: {
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#ff69b4',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 20,
|
fontSize: 20,
|
||||||
|
marginTop: screenHeight * 0.05,
|
||||||
|
color: 'black',
|
||||||
},
|
},
|
||||||
splashContainer: {
|
logo: {
|
||||||
flex: 1,
|
alignSelf: 'center',
|
||||||
justifyContent: 'center',
|
width: screenWidth * 0.5,
|
||||||
alignItems: 'center',
|
height: screenWidth * 0.2,
|
||||||
backgroundColor: '#f8f0fc',
|
resizeMode: 'contain',
|
||||||
height: '100%',
|
marginVertical: 10,
|
||||||
width: '100%',
|
},
|
||||||
|
welcomeText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 15,
|
||||||
|
marginVertical: 10,
|
||||||
|
color: 'black',
|
||||||
},
|
},
|
||||||
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',
|
||||||
camera: {
|
marginTop: '1%',
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
flashButton: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 20,
|
|
||||||
left: 100,
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#000',
|
|
||||||
borderRadius: 25,
|
|
||||||
},
|
|
||||||
galleryButton: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 20,
|
|
||||||
right: 100,
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#000',
|
|
||||||
borderRadius: 25,
|
|
||||||
},
|
|
||||||
scannedDataBox: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '10%',
|
|
||||||
left: '5%',
|
|
||||||
right: '5%',
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
welcomeText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 20,
|
|
||||||
marginVertical: 10,
|
|
||||||
color: 'black',
|
|
||||||
},
|
},
|
||||||
settingsButton: {
|
settingsButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 40,
|
top: screenHeight * 0.05,
|
||||||
right: 20,
|
right: 20,
|
||||||
zIndex: 2,
|
|
||||||
},
|
},
|
||||||
modalContainer: {
|
flashButton: {
|
||||||
flex: 1,
|
position: 'absolute',
|
||||||
|
bottom: screenHeight * 0.025,
|
||||||
|
left: screenWidth * 0.2,
|
||||||
|
width: screenWidth * 0.125,
|
||||||
|
height: screenWidth * 0.125,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: '90%',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
borderRadius: screenWidth * 0.0625,
|
||||||
|
},
|
||||||
|
galleryButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: screenHeight * 0.025,
|
||||||
|
right: screenWidth * 0.2,
|
||||||
|
width: screenWidth * 0.125,
|
||||||
|
height: screenWidth * 0.125,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
borderRadius: screenWidth * 0.0625,
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center', // Aligns the modal to the bottom by default
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
},
|
},
|
||||||
modalContent: {
|
modalContainer: {
|
||||||
width: '100%', // Adjust the width to cover more space
|
marginHorizontal: '5%',
|
||||||
height: '90%', // Adjust the height to cover more space
|
borderRadius: screenWidth * 0.025,
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
padding: 20, // Reduce the padding
|
padding: screenWidth * 0.025,
|
||||||
borderRadius: 10,
|
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',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
marginTop: 10,
|
marginTop: screenHeight * 0.01,
|
||||||
padding: 10,
|
padding: screenWidth * 0.025,
|
||||||
backgroundColor: '#ff69b4',
|
backgroundColor: '#ff69b4',
|
||||||
borderRadius: 5,
|
borderRadius: screenWidth * 0.0125,
|
||||||
},
|
},
|
||||||
closeButtonText: {
|
closeButtonText: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
banner: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: screenHeight * 0.4,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default QRScannerScreen;
|
export default QRScannerScreen;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,43 +111,119 @@ const SettingsScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [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 */}
|
||||||
{userAttributes ? (
|
<View style={styles.section}>
|
||||||
<View>
|
<View style={styles.profileContainer}>
|
||||||
<Text style={styles.userName}>Hello, {userAttributes?.name}</Text>
|
<Ionicons name="person-circle" size={60} color="#f41c87" style={styles.profileIcon} />
|
||||||
{googleAccessToken && (
|
<Text style={styles.userName}>Hello, {userName}</Text>
|
||||||
<Text>Google Access Token: {googleAccessToken.substring(0, 10)}...</Text>
|
|
||||||
)}
|
|
||||||
<SignOutButton />
|
|
||||||
</View>
|
</View>
|
||||||
|
{userAttributes ? (
|
||||||
|
<TouchableOpacity style={styles.logoutButton} onPress={handleSignOut}>
|
||||||
|
<Ionicons name="log-out-outline" size={24} color="#fff" style={styles.buttonIcon} />
|
||||||
|
<Text style={styles.logoutButtonText}>Log Out</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -143,17 +232,17 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f8f0fc',
|
backgroundColor: '#f8f0fc',
|
||||||
padding: 10, // Reduce padding
|
padding: 10,
|
||||||
width: '100%', // Increase width to fill the modal
|
width: '100%',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#ff69b4',
|
color: '#ff69b4',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
textAlign: 'center', // Center the header text
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
profileSection: {
|
section: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
@@ -162,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,
|
||||||
@@ -196,5 +344,4 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default SettingsScreen;
|
export default SettingsScreen;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 47 KiB |
BIN
temp/Sample QR/20 redirects.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 33 KiB |
BIN
temp/Sample QR/7458a3bbd950d7a3ae848ca11830f608.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
temp/Sample QR/EmailTO.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
temp/Sample QR/Invalid ssl.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
temp/Sample QR/Phone.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
temp/Sample QR/SMSTO.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
temp/Sample QR/Text.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
temp/Sample QR/WIFI no enc.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
temp/Sample QR/WIFI_WEP.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
temp/Sample QR/WIFI_WPA.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
temp/Sample QR/executable.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
temp/Sample QR/http_no_ssl.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
temp/Sample QR/large text.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
temp/Sample QR/redirect link.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
temp/Sample QR/whatsapp.png
Normal file
|
After Width: | Height: | Size: 200 KiB |