diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/AddressDisplay.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/AddressDisplay.tsx new file mode 100644 index 000000000..a2f96279a --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/AddressDisplay.tsx @@ -0,0 +1,38 @@ +import { formatAddress } from '@/utils/format'; +import { copyToClipboard } from '@/utils/string'; +import { DocumentDuplicateIcon } from '@heroicons/react/24/outline'; +import { Tooltip } from '@nextui-org/react'; +import { useTranslations } from 'next-intl'; + +export const AddressDisplay = ({ address }: { address: string }) => { + const t = useTranslations('AddressDisplay'); + return ( +
+

+ {t('title')}: +

+
+ + + {formatAddress(address)} + + + +
+
+ ); +}; diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx index 27d58ee3a..61765c331 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/CredentialPanel.tsx @@ -2,135 +2,28 @@ import { CheckCircleIcon, - DocumentDuplicateIcon, ExclamationCircleIcon, } from '@heroicons/react/24/outline'; import { Tooltip } from '@nextui-org/react'; import type { VerifiableCredential } from '@veramo/core'; -import clsx from 'clsx'; import { useTranslations } from 'next-intl'; import { usePathname, useRouter } from 'next/navigation'; -import { Fragment, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; -import { DIDDisplay } from '@/components/DIDDisplay'; import JsonModal from '@/components/JsonModal'; -import { formatAddress, getFirstWord } from '@/utils/format'; -import { convertTypes, copyToClipboard } from '@/utils/string'; -import { ImageLink } from '@/components/ImageLink'; +import { getFirstWord } from '@/utils/format'; +import { convertTypes } from '@/utils/string'; +import { Normal } from './templates/Normal'; +import { EduCTX } from './templates/EduCTX'; interface FormattedPanelProps { credential: VerifiableCredential; } -const AddressDisplay = ({ address }: { address: string }) => { - const t = useTranslations('AddressDisplay'); - return ( -
-

- {t('title')}: -

-
- - - {formatAddress(address)} - - - -
-
- ); -}; - -const DisplayDate = ({ text, date }: { text: string; date: string }) => ( -
-

- {text}: -

-

- {new Date(Date.parse(date)).toDateString()} -

-
-); - -const CredentialSubject = ({ - data, - viewJsonText, - selectJsonData, -}: { - data: Record; - viewJsonText: string; - selectJsonData: React.Dispatch>; -}) => ( - <> - {Object.entries(data).map(([key, value]: [string, any]) => { - if (value === null || value === '') return null; - return ( - - {(() => { - if (key === 'id') { - return ( - <> -
-
- -
-
- - ); - } - - if (key === 'address') return ; - if (key === 'image') return ; - - const isObject = !( - typeof value === 'string' || typeof value === 'number' - ); - key = key.replace(/([A-Z])/g, ' $1').trim(); - return ( -
-

- {key}: -

-
- {isObject ? ( - - ) : ( - value - )} -
-
- ); - })()} -
- ); - })} - -); +enum Templates { + Normal = 0, + EduCTX = 1, +} const CredentialPanel = ({ credential }: FormattedPanelProps) => { const t = useTranslations('CredentialPanel'); @@ -153,6 +46,47 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => { setJsonModalOpen(true); }; + const template = useMemo(() => { + const credentialTypes = Array.isArray(credential.type) + ? credential.type + : [credential.type]; + + if (credentialTypes.includes('EducationCredential')) { + return Templates.EduCTX; + } + + return Templates.Normal; + }, [credential]); + + const renderTemplate = useMemo(() => { + switch (template) { + case Templates.EduCTX: + return ( + + ); + default: + return ( + + ); + } + }, [credential, template]); + return ( <>
@@ -193,53 +127,7 @@ const CredentialPanel = ({ credential }: FormattedPanelProps) => {
-
-
-

- {t('subject')} -

- -
-
-
-
-

- {t('issuer')} -

-
-
- -
-
-
-
-

- {t('dates')} -

- - {credential.expirationDate && ( - - )} -
-
-
-
+ {renderTemplate}
{ diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayDate.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayDate.tsx new file mode 100644 index 000000000..3ef25789d --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayDate.tsx @@ -0,0 +1,13 @@ +export const DisplayDate = ({ text, date }: { text: string; date: string }) => { + const parsed = Date.parse(date); + return ( +
+

+ {text}: +

+

+ {!Number.isNaN(parsed) ? new Date(parsed).toLocaleDateString() : date} +

+
+ ); +}; diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayText.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayText.tsx new file mode 100644 index 000000000..0a9a7a4c3 --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/DisplayText.tsx @@ -0,0 +1,25 @@ +import { Tooltip } from '@nextui-org/react'; + +export const DisplayText = ({ + text, + value, + tooltip, +}: { text: string; value: string; tooltip?: string }) => ( +
+

+ {text}: +

+ {tooltip ? ( + +

+ {value} +

+
+ ) : ( +

{value}

+ )} +
+); diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/FormattedView.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/FormattedView.tsx index 354265443..296e99985 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/FormattedView.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/FormattedView.tsx @@ -7,46 +7,91 @@ import { InformationCircleIcon, } from '@heroicons/react/24/outline'; import { Pagination, Tooltip } from '@nextui-org/react'; -import type { IVerifyResult, VerifiableCredential } from '@veramo/core'; +import type { + VerifiableCredential, + VerifiablePresentation, +} from '@veramo/core'; import { useTranslations } from 'next-intl'; import { usePathname, useRouter } from 'next/navigation'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { VerificationInfoModal } from '@/components/VerificationInfoModal'; import { copyToClipboard } from '@/utils/string'; import CredentialPanel from './CredentialPanel'; import { formatDid } from '@/utils/format'; +import { + type VerificationResult, + VerificationService, +} from '@blockchain-lab-um/extended-verification'; +import { isError } from '@blockchain-lab-um/masca-connector'; +import { useToastStore } from '@/stores'; + +const verifyPresentation = async (presentation: VerifiablePresentation) => { + await VerificationService.init(); + + const verifiedResult = await VerificationService.verify(presentation); + + if (isError(verifiedResult)) { + console.error('Failed to verify presentation'); + setTimeout(() => { + useToastStore.setState({ + open: true, + title: 'Failed to verify presentation', + type: 'error', + loading: false, + link: null, + }); + }, 200); + return { verified: false } as VerificationResult; + } -export const FormattedView = ({ + return verifiedResult.data; +}; + +export const FormattedView = async ({ credential, - holder, - expirationDate, - issuanceDate, + presentation, page, total, - verificationResult, }: { credential: VerifiableCredential; - holder: string; - expirationDate: string | undefined; - issuanceDate: string | undefined; + presentation: VerifiablePresentation; page: string; total: number; - verificationResult: IVerifyResult; }) => { const t = useTranslations('FormattedView'); + const router = useRouter(); + const pathname = usePathname(); + + const { holder, expirationDate, issuanceDate } = presentation; + const [verificationInfoModalOpen, setVerificationInfoModalOpen] = useState(false); - const router = useRouter(); - const pathname = usePathname(); + const [verificationResult, setVerificationResult] = + useState(null); const isValid = useMemo(() => { if (!expirationDate) return true; return Date.parse(expirationDate) > Date.now(); }, [expirationDate]); + useEffect(() => { + verifyPresentation(presentation) + .then((result) => setVerificationResult(result)) + .catch((error) => { + console.error(error); + useToastStore.setState({ + open: true, + title: t('verify-failed'), + type: 'error', + loading: false, + link: null, + }); + }); + }, [presentation]); + return ( <>
diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx index aec4abff8..8173fbafe 100644 --- a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/page.tsx @@ -1,9 +1,7 @@ -import type { VerifiablePresentation } from '@veramo/core'; import { decodeCredentialToObject } from '@veramo/utils'; import { normalizeCredential } from 'did-jwt-vc'; import { notFound } from 'next/navigation'; -import { getAgent } from '@/app/api/veramoSetup'; import JsonPanel from '@/components/CredentialDisplay/JsonPanel'; import { convertTypes } from '@/utils/string'; import { FormattedView } from './FormattedView'; @@ -12,16 +10,6 @@ import { NormalViewButton } from './NormalViewButton'; export const revalidate = 0; -const verifyPresentation = async (presentation: VerifiablePresentation) => { - const agent = await getAgent(); - - const result = await agent.verifyPresentation({ - presentation, - }); - - return result; -}; - export default async function Page({ params: { id }, searchParams, @@ -48,20 +36,15 @@ export default async function Page({ const page = searchParams.page ?? '1'; const view = searchParams.view ?? 'Normal'; - const verificationResult = await verifyPresentation(presentation); - return (
{view === 'Normal' && ( )} {view === 'Json' && ( diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/EduCTX.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/EduCTX.tsx new file mode 100644 index 000000000..6e2a83643 --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/EduCTX.tsx @@ -0,0 +1,97 @@ +import type { VerifiableCredential } from '@veramo/core'; +import { DisplayDate } from '../DisplayDate'; +import { DisplayText } from '../DisplayText'; +import { DIDDisplay } from '@/components/DIDDisplay'; + +const CredentialSubject = ({ + data, +}: { + data: Record; +}) => ( + <> + + + + + + +); + +type EduCTXProps = { + credential: VerifiableCredential; + title: { + subject: string; + issuer: string; + dates: string; + }; +}; + +export const EduCTX = ({ credential, title }: EduCTXProps) => { + return ( +
+
+

+ {title.subject} +

+ +
+
+
+
+

+ {title.issuer} +

+ +
+
+ +
+
+
+
+

+ {title.dates} +

+ + + {credential.expirationDate && ( + + )} +
+
+
+
+ ); +}; diff --git a/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/Normal.tsx b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/Normal.tsx new file mode 100644 index 000000000..b245490e8 --- /dev/null +++ b/packages/dapp/src/app/[locale]/app/(public)/share-presentation/[id]/templates/Normal.tsx @@ -0,0 +1,138 @@ +import { DIDDisplay } from '@/components/DIDDisplay'; +import { Fragment } from 'react'; +import { AddressDisplay } from '../AddressDisplay'; +import { ImageLink } from '@/components/ImageLink'; +import clsx from 'clsx'; +import type { VerifiableCredential } from '@veramo/core'; +import { DisplayDate } from '../DisplayDate'; + +const CredentialSubject = ({ + data, + viewJsonText, + selectJsonData, +}: { + data: Record; + viewJsonText: string; + selectJsonData: React.Dispatch>; +}) => ( + <> + {Object.entries(data).map(([key, value]: [string, any]) => { + if (value === null || value === '') return null; + return ( + + {(() => { + if (key === 'id') { + return ( + <> +
+
+ +
+
+ + ); + } + + if (key === 'address') return ; + if (key === 'image') return ; + + const isObject = !( + typeof value === 'string' || typeof value === 'number' + ); + key = key.replace(/([A-Z])/g, ' $1').trim(); + return ( +
+

+ {key}: +

+
+ {isObject ? ( + + ) : ( + value + )} +
+
+ ); + })()} +
+ ); + })} + +); + +type NormalProps = { + credential: VerifiableCredential; + title: { + subject: string; + issuer: string; + dates: string; + }; + viewJsonText: string; + selectJsonData: (data: any) => void; +}; + +export const Normal = ({ + credential, + title, + viewJsonText, + selectJsonData, +}: NormalProps) => { + return ( +
+
+

+ {title.subject} +

+ +
+
+
+
+

+ {title.issuer} +

+
+
+ +
+
+
+
+

+ {title.dates} +

+ + {credential.expirationDate && ( + + )} +
+
+
+
+ ); +}; diff --git a/packages/dapp/src/messages/en.json b/packages/dapp/src/messages/en.json index 335a45b84..6d37707da 100644 --- a/packages/dapp/src/messages/en.json +++ b/packages/dapp/src/messages/en.json @@ -282,15 +282,15 @@ "invalid": "Invalid", "presentation-status": "Status", "presentation-valid": "Presentation is valid", - "presentation-invalid": "Presentation is invalid" + "presentation-invalid": "Presentation is invalid", + "verify-failed": "Failed to verify presentation" }, "CredentialPanel": { "title": "Credential", "status": "Status", "dates": "DATES", - "expiration-date": "Expiration Date", "issuer": "ISSUER", - "isuance-date": "Issuance Date", + "issuance-date": "Issuance Date", "subject": "SUBJECT", "view-json": "View JSON", "credential-valid": "Credential is valid",