Skip to content

Commit eb9145f

Browse files
authored
Merge pull request #205 from wwWallet/history-accuracy
History accuracy
2 parents dbe54f2 + ad4dfcf commit eb9145f

File tree

12 files changed

+212
-183
lines changed

12 files changed

+212
-183
lines changed

src/components/Credentials/ApiFetchCredential.ts

Lines changed: 0 additions & 83 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useState, useEffect } from "react"
2+
import { parseCredential } from "../../functions/parseCredential";
3+
4+
5+
export const CredentialImage = ({ credential, className, onClick }) => {
6+
const [parsedCredential, setParsedCredential] = useState(null);
7+
8+
useEffect(() => {
9+
parseCredential(credential).then((c) => {
10+
setParsedCredential(c);
11+
});
12+
}, []);
13+
14+
return (
15+
<>
16+
{parsedCredential &&
17+
<img src={parsedCredential.credentialBranding.image.url} alt={"Credential"} className={className} onClick={onClick} />
18+
}
19+
</>
20+
)
21+
}

src/components/Credentials/CredentialInfo.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { BiSolidCategoryAlt, BiSolidUserCircle } from 'react-icons/bi';
33
import { AiFillCalendar } from 'react-icons/ai';
44
import { RiPassExpiredFill } from 'react-icons/ri';
5-
import { MdTitle, MdGrade } from 'react-icons/md';
5+
import { MdTitle, MdGrade, MdOutlineNumbers } from 'react-icons/md';
66
import { GiLevelEndFlag } from 'react-icons/gi';
77
import { formatDate } from '../../functions/DateFormat';
8+
import { parseCredential } from '../../functions/parseCredential';
89

910
const getFieldIcon = (fieldName) => {
1011
switch (fieldName) {
@@ -14,6 +15,8 @@ const getFieldIcon = (fieldName) => {
1415
return <RiPassExpiredFill size={25} className="inline mr-1 mb-1" />;
1516
case 'dateOfBirth':
1617
return <AiFillCalendar size={25} className="inline mr-1 mb-1" />;
18+
case 'personalIdentifier':
19+
return <MdOutlineNumbers size={25} className="inline mr-1 mb-1" />
1720
case 'familyName':
1821
case 'firstName':
1922
return <BiSolidUserCircle size={25} className="inline mr-1 mb-1" />;
@@ -43,20 +46,29 @@ const renderRow = (fieldName, fieldValue) => {
4346
};
4447

4548
const CredentialInfo = ({ credential }) => {
49+
50+
const [parsedCredential, setParsedCredential] = useState(null);
51+
52+
useEffect(() => {
53+
parseCredential(credential).then((c) => {
54+
setParsedCredential(c);
55+
});
56+
}, []);
57+
4658
return (
4759
<div className=" pt-5 pr-2 w-full">
4860
<table className="lg:w-4/5">
4961
<tbody className="divide-y-4 divide-transparent">
50-
{credential && (
62+
{parsedCredential && (
5163
<>
52-
{renderRow('type', credential.type)}
53-
{renderRow('expdate', formatDate(credential.expdate))}
54-
{renderRow('familyName', credential.data.familyName)}
55-
{renderRow('firstName', credential.data.firstName)}
56-
{renderRow('dateOfBirth', credential.data.dateOfBirth)}
57-
{renderRow('diplomaTitle', credential.data.diplomaTitle)}
58-
{renderRow('eqfLevel', credential.data.eqfLevel)}
59-
{renderRow('grade', credential.data.grade)}
64+
{renderRow('expdate', formatDate(parsedCredential.expirationDate))}
65+
{renderRow('familyName', parsedCredential.credentialSubject.familyName)}
66+
{renderRow('firstName', parsedCredential.credentialSubject.firstName)}
67+
{renderRow('personalIdentifier', parsedCredential.credentialSubject.personalIdentifier)}
68+
{renderRow('dateOfBirth', parsedCredential.credentialSubject.dateOfBirth)}
69+
{renderRow('diplomaTitle', parsedCredential.credentialSubject.diplomaTitle)}
70+
{renderRow('eqfLevel', parsedCredential.credentialSubject.eqfLevel)}
71+
{renderRow('grade', parsedCredential.credentialSubject.grade)}
6072
</>
6173
)}
6274
</tbody>

src/components/Credentials/CredentialJson.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
// CredentialJson.js
22

3-
import React, { useState } from 'react';
3+
import React, { useEffect, useState } from 'react';
44

55
import { AiOutlineDown, AiOutlineUp } from 'react-icons/ai';
6+
import { parseCredential } from '../../functions/parseCredential';
67

78
const CredentialJson = ({ credential }) => {
89
const [showJsonCredentials, setShowJsonCredentials] = useState(false);
910

11+
const [parsedCredential, setParsedCredential] = useState(null);
12+
13+
useEffect(() => {
14+
parseCredential(credential).then((c) => {
15+
setParsedCredential(c);
16+
});
17+
}, []);
18+
1019
return (
1120
<div className=" lg:p-0 p-2 w-full">
1221
<div className="mb-2 flex items-center">
@@ -25,13 +34,13 @@ const CredentialJson = ({ credential }) => {
2534

2635
<hr className="my-2 border-t border-gray-500 py-2" />
2736

28-
{showJsonCredentials && credential ? (
37+
{showJsonCredentials && parsedCredential ? (
2938
<div>
3039
<textarea
3140
rows="10"
3241
readOnly
3342
className="w-full border rounded p-2 rounded-xl"
34-
value={credential.json}
43+
value={JSON.stringify(parsedCredential, null, 2)}
3544
/>
3645
</div>
3746
) : (

src/components/Credentials/StatusRibbon.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
// StatusRibbon.js
2-
import React from 'react';
2+
import React, { useEffect, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
4+
import { parseCredential } from '../../functions/parseCredential';
45

5-
const StatusRibbon = ({ expDate }) => {
6+
const StatusRibbon = ({ credential }) => {
67
const { t } = useTranslation();
78

9+
const [parsedCredential, setParsedCredential] = useState(null);
10+
811
const CheckExpired = (expDate) => {
912
const today = new Date();
1013
const expirationDate = new Date(expDate);
1114
return expirationDate < today;
1215
};
1316

17+
useEffect(() => {
18+
parseCredential(credential).then((c) => {
19+
setParsedCredential(c);
20+
})
21+
}, []);
22+
23+
1424
return (
15-
CheckExpired(expDate) && <div className={`absolute bottom-0 right-0 text-white text-xs py-1 px-3 rounded-tl-lg border-t border-l border-white ${CheckExpired(expDate) ? 'bg-red-500' : 'bg-green-500'}`}>
16-
{ t('statusRibbon.expired') }
17-
</div>
25+
<>
26+
{parsedCredential && CheckExpired(parsedCredential.expirationDate) &&
27+
<div className={`absolute bottom-0 right-0 text-white text-xs py-1 px-3 rounded-tl-lg border-t border-l border-white ${CheckExpired(parsedCredential.expirationDate) ? 'bg-red-500' : 'bg-green-500'}`}>
28+
{ t('statusRibbon.expired') }
29+
</div>
30+
}
31+
</>
1832
);
1933
};
2034

src/components/Popups/SelectCredentials.js

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FaShare } from 'react-icons/fa';
44
import { useTranslation } from 'react-i18next';
55
import StatusRibbon from '../../components/Credentials/StatusRibbon';
66
import { useApi } from '../../api';
7-
import { parseCredentialDependingOnFormat } from '../../components/Credentials/ApiFetchCredential';
7+
import { CredentialImage } from '../Credentials/CredentialImage';
88

99

1010
function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conformantCredentialsMap, verifierDomainName }) {
@@ -31,18 +31,11 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
3131

3232
try {
3333
const response = await api.get('/storage/vc');
34-
const simplifiedCredentialsPromises = response.data.vc_list
35-
.filter(vc => conformantCredentialsMap[keys[currentIndex]].credentials.includes(vc.credentialIdentifier))
36-
.map(async vc => {
37-
const credentialPayload = await parseCredentialDependingOnFormat(vc.credential, vc.format);
38-
return ({
39-
id: vc.credentialIdentifier,
40-
imageURL: vc.logoURL,
41-
expdate: credentialPayload['vc']["expirationDate"],
42-
})
43-
});
44-
const simplifiedCredentials = await Promise.all(simplifiedCredentialsPromises);
45-
console.log("Fields = ", conformantCredentialsMap[keys[currentIndex]].requestedFields)
34+
const simplifiedCredentials = response.data.vc_list
35+
.filter(vcEntity =>
36+
conformantCredentialsMap[keys[currentIndex]].credentials.includes(vcEntity.credentialIdentifier)
37+
);
38+
4639
setRequestedFields(conformantCredentialsMap[keys[currentIndex]].requestedFields);
4740
setImages(simplifiedCredentials);
4841
} catch (error) {
@@ -68,10 +61,10 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
6861
setCurrentIndex((i) => i + 1);
6962
}
7063

71-
const handleClick = (id) => {
64+
const handleClick = (credentialIdentifier) => {
7265
const descriptorId = keys[currentIndex];
7366
setCurrentSelectionMap((currentMap) => {
74-
currentMap[descriptorId] = id;
67+
currentMap[descriptorId] = credentialIdentifier;
7568
return currentMap;
7669
});
7770
setApplyTransition(false);
@@ -140,14 +133,8 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
140133
{images.map(image => (
141134
<div className="m-3 flex justify-center">
142135
<div className="relative rounded-xl w-2/3 overflow-hidden transition-shadow shadow-md hover:shadow-lg cursor-pointer">
143-
<img
144-
key={image.id}
145-
src={image.imageURL}
146-
alt={image.id}
147-
onClick={() => handleClick(image.id)}
148-
className="w-full object-cover rounded-xl"
149-
/>
150-
<StatusRibbon expDate={image.expdate} />
136+
<CredentialImage key={image.credentialIdentifier} credential={image.credential} onClick={() => handleClick(image.credentialIdentifier)} className={"w-full object-cover rounded-xl"} />
137+
<StatusRibbon credential={image.credential} />
151138
</div>
152139
</div>
153140
))}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { parseCredential } from "./parseCredential";
2+
3+
4+
export const extractCredentialFriendlyName = async (credential: string | object): Promise<string | undefined> => {
5+
const parsedCredential = await parseCredential(credential) as any;
6+
return parsedCredential.name ?? parsedCredential.id;
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { parseCredential } from "./parseCredential"
2+
3+
export const extractCredentialImageURL = async (credential: string | object): Promise<string | undefined> => {
4+
const parsedCredential = await parseCredential(credential) as any;
5+
return parsedCredential?.credentialBranding?.image?.url;
6+
}

src/functions/parseCredential.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import parseJwt from './ParseJwt';
2+
import {
3+
HasherAlgorithm,
4+
HasherAndAlgorithm,
5+
SdJwt,
6+
} from '@sd-jwt/core'
7+
8+
export enum CredentialFormat {
9+
VC_SD_JWT = "vc+sd-jwt",
10+
JWT_VC_JSON = "jwt_vc_json"
11+
}
12+
13+
const encoder = new TextEncoder();
14+
15+
// Encoding the string into a Uint8Array
16+
const hasherAndAlgorithm: HasherAndAlgorithm = {
17+
hasher: (input: string) => {
18+
return crypto.subtle.digest('SHA-256', encoder.encode(input)).then((v) => new Uint8Array(v));
19+
},
20+
algorithm: HasherAlgorithm.Sha256
21+
}
22+
23+
export const parseCredential = async (credential: string | object): Promise<object> => {
24+
if (typeof credential == 'string') { // is JWT
25+
if (credential.includes('~')) { // is SD-JWT
26+
return SdJwt.fromCompact<Record<string, unknown>, any>(credential)
27+
.withHasher(hasherAndAlgorithm)
28+
.getPrettyClaims()
29+
.then((payload) => payload.vc);
30+
}
31+
else { // is plain JWT
32+
return parseJwt(credential)
33+
.then((payload) => payload.vc);
34+
}
35+
}
36+
throw new Error("Type of credential is not supported")
37+
}

0 commit comments

Comments
 (0)