Skip to content

Commit c1f5f95

Browse files
authored
Merge pull request #203 from wwWallet/feat/display-vc-status
Display Credential Status
2 parents 5cf83a3 + eb9145f commit c1f5f95

File tree

13 files changed

+238
-172
lines changed

13 files changed

+238
-172
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
) : (
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// StatusRibbon.js
2+
import React, { useEffect, useState } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import { parseCredential } from '../../functions/parseCredential';
5+
6+
const StatusRibbon = ({ credential }) => {
7+
const { t } = useTranslation();
8+
9+
const [parsedCredential, setParsedCredential] = useState(null);
10+
11+
const CheckExpired = (expDate) => {
12+
const today = new Date();
13+
const expirationDate = new Date(expDate);
14+
return expirationDate < today;
15+
};
16+
17+
useEffect(() => {
18+
parseCredential(credential).then((c) => {
19+
setParsedCredential(c);
20+
})
21+
}, []);
22+
23+
24+
return (
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+
</>
32+
);
33+
};
34+
35+
export default StatusRibbon;

src/components/Popups/SelectCredentials.js

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import React, { useEffect, useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { FaShare } from 'react-icons/fa';
44
import { useTranslation } from 'react-i18next';
5-
5+
import StatusRibbon from '../../components/Credentials/StatusRibbon';
66
import { useApi } from '../../api';
7+
import { CredentialImage } from '../Credentials/CredentialImage';
78

89

910
function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conformantCredentialsMap, verifierDomainName }) {
@@ -20,7 +21,6 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
2021
const [renderContent, setRenderContent] = useState(showRequestedFields);
2122
const [applyTransition, setApplyTransition] = useState(false);
2223

23-
2424
useEffect(() => {
2525
const getData = async () => {
2626
if (currentIndex == Object.keys(conformantCredentialsMap).length) {
@@ -32,12 +32,10 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
3232
try {
3333
const response = await api.get('/storage/vc');
3434
const simplifiedCredentials = response.data.vc_list
35-
.filter(vc => conformantCredentialsMap[keys[currentIndex]].credentials.includes(vc.credentialIdentifier))
36-
.map(vc => ({
37-
id: vc.credentialIdentifier,
38-
imageURL: vc.logoURL,
39-
}));
40-
console.log("FIelds = ", conformantCredentialsMap[keys[currentIndex]].requestedFields)
35+
.filter(vcEntity =>
36+
conformantCredentialsMap[keys[currentIndex]].credentials.includes(vcEntity.credentialIdentifier)
37+
);
38+
4139
setRequestedFields(conformantCredentialsMap[keys[currentIndex]].requestedFields);
4240
setImages(simplifiedCredentials);
4341
} catch (error) {
@@ -63,10 +61,10 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
6361
setCurrentIndex((i) => i + 1);
6462
}
6563

66-
const handleClick = (id) => {
64+
const handleClick = (credentialIdentifier) => {
6765
const descriptorId = keys[currentIndex];
6866
setCurrentSelectionMap((currentMap) => {
69-
currentMap[descriptorId] = id;
67+
currentMap[descriptorId] = credentialIdentifier;
7068
return currentMap;
7169
});
7270
setApplyTransition(false);
@@ -131,16 +129,13 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
131129
</div>
132130
)}
133131

134-
<div className='mt-2 flex flex-wrap justify-center flex overflow-y-auto max-h-[40vh]'>
132+
<div className='flex flex-wrap justify-center flex overflow-y-auto max-h-[40vh]'>
135133
{images.map(image => (
136-
<div className="m-5">
137-
<img
138-
key={image.id}
139-
src={image.imageURL}
140-
alt={image.id}
141-
onClick={() => handleClick(image.id)}
142-
className="w-48 rounded-xl cursor-pointer"
143-
/>
134+
<div className="m-3 flex justify-center">
135+
<div className="relative rounded-xl w-2/3 overflow-hidden transition-shadow shadow-md hover:shadow-lg cursor-pointer">
136+
<CredentialImage key={image.credentialIdentifier} credential={image.credential} onClick={() => handleClick(image.credentialIdentifier)} className={"w-full object-cover rounded-xl"} />
137+
<StatusRibbon credential={image.credential} />
138+
</div>
144139
</div>
145140
))}
146141
</div>
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+
}

src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,5 +190,8 @@
190190
"sidebar": {
191191
"navItemLogout": "Logout",
192192
"poweredBy": "Powered by <docLinkWalletGithub>wwWallet</docLinkWalletGithub>"
193+
},
194+
"statusRibbon": {
195+
"expired": "Expired"
193196
}
194197
}

0 commit comments

Comments
 (0)