Skip to content

Commit

Permalink
feat: update download jwt button verify page (#116)
Browse files Browse the repository at this point in the history
* feat: update download JWT button in Verify page

* test: add test for json block

* ci: fix build pipeline

* ci: cp app-config.json when build

* refactor: check vc response from api in verify page

* refactor: response of issuing VC api

* refactor: change download button on verify page following the design

* docs: update document for rendering verified credential page

* test: update unit test in JsonBlock and CredentialTabs

* ci: update yarn lock

* refactor: pass decode jwt in verify page as props for json block and credential tabs

* refactor: change name of the props from  decodeCredential to decodedEnvelopedVC

* test: update test in CredentialTabs and JsonBlock

* refactor: remove unused code

* fix: resolve conflict

* refactor: remove unused code

* ci: update yarn lock to retry pipeline

* chore: adjust yarn lock

Signed-off-by: Nam Hoang <[email protected]>

* refactor: change code in download button of verify page

---------

Signed-off-by: Nam Hoang <[email protected]>
Co-authored-by: Nam Hoang <[email protected]>
  • Loading branch information
ldhyen99 and namhoang1604 authored Nov 1, 2024
1 parent c5c9d74 commit 962e099
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 103 deletions.
7 changes: 6 additions & 1 deletion documentation/docs/mock-apps/verify-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ sequenceDiagram
VS-->>V: Return Verification Result
V->>V: Render Verified Credential
V->>U: Display Verification Result and Credential
```
```

## 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.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 22 additions & 2 deletions packages/mock-app/src/__tests__/CredentialTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ describe('Credential tabs content', () => {
};
// Render the CredentialTabs component with the modified credential
render(<CredentialTabs credential={credential2} />);
// 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', () => {
Expand All @@ -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(<CredentialTabs credential={credential} />);

// 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;
});
});
20 changes: 0 additions & 20 deletions packages/mock-app/src/__tests__/JsonBlock.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { JsonBlock } from '../components/JsonBlock';

Expand Down Expand Up @@ -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(<JsonBlock credential={credential} />);
// 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;
});
});
9 changes: 4 additions & 5 deletions packages/mock-app/src/components/Credential/Credential.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box
sx={{
Expand All @@ -15,9 +15,8 @@ const Credential = ({ credential }: { credential: VerifiableCredential }) => {
width: '100%',
}}
>

<CredentialInfo credential={credential} />
<CredentialTabs credential={credential} />
<CredentialInfo credential={decodedEnvelopedVC ?? credential} />
<CredentialTabs credential={credential} decodedEnvelopedVC={decodedEnvelopedVC} />
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);

Expand Down
58 changes: 47 additions & 11 deletions packages/mock-app/src/components/CredentialTabs/CredentialTabs.tsx
Original file line number Diff line number Diff line change
@@ -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: <CredentialRender credential={credential} />,
children: <CredentialRender credential={decodedEnvelopedVC ?? credential} />,
},
{
label: 'JSON',
children: <JsonBlock credential={credential} />,
children: <JsonBlock credential={decodedEnvelopedVC ?? credential} />,
},
];

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);
}

Expand All @@ -43,12 +47,44 @@ const CredentialTabs = ({ credential }: { credential: VerifiableCredential }) =>

return (
<Box sx={{ width: '100%', bgcolor: 'background.paper', minHeight: '300px' }}>
<Tabs value={currentTabIndex} onChange={handleChange} centered>
{credentialTabs?.map((item, index) => <Tab key={index} label={item.label} />)}
</Tabs>
{/* Header Row */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
gap: isMobile ? 1 : 2,
maxWidth: '800px',
margin: 'auto',
}}
>
{/* Tabs aligned to the left */}
<Tabs
value={currentTabIndex}
onChange={handleChange}
sx={{
flexGrow: 1,
minWidth: 0,
justifyContent: 'flex-start',
}}
variant='scrollable'
scrollButtons={isMobile ? 'auto' : false}
>
{credentialTabs.map((item, index) => (
<Tab key={index} label={item.label} />
))}
</Tabs>

{/* Download Button */}
<DownloadCredentialButton credential={credential} />
</Box>

{credentialTabs?.map((item, index) => (
<TabPanel key={index} value={currentTabIndex} index={index} children={item.children} />
{/* Tab Panels */}
{credentialTabs.map((item, index) => (
<TabPanel key={index} value={currentTabIndex} index={index}>
{item.children}
</TabPanel>
))}
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ? (
<IconButton color='primary' aria-label='download' onClick={handleClickDownloadVC}>
<CloudDownloadOutlinedIcon />
</IconButton>
) : (
<Button variant='text' onClick={handleClickDownloadVC} startIcon={<CloudDownloadOutlinedIcon />}>
Download
</Button>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DownloadCredentialButton';
23 changes: 3 additions & 20 deletions packages/mock-app/src/components/JsonBlock/JsonBlock.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Card sx={{ width: '100%', textAlign: 'left', overflowX: 'scroll' }}>
<Button onClick={handleClickDownloadVC} variant='contained'>
Download
</Button>
<CardContent>
<pre>{JSON.stringify(credential, null, 2)}</pre>
</CardContent>
Expand Down
2 changes: 1 addition & 1 deletion packages/mock-app/src/pages/Verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const Verify = () => {

return (
<BackButton>
<Credential credential={customCredential ?? credential} />
<Credential credential={credential} decodedEnvelopedVC={customCredential} />
</BackButton>
);
default:
Expand Down
6 changes: 6 additions & 0 deletions packages/mock-app/src/types/common.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UnsignedCredential, VerifiableCredential } from '@vckit/core-types';
import { IGenericFeatureProps } from '../components/GenericFeature';

export interface IFeature extends IGenericFeatureProps {
Expand Down Expand Up @@ -27,3 +28,8 @@ export interface IStyles {
tertiaryColor: string;
menuIconColor?: string;
}

export interface CredentialComponentProps {
credential: VerifiableCredential;
decodedEnvelopedVC?: UnsignedCredential | null;
}
Loading

0 comments on commit 962e099

Please sign in to comment.