diff --git a/documentation/docs/mock-apps/verify-app.md b/documentation/docs/mock-apps/verify-app.md index 85cd6e72..f514e92f 100644 --- a/documentation/docs/mock-apps/verify-app.md +++ b/documentation/docs/mock-apps/verify-app.md @@ -28,4 +28,9 @@ sequenceDiagram VS-->>V: Return Verification Result V->>V: Render Verified Credential V->>U: Display Verification Result and Credential -``` \ No newline at end of file +``` + +## Rendering Verified Credential +The UI of this page includes these information fields: Type, Issued by and Issue date. Besides, the page also contains the tab panel for HTML template, JSON data and the download button. + +With the download button, it will download the JSON data or JWT data if the credential is JWT. diff --git a/package.json b/package.json index f05ef70f..fc45ed0c 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ }, "resolutions": { "@types/eslint": "^8.4.6", - "strip-ansi": "6.0.0" + "strip-ansi": "6.0.0", + "string-width": "4.0.0" }, "engines": { "node": ">= 20.12.2" diff --git a/packages/mock-app/src/__tests__/CredentialTabs.test.tsx b/packages/mock-app/src/__tests__/CredentialTabs.test.tsx index 7ee90573..c38813ce 100644 --- a/packages/mock-app/src/__tests__/CredentialTabs.test.tsx +++ b/packages/mock-app/src/__tests__/CredentialTabs.test.tsx @@ -65,8 +65,8 @@ describe('Credential tabs content', () => { }; // Render the CredentialTabs component with the modified credential render(); - // Expecting the text 'CredentialRender' to be present in the rendered component - expect(screen.getByText('CredentialRender')).not.toBeNull(); + // Expecting the text 'Rendered' to be present in the rendered component + expect(screen.getByText('Rendered')).not.toBeNull(); }); it('should display on change value', () => { @@ -78,4 +78,24 @@ describe('Credential tabs content', () => { // Expecting the tab with 'selected' attribute to have accessible name 'Rendered' expect(screen.getByRole('tab', { selected: true })).toHaveAccessibleName('Rendered'); }); + + it('should display download button', () => { + // Mocking the URL.createObjectURL function + const mockCreateObjectURL = jest.fn(); + global.URL.createObjectURL = mockCreateObjectURL; + + // Render component with the mock credential + render(); + + // Find the button with text 'Download', simulate a click event on it + const button = screen.getByText(/Download/i); + button.click(); + + // Expecting the button with text 'Download' to be present in the rendered component + expect(screen.getByText(/Download/i)).not.toBeNull(); + // Expecting the URL.createObjectURL function to have been called + expect(mockCreateObjectURL).toHaveBeenCalled(); + // Restore the original URL.createObjectURL to avoid side effects on other tests + global.URL.createObjectURL = mockCreateObjectURL; + }); }); diff --git a/packages/mock-app/src/__tests__/JsonBlock.test.tsx b/packages/mock-app/src/__tests__/JsonBlock.test.tsx index 3ded5f14..dd923f7c 100644 --- a/packages/mock-app/src/__tests__/JsonBlock.test.tsx +++ b/packages/mock-app/src/__tests__/JsonBlock.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import { JsonBlock } from '../components/JsonBlock'; @@ -40,23 +39,4 @@ describe('Json block content', () => { // Expecting the text 'VerifiableCredential' to be present in the rendered component expect(screen.getByText(/VerifiableCredential/i)).not.toBeNull(); }); - - it('should download credential when click on button Download', () => { - // Mocking the URL.createObjectURL function - const mockCreateObjectURL = jest.fn(); - global.URL.createObjectURL = mockCreateObjectURL; - - // Render the JsonBlock component with the mock credential - render(); - // Find the button with text 'Download', simulate a click event on it - const button = screen.getByText(/Download/i); - button.click(); - - // Expecting the button with text 'Download' to be present in the rendered component - expect(screen.getByText(/Download/i)).not.toBeNull(); - // Expecting the URL.createObjectURL function to have been called - expect(mockCreateObjectURL).toHaveBeenCalled(); - // Restore the original URL.createObjectURL to avoid side effects on other tests - global.URL.createObjectURL = mockCreateObjectURL; - }); }); diff --git a/packages/mock-app/src/components/Credential/Credential.tsx b/packages/mock-app/src/components/Credential/Credential.tsx index 14bc140f..0192da8a 100644 --- a/packages/mock-app/src/components/Credential/Credential.tsx +++ b/packages/mock-app/src/components/Credential/Credential.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Box } from '@mui/material'; -import { VerifiableCredential } from '@vckit/core-types'; import { CredentialInfo } from '../CredentialInfo'; import { CredentialTabs } from '../CredentialTabs'; +import { CredentialComponentProps } from '../../types/common.types'; -const Credential = ({ credential }: { credential: VerifiableCredential }) => { +const Credential = ({ credential, decodedEnvelopedVC }: CredentialComponentProps) => { return ( { width: '100%', }} > - - - + + ); }; diff --git a/packages/mock-app/src/components/CredentialInfo/CredentialInfo.tsx b/packages/mock-app/src/components/CredentialInfo/CredentialInfo.tsx index 8a43cd8c..89804942 100644 --- a/packages/mock-app/src/components/CredentialInfo/CredentialInfo.tsx +++ b/packages/mock-app/src/components/CredentialInfo/CredentialInfo.tsx @@ -1,16 +1,16 @@ import React, { useMemo } from 'react'; import moment from 'moment'; import { List, ListItem, ListItemText } from '@mui/material'; -import { IssuerType, VerifiableCredential } from '@vckit/core-types'; +import { IssuerType, UnsignedCredential, VerifiableCredential } from '@vckit/core-types'; -const CredentialInfo = ({ credential }: { credential: VerifiableCredential }) => { +const CredentialInfo = ({ credential }: { credential: VerifiableCredential | UnsignedCredential }) => { const credentialType = useMemo(() => { if (typeof credential.type === 'string') { return credential.type; } const types = credential?.type as string[]; - const type = types.find((item) => item !== 'VerifiableCredential'); + const type = types?.find((item) => item !== 'VerifiableCredential'); if (type) { return type; } diff --git a/packages/mock-app/src/components/CredentialRender/CredentialRender.tsx b/packages/mock-app/src/components/CredentialRender/CredentialRender.tsx index e9c483b1..4765d764 100644 --- a/packages/mock-app/src/components/CredentialRender/CredentialRender.tsx +++ b/packages/mock-app/src/components/CredentialRender/CredentialRender.tsx @@ -1,13 +1,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Renderer, WebRenderingTemplate2022 } from '@vckit/renderer'; -import { VerifiableCredential } from '@vckit/core-types'; +import { UnsignedCredential, VerifiableCredential } from '@vckit/core-types'; import { Box, CircularProgress } from '@mui/material'; import { convertBase64ToString } from '../../utils'; /** * CredentialRender component is used to render the credential */ -const CredentialRender = ({ credential }: { credential: VerifiableCredential }) => { +const CredentialRender = ({ credential }: { credential: VerifiableCredential | UnsignedCredential }) => { const [documents, setDocuments] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/packages/mock-app/src/components/CredentialTabs/CredentialTabs.tsx b/packages/mock-app/src/components/CredentialTabs/CredentialTabs.tsx index 1e4f4af5..d97ecd21 100644 --- a/packages/mock-app/src/components/CredentialTabs/CredentialTabs.tsx +++ b/packages/mock-app/src/components/CredentialTabs/CredentialTabs.tsx @@ -1,29 +1,33 @@ import React, { useEffect } from 'react'; -import { Box, Tab, Tabs } from '@mui/material'; -import { VerifiableCredential } from '@vckit/core-types'; +import { Box, Tab, Tabs, useMediaQuery, useTheme } from '@mui/material'; + import CredentialRender from '../CredentialRender/CredentialRender'; import { JsonBlock } from '../JsonBlock'; +import { CredentialComponentProps } from '../../types/common.types'; +import { DownloadCredentialButton } from '../DownloadCredentialButton/DownloadCredentialButton'; -const CredentialTabs = ({ credential }: { credential: VerifiableCredential }) => { +const CredentialTabs = ({ credential, decodedEnvelopedVC }: CredentialComponentProps) => { const credentialTabs = [ { label: 'Rendered', - children: , + children: , }, { label: 'JSON', - children: , + children: , }, ]; const [currentTabIndex, setCurrentTabIndex] = React.useState(0); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); useEffect(() => { configDefaultTabs(); }, [credential]); const configDefaultTabs = () => { - if (credential?.render?.[0]?.template) { + if (decodedEnvelopedVC?.render?.[0]?.template) { return setCurrentTabIndex(0); } @@ -43,12 +47,44 @@ const CredentialTabs = ({ credential }: { credential: VerifiableCredential }) => return ( - - {credentialTabs?.map((item, index) => )} - + {/* Header Row */} + + {/* Tabs aligned to the left */} + + {credentialTabs.map((item, index) => ( + + ))} + + + {/* Download Button */} + + - {credentialTabs?.map((item, index) => ( - + {/* Tab Panels */} + {credentialTabs.map((item, index) => ( + + {item.children} + ))} ); diff --git a/packages/mock-app/src/components/DownloadCredentialButton/DownloadCredentialButton.tsx b/packages/mock-app/src/components/DownloadCredentialButton/DownloadCredentialButton.tsx new file mode 100644 index 00000000..b45d0371 --- /dev/null +++ b/packages/mock-app/src/components/DownloadCredentialButton/DownloadCredentialButton.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import CloudDownloadOutlinedIcon from '@mui/icons-material/CloudDownloadOutlined'; +import { Button, IconButton, useMediaQuery, useTheme } from '@mui/material'; +import { UnsignedCredential, VerifiableCredential } from '@vckit/core-types'; + +export const DownloadCredentialButton = ({ credential }: { credential: VerifiableCredential | UnsignedCredential }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + /** + * handle click on download button + */ + const handleClickDownloadVC = async () => { + const element = document.createElement('a'); + const file = new Blob([JSON.stringify({ verifiableCredential: credential }, null, 2)], { + type: 'text/plain', + }); + element.href = URL.createObjectURL(file); + element.download = 'vc.json'; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + }; + + return ( + <> + {isMobile ? ( + + + + ) : ( + }> + Download + + )} + > + ); +}; diff --git a/packages/mock-app/src/components/DownloadCredentialButton/index.ts b/packages/mock-app/src/components/DownloadCredentialButton/index.ts new file mode 100644 index 00000000..692b147b --- /dev/null +++ b/packages/mock-app/src/components/DownloadCredentialButton/index.ts @@ -0,0 +1 @@ +export * from './DownloadCredentialButton'; diff --git a/packages/mock-app/src/components/JsonBlock/JsonBlock.tsx b/packages/mock-app/src/components/JsonBlock/JsonBlock.tsx index d67d94da..495ae842 100644 --- a/packages/mock-app/src/components/JsonBlock/JsonBlock.tsx +++ b/packages/mock-app/src/components/JsonBlock/JsonBlock.tsx @@ -1,28 +1,11 @@ import React from 'react'; -import { Button, Card, CardContent } from '@mui/material'; -import { VerifiableCredential } from '@vckit/core-types'; - -const JsonBlock = ({ credential }: { credential: VerifiableCredential }) => { - /** - * handle click on download button - */ - const handleClickDownloadVC = async () => { - const element = document.createElement('a'); - const file = new Blob([JSON.stringify(credential, null, 2)], { - type: 'text/plain', - }); - element.href = URL.createObjectURL(file); - element.download = 'vc.json'; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); - }; +import { Card, CardContent } from '@mui/material'; +import { UnsignedCredential, VerifiableCredential } from '@vckit/core-types'; +const JsonBlock = ({ credential }: { credential: VerifiableCredential | UnsignedCredential }) => { return ( <> - - Download - {JSON.stringify(credential, null, 2)} diff --git a/packages/mock-app/src/pages/Verify.tsx b/packages/mock-app/src/pages/Verify.tsx index 9868066e..f7784757 100644 --- a/packages/mock-app/src/pages/Verify.tsx +++ b/packages/mock-app/src/pages/Verify.tsx @@ -156,7 +156,7 @@ const Verify = () => { return ( - + ); default: diff --git a/packages/mock-app/src/types/common.types.ts b/packages/mock-app/src/types/common.types.ts index 332b1926..a203a514 100644 --- a/packages/mock-app/src/types/common.types.ts +++ b/packages/mock-app/src/types/common.types.ts @@ -1,3 +1,4 @@ +import { UnsignedCredential, VerifiableCredential } from '@vckit/core-types'; import { IGenericFeatureProps } from '../components/GenericFeature'; export interface IFeature extends IGenericFeatureProps { @@ -27,3 +28,8 @@ export interface IStyles { tertiaryColor: string; menuIconColor?: string; } + +export interface CredentialComponentProps { + credential: VerifiableCredential; + decodedEnvelopedVC?: UnsignedCredential | null; +} diff --git a/yarn.lock b/yarn.lock index ec3d56e8..700cc326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4805,9 +4805,9 @@ "@types/node" "*" "@types/node@*": - version "22.8.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.1.tgz#b39d4b98165e2ae792ce213f610c7c6108ccfa16" - integrity sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg== + version "22.8.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.2.tgz#8e82bb8201c0caf751dcdc61b0a262d2002d438b" + integrity sha512-NzaRNFV+FZkvK/KLCsNdTvID0SThyrs5SHB6tsD/lajr22FGC73N2QeDPM2wHtVde8mgcXuSsHQkH5cX1pbPLw== dependencies: undici-types "~6.19.8" @@ -4824,9 +4824,9 @@ integrity sha512-NF5ajYn+dq0tRfswdyp8Df75h7D9z+L8TCIwrXoh46ZLK6KZVXkRhf/luXaZytvm/keUo9vU4m1Bg39St91a5w== "@types/node@^18.0.0": - version "18.19.59" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.59.tgz#2de1b95b0b468089b616b2feb809755d70a74949" - integrity sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ== + version "18.19.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.60.tgz#3fca49e78e78588ab873af85e2bc2bbb9db8cdc4" + integrity sha512-cYRj7igVqgxhlHFdBHHpU2SNw3+dN2x0VTZJtLYk6y/ieuGN4XiBgtDjYVktM/yk2y/8pKMileNc6IoEzEJnUw== dependencies: undici-types "~5.26.4" @@ -5388,7 +5388,7 @@ acorn@^7.1.1, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.1, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.1, acorn@^8.14.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -6546,9 +6546,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001673" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz#5aa291557af1c71340e809987367410aab7a5a9e" - integrity sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw== + version "1.0.30001674" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz#eb200a716c3e796d33d30b9c8890517a72f862c8" + integrity sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw== canonicalize@^1.0.1: version "1.0.8" @@ -8078,11 +8078,6 @@ duplexify@^3.5.0, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -8096,9 +8091,9 @@ ejs@^3.1.6, ejs@^3.1.7, ejs@^3.1.8: jake "^10.8.5" electron-to-chromium@^1.5.41: - version "1.5.47" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7" - integrity sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ== + version "1.5.49" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz#9358f514ab6eeed809a8689f4b39ea5114ae729c" + integrity sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A== elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.5.5: version "6.6.0" @@ -16337,23 +16332,14 @@ string-natural-compare@^3.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +string-width@4.0.0, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.1, string-width@^5.1.2: + version "4.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.0.0.tgz#44fe19da01e4e52ba868ef6734207f5bbe11be51" + integrity sha512-r6JqKDbluBXG8TvRNZNA7ZNQgDc1q1Uw5Po/fHQMiDfQNxIdBGkiJ3sE0HfdTaOmKjh7kVCOjL9pNKiowqtIHw== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" + strip-ansi "^5.1.0" string.prototype.includes@^2.0.1: version "2.0.1" @@ -16448,7 +16434,7 @@ stringify-object@^3.3.0: dependencies: ansi-regex "^5.0.1" -strip-ansi@6.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +strip-ansi@6.0.0, strip-ansi@^5.1.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== @@ -17326,11 +17312,11 @@ unpipe@1.0.0, unpipe@~1.0.0: integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== unplugin@^1.3.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.14.1.tgz#c76d6155a661e43e6a897bce6b767a1ecc344c1a" - integrity sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w== + version "1.15.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.15.0.tgz#cd1e92e537ab14a03354d6f83f29d536fac2e5a9" + integrity sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA== dependencies: - acorn "^8.12.1" + acorn "^8.14.0" webpack-virtual-modules "^0.6.2" unquote@~1.1.1:
{JSON.stringify(credential, null, 2)}