diff --git a/.editorconfig b/.editorconfig index 048cfadd..aed7f1ef 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,7 @@ indent_style = tab [{package.json,tsconfig.json,.vscode/settings.json}] indent_style = space -[*.{yml,yaml}] # YAML does not allow tab indentation +# YAML does not allow tab indentation +[*.{yml,yaml}] indent_style = space indent_size = 2 diff --git a/src/assets/images/custom_template.svg b/src/assets/images/custom_template.svg new file mode 100644 index 00000000..052b4d85 --- /dev/null +++ b/src/assets/images/custom_template.svg @@ -0,0 +1,12 @@ + + + + + {{backgroundImageBase64}} + {{logoBase64}} + + {{name}} + {{description}} + {{/expiry_date}} + diff --git a/src/components/Auth/LoginLayout.tsx b/src/components/Auth/LoginLayout.tsx index bef53f96..19b4b7ba 100644 --- a/src/components/Auth/LoginLayout.tsx +++ b/src/components/Auth/LoginLayout.tsx @@ -9,8 +9,8 @@ export default function LoginLayout({ children, heading }: { children: React.Rea const { t } = useTranslation(); return (
-
-
+
+
logo diff --git a/src/components/Credentials/CredentialImage.js b/src/components/Credentials/CredentialImage.js index d0f2e4a1..d1e72a89 100644 --- a/src/components/Credentials/CredentialImage.js +++ b/src/components/Credentials/CredentialImage.js @@ -1,11 +1,9 @@ import { useState, useEffect, useContext } from "react"; import StatusRibbon from '../../components/Credentials/StatusRibbon'; import ContainerContext from '../../context/ContainerContext'; -import RenderSvgTemplate from "./RenderSvgTemplate"; const CredentialImage = ({ credential, className, onClick, showRibbon = true }) => { const [parsedCredential, setParsedCredential] = useState(null); - const [svgImage, setSvgImage] = useState(null); const container = useContext(ContainerContext); useEffect(() => { @@ -20,25 +18,13 @@ const CredentialImage = ({ credential, className, onClick, showRibbon = true }) }, [credential, container]); - const handleSvgGenerated = (svgUri) => { - setSvgImage(svgUri); - }; - return ( <> - {parsedCredential && parsedCredential.credentialImage && parsedCredential.credentialImage.credentialImageSvgTemplateURL ? ( - <> - - {parsedCredential && svgImage && ( - {"Credential"} - )} - - ) : parsedCredential && parsedCredential.credentialImage && parsedCredential.credentialImage.credentialImageURL && ( + {parsedCredential && parsedCredential.credentialImage && ( {"Credential"} )} - {parsedCredential && showRibbon && - + } ); diff --git a/src/components/Credentials/RenderCustomSvgTemplate.js b/src/components/Credentials/RenderCustomSvgTemplate.js new file mode 100644 index 00000000..64dfa96d --- /dev/null +++ b/src/components/Credentials/RenderCustomSvgTemplate.js @@ -0,0 +1,62 @@ +import axios from 'axios'; +import jsonpointer from 'jsonpointer'; +import { formatDate } from '../../functions/DateFormat'; +import customTemplate from '../../assets/images/custom_template.svg'; + +async function getBase64Image(url) { + if (!url) return null; + try { + const response = await axios.get(url, { responseType: 'blob' }); + const blob = response.data; + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + console.error("Failed to load image", url); + return null; + } +} + +const renderCustomSvgTemplate = async ({ beautifiedForm, name, description, logoURL, logoAltText, backgroundColor, textColor, backgroundImageURL }) => { + try { + const response = await fetch(customTemplate); + if (!response.ok) throw new Error("Failed to fetch SVG template"); + + let svgContent = await response.text(); + + const backgroundImageBase64 = await getBase64Image(backgroundImageURL); + const logoBase64 = await getBase64Image(logoURL); + + svgContent = svgContent + .replace(/{{backgroundColor}}/g, backgroundColor) + .replace( + /{{backgroundImageBase64}}/g, + backgroundImageBase64 + ? `` + : '' + ) + .replace( + /{{logoBase64}}/g, + logoBase64 + ? `${logoAltText}` + : '' + ) + .replace(/{{name}}/g, name) + .replace(/{{textColor}}/g, textColor) + .replace(/{{description}}/g, description); + + const expiryDate = jsonpointer.get(beautifiedForm, "/expiry_date"); + svgContent = svgContent.replace(/{{\/expiry_date}}/g, expiryDate ? `Expiry Date: ${formatDate(expiryDate, 'date')}` : ''); + + return `data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}`; + } catch (error) { + console.error("Error rendering SVG template", error); + return null; + } +}; + +export default renderCustomSvgTemplate; diff --git a/src/components/Credentials/RenderSvgTemplate.js b/src/components/Credentials/RenderSvgTemplate.js index 7589a8fa..a42777cc 100644 --- a/src/components/Credentials/RenderSvgTemplate.js +++ b/src/components/Credentials/RenderSvgTemplate.js @@ -1,48 +1,34 @@ -import { useState, useEffect } from 'react'; +import axios from 'axios'; import jsonpointer from 'jsonpointer'; import { formatDate } from '../../functions/DateFormat'; -const RenderSvgTemplate = ({ credential, onSvgGenerated }) => { - const [svgContent, setSvgContent] = useState(null); - - useEffect(() => { - const fetchSvgContent = async () => { - try { - const response = await fetch(credential.credentialImage.credentialImageSvgTemplateURL); - if (!response.ok) { - throw new Error(`Failed to fetch SVG from ${credential.credentialImage.credentialImageSvgTemplateURL}`); - } - - const svgText = await response.text(); - setSvgContent(svgText); - } catch (error) { - console.error(error); - } - }; - - if (credential.credentialImage.credentialImageSvgTemplateURL) { - fetchSvgContent(); +const renderSvgTemplate = async ({ beautifiedForm, credentialImageSvgTemplateURL }) => { + let svgContent = null; + try { + const response = await axios.get(credentialImageSvgTemplateURL); + if (response.status !== 200) { + throw new Error(`Failed to fetch SVG`); } - }, [credential.credentialImage.credentialImageSvgTemplateURL]); - - useEffect(() => { - if (svgContent) { - const regex = /{{([^}]+)}}/g; - - const replacedSvgText = svgContent.replace(regex, (_match, content) => { - let res = jsonpointer.get(credential.beautifiedForm, content.trim()); - if (res !== undefined) { - res = formatDate(res, 'date'); - return res; - } - return '-'; - }); - const dataUri = `data:image/svg+xml;utf8,${encodeURIComponent(replacedSvgText)}`; - onSvgGenerated(dataUri); - } - }, [svgContent, credential.beautifiedForm, onSvgGenerated]); + svgContent = response.data; + } catch (error) { + return null; // Return null if fetching fails + } + + if (svgContent) { + const regex = /{{([^}]+)}}/g; + const replacedSvgText = svgContent.replace(regex, (_match, content) => { + let res = jsonpointer.get(beautifiedForm, content.trim()); + if (res !== undefined) { + res = formatDate(res, 'date'); + return res; + } + return '-'; + }); + const dataUri = `data:image/svg+xml;utf8,${encodeURIComponent(replacedSvgText)}`; + return dataUri; // Return the data URI for the SVG + } - return null; + return null; // Return null if no SVG content is available }; -export default RenderSvgTemplate; +export default renderSvgTemplate; diff --git a/src/components/Credentials/StatusRibbon.js b/src/components/Credentials/StatusRibbon.js index 4c6bb844..2a743b03 100644 --- a/src/components/Credentials/StatusRibbon.js +++ b/src/components/Credentials/StatusRibbon.js @@ -1,27 +1,11 @@ // StatusRibbon.js -import React, { useEffect, useState, useContext } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import ContainerContext from '../../context/ContainerContext'; -import {CheckExpired} from '../../functions/CheckExpired'; +import { CheckExpired } from '../../functions/CheckExpired'; -const StatusRibbon = ({ credential }) => { +const StatusRibbon = ({ parsedCredential }) => { const { t } = useTranslation(); - const [parsedCredential, setParsedCredential] = useState(null); - const container = useContext(ContainerContext); - - useEffect(() => { - if (container) { - container.credentialParserRegistry.parse(credential).then((c) => { - if ('error' in c) { - return; - } - setParsedCredential(c.beautifiedForm); - }); - } - - }, [credential, container]); - return ( <> {parsedCredential && CheckExpired(parsedCredential.expiry_date) && diff --git a/src/components/Layout/Header.js b/src/components/Layout/Header.js index 7dd9a2b3..a42f99ed 100644 --- a/src/components/Layout/Header.js +++ b/src/components/Layout/Header.js @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import { AiOutlineMenu } from "react-icons/ai"; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import ConnectionStatusIcon from './Navigation/ConnectionStatusIcon'; diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index ddeeac0e..7ef41a12 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -9,7 +9,7 @@ const Layout = ({ children }) => { const toggleSidebar = () => setIsOpen(!isOpen); return ( -
+
{/* Header */} diff --git a/src/components/Layout/Navigation/BottomNav.js b/src/components/Layout/Navigation/BottomNav.js index c63caf37..9b97c435 100644 --- a/src/components/Layout/Navigation/BottomNav.js +++ b/src/components/Layout/Navigation/BottomNav.js @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext } from 'react'; import { FaWallet, FaUserCircle } from "react-icons/fa"; import { IoIosAddCircle, IoIosSend } from "react-icons/io"; import { useLocation, useNavigate } from 'react-router-dom'; diff --git a/src/components/Layout/Navigation/Sidebar.js b/src/components/Layout/Navigation/Sidebar.js index 29440798..e63fbc43 100644 --- a/src/components/Layout/Navigation/Sidebar.js +++ b/src/components/Layout/Navigation/Sidebar.js @@ -30,7 +30,7 @@ const NavItem = ({ }; const Sidebar = ({ isOpen, toggle }) => { - const { isOnline, updateAvailable } = useContext(StatusContext); + const { updateAvailable } = useContext(StatusContext); const { api, logout } = useContext(SessionContext); const { username, displayName } = api.getSession(); const location = useLocation(); @@ -56,8 +56,8 @@ const Sidebar = ({ isOpen, toggle }) => { return (
diff --git a/src/components/Popups/RedirectPopup.js b/src/components/Popups/RedirectPopup.js index 9f6b485a..9674d6f4 100644 --- a/src/components/Popups/RedirectPopup.js +++ b/src/components/Popups/RedirectPopup.js @@ -1,9 +1,7 @@ import React, { useState, useEffect } from 'react'; -import Modal from 'react-modal'; import { FaShare } from 'react-icons/fa'; import { useTranslation } from 'react-i18next'; import Button from '../Buttons/Button'; -import Spinner from '../Shared/Spinner'; import PopupLayout from './PopupLayout'; const RedirectPopup = ({ loading, availableCredentialConfigurations, onClose, handleContinue, popupTitle, popupMessage }) => { @@ -19,7 +17,7 @@ const RedirectPopup = ({ loading, availableCredentialConfigurations, onClose, ha if (availableCredentialConfigurations) { setSelectedConfiguration(Object.keys(availableCredentialConfigurations)[0]) } - }, []) + }, [availableCredentialConfigurations, setSelectedConfiguration]) const handleOptionChange = (event) => { if (availableCredentialConfigurations) { diff --git a/src/components/Popups/SelectCredentialsPopup.js b/src/components/Popups/SelectCredentialsPopup.js index 2fc12ce8..ee043201 100644 --- a/src/components/Popups/SelectCredentialsPopup.js +++ b/src/components/Popups/SelectCredentialsPopup.js @@ -71,7 +71,6 @@ function SelectCredentialsPopup({ isOpen, setIsOpen, setSelectionMap, conformant const [selectedCredential, setSelectedCredential] = useState(null); const container = useContext(ContainerContext); const screenType = useScreenType(); - const [credentialDisplay, setCredentialDisplay] = useState({}); const [currentSlide, setCurrentSlide] = useState(1); useEffect(() => { @@ -115,6 +114,7 @@ function SelectCredentialsPopup({ isOpen, setIsOpen, setSelectionMap, conformant keys, setSelectionMap, setIsOpen, + container.credentialParserRegistry, ]); useEffect(() => { @@ -197,7 +197,7 @@ function SelectCredentialsPopup({ isOpen, setIsOpen, setSelectionMap, conformant return ( -
+
{stepTitles && (

diff --git a/src/components/Shared/Slider.js b/src/components/Shared/Slider.js index dcf5e19a..55c5c30a 100644 --- a/src/components/Shared/Slider.js +++ b/src/components/Shared/Slider.js @@ -9,8 +9,8 @@ import { EffectCards } from 'swiper/modules'; import 'swiper/css'; import 'swiper/css/effect-cards'; -const Slider = ({ items, renderSlideContent, onSlideChange }) => { - const [currentSlide, setCurrentSlide] = useState(1); +const Slider = ({ items, renderSlideContent, onSlideChange, initialSlide = 1 }) => { + const [currentSlide, setCurrentSlide] = useState(initialSlide); const sliderRef = useRef(null); const { t } = useTranslation(); @@ -33,6 +33,7 @@ const Slider = ({ items, renderSlideContent, onSlideChange }) => { grabCursor={true} modules={[EffectCards]} slidesPerView={1} + initialSlide={currentSlide -1} onSlideChange={(swiper) => { setCurrentSlide(swiper.activeIndex + 1); if (onSlideChange) onSlideChange(swiper.activeIndex); diff --git a/src/components/Shared/Spinner.js b/src/components/Shared/Spinner.js index 740c0ce2..99f7bc24 100644 --- a/src/components/Shared/Spinner.js +++ b/src/components/Shared/Spinner.js @@ -11,7 +11,7 @@ function Spinner() { }, []); return ( -
+
diff --git a/src/components/WelcomeTourGuide/WelcomeTourGuide.js b/src/components/WelcomeTourGuide/WelcomeTourGuide.js index 2483c64a..7fe392d7 100644 --- a/src/components/WelcomeTourGuide/WelcomeTourGuide.js +++ b/src/components/WelcomeTourGuide/WelcomeTourGuide.js @@ -21,7 +21,7 @@ const TourGuide = ({ toggleMenu, isOpen }) => { useEffect(() => { const getStepSelectorSmallScreen = (stepName) => { - if (screenType != 'desktop') { + if (screenType !== 'desktop') { return stepName + '-small-screen'; } else { return stepName; @@ -76,7 +76,7 @@ const TourGuide = ({ toggleMenu, isOpen }) => { return { ...step, action: () => { - if (screenType != 'desktop') { + if (screenType !== 'desktop') { if (index >= 5 && index <= 6 && !isOpen) { toggleMenu(); } else if ((index < 5 || index > 6) && isOpen) { @@ -88,7 +88,7 @@ const TourGuide = ({ toggleMenu, isOpen }) => { }); setSteps(updatedSteps); - }, [t, toggleMenu, isOpen]); + }, [t, toggleMenu, isOpen, screenType]); const startTour = () => { setIsModalOpen(false); diff --git a/src/context/ContainerContext.tsx b/src/context/ContainerContext.tsx index bb98f830..59c5dfed 100644 --- a/src/context/ContainerContext.tsx +++ b/src/context/ContainerContext.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useContext, useMemo, createContext } from "react"; +import { useEffect, useState, useContext, createContext } from "react"; import { DIContainer } from "../lib/DIContainer"; import { IHttpProxy } from "../lib/interfaces/IHttpProxy"; import { IOpenID4VCIClient } from "../lib/interfaces/IOpenID4VCIClient"; @@ -21,8 +21,9 @@ import { parseSdJwtCredential } from "../functions/parseSdJwtCredential"; import { CredentialConfigurationSupported } from "../lib/schemas/CredentialConfigurationSupportedSchema"; import { generateRandomIdentifier } from "../lib/utils/generateRandomIdentifier"; import { fromBase64 } from "../util"; -import defaulCredentialImage from "../assets/images/cred.png"; -import { UserData } from "../api/types"; +import defaultCredentialImage from "../assets/images/cred.png"; +import renderSvgTemplate from "../components/Credentials/RenderSvgTemplate"; +import renderCustomSvgTemplate from "../components/Credentials/RenderCustomSvgTemplate"; import { MDoc } from "@auth0/mdl"; import { deviceResponseParser, mdocPIDParser } from "../lib/utils/mdocPIDParser"; @@ -51,9 +52,15 @@ export const ContainerContextProvider = ({ children }) => { const [container, setContainer] = useState(null); const [isInitialized, setIsInitialized] = useState(false); // New flag + useEffect(() => { + window.addEventListener('generatedProof', (e) => { + setIsInitialized(false); + }); + }, []); + useEffect(() => { const initialize = async () => { - if (isInitialized || !isLoggedIn || !api || container !== null) return; + if (isInitialized || !isLoggedIn || !api) return; console.log('Initializing container...'); setIsInitialized(true); @@ -102,47 +109,59 @@ export const ContainerContextProvider = ({ children }) => { credentialHeader.vctm.display[0][defaultLocale]?.rendering?.svg_templates[0]?.uri : null; - let credentialFriendlyName = credentialHeader?.vctm?.display && credentialHeader.vctm.display[0] && credentialHeader.vctm.display[0][defaultLocale] ? - credentialHeader.vctm.display[0][defaultLocale]?.name - : null; + let credentialFriendlyName = credentialHeader?.vctm?.display?.[0]?.[defaultLocale]?.name + || credentialConfigurationSupportedObj?.display?.[0]?.name + || "Credential"; - // get credential friendly name from openid credential issuer metadata - if (!credentialFriendlyName && credentialConfigurationSupportedObj && credentialConfigurationSupportedObj?.display && credentialConfigurationSupportedObj?.display.length > 0) { - credentialFriendlyName = credentialConfigurationSupportedObj?.display[0]?.name; - } + let credentialDescription = credentialHeader?.vctm?.display?.[0]?.[defaultLocale]?.description + || credentialConfigurationSupportedObj?.display?.[0]?.description + || "Credential"; - if (!credentialFriendlyName) { // fallback value - credentialFriendlyName = "Credential"; - } + const svgContent = await renderSvgTemplate({ beautifiedForm: result.beautifiedForm, credentialImageSvgTemplateURL: credentialImageSvgTemplateURL }); + + const simple = credentialHeader?.vctm?.display?.[0]?.[defaultLocale]?.rendering?.simple; + const issuerMetadata = credentialConfigurationSupportedObj?.display?.[0]; - if (credentialImageSvgTemplateURL) { + if (svgContent) { return { beautifiedForm: result.beautifiedForm, credentialImage: { - credentialImageSvgTemplateURL: credentialImageSvgTemplateURL + credentialImageURL: svgContent }, credentialFriendlyName, } } - else if (credentialHeader?.vctm || credentialConfigurationSupportedObj) { - let credentialImageURL = credentialHeader?.vctm?.display && credentialHeader.vctm.display[0] && credentialHeader.vctm.display[0][defaultLocale] ? - credentialHeader.vctm.display[0][defaultLocale]?.rendering?.simple?.logo?.uri - : null; - - if (!credentialImageURL) { // provide fallback method through the OpenID credential issuer metadata - credentialImageURL = credentialConfigurationSupportedObj?.display?.length > 0 ? credentialConfigurationSupportedObj.display[0]?.background_image?.uri : null; - } - if (!credentialImageURL) { - credentialImageURL = credentialConfigurationSupportedObj?.display?.length > 0 ? credentialConfigurationSupportedObj.display[0]?.logo?.url : null; - } - if (!credentialImageURL) { - credentialImageURL = defaulCredentialImage; + else if (simple) { + // Simple style + let logoURL = simple?.logo?.uri || null; + let logoAltText = simple?.logo?.alt_text || 'Credential logo'; + let backgroundColor = simple?.background_color || '#808080'; + let textColor = simple?.text_color || '#000000'; + + const svgCustomContent = await renderCustomSvgTemplate({ beautifiedForm: result.beautifiedForm, name: credentialFriendlyName, description: credentialDescription, logoURL, logoAltText, backgroundColor, textColor, backgroundImageURL: null }); + return { + beautifiedForm: result.beautifiedForm, + credentialImage: { + credentialImageURL: svgCustomContent || defaultCredentialImage, + }, + credentialFriendlyName, } - + } + else if (issuerMetadata) { + // Issuer Metadata style + let name = issuerMetadata?.name || 'Credential'; + let description = issuerMetadata?.description || ''; + let logoURL = issuerMetadata?.logo?.uri || null; + let logoAltText = issuerMetadata?.logo?.alt_text || 'Credential logo'; + let backgroundColor = issuerMetadata?.background_color || '#808080'; + let textColor = issuerMetadata?.text_color || '#000000'; + let backgroundImageURL = issuerMetadata?.background_image?.uri || null; + + const svgCustomContent = await renderCustomSvgTemplate({ beautifiedForm: result.beautifiedForm, name, description, logoURL, logoAltText, backgroundColor, textColor, backgroundImageURL }); return { beautifiedForm: result.beautifiedForm, credentialImage: { - credentialImageURL: credentialImageURL, + credentialImageURL: svgCustomContent || defaultCredentialImage, }, credentialFriendlyName, } @@ -151,7 +170,7 @@ export const ContainerContextProvider = ({ children }) => { return { beautifiedForm: result.beautifiedForm, credentialImage: { - credentialImageURL: defaulCredentialImage, + credentialImageURL: defaultCredentialImage, }, credentialFriendlyName, } @@ -192,7 +211,7 @@ export const ContainerContextProvider = ({ children }) => { cont.resolve('HttpProxy'), cont.resolve('OpenID4VCIClientStateRepository'), async (cNonce: string, audience: string, clientId: string): Promise<{ jws: string }> => { - const [{ proof_jwt }, newPrivateData, keystoreCommit] = await keystore.generateOpenid4vciProof(cNonce, audience, clientId); + const [{ proof_jwts: [proof_jwt] }, newPrivateData, keystoreCommit] = await keystore.generateOpenid4vciProofs([{ nonce: cNonce, audience, issuer: clientId }]); await api.updatePrivateData(newPrivateData); await keystoreCommit(); return { jws: proof_jwt }; @@ -278,7 +297,7 @@ export const ContainerContextProvider = ({ children }) => { }; initialize(); - }, [isLoggedIn, api, container, isInitialized]); + }, [isLoggedIn, api, container, isInitialized, keystore]); return ( diff --git a/src/hooks/useCheckURL.ts b/src/hooks/useCheckURL.ts index 2a0d44d7..c04923be 100644 --- a/src/hooks/useCheckURL.ts +++ b/src/hooks/useCheckURL.ts @@ -33,91 +33,92 @@ function useCheckURL(urlToCheck: string): { const [typeMessagePopup, setTypeMessagePopup] = useState(""); const { t } = useTranslation(); - async function handle(urlToCheck: string) { - const userHandleB64u = keystore.getUserHandleB64u(); - if (!userHandleB64u) { - throw new Error("User handle could not be extracted from keystore"); + useEffect(() => { + if (!isLoggedIn || !container || !urlToCheck || !keystore || !api || !t) { + return; } - const u = new URL(urlToCheck); - if (u.protocol === 'openid-credential-offer' || u.searchParams.get('credential_offer') || u.searchParams.get('credential_offer_uri')) { - for (const credentialIssuerIdentifier of Object.keys(container.openID4VCIClients)) { - await container.openID4VCIClients[credentialIssuerIdentifier].handleCredentialOffer(u.toString(), userHandleB64u) - .then(({ credentialIssuer, selectedCredentialConfigurationId, issuer_state }) => { - return container.openID4VCIClients[credentialIssuerIdentifier].generateAuthorizationRequest(selectedCredentialConfigurationId, userHandleB64u, issuer_state); - }) - .then(({ url, client_id, request_uri }) => { - if (url) { - window.location.href = url; - } - }) - .catch((err) => console.error(err)); + + async function handle(urlToCheck: string) { + const userHandleB64u = keystore.getUserHandleB64u(); + if (!userHandleB64u) { + throw new Error("User handle could not be extracted from keystore"); } - } - else if (u.searchParams.get('code')) { - for (const credentialIssuerIdentifier of Object.keys(container.openID4VCIClients)) { - addLoader(); - await container.openID4VCIClients[credentialIssuerIdentifier].handleAuthorizationResponse(urlToCheck, userHandleB64u) - .then(() => { - removeLoader(); - }) - .catch(err => { - console.log("Error during the handling of authorization response") - window.history.replaceState({}, '', `${window.location.pathname}`); - console.error(err) - removeLoader(); - }); + const u = new URL(urlToCheck); + if (u.protocol === 'openid-credential-offer' || u.searchParams.get('credential_offer') || u.searchParams.get('credential_offer_uri')) { + for (const credentialIssuerIdentifier of Object.keys(container.openID4VCIClients)) { + await container.openID4VCIClients[credentialIssuerIdentifier].handleCredentialOffer(u.toString(), userHandleB64u) + .then(({ credentialIssuer, selectedCredentialConfigurationId, issuer_state }) => { + return container.openID4VCIClients[credentialIssuerIdentifier].generateAuthorizationRequest(selectedCredentialConfigurationId, userHandleB64u, issuer_state); + }) + .then(({ url, client_id, request_uri }) => { + if (url) { + window.location.href = url; + } + }) + .catch((err) => console.error(err)); + } } - } - else { - await container.openID4VPRelyingParty.handleAuthorizationRequest(urlToCheck).then((result) => { - if ('err' in result) { - if (result.err === HandleAuthorizationRequestError.INSUFFICIENT_CREDENTIALS) { - setTextMessagePopup({ title: `${t('messagePopup.insufficientCredentials.title')}`, description: `${t('messagePopup.insufficientCredentials.description')}` }); - setTypeMessagePopup('error'); - setMessagePopup(true); - } - else if (result.err === HandleAuthorizationRequestError.ONLY_ONE_INPUT_DESCRIPTOR_IS_SUPPORTED) { - setTextMessagePopup({ title: `${t('messagePopup.onlyOneInputDescriptor.title')}`, description: `${t('messagePopup.onlyOneInputDescriptor.description')}` }); - setTypeMessagePopup('error'); - setMessagePopup(true); - } - else if (result.err == HandleAuthorizationRequestError.NONTRUSTED_VERIFIER) { - setTextMessagePopup({ title: `${t('messagePopup.nonTrustedVerifier.title')}`, description: `${t('messagePopup.nonTrustedVerifier.description')}` }); - setTypeMessagePopup('error'); - setMessagePopup(true); - } - return; + else if (u.searchParams.get('code')) { + for (const credentialIssuerIdentifier of Object.keys(container.openID4VCIClients)) { + addLoader(); + await container.openID4VCIClients[credentialIssuerIdentifier].handleAuthorizationResponse(urlToCheck, userHandleB64u) + .then(() => { + removeLoader(); + }) + .catch(err => { + console.log("Error during the handling of authorization response") + window.history.replaceState({}, '', `${window.location.pathname}`); + console.error(err) + removeLoader(); + }); } - const { conformantCredentialsMap, verifierDomainName } = result; - const jsonedMap = Object.fromEntries(conformantCredentialsMap); - window.history.replaceState({}, '', `${window.location.pathname}`); - setVerifierDomainName(verifierDomainName); - setConformantCredentialsMap(jsonedMap); - setShowSelectCredentialsPopup(true); - }).catch(err => { - console.log("Failed to handle authorization req"); - console.error(err) - }) - } + } + else { + await container.openID4VPRelyingParty.handleAuthorizationRequest(urlToCheck).then((result) => { + if ('err' in result) { + if (result.err === HandleAuthorizationRequestError.INSUFFICIENT_CREDENTIALS) { + setTextMessagePopup({ title: `${t('messagePopup.insufficientCredentials.title')}`, description: `${t('messagePopup.insufficientCredentials.description')}` }); + setTypeMessagePopup('error'); + setMessagePopup(true); + } + else if (result.err === HandleAuthorizationRequestError.ONLY_ONE_INPUT_DESCRIPTOR_IS_SUPPORTED) { + setTextMessagePopup({ title: `${t('messagePopup.onlyOneInputDescriptor.title')}`, description: `${t('messagePopup.onlyOneInputDescriptor.description')}` }); + setTypeMessagePopup('error'); + setMessagePopup(true); + } + else if (result.err === HandleAuthorizationRequestError.NONTRUSTED_VERIFIER) { + setTextMessagePopup({ title: `${t('messagePopup.nonTrustedVerifier.title')}`, description: `${t('messagePopup.nonTrustedVerifier.description')}` }); + setTypeMessagePopup('error'); + setMessagePopup(true); + } + return; + } + const { conformantCredentialsMap, verifierDomainName } = result; + const jsonedMap = Object.fromEntries(conformantCredentialsMap); + window.history.replaceState({}, '', `${window.location.pathname}`); + setVerifierDomainName(verifierDomainName); + setConformantCredentialsMap(jsonedMap); + setShowSelectCredentialsPopup(true); + }).catch(err => { + console.log("Failed to handle authorization req"); + console.error(err) + }) + } - const urlParams = new URLSearchParams(window.location.search); - const state = urlParams.get('state'); - const error = urlParams.get('error'); - if (urlToCheck && isLoggedIn && state && error) { - window.history.replaceState({}, '', `${window.location.pathname}`); - const errorDescription = urlParams.get('error_description'); - setTextMessagePopup({ title: error, description: errorDescription }); - setTypeMessagePopup('error'); - setMessagePopup(true); + const urlParams = new URLSearchParams(window.location.search); + const state = urlParams.get('state'); + const error = urlParams.get('error'); + if (urlToCheck && isLoggedIn && state && error) { + window.history.replaceState({}, '', `${window.location.pathname}`); + const errorDescription = urlParams.get('error_description'); + setTextMessagePopup({ title: error, description: errorDescription }); + setTypeMessagePopup('error'); + setMessagePopup(true); + } } - } - useEffect(() => { - if (!isLoggedIn || !container || !urlToCheck || !keystore || !api || !t) { - return; - } handle(urlToCheck); - }, [api, keystore, t, urlToCheck, isLoggedIn, container]); + }, [api, keystore, t, urlToCheck, isLoggedIn, container, addLoader, removeLoader]); useEffect(() => { if (selectionMap) { diff --git a/src/hooks/useFetchPresentations.js b/src/hooks/useFetchPresentations.js index 5cb42f29..1c675a0b 100644 --- a/src/hooks/useFetchPresentations.js +++ b/src/hooks/useFetchPresentations.js @@ -26,7 +26,7 @@ const useFetchPresentations = (api, credentialId = "", historyId = "") => { } if (historyId) { - vpListFromApi = vpListFromApi.filter(item => item.id == historyId); + vpListFromApi = vpListFromApi.filter(item => item.id.toString() === historyId); } setHistory(Array.isArray(vpListFromApi) ? vpListFromApi : [vpListFromApi]); diff --git a/src/lib/interfaces/ICredentialParser.ts b/src/lib/interfaces/ICredentialParser.ts index 500b2514..04de3336 100644 --- a/src/lib/interfaces/ICredentialParser.ts +++ b/src/lib/interfaces/ICredentialParser.ts @@ -2,7 +2,7 @@ import { PresentationDefinitionType } from "../types/presentationDefinition.type export interface ICredentialParserRegistry { addParser(parser: ICredentialParser): void; - parse(rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; } | { credentialImageSvgTemplateURL: string; }; beautifiedForm: any; } | { error: string }>; + parse(rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; }; beautifiedForm: any; } | { error: string }>; } export interface ICredentialParser { @@ -12,5 +12,5 @@ export interface ICredentialParser { * @param presentationDefinitionFilter if defined, then the befautified form will include only the attributes defined in the presentation definition. This can be used * in the presentation flow when the user is prompted to select credentials to present */ - parse(rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; } | { credentialImageSvgTemplateURL: string; }; beautifiedForm: any; } | { error: string }>; + parse(rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; }; beautifiedForm: any; } | { error: string }>; } diff --git a/src/lib/interfaces/IOpenID4VPRelyingParty.ts b/src/lib/interfaces/IOpenID4VPRelyingParty.ts index 744730ba..6aa80313 100644 --- a/src/lib/interfaces/IOpenID4VPRelyingParty.ts +++ b/src/lib/interfaces/IOpenID4VPRelyingParty.ts @@ -9,4 +9,5 @@ export enum HandleAuthorizationRequestError { MISSING_PRESENTATION_DEFINITION_URI, ONLY_ONE_INPUT_DESCRIPTOR_IS_SUPPORTED, NONTRUSTED_VERIFIER, + INVALID_RESPONSE_MODE, } diff --git a/src/lib/schemas/CredentialConfigurationSupportedSchema.ts b/src/lib/schemas/CredentialConfigurationSupportedSchema.ts index 9603f364..3d82118e 100644 --- a/src/lib/schemas/CredentialConfigurationSupportedSchema.ts +++ b/src/lib/schemas/CredentialConfigurationSupportedSchema.ts @@ -11,13 +11,16 @@ const commonSchema = z.object({ display: z.array(z.object({ name: z.string(), description: z.string().optional(), + background_color: z.string().optional(), + text_color: z.string().optional(), + alt_text: z.string().optional(), background_image: z.object({ uri: z.string() }).optional(), locale: z.string().optional(), logo: z.object({ - url: z.string(), - alt_text: z.string(), + uri: z.string(), + alt_text: z.string().optional(), }).optional(), })).optional(), scope: z.string(), diff --git a/src/lib/services/CredentialParserRegistry.ts b/src/lib/services/CredentialParserRegistry.ts index 1af382a2..20498c61 100644 --- a/src/lib/services/CredentialParserRegistry.ts +++ b/src/lib/services/CredentialParserRegistry.ts @@ -10,7 +10,7 @@ export class CredentialParserRegistry implements ICredentialParserRegistry { /** * optimize parsing time by caching alread parsed objects because parse() can be called multiple times in a single view */ - private parsedObjectsCache = new Map(); + private parsedObjectsCache = new Map(); addParser(parser: ICredentialParser): void { this.parserList.push(parser); diff --git a/src/lib/services/OpenID4VCIClient.ts b/src/lib/services/OpenID4VCIClient.ts index 87373ba8..f175e441 100644 --- a/src/lib/services/OpenID4VCIClient.ts +++ b/src/lib/services/OpenID4VCIClient.ts @@ -45,7 +45,7 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { throw new Error("Only authorization_code grant is supported"); } - if (offer.credential_issuer != this.config.credentialIssuerIdentifier) { + if (offer.credential_issuer !== this.config.credentialIssuerIdentifier) { return; } @@ -143,7 +143,7 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { return; } const s = await this.openID4VCIClientStateRepository.getByStateAndUserHandle(state, userHandleB64U); - if (!s || !s.credentialIssuerIdentifier || s.credentialIssuerIdentifier != this.config.credentialIssuerIdentifier) { + if (!s || !s.credentialIssuerIdentifier || s.credentialIssuerIdentifier !== this.config.credentialIssuerIdentifier) { return; } await this.requestCredentials({ @@ -295,7 +295,7 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { console.log("== response = ", response) try { // try to extract the response and update the OpenID4VCIClientStateRepository const { - data: { access_token, c_nonce, expires_in, c_nonce_expires_in, refresh_token, token_type }, + data: { access_token, c_nonce, expires_in, c_nonce_expires_in, refresh_token }, } = response; if (!access_token) { @@ -329,7 +329,7 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { private async credentialRequest(response: any, flowState: OpenID4VCIClientState) { const { - data: { access_token, c_nonce, expires_in, c_nonce_expires_in }, + data: { access_token, c_nonce }, } = response; @@ -362,6 +362,9 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { const generateProofResult = await this.generateNonceProof(c_nonce, this.config.credentialIssuerIdentifier, this.config.clientId); jws = generateProofResult.jws; console.log("proof = ", jws) + if (jws) { + dispatchEvent(new CustomEvent("generatedProof")); + } } catch (err) { console.error(err); @@ -377,10 +380,10 @@ export class OpenID4VCIClient implements IOpenID4VCIClient { "format": this.config.credentialIssuerMetadata.credential_configurations_supported[flowState.credentialConfigurationId].format, } as any; - if (credentialConfigurationSupported.format == VerifiableCredentialFormat.SD_JWT_VC && credentialConfigurationSupported.vct) { + if (credentialConfigurationSupported.format === VerifiableCredentialFormat.SD_JWT_VC && credentialConfigurationSupported.vct) { credentialEndpointBody.vct = credentialConfigurationSupported.vct; } - else if (credentialConfigurationSupported.format == VerifiableCredentialFormat.MSO_MDOC && credentialConfigurationSupported.doctype) { + else if (credentialConfigurationSupported.format === VerifiableCredentialFormat.MSO_MDOC && credentialConfigurationSupported.doctype) { credentialEndpointBody.doctype = credentialConfigurationSupported.doctype; } diff --git a/src/lib/services/OpenID4VCIClientStateRepository.ts b/src/lib/services/OpenID4VCIClientStateRepository.ts index 992618df..9f714d77 100644 --- a/src/lib/services/OpenID4VCIClientStateRepository.ts +++ b/src/lib/services/OpenID4VCIClientStateRepository.ts @@ -15,9 +15,18 @@ export class OpenID4VCIClientStateRepository implements IOpenID4VCIClientStateRe } async getByStateAndUserHandle(state: string, userHandleB64U: string): Promise { const array = JSON.parse(localStorage.getItem(this.key)) as Array; - const res = array.filter((s) => s.state == state && s.userHandleB64U == userHandleB64U)[0]; - if (res && (!res.created || typeof res.created != 'number' || res.tokenResponse?.data?.refresh_token && Math.floor(Date.now() / 1000) - res.created > this.refreshTokenMaxAgeInSeconds)) { - const updatedArray = array.filter((x) => x.state != res.state); // remove the state + const res = array.filter((s) => s.state === state && s.userHandleB64U === userHandleB64U)[0]; + if (res && + ( + !res.created || + typeof res.created !== 'number' || + ( + res.tokenResponse?.data?.refresh_token && + Math.floor(Date.now() / 1000) - res.created > this.refreshTokenMaxAgeInSeconds + ) + ) + ) { + const updatedArray = array.filter((x) => x.state !== res.state); // remove the state localStorage.setItem(this.key, JSON.stringify(updatedArray)); return null; } @@ -26,9 +35,18 @@ export class OpenID4VCIClientStateRepository implements IOpenID4VCIClientStateRe async getByCredentialConfigurationIdAndUserHandle(credentialConfigurationId: string, userHandleB64U: string): Promise { const array = JSON.parse(localStorage.getItem(this.key)) as Array; - const res = array.filter((s) => s.credentialConfigurationId == credentialConfigurationId && s.userHandleB64U == userHandleB64U)[0]; - if (res && (!res.created || typeof res.created != 'number' || res.tokenResponse?.data?.refresh_token && Math.floor(Date.now() / 1000) - res.created > this.refreshTokenMaxAgeInSeconds)) { - const updatedArray = array.filter((x) => x.state != res.state); // remove the state + const res = array.filter((s) => s.credentialConfigurationId === credentialConfigurationId && s.userHandleB64U === userHandleB64U)[0]; + if (res && + ( + !res.created || + typeof res.created != 'number' || + ( + res.tokenResponse?.data?.refresh_token && + Math.floor(Date.now() / 1000) - res.created > this.refreshTokenMaxAgeInSeconds + ) + ) + ) { + const updatedArray = array.filter((x) => x.state !== res.state); // remove the state localStorage.setItem(this.key, JSON.stringify(updatedArray)); return null; } @@ -39,7 +57,7 @@ export class OpenID4VCIClientStateRepository implements IOpenID4VCIClientStateRe const existingState = await this.getByCredentialConfigurationIdAndUserHandle(s.credentialConfigurationId, s.userHandleB64U); if (existingState) { // remove the existing state for this configuration id const array = JSON.parse(localStorage.getItem(this.key)) as Array; - const updatedArray = array.filter((x) => x.credentialConfigurationId != s.credentialConfigurationId); + const updatedArray = array.filter((x) => x.credentialConfigurationId !== s.credentialConfigurationId); localStorage.setItem(this.key, JSON.stringify(updatedArray)); } let data = localStorage.getItem(this.key); @@ -67,7 +85,7 @@ export class OpenID4VCIClientStateRepository implements IOpenID4VCIClientStateRe return; } const array = JSON.parse(localStorage.getItem(this.key)) as Array; - const updatedArray = array.filter((x) => x.state != newState.state); // remove the state that is going to be changed + const updatedArray = array.filter((x) => x.state !== newState.state); // remove the state that is going to be changed updatedArray.push(newState); // commit changes localStorage.setItem(this.key, JSON.stringify(updatedArray)); diff --git a/src/lib/services/OpenID4VPRelyingParty.ts b/src/lib/services/OpenID4VPRelyingParty.ts index cf40e9e0..7bb7b6bb 100644 --- a/src/lib/services/OpenID4VPRelyingParty.ts +++ b/src/lib/services/OpenID4VPRelyingParty.ts @@ -5,7 +5,7 @@ import { HasherAlgorithm, HasherAndAlgorithm, SdJwt } from "@sd-jwt/core"; import { VerifiableCredentialFormat } from "../schemas/vc"; import { generateRandomIdentifier } from "../utils/generateRandomIdentifier"; import { base64url, CompactEncrypt, importJWK, importX509, jwtVerify } from "jose"; -import { OpenID4VPRelyingPartyState } from "../types/OpenID4VPRelyingPartyState"; +import { OpenID4VPRelyingPartyState, ResponseMode, ResponseModeSchema } from "../types/OpenID4VPRelyingPartyState"; import { OpenID4VPRelyingPartyStateRepository } from "./OpenID4VPRelyingPartyStateRepository"; import { IHttpProxy } from "../interfaces/IHttpProxy"; import { ICredentialParserRegistry } from "../interfaces/ICredentialParser"; @@ -40,7 +40,7 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { let presentation_definition = authorizationRequest.searchParams.get('presentation_definition') ? JSON.parse(authorizationRequest.searchParams.get('presentation_definition')) : null; let presentation_definition_uri = authorizationRequest.searchParams.get('presentation_definition_uri'); let client_metadata = authorizationRequest.searchParams.get('client_metadata') ? JSON.parse(authorizationRequest.searchParams.get('client_metadata')) : null; - + let response_mode = authorizationRequest.searchParams.get('response_mode') ? JSON.parse(authorizationRequest.searchParams.get('response_mode')) : null; if (presentation_definition_uri) { const presentationDefinitionFetch = await this.httpProxy.get(presentation_definition_uri, {}); presentation_definition = presentationDefinitionFetch.data; @@ -67,6 +67,9 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { presentation_definition = p.presentation_definition; response_uri = p.response_uri ?? p.redirect_uri; client_metadata = p.client_metadata; + if (p.response_mode) { + response_mode = p.response_mode; + } state = p.state; nonce = p.nonce; @@ -80,7 +83,7 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { } const altNames = await extractSAN('-----BEGIN CERTIFICATE-----\n' + parsedHeader.x5c[0] + '\n-----END CERTIFICATE-----'); - if (OPENID4VP_SAN_DNS_CHECK && !altNames || altNames.length === 0) { + if (OPENID4VP_SAN_DNS_CHECK && (!altNames || altNames.length === 0)) { console.log("No SAN found"); return { err: HandleAuthorizationRequestError.NONTRUSTED_VERIFIER } } @@ -111,13 +114,12 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { } } + const lastUsedNonce = sessionStorage.getItem('last_used_nonce'); - if (lastUsedNonce && nonce == lastUsedNonce) { + if (lastUsedNonce && nonce === lastUsedNonce) { throw new Error("last used nonce"); } - const vcList = await this.getAllStoredVerifiableCredentials().then((res) => res.verifiableCredentials); - if (!presentation_definition) { return { err: HandleAuthorizationRequestError.MISSING_PRESENTATION_DEFINITION }; } @@ -125,13 +127,21 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { return { err: HandleAuthorizationRequestError.ONLY_ONE_INPUT_DESCRIPTOR_IS_SUPPORTED }; } + const { error } = ResponseModeSchema.safeParse(response_mode); + if (error) { + return { err: HandleAuthorizationRequestError.INVALID_RESPONSE_MODE }; + } + + const vcList = await this.getAllStoredVerifiableCredentials().then((res) => res.verifiableCredentials); + await this.openID4VPRelyingPartyStateRepository.store(new OpenID4VPRelyingPartyState( presentation_definition, nonce, response_uri, client_id, state, - client_metadata + client_metadata, + response_mode, )); const mapping = new Map(); @@ -217,7 +227,7 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { const S = await this.openID4VPRelyingPartyStateRepository.retrieve(); console.log("send AuthorizationResponse: S = ", S) console.log("send AuthorizationResponse: Sess = ", sessionStorage.getItem('last_used_nonce')); - if (S?.nonce == "" || (sessionStorage.getItem('last_used_nonce') && S.nonce == sessionStorage.getItem('last_used_nonce'))) { + if (S?.nonce === "" || (sessionStorage.getItem('last_used_nonce') && S.nonce === sessionStorage.getItem('last_used_nonce'))) { console.info("OID4VP: Non existent flow"); return {}; } @@ -382,7 +392,7 @@ export class OpenID4VPRelyingParty implements IOpenID4VPRelyingParty { const formData = new URLSearchParams(); - if (S.client_metadata.authorization_encrypted_response_alg && S.client_metadata.jwks.keys.length > 0) { + if (S.response_mode === ResponseMode.DIRECT_POST_JWT && S.client_metadata.authorization_encrypted_response_alg && S.client_metadata.jwks.keys.length > 0) { const rp_eph_pub_jwk = S.client_metadata.jwks.keys[0]; const rp_eph_pub = await importJWK(rp_eph_pub_jwk, S.client_metadata.authorization_encrypted_response_alg); const jwe = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify({ diff --git a/src/lib/types/OpenID4VPRelyingPartyState.ts b/src/lib/types/OpenID4VPRelyingPartyState.ts index c3f95389..98721b82 100644 --- a/src/lib/types/OpenID4VPRelyingPartyState.ts +++ b/src/lib/types/OpenID4VPRelyingPartyState.ts @@ -1,6 +1,13 @@ import { JWK } from "jose"; import { PresentationDefinitionType } from "./presentationDefinition.type"; +import * as z from 'zod'; +export enum ResponseMode { + DIRECT_POST = 'direct_post', + DIRECT_POST_JWT = 'direct_post.jwt', +} + +export const ResponseModeSchema = z.nativeEnum(ResponseMode); type ClientMetadata = { jwks?: { keys: JWK[] }, @@ -19,7 +26,8 @@ export class OpenID4VPRelyingPartyState { public response_uri: string, public client_id: string, public state: string, - public client_metadata: ClientMetadata + public client_metadata: ClientMetadata, + public response_mode: ResponseMode, ) { } public serialize(): string { @@ -30,11 +38,12 @@ export class OpenID4VPRelyingPartyState { client_id: this.client_id, state: this.state, client_metadata: this.client_metadata, + response_mode: this.response_mode, }); } public static deserialize(storedValue: string): OpenID4VPRelyingPartyState { - const { presentation_definition, nonce, response_uri, client_id, state, client_metadata } = JSON.parse(storedValue) as OpenID4VPRelyingPartyState; - return new OpenID4VPRelyingPartyState(presentation_definition, nonce, response_uri, client_id, state, client_metadata); + const { presentation_definition, nonce, response_uri, client_id, state, client_metadata, response_mode } = JSON.parse(storedValue) as OpenID4VPRelyingPartyState; + return new OpenID4VPRelyingPartyState(presentation_definition, nonce, response_uri, client_id, state, client_metadata, response_mode); } } diff --git a/src/lib/utils/mdocPIDParser.ts b/src/lib/utils/mdocPIDParser.ts index 949b69ee..94bc2634 100644 --- a/src/lib/utils/mdocPIDParser.ts +++ b/src/lib/utils/mdocPIDParser.ts @@ -7,7 +7,7 @@ import defaulCredentialImage from "../../assets/images/cred.png"; export const deviceResponseParser: ICredentialParser = { - parse: async function (rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; } | { credentialImageSvgTemplateURL: string; }; beautifiedForm: any; } | { error: string; }> { + parse: async function (rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; }; beautifiedForm: any; } | { error: string }> { if (typeof rawCredential != 'string') { return { error: "Not for this parser" }; @@ -35,7 +35,7 @@ export const deviceResponseParser: ICredentialParser = { } export const mdocPIDParser: ICredentialParser = { - parse: async function (rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; } | { credentialImageSvgTemplateURL: string; }; beautifiedForm: any; } | { error: string; }> { + parse: async function (rawCredential: object | string, presentationDefinitionFilter?: PresentationDefinitionType): Promise<{ credentialFriendlyName: string; credentialImage: { credentialImageURL: string; }; beautifiedForm: any; } | { error: string }> { console.log("Raw cred = ", rawCredential) if (typeof rawCredential != 'string') { return { error: "Not for this parser" }; diff --git a/src/pages/Home/Home.js b/src/pages/Home/Home.js index b836b12f..19d3e267 100644 --- a/src/pages/Home/Home.js +++ b/src/pages/Home/Home.js @@ -1,5 +1,5 @@ // External libraries -import React, { useState, useContext, useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -10,6 +10,7 @@ import CredentialsContext from '../../context/CredentialsContext'; // Hooks import useFetchPresentations from '../../hooks/useFetchPresentations'; import useScreenType from '../../hooks/useScreenType'; +import { useSessionStorage } from '../../hooks/useStorage'; // Components import { H1 } from '../../components/Shared/Heading'; @@ -22,7 +23,7 @@ const Home = () => { const { vcEntityList, latestCredentials, getData } = useContext(CredentialsContext); const { api } = useContext(SessionContext); const history = useFetchPresentations(api); - const [currentSlide, setCurrentSlide] = useState(1); + const [currentSlide, setCurrentSlide,] = api.useClearOnClearSession(useSessionStorage('currentSlide', 1)); const screenType = useScreenType(); const navigate = useNavigate(); @@ -73,6 +74,7 @@ const Home = () => { setCurrentSlide(currentIndex + 1)} /> diff --git a/src/pages/NotFound/NotFound.js b/src/pages/NotFound/NotFound.js index d94cf4e2..17993b08 100644 --- a/src/pages/NotFound/NotFound.js +++ b/src/pages/NotFound/NotFound.js @@ -15,7 +15,7 @@ const NotFound = () => { return (
-
+
logo diff --git a/src/services/LocalStorageKeystore.ts b/src/services/LocalStorageKeystore.ts index 6f5b6346..bddac1b3 100644 --- a/src/services/LocalStorageKeystore.ts +++ b/src/services/LocalStorageKeystore.ts @@ -68,8 +68,8 @@ export interface LocalStorageKeystore { getUserHandleB64u(): string | null, signJwtPresentation(nonce: string, audience: string, verifiableCredentials: any[]): Promise<{ vpjwt: string }>, - generateOpenid4vciProof(nonce: string, audience: string, issuer: string): Promise<[ - { proof_jwt: string }, + generateOpenid4vciProofs(requests: { nonce: string, audience: string, issuer: string }[]): Promise<[ + { proof_jwts: string[] }, AsymmetricEncryptedContainer, CommitCallback, ]>, @@ -372,20 +372,27 @@ export function useLocalStorageKeystore(): LocalStorageKeystore { await keystore.signJwtPresentation(await openPrivateData(), nonce, audience, verifiableCredentials) ), - generateOpenid4vciProof: async (nonce: string, audience: string, issuer: string): Promise<[ - { proof_jwt: string }, + generateOpenid4vciProofs: async (requests: { nonce: string, audience: string, issuer: string }[]): Promise<[ + { proof_jwts: string[] }, AsymmetricEncryptedContainer, CommitCallback, ]> => ( - await editPrivateData(async (container) => - await keystore.generateOpenid4vciProof( - container, - config.DID_KEY_VERSION, - nonce, - audience, - issuer - ), - ) + await editPrivateData(async (originalContainer) => { + let container = originalContainer; + let proof_jwts = []; + for (const { nonce, audience, issuer } of requests) { + const [{ proof_jwt }, newContainer] = await keystore.generateOpenid4vciProof( + container, + config.DID_KEY_VERSION, + nonce, + audience, + issuer + ); + proof_jwts.push(proof_jwt); + container = newContainer; + } + return [{ proof_jwts }, container]; + }) ), generateDeviceResponse: async (mdocCredential: MDoc, presentationDefinition: any, mdocGeneratedNonce: string, verifierGeneratedNonce: string, clientId: string, responseUri: string): Promise<{ deviceResponseMDoc: MDoc }> => ( diff --git a/src/services/SigningRequestHandlers.ts b/src/services/SigningRequestHandlers.ts index 73de27ad..bd644b7c 100644 --- a/src/services/SigningRequestHandlers.ts +++ b/src/services/SigningRequestHandlers.ts @@ -25,7 +25,7 @@ export function SigningRequestHandlerService(): SigningRequestHandlers { }, handleGenerateOpenid4vciProofSigningRequest: async (api: BackendApi, socket, keystore, { message_id, audience, nonce, issuer }) => { - const [{ proof_jwt }, newPrivateData, keystoreCommit] = await keystore.generateOpenid4vciProof(nonce, audience, issuer) + const [{ proof_jwts: [proof_jwt] }, newPrivateData, keystoreCommit] = await keystore.generateOpenid4vciProofs([{ nonce, audience, issuer }]) await api.updatePrivateData(newPrivateData); await keystoreCommit(); console.log("proof jwt = ", proof_jwt);