Skip to content

Commit 9619941

Browse files
committed
Support for ICAO's Visible Digital Seals
1 parent 0d07864 commit 9619941

File tree

8 files changed

+406
-1
lines changed

8 files changed

+406
-1
lines changed

app/components/VDSTestCard.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, {Component} from 'react';
2+
import { View, Image, Button, FlatList, TouchableOpacity } from 'react-native';
3+
import { Text, Divider } from 'react-native-elements';
4+
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
5+
6+
import { CardStyles as styles } from '../themes/CardStyles'
7+
8+
import Moment from 'moment';
9+
10+
const DISEASE = {
11+
"RA01":"COVID-19",
12+
"RA01.0":"COVID-19"
13+
};
14+
15+
const VACCINE_TYPES = {
16+
"XM68M6": "Unspecified",
17+
"XM1NL1": "Inactivated virus",
18+
"XM5DF6": "Live attenuated virus", 
19+
"XM9QW8": "Non-replicating viral vector", 
20+
"XM0CX4": "Replicating viral vector",
21+
"XM5JC5": "Virus protein subunit",
22+
"XM1J92": "Virus-like particle (VLP)", 
23+
"XM6AT1": "DNA based", 
24+
"XM0GQ8": "RNA based"
25+
}
26+
27+
28+
29+
export default class VDSTestCard extends Component {
30+
31+
showQR = (card) => {
32+
this.props.navigation.navigate({name: 'QRShow', params: {
33+
qr: card.rawQR,
34+
title: this.formatPerson(),
35+
detail: this.formatCI(),
36+
signedBy: this.formatSignedBy()
37+
}
38+
});
39+
}
40+
41+
cert = () => {
42+
return this.props.detail.cert ? this.props.detail.cert : this.props.detail;
43+
}
44+
45+
formatDoB = () => {
46+
if (this.cert().data.msg.pid.dob === undefined || this.cert().data.msg.pid.dob === "") return "";
47+
return "DoB: " + Moment(this.cert().data.msg.pid.dob).format('MMM DD, YYYY')
48+
}
49+
50+
formatCI = () => {
51+
return "ID: " + this.cert().data.msg.pid.i;
52+
}
53+
54+
formatUVCI = () => {
55+
return "Vax ID: " + this.cert().data.msg.uvci;
56+
}
57+
58+
formatExpiresOn = () => {
59+
if (this.cert().exp === undefined || this.cert().exp === "") return "";
60+
return Moment(this.cert().exp*1000).format('MMM DD, YYYY')
61+
}
62+
63+
formatPerson = () => {
64+
if (this.cert().data.msg.pid.n) {
65+
let name = this.cert().data.msg.pid.n.replace(" ", ", ");
66+
names = name.split(" ");
67+
for (i=2; i<names.length; i++) {
68+
names[i] = names[i][0];
69+
}
70+
return names.join(" ");
71+
} else
72+
return "Unkown";
73+
}
74+
75+
formatSignedBy = () => {
76+
let line = "Signed by ";
77+
if (this.cert().data.hdr.is)
78+
line += this.cert().data.hdr.is;
79+
else
80+
line += this.props.detail.pub_key.toLowerCase();
81+
82+
return line;
83+
}
84+
85+
renderCard = () => {
86+
return (
87+
<View style={[styles.card, {backgroundColor:this.props.colors.primary}]}>
88+
<View style={{flexDirection:'row', justifyContent:'space-between', alignItems:'center'}}>
89+
<Text style={styles.notes}>{Moment(this.props.detail.scanDate).format('MMM DD, hh:mma')} - Vaccination</Text>
90+
<FontAwesome5 style={styles.button} name={'trash'} onPress={() => this.props.removeItem(this.props.detail.signature)} solid/>
91+
</View>
92+
93+
<View style={styles.row}>
94+
<Text style={styles.title}>{this.formatPerson()}</Text>
95+
</View>
96+
97+
<View style={styles.row}>
98+
<Text style={styles.notes}>{this.formatDoB()}. {this.formatCI()}</Text>
99+
</View>
100+
101+
<View style={styles.row}>
102+
<Text style={styles.notes}>{this.formatUVCI()}</Text>
103+
</View>
104+
105+
<Divider style={[styles.divisor, {borderBottomColor:this.props.colors.cardText}]} />
106+
107+
<FlatList
108+
listKey={this.props.detail.signature+"ve"}
109+
data={this.cert().data.msg.ve}
110+
keyExtractor={item => (this.props.detail.signature+item.des)}
111+
renderItem={({item}) => {
112+
return (
113+
<View style={styles.groupLine}>
114+
115+
<FlatList
116+
listKey={this.props.detail.signature+item.des+"vd"}
117+
data={item.vd}
118+
keyExtractor={subitem => (this.props.detail.signature+item.des+subitem.dvc)}
119+
renderItem={ (subitem) => {
120+
console.log(subitem);
121+
return (
122+
<View style={styles.groupLine}>
123+
124+
<View style={{alignItems: 'center'}}>
125+
<Text style={styles.subtitle}>{DISEASE[item.dis]} Vaccine {subitem.item.seq}/{item.vd.length}</Text>
126+
</View>
127+
128+
<View style={{alignItems: 'center'}}>
129+
<Text style={styles.notes}>{item.nam} (#{subitem.item.lot})</Text>
130+
</View>
131+
132+
<View style={{alignItems: 'center'}}>
133+
<Text style={styles.notes}>Date: {subitem.item.dvc}</Text>
134+
</View>
135+
136+
<View style={{alignItems: 'center'}}>
137+
<Text style={styles.notes}>Location: {subitem.item.adm}, {subitem.item.ctr}</Text>
138+
</View>
139+
140+
<Divider style={[styles.divisor, {borderBottomColor:this.props.colors.cardText}]} />
141+
</View>
142+
)
143+
}} />
144+
</View>
145+
)
146+
}} />
147+
148+
<View style={{flexDirection:'row', alignItems: 'center', paddingRight: 10}}>
149+
<FontAwesome5 style={styles.icon} name={'check-circle'} solid/>
150+
<Text style={styles.notes}>{this.formatSignedBy()}</Text>
151+
</View>
152+
</View>
153+
);
154+
}
155+
156+
157+
render() {
158+
return this.props.pressable ?
159+
( <TouchableOpacity onPress={() => this.showQR(this.props.detail)}>
160+
{this.renderCard()}
161+
</TouchableOpacity>
162+
) : this.renderCard();
163+
}
164+
}

app/components/VDSVaxCard.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React, {Component} from 'react';
2+
import { View, Image, Button, FlatList, TouchableOpacity } from 'react-native';
3+
import { Text, Divider } from 'react-native-elements';
4+
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
5+
6+
import { CardStyles as styles } from '../themes/CardStyles'
7+
8+
import Moment from 'moment';
9+
10+
const DISEASE = {
11+
"RA01":"COVID-19",
12+
"RA01.0":"COVID-19"
13+
};
14+
15+
const VACCINE_TYPES = {
16+
"XM68M6": "Unspecified",
17+
"XM1NL1": "Inactivated virus",
18+
"XM5DF6": "Live attenuated virus", 
19+
"XM9QW8": "Non-replicating viral vector", 
20+
"XM0CX4": "Replicating viral vector",
21+
"XM5JC5": "Virus protein subunit",
22+
"XM1J92": "Virus-like particle (VLP)", 
23+
"XM6AT1": "DNA based", 
24+
"XM0GQ8": "RNA based"
25+
}
26+
27+
28+
29+
export default class VDSVaxCard extends Component {
30+
31+
showQR = (card) => {
32+
this.props.navigation.navigate({name: 'QRShow', params: {
33+
qr: card.rawQR,
34+
title: this.formatPerson(),
35+
detail: this.formatCI(),
36+
signedBy: this.formatSignedBy()
37+
}
38+
});
39+
}
40+
41+
cert = () => {
42+
return this.props.detail.cert ? this.props.detail.cert : this.props.detail;
43+
}
44+
45+
formatDoB = () => {
46+
if (this.cert().data.msg.pid.dob === undefined || this.cert().data.msg.pid.dob === "") return "";
47+
return "DoB: " + Moment(this.cert().data.msg.pid.dob).format('MMM DD, YYYY')
48+
}
49+
50+
formatCI = () => {
51+
return "ID: " + this.cert().data.msg.pid.i;
52+
}
53+
54+
formatUVCI = () => {
55+
return "Vax ID: " + this.cert().data.msg.uvci;
56+
}
57+
58+
formatExpiresOn = () => {
59+
if (this.cert().exp === undefined || this.cert().exp === "") return "";
60+
return Moment(this.cert().exp*1000).format('MMM DD, YYYY')
61+
}
62+
63+
formatPerson = () => {
64+
if (this.cert().data.msg.pid.n) {
65+
let name = this.cert().data.msg.pid.n.replace(" ", ", ");
66+
names = name.split(" ");
67+
for (i=2; i<names.length; i++) {
68+
names[i] = names[i][0];
69+
}
70+
return names.join(" ");
71+
} else
72+
return "Unkown";
73+
}
74+
75+
formatSignedBy = () => {
76+
let line = "Signed by ";
77+
if (this.cert().data.hdr.is)
78+
line += this.cert().data.hdr.is;
79+
else
80+
line += this.props.detail.pub_key.toLowerCase();
81+
82+
return line;
83+
}
84+
85+
renderCard = () => {
86+
return (
87+
<View style={[styles.card, {backgroundColor:this.props.colors.primary}]}>
88+
<View style={{flexDirection:'row', justifyContent:'space-between', alignItems:'center'}}>
89+
<Text style={styles.notes}>{Moment(this.props.detail.scanDate).format('MMM DD, hh:mma')} - Vaccination</Text>
90+
<FontAwesome5 style={styles.button} name={'trash'} onPress={() => this.props.removeItem(this.props.detail.signature)} solid/>
91+
</View>
92+
93+
<View style={styles.row}>
94+
<Text style={styles.title}>{this.formatPerson()}</Text>
95+
</View>
96+
97+
<View style={styles.row}>
98+
<Text style={styles.notes}>{this.formatDoB()}. {this.formatCI()}</Text>
99+
</View>
100+
101+
<View style={styles.row}>
102+
<Text style={styles.notes}>{this.formatUVCI()}</Text>
103+
</View>
104+
105+
<Divider style={[styles.divisor, {borderBottomColor:this.props.colors.cardText}]} />
106+
107+
<FlatList
108+
listKey={this.props.detail.signature+"ve"}
109+
data={this.cert().data.msg.ve}
110+
keyExtractor={item => this.props.detail.signature+item.nam}
111+
renderItem={({item}) => {
112+
return (
113+
<View style={styles.groupLine}>
114+
115+
<FlatList
116+
listKey={this.props.detail.signature+item.nam+"vd"}
117+
data={item.vd}
118+
keyExtractor={subitem => this.props.detail.signature+item.nam+subitem.dvc}
119+
renderItem={ (subitem) => {
120+
return (
121+
<View style={styles.groupLine}>
122+
123+
<View style={{alignItems: 'center'}}>
124+
<Text style={styles.subtitle}>{DISEASE[item.dis]} Vaccine {subitem.item.seq}/{item.vd.length}</Text>
125+
</View>
126+
127+
<View style={{alignItems: 'center'}}>
128+
<Text style={styles.notes}>{item.nam} (#{subitem.item.lot})</Text>
129+
</View>
130+
131+
<View style={{alignItems: 'center'}}>
132+
<Text style={styles.notes}>Date: {subitem.item.dvc}</Text>
133+
</View>
134+
135+
<View style={{alignItems: 'center'}}>
136+
<Text style={styles.notes}>Location: {subitem.item.adm}, {subitem.item.ctr}</Text>
137+
</View>
138+
139+
<Divider style={[styles.divisor, {borderBottomColor:this.props.colors.cardText}]} />
140+
</View>
141+
)
142+
}} />
143+
</View>
144+
)
145+
}} />
146+
147+
<View style={{flexDirection:'row', alignItems: 'center', paddingRight: 10}}>
148+
<FontAwesome5 style={styles.icon} name={'check-circle'} solid/>
149+
<Text style={styles.notes}>{this.formatSignedBy()}</Text>
150+
</View>
151+
</View>
152+
);
153+
}
154+
155+
156+
render() {
157+
return this.props.pressable ?
158+
( <TouchableOpacity onPress={() => this.showQR(this.props.detail)}>
159+
{this.renderCard()}
160+
</TouchableOpacity>
161+
) : this.renderCard();
162+
}
163+
}

app/screens/Entry.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import PassKeyCard from './../components/PassKeyCard';
1818
import SHCCard from './../components/SHCCard';
1919
import DCCCard from './../components/DCCCard';
2020
import DCCUYCard from './../components/DCCUYCard';
21+
import VDSVaxCard from './../components/VDSVaxCard';
22+
import VDSTestCard from './../components/VDSTestCard';
2123

2224
import { listCards, removeCard } from './../utils/StorageManager';
2325

@@ -121,6 +123,10 @@ function Entry({ navigation }) {
121123
return <View style={styles.listItem}><DCCCard detail={item} colors={colors} navigation={navigation} removeItem={removeItem} pressable/></View>
122124
if (item.format === "DCC" && item.type === "UY")
123125
return <View style={styles.listItem}><DCCUYCard detail={item} colors={colors} navigation={navigation} removeItem={removeItem} pressable/></View>
126+
if (item.format === "VDS" && item.type === "icao.vacc")
127+
return <View style={styles.listItem}><VDSVaxCard detail={item} colors={colors} navigation={navigation} removeItem={removeItem} pressable/></View>
128+
if (item.format === "VDS" && item.type === "icao.test")
129+
return <View style={styles.listItem}><VDSTestCard detail={item} colors={colors} navigation={navigation} removeItem={removeItem} pressable/></View>
124130

125131

126132
}} />

app/screens/QRReader.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {importPCF} from '../utils/ImportPCF';
99
import {importDivoc} from '../utils/ImportDivoc';
1010
import {importSHC} from '../utils/ImportSHC';
1111
import {importDCC} from '../utils/ImportDCC';
12+
import {importVDS} from '../utils/ImportVDS';
1213

1314
const screenHeight = Math.round(Dimensions.get('window').height);
1415

@@ -65,7 +66,11 @@ function QRReader({ navigation }) {
6566
}
6667

6768
if (e.data && e.data.startsWith("{")) {
68-
await checkResult(await importDivoc(e.data));
69+
if (e.data.includes("icao")) {
70+
await checkResult(await importVDS(e.data));
71+
} else {
72+
await checkResult(await importDivoc(e.data));
73+
}
6974
return;
7075
}
7176

app/screens/QRResult.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import PassKeyCard from './../components/PassKeyCard';
1515
import SHCCard from './../components/SHCCard';
1616
import DCCCard from './../components/DCCCard';
1717
import DCCUYCard from './../components/DCCUYCard';
18+
import VDSVAXCard from './../components/VDSVaxCard';
19+
import VDSTESTCard from './../components/VDSTestCard';
1820

1921
import { removeCard } from './../utils/StorageManager';
2022

@@ -56,6 +58,8 @@ function QRResult({ navigation, route }) {
5658
{ qr.type === "FHIRBundle" && <SHCCard detail={qr} colors={colors} removeItem={removeItem} /> }
5759
{ qr.type === "DCC" && <DCCCard detail={qr} colors={colors} removeItem={removeItem} /> }
5860
{ qr.type === "UY" && <DCCUYCard detail={qr} colors={colors} removeItem={removeItem} /> }
61+
{ qr.type === "icao.vacc" && <VDSVAXCard detail={qr} colors={colors} removeItem={removeItem} /> }
62+
{ qr.type === "icao.test" && <VDSTESTCard detail={qr} colors={colors} removeItem={removeItem} /> }
5963
</View>
6064
<TouchableOpacity
6165
style={[styles.button, {backgroundColor: colors.primary}]}

0 commit comments

Comments
 (0)