Prints QR Code

Former-commit-id: d6f5be66263cc38ba56f0dd5e268ef0facd80800
This commit is contained in:
Aaron Louie 2020-09-03 18:27:44 -04:00
parent 4e320b6619
commit 7cf182fe11
7 changed files with 215 additions and 65 deletions

21
App.tsx
View File

@ -18,7 +18,6 @@ import {
TextInput, TextInput,
Title Title
} from 'react-native-paper'; } from 'react-native-paper';
import QRCode from 'react-native-qrcode-svg';
import {expo as appExpo} from './app.json'; import {expo as appExpo} from './app.json';
import {CancelButton} from './components/Common'; import {CancelButton} from './components/Common';
import {BarCodeDisplay, PrintButton, PrintingMessage} from './components/Print'; import {BarCodeDisplay, PrintButton, PrintingMessage} from './components/Print';
@ -61,7 +60,6 @@ export default function Main() {
const [barCodeId, setBarCodeId] = useState<string>(''); const [barCodeId, setBarCodeId] = useState<string>('');
const [sampleDate, setSampleDate] = useState<Date>(new Date()); const [sampleDate, setSampleDate] = useState<Date>(new Date());
const [locationStr, setLocationStr] = useState<string>('4321'); const [locationStr, setLocationStr] = useState<string>('4321');
const [svgQrCode, setSvgQrCode] = useState<any>();
const [errorMessage, setErrorMessage] = useState<string>(''); const [errorMessage, setErrorMessage] = useState<string>('');
const [samples, setSamples] = useState<Sample[]>([]); const [samples, setSamples] = useState<Sample[]>([]);
@ -114,16 +112,13 @@ export default function Main() {
const pattern = /^[\d]{14}$|^[\d]{9}$/; const pattern = /^[\d]{14}$|^[\d]{9}$/;
if (pattern.test(barCodeString)) { if (pattern.test(barCodeString)) {
const cardId = e.data.slice(0, 9); const cardId = e.data.slice(0, 9);
setBarCodeId(cardId); const newSampleDate = new Date();
setSampleDate(new Date()); const newSampleId = [cardId, format(newSampleDate, 'yyyyMMddHHmm'), locationStr].join('-');
setAppState(BarcodeScannerAppState.SCANNED);
setSampleId([barCodeId, format(sampleDate, 'yyyyMMddHHmm'), locationStr].join('-'));
console.log('sampleId', sampleId); setSampleId(newSampleId);
new QRCode({value: sampleId, ecl: 'H', getRef: c => { setBarCodeId(cardId);
setSvgQrCode(c); setSampleDate(newSampleDate);
console.log('svgQrCode', svgQrCode); setAppState(BarcodeScannerAppState.SCANNED);
}});
} else { } else {
setErrorMessage(`The barcode data "${e.data}" is not from a valid ID card.`); setErrorMessage(`The barcode data "${e.data}" is not from a valid ID card.`);
setAppState(BarcodeScannerAppState.ERROR); setAppState(BarcodeScannerAppState.ERROR);
@ -196,7 +191,7 @@ export default function Main() {
Location number must be exactly 4 digits. No other characters are allowed. Location number must be exactly 4 digits. No other characters are allowed.
</HelperText> </HelperText>
<Button <Button
icon="save" icon="content-save"
mode="contained" mode="contained"
color={colors.primary} color={colors.primary}
style={{marginBottom: 10}} style={{marginBottom: 10}}
@ -253,7 +248,6 @@ export default function Main() {
barCodeId={barCodeId} barCodeId={barCodeId}
date={sampleDate} date={sampleDate}
location={locationStr} location={locationStr}
svg={svgQrCode}
/> />
</View>; </View>;
case BarcodeScannerAppState.SCANNED: case BarcodeScannerAppState.SCANNED:
@ -263,7 +257,6 @@ export default function Main() {
barCodeId={barCodeId} barCodeId={barCodeId}
date={sampleDate} date={sampleDate}
location={locationStr} location={locationStr}
svg={svgQrCode}
/> />
<ActionButtons/> <ActionButtons/>
</View>; </View>;

View File

@ -1,3 +1,6 @@
import AsyncStorage from '@react-native-community/async-storage';
import {format} from 'date-fns';
import * as Print from 'expo-print';
import React, {ReactElement, useEffect, useState} from 'react'; import React, {ReactElement, useEffect, useState} from 'react';
import {Text, View} from 'react-native'; import {Text, View} from 'react-native';
import {Button, Title} from 'react-native-paper'; import {Button, Title} from 'react-native-paper';
@ -5,9 +8,8 @@ import QRCode from 'react-native-qrcode-svg';
import {BarCodeProps, ButtonProps, PrintingProps} from '../models/ElementProps'; import {BarCodeProps, ButtonProps, PrintingProps} from '../models/ElementProps';
import {Sample} from '../models/Sample'; import {Sample} from '../models/Sample';
import {colors, styles} from './Styles'; import {colors, styles} from './Styles';
import AsyncStorage from '@react-native-community/async-storage';
import * as Print from 'expo-print'; const qrcode = require('qrcode');
import {format} from 'date-fns'
enum PrintStatus { enum PrintStatus {
SAVING = 'SAVING', SAVING = 'SAVING',
@ -25,14 +27,25 @@ const _save = (props: PrintingProps): Promise<void> => {
return AsyncStorage.setItem(props.id, JSON.stringify(storageVal)); return AsyncStorage.setItem(props.id, JSON.stringify(storageVal));
} }
const _print = (props: PrintingProps): Promise<void> => { const _print = async (props: PrintingProps): Promise<void> => {
console.log('props.svg', props.svg); const svgString = await qrcode.toString(props.id, {
width: 72, // 20mm
height: 72,
margin: 10,
errorCorrectionLevel: 'high',
type: 'svg',
color: {
light: '#ffffff00',
dark: '#000',
}
});
return Print.printAsync({ return Print.printAsync({
html: ` html: `
<style> <style>
@media print { @media print {
@page { @page {
size: 2in 1.25in; size: 28.6mm;
margin: 0; margin: 0;
} }
@ -41,35 +54,61 @@ const _print = (props: PrintingProps): Promise<void> => {
padding: 0; padding: 0;
} }
div.box { .circle {
width: 2in; position: absolute;
height: 1.25in; top: 0;
left: 0;
width: 28.6mm;
height: 28.6mm;
color: #000; color: #000;
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
border-radius: 28.6mm;
background-color: transparent;
} }
div.box p { .circle .date,
font-size: 10pt; .circle .time,
.circle .location,
.circle .barCodeId {
position: absolute;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-size: 6pt;
font-family: monospace;
text-align: center;
line-height: 1;
} }
.circle .date { top: 3.5mm; left: 0; width: 100%; }
.circle .time { top: 11mm; left: 1.5mm; width: 4mm; }
.circle .location { top: 11mm; right: 1.5mm; width: 4mm; }
.circle .barCodeId { bottom: 3mm; left: 0; width: 100%; }
svg { svg {
position: absolute; position: absolute;
bottom: 0; top: 0;
left: 0; left: 0;
width: 28.6mm;
height: 28.6mm;
} }
} }
</style> </style>
<div class="box"> <div class="circle" />
<p>ID#: ${props.barCodeId}</p> ${svgString}
<p>Date: ${props.date.toLocaleDateString()} ${props.date.toLocaleTimeString()}</p> <div class="date">${format(props.date, 'yyyy-MM-dd')}</div>
<p>Loc#: ${props.location}</p> <div class="time">
${props.svg} T<br />
<p>${props.id}</p> ${format(props.date, 'HH')}<br />
${format(props.date, 'mm')}
</div> </div>
<div class="location">
L<br />
${props.location.slice(0, 2)}<br />
${props.location.slice(2)}<br />
</div>
<div class="barCodeId">#${props.barCodeId}</div>
`, `,
}); });
} }
@ -121,12 +160,11 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
barCodeId={props.barCodeId} barCodeId={props.barCodeId}
date={props.date} date={props.date}
location={props.location} location={props.location}
svg={props.svg}
/> />
</View> </View>
<View style={styles.container}> <View style={styles.container}>
<Title style={styles.heading}>{statusStr}</Title> <Title style={styles.heading}>{statusStr}</Title>
<RetryButton /> <RetryButton/>
<Button <Button
icon="cancel" icon="cancel"
mode={printStatus === PrintStatus.DONE ? 'contained' : 'text'} mode={printStatus === PrintStatus.DONE ? 'contained' : 'text'}
@ -141,11 +179,10 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
export const BarCodeDisplay = (props: BarCodeProps): ReactElement => { export const BarCodeDisplay = (props: BarCodeProps): ReactElement => {
console.log('BarCodeDisplay props.svg', props.svg);
return <View style={styles.printPreview}> return <View style={styles.printPreview}>
<Text style={styles.label}>ID#: {props.id}</Text> <Text style={styles.label}>ID#: {props.id}</Text>
<Text style={styles.label}>Date: {props.date.toLocaleDateString()}, {props.date.toLocaleTimeString()}</Text> <Text style={styles.label}>Date: {props.date.toLocaleDateString()}, {props.date.toLocaleTimeString()}</Text>
<Text style={styles.label}>Location {props.location}</Text> <Text style={styles.label}>Location {props.location}</Text>
<QRCode value={props.id} /> <QRCode value={props.id}/>
</View>; </View>;
} }

View File

@ -1,6 +1,6 @@
import {BarCodeEvent, BarCodeScanner} from 'expo-barcode-scanner'; import {BarCodeScanner} from 'expo-barcode-scanner';
import React, {ReactElement, useState} from 'react'; import React, {ReactElement, useState} from 'react';
import {View, Text} from 'react-native'; import {Text, View} from 'react-native';
import {Button, DefaultTheme, HelperText, Subheading, TextInput, Title} from 'react-native-paper'; import {Button, DefaultTheme, HelperText, Subheading, TextInput, Title} from 'react-native-paper';
import {ButtonProps, ScannerProps} from '../models/ElementProps'; import {ButtonProps, ScannerProps} from '../models/ElementProps';
import {colors, styles} from './Styles'; import {colors, styles} from './Styles';
@ -26,7 +26,7 @@ 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> </View>
<Text style={styles.subtitle}> <Text style={styles.subtitle}>
Hold your ID card up, with the barcode facing the camera. Keep the card in the green box. Hold your ID card up, with the barcode facing the camera. Keep the card in the green box.
@ -50,7 +50,7 @@ export const Scanner = (props: ScannerProps): ReactElement => {
</View>; </View>;
} }
return <ScanCamera key={componentKey} />; return <ScanCamera key={componentKey}/>;
}; };
export const ScanButton = (props: ButtonProps): ReactElement => { export const ScanButton = (props: ButtonProps): ReactElement => {
@ -81,7 +81,6 @@ export const IdNumberInput = (props: ScannerProps): ReactElement => {
}; };
const onSubmit = () => { const onSubmit = () => {
console.log('onSubmit inputStr =', inputStr);
props.onScanned({type: '', data: inputStr}); props.onScanned({type: '', data: inputStr});
} }
@ -89,7 +88,8 @@ export const IdNumberInput = (props: ScannerProps): ReactElement => {
<Title style={styles.headingInverse}>Settings</Title> <Title style={styles.headingInverse}>Settings</Title>
<View style={{marginBottom: 10}}> <View style={{marginBottom: 10}}>
<Subheading style={{color: DefaultTheme.colors.text, marginBottom: 60}}> <Subheading style={{color: DefaultTheme.colors.text, marginBottom: 60}}>
Please double check that you have entered the number correctly. Entering an incorrect ID number will prevent patients from receiving their test results. Please double check that you have entered the number correctly. Entering an incorrect ID number will prevent
patients from receiving their test results.
</Subheading> </Subheading>
<TextInput <TextInput
label="ID #" label="ID #"

View File

@ -1,6 +1,5 @@
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import {DefaultTheme, DarkTheme} from 'react-native-paper'; import {DarkTheme, DefaultTheme} from 'react-native-paper';
export const colors = { export const colors = {
...DarkTheme.colors, ...DarkTheme.colors,

View File

@ -14,7 +14,6 @@ export interface BarCodeProps extends ElementProps {
barCodeId: string; barCodeId: string;
date: Date; date: Date;
location: string; location: string;
svg: any;
} }
export interface ButtonProps extends ElementProps { export interface ButtonProps extends ElementProps {

158
package-lock.json generated
View File

@ -5953,6 +5953,11 @@
"integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==",
"dev": true "dev": true
}, },
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs="
},
"dom-serializer": { "dom-serializer": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -13059,6 +13064,131 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
}, },
"qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"requires": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"dependencies": {
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"qrcode-svg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
"integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw=="
},
"qs": { "qs": {
"version": "6.9.4", "version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
@ -13389,25 +13519,6 @@
"use-subscription": "^1.0.0" "use-subscription": "^1.0.0"
} }
}, },
"react-native-barcode-builder": {
"version": "github:cdesch/react-native-barcode-builder#30d6699c3c7f8ed590fc38cbedbae0a5b73b6008",
"from": "github:cdesch/react-native-barcode-builder#master",
"requires": {
"jsbarcode": "^3.8.0",
"react-native-svg": "^9.13.3"
},
"dependencies": {
"react-native-svg": {
"version": "9.13.6",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-9.13.6.tgz",
"integrity": "sha512-vjjuJhEhQCwWjqsgWyGy6/C/LIBM2REDxB40FU1PMhi8T3zQUwUHnA6M15pJKlQG8vaZyA+QnLyIVhjtujRgig==",
"requires": {
"css-select": "^2.0.2",
"css-tree": "^1.0.0-alpha.37"
}
}
}
},
"react-native-canvas": { "react-native-canvas": {
"version": "0.1.37", "version": "0.1.37",
"resolved": "https://registry.npmjs.org/react-native-canvas/-/react-native-canvas-0.1.37.tgz", "resolved": "https://registry.npmjs.org/react-native-canvas/-/react-native-canvas-0.1.37.tgz",
@ -13455,6 +13566,15 @@
"resolved": "https://registry.npmjs.org/react-native-print/-/react-native-print-0.6.0.tgz", "resolved": "https://registry.npmjs.org/react-native-print/-/react-native-print-0.6.0.tgz",
"integrity": "sha512-lWGI5JoB/crLRlukuB7FMmfjSOwC8Cia9JHVEFjpnQWGtnRSLY+oRYwgFLzll7XPi7LqUNohFKT+jsRMA/1bRg==" "integrity": "sha512-lWGI5JoB/crLRlukuB7FMmfjSOwC8Cia9JHVEFjpnQWGtnRSLY+oRYwgFLzll7XPi7LqUNohFKT+jsRMA/1bRg=="
}, },
"react-native-qrcode-svg": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.0.6.tgz",
"integrity": "sha512-b+/teD+xj17VDujJzf956U2+9mX+gKwVJss2aqmhEIyjP7+TVOuE08D3UkzfOCWXE8gppcUTTz5gkY1NXgfwyQ==",
"requires": {
"prop-types": "^15.5.10",
"qrcode": "^1.3.2"
}
},
"react-native-reanimated": { "react-native-reanimated": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.9.0.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.9.0.tgz",

View File

@ -17,6 +17,8 @@
"expo-updates": "~0.2.10", "expo-updates": "~0.2.10",
"firebase": "7.9.0", "firebase": "7.9.0",
"jsbarcode": "^3.11.0", "jsbarcode": "^3.11.0",
"qrcode": "^1.4.4",
"qrcode-svg": "^1.1.0",
"react": "~16.11.0", "react": "~16.11.0",
"react-dom": "~16.11.0", "react-dom": "~16.11.0",
"react-native": "~0.62.2", "react-native": "~0.62.2",