Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display Credential Status #203

Merged
merged 11 commits into from
Mar 26, 2024
83 changes: 0 additions & 83 deletions src/components/Credentials/ApiFetchCredential.ts

This file was deleted.

21 changes: 21 additions & 0 deletions src/components/Credentials/CredentialImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useState, useEffect } from "react"
import { parseCredential } from "../../functions/parseCredential";


export const CredentialImage = ({ credential, className, onClick }) => {
const [parsedCredential, setParsedCredential] = useState(null);

useEffect(() => {
parseCredential(credential).then((c) => {
setParsedCredential(c);
});
}, []);

return (
<>
{parsedCredential &&
<img src={parsedCredential.credentialBranding.image.url} alt={"Credential"} className={className} onClick={onClick} />
}
</>
)
}
34 changes: 23 additions & 11 deletions src/components/Credentials/CredentialInfo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { BiSolidCategoryAlt, BiSolidUserCircle } from 'react-icons/bi';
import { AiFillCalendar } from 'react-icons/ai';
import { RiPassExpiredFill } from 'react-icons/ri';
import { MdTitle, MdGrade } from 'react-icons/md';
import { MdTitle, MdGrade, MdOutlineNumbers } from 'react-icons/md';
import { GiLevelEndFlag } from 'react-icons/gi';
import { formatDate } from '../../functions/DateFormat';
import { parseCredential } from '../../functions/parseCredential';

const getFieldIcon = (fieldName) => {
switch (fieldName) {
Expand All @@ -14,6 +15,8 @@ const getFieldIcon = (fieldName) => {
return <RiPassExpiredFill size={25} className="inline mr-1 mb-1" />;
case 'dateOfBirth':
return <AiFillCalendar size={25} className="inline mr-1 mb-1" />;
case 'personalIdentifier':
return <MdOutlineNumbers size={25} className="inline mr-1 mb-1" />
case 'familyName':
case 'firstName':
return <BiSolidUserCircle size={25} className="inline mr-1 mb-1" />;
Expand Down Expand Up @@ -43,20 +46,29 @@ const renderRow = (fieldName, fieldValue) => {
};

const CredentialInfo = ({ credential }) => {

const [parsedCredential, setParsedCredential] = useState(null);

useEffect(() => {
parseCredential(credential).then((c) => {
setParsedCredential(c);
});
}, []);

return (
<div className=" pt-5 pr-2 w-full">
<table className="lg:w-4/5">
<tbody className="divide-y-4 divide-transparent">
{credential && (
{parsedCredential && (
<>
{renderRow('type', credential.type)}
{renderRow('expdate', formatDate(credential.expdate))}
{renderRow('familyName', credential.data.familyName)}
{renderRow('firstName', credential.data.firstName)}
{renderRow('dateOfBirth', credential.data.dateOfBirth)}
{renderRow('diplomaTitle', credential.data.diplomaTitle)}
{renderRow('eqfLevel', credential.data.eqfLevel)}
{renderRow('grade', credential.data.grade)}
{renderRow('expdate', formatDate(parsedCredential.expirationDate))}
{renderRow('familyName', parsedCredential.credentialSubject.familyName)}
{renderRow('firstName', parsedCredential.credentialSubject.firstName)}
{renderRow('personalIdentifier', parsedCredential.credentialSubject.personalIdentifier)}
{renderRow('dateOfBirth', parsedCredential.credentialSubject.dateOfBirth)}
{renderRow('diplomaTitle', parsedCredential.credentialSubject.diplomaTitle)}
{renderRow('eqfLevel', parsedCredential.credentialSubject.eqfLevel)}
{renderRow('grade', parsedCredential.credentialSubject.grade)}
</>
)}
</tbody>
Expand Down
15 changes: 12 additions & 3 deletions src/components/Credentials/CredentialJson.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// CredentialJson.js

import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';

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

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

const [parsedCredential, setParsedCredential] = useState(null);

useEffect(() => {
parseCredential(credential).then((c) => {
setParsedCredential(c);
});
}, []);

return (
<div className=" lg:p-0 p-2 w-full">
<div className="mb-2 flex items-center">
Expand All @@ -25,13 +34,13 @@ const CredentialJson = ({ credential }) => {

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

{showJsonCredentials && credential ? (
{showJsonCredentials && parsedCredential ? (
<div>
<textarea
rows="10"
readOnly
className="w-full border rounded p-2 rounded-xl"
value={credential.json}
value={JSON.stringify(parsedCredential, null, 2)}
/>
</div>
) : (
Expand Down
35 changes: 35 additions & 0 deletions src/components/Credentials/StatusRibbon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// StatusRibbon.js
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { parseCredential } from '../../functions/parseCredential';

const StatusRibbon = ({ credential }) => {
const { t } = useTranslation();

const [parsedCredential, setParsedCredential] = useState(null);

const CheckExpired = (expDate) => {
const today = new Date();
const expirationDate = new Date(expDate);
return expirationDate < today;
};

useEffect(() => {
parseCredential(credential).then((c) => {
setParsedCredential(c);
})
}, []);


return (
<>
{parsedCredential && CheckExpired(parsedCredential.expirationDate) &&
<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'}`}>
{ t('statusRibbon.expired') }
</div>
}
</>
);
};

export default StatusRibbon;
33 changes: 14 additions & 19 deletions src/components/Popups/SelectCredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FaShare } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';

import StatusRibbon from '../../components/Credentials/StatusRibbon';
import { useApi } from '../../api';
import { CredentialImage } from '../Credentials/CredentialImage';


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


useEffect(() => {
const getData = async () => {
if (currentIndex == Object.keys(conformantCredentialsMap).length) {
Expand All @@ -32,12 +32,10 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
try {
const response = await api.get('/storage/vc');
const simplifiedCredentials = response.data.vc_list
.filter(vc => conformantCredentialsMap[keys[currentIndex]].credentials.includes(vc.credentialIdentifier))
.map(vc => ({
id: vc.credentialIdentifier,
imageURL: vc.logoURL,
}));
console.log("FIelds = ", conformantCredentialsMap[keys[currentIndex]].requestedFields)
.filter(vcEntity =>
conformantCredentialsMap[keys[currentIndex]].credentials.includes(vcEntity.credentialIdentifier)
);

setRequestedFields(conformantCredentialsMap[keys[currentIndex]].requestedFields);
setImages(simplifiedCredentials);
} catch (error) {
Expand All @@ -63,10 +61,10 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
setCurrentIndex((i) => i + 1);
}

const handleClick = (id) => {
const handleClick = (credentialIdentifier) => {
const descriptorId = keys[currentIndex];
setCurrentSelectionMap((currentMap) => {
currentMap[descriptorId] = id;
currentMap[descriptorId] = credentialIdentifier;
return currentMap;
});
setApplyTransition(false);
Expand Down Expand Up @@ -131,16 +129,13 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
</div>
)}

<div className='mt-2 flex flex-wrap justify-center flex overflow-y-auto max-h-[40vh]'>
<div className='flex flex-wrap justify-center flex overflow-y-auto max-h-[40vh]'>
{images.map(image => (
<div className="m-5">
<img
key={image.id}
src={image.imageURL}
alt={image.id}
onClick={() => handleClick(image.id)}
className="w-48 rounded-xl cursor-pointer"
/>
<div className="m-3 flex justify-center">
<div className="relative rounded-xl w-2/3 overflow-hidden transition-shadow shadow-md hover:shadow-lg cursor-pointer">
<CredentialImage key={image.credentialIdentifier} credential={image.credential} onClick={() => handleClick(image.credentialIdentifier)} className={"w-full object-cover rounded-xl"} />
<StatusRibbon credential={image.credential} />
</div>
</div>
))}
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/functions/extractCredentialFriendlyName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { parseCredential } from "./parseCredential";


export const extractCredentialFriendlyName = async (credential: string | object): Promise<string | undefined> => {
const parsedCredential = await parseCredential(credential) as any;
return parsedCredential.name ?? parsedCredential.id;
}
6 changes: 6 additions & 0 deletions src/functions/extractCredentialImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { parseCredential } from "./parseCredential"

export const extractCredentialImageURL = async (credential: string | object): Promise<string | undefined> => {
const parsedCredential = await parseCredential(credential) as any;
return parsedCredential?.credentialBranding?.image?.url;
}
37 changes: 37 additions & 0 deletions src/functions/parseCredential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import parseJwt from './ParseJwt';
import {
HasherAlgorithm,
HasherAndAlgorithm,
SdJwt,
} from '@sd-jwt/core'

export enum CredentialFormat {
VC_SD_JWT = "vc+sd-jwt",
JWT_VC_JSON = "jwt_vc_json"
}

const encoder = new TextEncoder();

// Encoding the string into a Uint8Array
const hasherAndAlgorithm: HasherAndAlgorithm = {
hasher: (input: string) => {
return crypto.subtle.digest('SHA-256', encoder.encode(input)).then((v) => new Uint8Array(v));
},
algorithm: HasherAlgorithm.Sha256
}

export const parseCredential = async (credential: string | object): Promise<object> => {
if (typeof credential == 'string') { // is JWT
if (credential.includes('~')) { // is SD-JWT
return SdJwt.fromCompact<Record<string, unknown>, any>(credential)
.withHasher(hasherAndAlgorithm)
.getPrettyClaims()
.then((payload) => payload.vc);
}
else { // is plain JWT
return parseJwt(credential)
.then((payload) => payload.vc);
}
}
throw new Error("Type of credential is not supported")
}
Loading
Loading