Refactors settings into config file. Syncs data with Firebase if connected.

This commit is contained in:
Aaron Louie 2020-09-14 13:41:22 -04:00
parent 678eae1121
commit 98eac004c8
12 changed files with 175 additions and 69 deletions

1
.gitignore vendored
View File

@ -668,3 +668,4 @@ buck-out/
.expo/*
web-build/
*.hprof
config/default.tsx

30
App.tsx
View File

@ -16,7 +16,7 @@ import {IdNumberInput, InitialsInput, InputIdButton, ScanButton, Scanner} from '
import {SettingsScreen} from './components/Settings';
import {styles, theme} from './components/Styles';
import {sendDataToFirebase, SyncMessage} from './components/Sync';
import {dateFormat, firebaseConfig} from './config/default';
import {firebaseConfig, defaults} from './config/default';
import {BarcodeScannerAppState} from './models/BarcodeScannerAppState';
import {CameraType, ElementProps, StateProps} from './models/ElementProps';
import {LineCount} from './models/LineCount';
@ -33,20 +33,20 @@ if (firebase.apps.length === 0) {
}
const db = firebase.firestore();
const samplesCollection = db.collection('samples');
const countsCollection = db.collection('counts');
const samplesCollection = db.collection(defaults.samplesCollection);
const countsCollection = db.collection(defaults.countsCollection);
export default function Main() {
const [appState, setAppState] = useState<BarcodeScannerAppState>(BarcodeScannerAppState.INITIAL);
const [sampleId, setSampleId] = useState<string>('');
const [barCodeId, setBarCodeId] = useState<string>('');
const [sampleDate, setSampleDate] = useState<Date>(new Date());
const [locationStr, setLocationStr] = useState<string>('0000');
const [locationStr, setLocationStr] = useState<string>(defaults.locationId);
const [errorMessage, setErrorMessage] = useState<string>('');
const [samples, setSamples] = useState<Sample[]>([]);
const [lineCounts, setLineCounts] = useState<LineCount[]>([]);
const [cameraType, setCameraType] = useState<CameraType>('back');
const [numCopies, setNumCopies] = useState<number>(0);
const [cameraType, setCameraType] = useState<CameraType>(defaults.cameraType);
const [numCopies, setNumCopies] = useState<number>(defaults.numCopies);
const [isConnected, setIsConnected] = useState<boolean>(false);
const [initials, setInitials] = useState<string>('');
@ -69,6 +69,7 @@ export default function Main() {
});
// Watch for changes to internet connectivity.
// TODO: Set up a timer that periodically syncs data with the database if connected.
NetInfo.addEventListener((state: NetInfoState) => {
if (state.type === NetInfoStateType.wifi) {
setIsConnected(!!(state.isConnected && state.isInternetReachable));
@ -100,7 +101,7 @@ export default function Main() {
setAppState(BarcodeScannerAppState.INPUT_LINE_COUNT);
};
const _print = () => setAppState(BarcodeScannerAppState.PRINTING);
const _printed = () => setAppState(BarcodeScannerAppState.PRINTED);
const _sync = () => setAppState(BarcodeScannerAppState.SYNC);
const _home = () => setAppState(BarcodeScannerAppState.DEFAULT);
const _settings = () => setAppState(BarcodeScannerAppState.SETTINGS);
@ -125,21 +126,22 @@ export default function Main() {
const handleInitialsInput = (newInitials: string) => {
setInitials(newInitials);
const newSampleId = [barCodeId, newInitials, format(sampleDate, dateFormat), locationStr].join('-');
const newSampleId = [barCodeId, newInitials, format(sampleDate, defaults.dateEncodedFormat), locationStr].join('-');
setSampleId(newSampleId);
setAppState(BarcodeScannerAppState.SCANNED);
};
const handleLineCountSubmitted = (newCount: number) => {
const now = new Date();
const newId = `${locationStr}-${format(now, dateFormat)}`;
const newId = `${locationStr}-${format(now, defaults.dateEncodedFormat)}`;
const newData: LineCount = {
id: newId,
lineCount: newCount,
locationId: locationStr,
createdAt: now,
};
sendDataToFirebase([newData], countsCollection);
AsyncStorage.setItem(newData.id, JSON.stringify(newData)).then(_sync);
}
const ErrorMessage = (props: ElementProps): ReactElement => {
@ -170,7 +172,7 @@ export default function Main() {
</View>
}
function App(props: StateProps): ReactElement {
const AppContent = (props: StateProps): ReactElement => {
switch (props.appState) {
case BarcodeScannerAppState.INITIAL:
return <LoadingMessage/>;
@ -180,7 +182,7 @@ export default function Main() {
<InputIdButton onClicked={_inputIdNumber}/>
<InputLineCountButton onClicked={_inputLineCount}/>
</View>;
case BarcodeScannerAppState.PRINTED:
case BarcodeScannerAppState.SYNC:
return <SyncMessage
isConnected={isConnected}
samplesCollection={samplesCollection}
@ -192,7 +194,7 @@ export default function Main() {
return <View style={styles.container}>
<PrintingMessage
numCopies={numCopies}
onCancel={_printed}
onCancel={_sync}
id={sampleId}
barCodeId={barCodeId}
date={sampleDate}
@ -270,7 +272,7 @@ export default function Main() {
<Appbar.Action icon="settings" onPress={_settings}/>
</Appbar.Header>
<SafeAreaView style={styles.safeAreaView}>
<App appState={appState}/>
<AppContent appState={appState}/>
</SafeAreaView>
</PaperProvider>
);

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
#Fri Sep 11 17:23:10 EDT 2020
VERSION_NAME=1.0.7
VERSION_BUILD=16
VERSION_CODE=11
#Sun Sep 13 08:50:53 EDT 2020
VERSION_NAME=1.0.10
VERSION_BUILD=19
VERSION_CODE=14

View File

@ -5,7 +5,7 @@ import React, {ReactElement, useEffect, useState} from 'react';
import {Text, View} from 'react-native';
import {Button, Title} from 'react-native-paper';
import QRCode from 'react-native-qrcode-svg';
import {dateDisplayFormat} from '../config/default';
import {defaults} from '../config/default';
import {BarCodeProps, ButtonProps, PrintingProps} from '../models/ElementProps';
import {Sample} from '../models/Sample';
import {colors, styles} from './Styles';
@ -223,7 +223,7 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
export const BarCodeDisplay = (props: BarCodeProps): ReactElement => {
return <View style={styles.printPreview}>
<Text style={styles.label}>ID #: {props.id}</Text>
<Text style={styles.label}>Date: {format(props.date, dateDisplayFormat)}</Text>
<Text style={styles.label}>Date: {format(props.date, defaults.dateDisplayFormat)}</Text>
<Text style={styles.label}>Location #: {props.location}</Text>
<QRCode value={props.id}/>
</View>;

View File

@ -110,7 +110,7 @@ export const InitialsInput = (props: InputInitialsProps): ReactElement => {
};
const onSubmit = () => {
props.onSave(inputStr);
props.onSave(inputStr.toLowerCase());
}
return <View style={styles.settings}>
@ -122,7 +122,7 @@ export const InitialsInput = (props: InputInitialsProps): ReactElement => {
<TextInput
label="Initials"
value={inputStr}
onChangeText={inputStr => setInputStr(inputStr.toLowerCase())}
onChangeText={inputStr => setInputStr(inputStr)}
mode="outlined"
theme={DefaultTheme}
/>

View File

@ -1,9 +1,9 @@
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 {Snackbar, Title} from 'react-native-paper';
import {defaults} from '../config/default';
import {CollectionMeta} from '../models/Collection';
import {SyncProps} from '../models/ElementProps';
import {LineCount} from '../models/LineCount';
import {Sample} from '../models/Sample';
@ -19,38 +19,70 @@ export const sendDataToFirebase = async (newData: Array<Sample | LineCount>, col
export const SyncMessage = (props: SyncProps): ReactElement => {
const [syncStatus, setSyncStatus] = useState<string>('Syncing data...');
const [isError, setIsError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');
// Display an error message if Firebase sync fails.
const _handleError = (error: any, dataTypeLabel: string) => {
setIsError(true);
setErrorMessage(error);
setSyncStatus(`Error occurred while syncing ${dataTypeLabel} data.`);
props.onCancel();
};
// Delete locally-cached data from AsyncStorage
const _clearLocalCache = (keysToRemove: string[], dataTypeLabel: string) => {
AsyncStorage.multiRemove(keysToRemove).then(() => {
setSyncStatus(`${dataTypeLabel} data synced.`);
props.onSync();
});
}
// Upload any new locally-stored items to Firebase, then remove them from local storage.
const _syncCollection = (localStorageKeys: string[], collection: CollectionMeta) => {
const newItemsKeys = localStorageKeys.filter(k => collection.keyRegex.test(k));
AsyncStorage.multiGet(newItemsKeys, (errors, dataTuples) => {
if (dataTuples && (dataTuples.length > 0)) {
const data: Sample[] | LineCount[] = [];
dataTuples.forEach(t => {
if (t[1] !== null) {
const lineCount = JSON.parse(t[1]);
data.push(lineCount);
}
});
sendDataToFirebase(data, collection.firebaseCollection)
.then(() => _clearLocalCache(newItemsKeys, collection.label))
.catch(error => _handleError(error, collection.label));
}
});
}
useEffect(() => {
// TODO: Alternatively, set up a timer that periodically syncs data with the database.
// Detect when user is online. If online, sync data with Firebase.
if (props.isConnected) {
// Get the collection subscription
const unsubscribe = props.samplesCollection.onSnapshot(q => {}, e => {});
const collectionsToSync = [
{
firebaseCollection: props.countsCollection,
keyRegex: defaults.lineCountRegex,
label: 'Line Counts',
unsubscribe: props.countsCollection.onSnapshot(q => {}, e => {}),
},
{
firebaseCollection: props.samplesCollection,
keyRegex: defaults.qrCodeRegex,
label: 'QR Codes',
unsubscribe: props.samplesCollection.onSnapshot(q => {}, e => {}),
},
];
// Upload any changes to Firebase
// Upload new data to Firebase
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(() => {
// TODO: Delete stored keys in AsyncStorage
setSyncStatus('Data synced.');
props.onSync();
});
collectionsToSync.forEach(c => _syncCollection(keys, c));
});
return () => unsubscribe();
// Unsubscribe from all collections
return () => collectionsToSync.forEach(c => c.unsubscribe());
} else {
// If not online, just go home.
setSyncStatus('Device is not online. Skipping sync...');
@ -61,6 +93,11 @@ export const SyncMessage = (props: SyncProps): ReactElement => {
return <View style={styles.container}>
<View style={styles.centerMiddle}>
<Title style={styles.heading}>{syncStatus}</Title>
<Snackbar
visible={isError}
onDismiss={props.onCancel}
style={styles.error}
>{errorMessage === '' ? 'Something went wrong. Try again.' : errorMessage}</Snackbar>
<CancelButton onClicked={props.onCancel} />
</View>
</View>;

View File

@ -1,3 +1,7 @@
import {AppDefaults} from '../models/Default';
import {CameraType} from '../models/ElementProps';
// Firebase project config from https://console.firebase.google.com > Project Settings > General > Your apps > Web App
export const firebaseConfig = {
apiKey: 'api_key_goes_here',
authDomain: 'auth_domain_goes_here',
@ -5,8 +9,18 @@ export const firebaseConfig = {
projectId: 'project_id_goes_here',
storageBucket: 'storage_bucket_goes_here',
messagingSenderId: 'sender_id_goes_here',
appId: 'app_id_goes_here'
appId: 'app_id_goes_here',
};
export const dateFormat = 'yyyyMMddHHmm';
export const dateDisplayFormat = 'MM/dd/yyyy, hh:mm aa';
// Default form field and data values
export const defaults: AppDefaults = {
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
numCopies: 2, // Default number of copies of labels to print. Can be overridden by user setting.
cameraType: 'back' as CameraType, // Which camera to use for capturing bar codes. Can be overridden by user setting.
locationId: '0000', // Default location ID. Can be overridden by user setting.
lineCountRegex: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
qrCodeRegex: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
}

30
config/example.tsx Normal file
View File

@ -0,0 +1,30 @@
/**
Example configuration file. Make a copy of this file, name it "default.tsx", and place it in the config directory.
Then modify the values below to match the actual Firebase configuration.
*/
import {AppDefaults} from '../models/Default';
import {CameraType} from '../models/ElementProps';
// Firebase project config from https://console.firebase.google.com > Project Settings > General > Your apps > Web App
export const firebaseConfig = {
apiKey: 'api_key_goes_here',
authDomain: 'auth_domain_goes_here',
databaseURL: 'database_url_goes_here',
projectId: 'project_id_goes_here',
storageBucket: 'storage_bucket_goes_here',
messagingSenderId: 'sender_id_goes_here',
appId: 'app_id_goes_here',
};
// Default form field and data values
export const defaults: AppDefaults = {
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
numCopies: 2, // Default number of copies of labels to print. Can be overridden by user setting.
cameraType: 'back' as CameraType, // Which camera to use for capturing bar codes. Can be overridden by user setting.
locationId: '0000', // Default location ID. Can be overridden by user setting.
lineCountRegex: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
qrCodeRegex: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
}

View File

@ -7,7 +7,7 @@ export enum BarcodeScannerAppState {
INPUT_INITIALS = 'INPUT_INITIALS',
INPUT_LINE_COUNT = 'INPUT_LINE_COUNT',
PRINTING = 'PRINTING',
PRINTED = 'PRINTED',
SYNC = 'SYNC',
ERROR = 'ERROR',
SETTINGS = 'SETTINGS',
}

9
models/Collection.tsx Normal file
View File

@ -0,0 +1,9 @@
import * as firebase from 'firebase';
import 'firebase/firestore';
export interface CollectionMeta {
firebaseCollection: firebase.firestore.CollectionReference;
keyRegex: RegExp;
label: string;
unsubscribe: () => void;
}

13
models/Default.tsx Normal file
View File

@ -0,0 +1,13 @@
import {CameraType} from './ElementProps';
export interface AppDefaults {
countsCollection: string;
samplesCollection: string;
dateEncodedFormat: string;
dateDisplayFormat: string;
numCopies: number;
cameraType: CameraType;
locationId: string;
lineCountRegex: RegExp;
qrCodeRegex: RegExp;
}