Syncs database when connected to the network.

This commit is contained in:
Aaron Louie 2020-09-11 17:23:26 -04:00
parent b1cae17dcd
commit 10ceb3e337
9 changed files with 731 additions and 660 deletions

63
App.tsx
View File

@ -1,8 +1,8 @@
// @refresh reset // @refresh reset
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import NetInfo, {NetInfoState, NetInfoStateType} from '@react-native-community/netinfo';
import {format, parse} from 'date-fns'; import {format, parse} from 'date-fns';
import {BarCodeEvent, BarCodeScanner, PermissionResponse} from 'expo-barcode-scanner'; import {BarCodeEvent, BarCodeScanner, PermissionResponse} from 'expo-barcode-scanner';
// @ts-ignore
import * as firebase from 'firebase'; import * as firebase from 'firebase';
import 'firebase/firestore'; import 'firebase/firestore';
import React, {ReactElement, useCallback, useEffect, useState} from 'react'; import React, {ReactElement, useCallback, useEffect, useState} from 'react';
@ -15,21 +15,13 @@ import {BarCodeDisplay, PrintButton, PrintingMessage} from './components/Print';
import {IdNumberInput, InputIdButton, ScanButton, Scanner} from './components/Scan'; import {IdNumberInput, InputIdButton, ScanButton, Scanner} from './components/Scan';
import {SettingsScreen} from './components/Settings'; import {SettingsScreen} from './components/Settings';
import {colors, styles} from './components/Styles'; import {colors, styles} from './components/Styles';
import {sendDataToFirebase, SyncMessage} from './components/Sync';
import {dateFormat, firebaseConfig} from './config/default';
import {BarcodeScannerAppState} from './models/BarcodeScannerAppState'; import {BarcodeScannerAppState} from './models/BarcodeScannerAppState';
import {CameraType, ElementProps, StateProps} from './models/ElementProps'; import {CameraType, ElementProps, StateProps} from './models/ElementProps';
import {LineCount} from './models/LineCount'; import {LineCount} from './models/LineCount';
import {Sample} from './models/Sample'; import {Sample} from './models/Sample';
const firebaseConfig = {
apiKey: 'api_key_goes_here',
authDomain: "uva-covid19-testing-kiosk.firebaseapp.com",
databaseURL: "https://uva-covid19-testing-kiosk.firebaseio.com",
projectId: 'project_id_goes_here',
storageBucket: "uva-covid19-testing-kiosk.appspot.com",
messagingSenderId: 'sender_id_goes_here',
appId: 'app_id_goes_here'
};
// Initialize Firebase if not already initialized. // Initialize Firebase if not already initialized.
if (firebase.apps.length === 0) { if (firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig); firebase.initializeApp(firebaseConfig);
@ -37,13 +29,12 @@ if (firebase.apps.length === 0) {
YellowBox.ignoreWarnings([ YellowBox.ignoreWarnings([
'Setting a timer for a long period of time', // Ignore Firebase timer warnings 'Setting a timer for a long period of time', // Ignore Firebase timer warnings
'Remote debugger is in a background tab', // Ignore Firebase timer warnings 'Remote debugger is in a background tab', // Ignore remote debugger warnings
]); ]);
const db = firebase.firestore(); const db = firebase.firestore();
const samplesCollection = db.collection('samples'); const samplesCollection = db.collection('samples');
const countsCollection = db.collection('counts'); const countsCollection = db.collection('counts');
const dateFormat = 'yyyyMMddHHmm';
const theme = { const theme = {
...DefaultTheme, ...DefaultTheme,
@ -61,6 +52,7 @@ export default function Main() {
const [lineCounts, setLineCounts] = useState<LineCount[]>([]); const [lineCounts, setLineCounts] = useState<LineCount[]>([]);
const [cameraType, setCameraType] = useState<CameraType>('back'); const [cameraType, setCameraType] = useState<CameraType>('back');
const [numCopies, setNumCopies] = useState<number>(0); const [numCopies, setNumCopies] = useState<number>(0);
const [isConnected, setIsConnected] = useState<boolean>(false);
const defaultsInitializers = { const defaultsInitializers = {
'default.cameraType': (s: string) => setCameraType(s as CameraType), 'default.cameraType': (s: string) => setCameraType(s as CameraType),
@ -79,6 +71,12 @@ export default function Main() {
}); });
}); });
NetInfo.addEventListener((state: NetInfoState) => {
if (state.type === NetInfoStateType.wifi) {
setIsConnected(!!(state.isConnected && state.isInternetReachable));
}
});
BarCodeScanner.requestPermissionsAsync().then((value: PermissionResponse) => { BarCodeScanner.requestPermissionsAsync().then((value: PermissionResponse) => {
if (value.granted) { if (value.granted) {
setAppState(BarcodeScannerAppState.DEFAULT); setAppState(BarcodeScannerAppState.DEFAULT);
@ -182,10 +180,6 @@ export default function Main() {
setLineCounts((previousLineCounts) => previousLineCounts.concat(newLineCounts)); setLineCounts((previousLineCounts) => previousLineCounts.concat(newLineCounts));
}, [lineCounts]); }, [lineCounts]);
const sendDataToFirebase = async (newData: Array<Sample|LineCount>, collection: firebase.firestore.CollectionReference) => {
const writes = newData.map((s: Sample|LineCount) => collection.doc(s.id).set(s));
await Promise.all(writes);
}
const ErrorMessage = (props: ElementProps): ReactElement => { const ErrorMessage = (props: ElementProps): ReactElement => {
return <View style={styles.fullScreen}> return <View style={styles.fullScreen}>
@ -208,10 +202,6 @@ export default function Main() {
>Loading...</Snackbar>; >Loading...</Snackbar>;
} }
const SuccessMessage = (props: ElementProps): ReactElement => {
return <Title>Your barcode label has printed successfully.</Title>;
}
const ActionButtons = (props: ElementProps): ReactElement => { const ActionButtons = (props: ElementProps): ReactElement => {
return <View> return <View>
<PrintButton onClicked={_print}/> <PrintButton onClicked={_print}/>
@ -230,28 +220,13 @@ export default function Main() {
<InputLineCountButton onClicked={_inputLineCount}/> <InputLineCountButton onClicked={_inputLineCount}/>
</View>; </View>;
case BarcodeScannerAppState.PRINTED: case BarcodeScannerAppState.PRINTED:
// TODO: Detect when user is online. If online, sync data with Firebase. If not online, just go home. Alternatively, set up a timer that periodically syncs data with the database. return <SyncMessage
isConnected={isConnected}
// Upload any changes to Firebase samplesCollection={samplesCollection}
AsyncStorage.getAllKeys().then(keys => { countsCollection={countsCollection}
const newSamples = keys onCancel={_home}
.filter(s => /^[\d]{9}-[\d]{12}-[\d]{4}$/.test(s)) onSync={_home}
.map(s => { />;
const propsArray = s.split('-');
return {
id: s,
barcodeId: propsArray[0],
createdAt: parse(propsArray[1], dateFormat, new Date()),
locationId: propsArray[2],
} as Sample;
});
sendDataToFirebase(newSamples, samplesCollection).then(() => {
return _home;
});
});
return <SuccessMessage/>;
case BarcodeScannerAppState.PRINTING: case BarcodeScannerAppState.PRINTING:
return <View style={styles.container}> return <View style={styles.container}>
<PrintingMessage <PrintingMessage
@ -325,7 +300,7 @@ export default function Main() {
return ( return (
<PaperProvider theme={theme}> <PaperProvider theme={theme}>
<Appbar.Header dark={true}> <Appbar.Header dark={true} style={isConnected ? styles.connected : styles.disconnected}>
<Appbar.Content title={`${appExpo.description} #${locationStr}`}/> <Appbar.Content title={`${appExpo.description} #${locationStr}`}/>
<Appbar.Action icon="home" onPress={_home}/> <Appbar.Action icon="home" onPress={_home}/>
<Appbar.Action icon="settings" onPress={_settings}/> <Appbar.Action icon="settings" onPress={_settings}/>

View File

@ -2,6 +2,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" /> <application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest> </manifest>

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
#Thu Sep 10 15:14:20 EDT 2020 #Fri Sep 11 17:23:10 EDT 2020
VERSION_NAME=1.0.4 VERSION_NAME=1.0.7
VERSION_BUILD=13 VERSION_BUILD=16
VERSION_CODE=8 VERSION_CODE=11

View File

@ -17,10 +17,10 @@ export const Scanner = (props: ScannerProps): ReactElement => {
</View> </View>
<View style={styles.centerMiddle}> <View style={styles.centerMiddle}>
<View style={styles.captureBox}/> <View style={styles.captureBox}/>
</View>
<Subheading style={styles.shadow}> <Subheading style={styles.shadow}>
Instruct the patient to hold their card up with the barcode facing the camera. Keep the barcode in the orange box. Instruct the patient to hold their card up with the barcode facing the camera. Keep the barcode in the orange box.
</Subheading> </Subheading>
</View>
<View style={styles.centerMiddle}> <View style={styles.centerMiddle}>
<Button <Button
mode="text" mode="text"

View File

@ -46,6 +46,12 @@ export const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
connected: {
backgroundColor: colors.primary
},
disconnected: {
backgroundColor: '#333333'
},
container: { container: {
..._common.container, ..._common.container,
..._common.dark, ..._common.dark,

59
components/Sync.tsx Normal file
View File

@ -0,0 +1,59 @@
import AsyncStorage from '@react-native-community/async-storage';
import {parse} from 'date-fns';
import React, {ReactElement, useEffect, useState} from 'react';
import {View} from 'react-native';
import {Title} from 'react-native-paper';
import {dateFormat} from '../config/default';
import {SyncProps} from '../models/ElementProps';
import {LineCount} from '../models/LineCount';
import {Sample} from '../models/Sample';
import {CancelButton} from './Common';
import {styles} from './Styles';
// @ts-ignore
import * as firebase from 'firebase';
import 'firebase/firestore';
export const sendDataToFirebase = async (newData: Array<Sample | LineCount>, collection: firebase.firestore.CollectionReference) => {
const writes = newData.map((s: Sample | LineCount) => collection.doc(s.id).set(s));
await Promise.all(writes);
}
export const SyncMessage = (props: SyncProps): ReactElement => {
const [syncStatus, setSyncStatus] = useState<string>('Syncing data...');
useEffect(() => {
// TODO: Detect when user is online. If online, sync data with Firebase. If not online, just go home. Alternatively, set up a timer that periodically syncs data with the database.
// Upload any changes to Firebase
if (props.isConnected) {
AsyncStorage.getAllKeys().then(keys => {
const newSamples = keys
.filter(s => /^[\d]{9}-[\d]{12}-[\d]{4}$/.test(s))
.map(s => {
const propsArray = s.split('-');
return {
id: s,
barcodeId: propsArray[0],
createdAt: parse(propsArray[1], dateFormat, new Date()),
locationId: propsArray[2],
} as Sample;
});
sendDataToFirebase(newSamples, props.samplesCollection).then(() => {
setSyncStatus('Data synced.');
props.onSync();
});
});
} else {
setSyncStatus('Device is not online. Skipping sync...');
props.onCancel();
}
});
return <View style={styles.container}>
<View style={styles.centerMiddle}>
<Title style={styles.heading}>{syncStatus}</Title>
<CancelButton onClicked={props.onCancel} />
</View>
</View>;
}

11
config/default.tsx Normal file
View File

@ -0,0 +1,11 @@
export const firebaseConfig = {
apiKey: 'api_key_goes_here',
authDomain: "uva-covid19-testing-kiosk.firebaseapp.com",
databaseURL: "https://uva-covid19-testing-kiosk.firebaseio.com",
projectId: 'project_id_goes_here',
storageBucket: "uva-covid19-testing-kiosk.appspot.com",
messagingSenderId: 'sender_id_goes_here',
appId: 'app_id_goes_here'
};
export const dateFormat = 'yyyyMMddHHmm';

View File

@ -1,8 +1,9 @@
import {BarCodeScannedCallback} from 'expo-barcode-scanner'; import {BarCodeScannedCallback} from 'expo-barcode-scanner';
import {BarcodeScannerAppState} from './BarcodeScannerAppState'; import {BarcodeScannerAppState} from './BarcodeScannerAppState';
import * as firebase from 'firebase';
import 'firebase/firestore';
export declare type CameraType = number | 'front' | 'back' | undefined; export declare type CameraType = number | 'front' | 'back' | undefined;
export declare type CheckedStatus ='checked' | 'unchecked' | undefined;
export interface ElementProps { export interface ElementProps {
title?: string; title?: string;
@ -42,6 +43,14 @@ export interface ScannerProps extends ElementProps {
cameraType: CameraType; cameraType: CameraType;
} }
export interface SyncProps extends ElementProps {
isConnected: boolean;
onSync: () => void;
onCancel: () => void;
samplesCollection: firebase.firestore.CollectionReference<firebase.firestore.DocumentData>;
countsCollection: firebase.firestore.CollectionReference<firebase.firestore.DocumentData>;
}
export interface PrintingProps extends BarCodeProps { export interface PrintingProps extends BarCodeProps {
numCopies: number; numCopies: number;
onCancel: () => void; onCancel: () => void;