Skip to content

Commit

Permalink
Merge pull request #338 from Concordium/extra-details
Browse files Browse the repository at this point in the history
Extra details
  • Loading branch information
orhoj authored Aug 17, 2023
2 parents 326cdaf + 638e5b7 commit a0eb36e
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.verifiable-credential-list {
.verifiable-credential-wrapper {
height: calc(100% - 56px);
background-color: $color-bg;
overflow-y: auto;
Expand All @@ -20,6 +20,12 @@
box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0);
position: relative;

&__clickable {
&:hover {
cursor: pointer;
}
}

&__header {
display: flex;
align-items: center;
Expand Down Expand Up @@ -90,6 +96,7 @@
}

&-value {
overflow-wrap: break-word;
display: block;
font-size: rem(10px);
font-weight: $font-weight-light;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function DisplayImage({ image }: { image: MetadataUrl }) {
/**
* Renders a verifiable credential attribute.
*/
function DisplayAttribute({
export function DisplayAttribute({
attributeKey,
attributeValue,
attributeTitle,
Expand All @@ -49,7 +49,7 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className
if (onClick) {
return (
<div
className={clsx('verifiable-credential', className)}
className={clsx('verifiable-credential verifiable-credential__clickable', className)}
style={{ backgroundColor: metadata.backgroundColor }}
onClick={onClick}
onKeyDown={(e) => {
Expand Down Expand Up @@ -94,6 +94,22 @@ function applySchema(
};
}

export function VerifiableCredentialCardHeader({
metadata,
credentialStatus,
}: {
metadata: VerifiableCredentialMetadata;
credentialStatus: VerifiableCredentialStatus;
}) {
return (
<header className="verifiable-credential__header">
<Logo logo={metadata.logo} />
<div className="verifiable-credential__header__title">{metadata.title}</div>
<StatusIcon status={credentialStatus} />
</header>
);
}

interface CardProps extends ClassName {
credentialSubject: Omit<CredentialSubject, 'id'>;
schema: VerifiableCredentialSchema;
Expand All @@ -114,11 +130,7 @@ export function VerifiableCredentialCard({

return (
<ClickableVerifiableCredential className={className} onClick={onClick} metadata={metadata}>
<header className="verifiable-credential__header">
<Logo logo={metadata.logo} />
<div className="verifiable-credential__header__title">{metadata.title}</div>
<StatusIcon status={credentialStatus} />
</header>
<VerifiableCredentialCardHeader credentialStatus={credentialStatus} metadata={metadata} />
{metadata.image && <DisplayImage image={metadata.image} />}
<div className="verifiable-credential__body-attributes">
{attributes &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types';
import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar';
Expand All @@ -9,6 +9,7 @@ import { grpcClientAtom } from '@popup/store/settings';
import { absoluteRoutes } from '@popup/constants/routes';
import { useHdWallet } from '@popup/shared/utils/account-helpers';
import {
CredentialQueryResponse,
VerifiableCredentialMetadata,
buildRevokeTransaction,
buildRevokeTransactionParameters,
Expand All @@ -17,12 +18,64 @@ import {
getRevokeTransactionExecutionEnergyEstimate,
} from '@shared/utils/verifiable-credential-helpers';
import { fetchContractName } from '@shared/utils/token-helpers';
import { ClassName } from 'wallet-common-helpers';
import { TimeStampUnit, dateFromTimestamp, ClassName } from 'wallet-common-helpers';
import { withDateAndTime } from '@shared/utils/time-helpers';
import { accountRoutes } from '../Account/routes';
import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer';
import RevokeIcon from '../../../assets/svg/revoke.svg';
import { useCredentialEntry } from './VerifiableCredentialHooks';
import { VerifiableCredentialCard } from './VerifiableCredentialCard';
import { DisplayAttribute, VerifiableCredentialCard, VerifiableCredentialCardHeader } from './VerifiableCredentialCard';

/**
* Component for displaying the extra details about a verifiable credential, i.e. the
* credential holder id, when it is valid from and, if available, when it is valid until.
*/
function VerifiableCredentialExtraDetails({
credentialEntry,
status,
metadata,
className,
}: {
credentialEntry: CredentialQueryResponse;
status: VerifiableCredentialStatus;
metadata: VerifiableCredentialMetadata;
} & ClassName) {
const { t } = useTranslation('verifiableCredential');

const validFrom = dateFromTimestamp(credentialEntry.credentialInfo.validFrom, TimeStampUnit.milliSeconds);
const validUntil = credentialEntry.credentialInfo.validUntil
? dateFromTimestamp(credentialEntry.credentialInfo.validUntil, TimeStampUnit.milliSeconds)
: undefined;
const validFromFormatted = withDateAndTime(validFrom);
const validUntilFormatted = withDateAndTime(validUntil);

return (
<div className="verifiable-credential-wrapper">
<div className={`verifiable-credential ${className}`} style={{ backgroundColor: metadata.backgroundColor }}>
<VerifiableCredentialCardHeader credentialStatus={status} metadata={metadata} />
<div className="verifiable-credential__body-attributes">
<DisplayAttribute
attributeKey="credentialHolderId"
attributeTitle={t('details.id')}
attributeValue={credentialEntry.credentialInfo.credentialHolderId}
/>
<DisplayAttribute
attributeKey="validFrom"
attributeTitle={t('details.validFrom')}
attributeValue={validFromFormatted}
/>
{credentialEntry.credentialInfo.validUntil !== undefined && (
<DisplayAttribute
attributeKey="validUntil"
attributeTitle={t('details.validUntil')}
attributeValue={validUntilFormatted}
/>
)}
</div>
</div>
</div>
);
}

interface CredentialDetailsProps extends ClassName {
credential: VerifiableCredential;
Expand All @@ -46,6 +99,7 @@ export default function VerifiableCredentialDetails({
const client = useAtomValue(grpcClientAtom);
const hdWallet = useHdWallet();
const credentialEntry = useCredentialEntry(credential);
const [showExtraDetails, setShowExtraDetails] = useState(false);

const goToConfirmPage = useCallback(async () => {
if (credentialEntry === undefined || hdWallet === undefined) {
Expand Down Expand Up @@ -89,25 +143,37 @@ export default function VerifiableCredentialDetails({
}, [client, credential, hdWallet, credentialEntry, nav, pathname]);

const menuButton: MenuButton | undefined = useMemo(() => {
if (
credentialEntry === undefined ||
!credentialEntry.credentialInfo.holderRevocable ||
status === VerifiableCredentialStatus.Revoked
) {
if (credentialEntry === undefined) {
return undefined;
}

return {
type: ButtonTypes.More,
items: [
{
title: t('menu.revoke'),
icon: <RevokeIcon />,
onClick: goToConfirmPage,
},
],
};
}, [credentialEntry, goToConfirmPage]);
const menuButtons = [];

if (credentialEntry?.credentialInfo.holderRevocable && status !== VerifiableCredentialStatus.Revoked) {
const revokeButton = {
title: t('menu.revoke'),
icon: <RevokeIcon />,
onClick: goToConfirmPage,
};
menuButtons.push(revokeButton);
}

if (!showExtraDetails) {
const detailsButton = {
title: t('menu.details'),
onClick: () => setShowExtraDetails(true),
};
menuButtons.push(detailsButton);
}

if (menuButtons.length > 0) {
return {
type: ButtonTypes.More,
items: menuButtons,
};
}
return undefined;
}, [credentialEntry?.credentialInfo.holderRevocable, goToConfirmPage, showExtraDetails]);

// Wait for the credential entry to be loaded from the chain, and for the HdWallet
// to be loaded to be ready to derive keys.
Expand All @@ -117,16 +183,30 @@ export default function VerifiableCredentialDetails({

return (
<>
<Topbar title={t('topbar.details')} onBackButtonClick={backButtonOnClick} menuButton={menuButton} />
<div className="verifiable-credential-list">
<VerifiableCredentialCard
<Topbar
title={t('topbar.details')}
onBackButtonClick={() => (showExtraDetails ? setShowExtraDetails(false) : backButtonOnClick())}
menuButton={menuButton}
/>
{showExtraDetails && (
<VerifiableCredentialExtraDetails
className={className}
credentialSubject={credential.credentialSubject}
schema={schema}
credentialStatus={status}
credentialEntry={credentialEntry}
status={status}
metadata={metadata}
/>
</div>
)}
{!showExtraDetails && (
<div className="verifiable-credential-wrapper">
<VerifiableCredentialCard
className={className}
credentialSubject={credential.credentialSubject}
schema={schema}
credentialStatus={status}
metadata={metadata}
/>
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import VerifiableCredentialDetails from './VerifiableCredentialDetails';
* Component to display while loading verifiable credentials from storage.
*/
function LoadingVerifiableCredentials() {
return <div className="verifiable-credential-list" />;
return <div className="verifiable-credential-wrapper" />;
}

/**
Expand All @@ -37,7 +37,7 @@ function NoVerifiableCredentials() {
return (
<>
<Topbar title={t('topbar.list')} />
<div className="verifiable-credential-list">
<div className="verifiable-credential-wrapper">
<div className="flex-column align-center">
<p className="m-t-20 m-h-30">You do not have any verifiable credentials in your wallet.</p>
</div>
Expand Down Expand Up @@ -125,7 +125,7 @@ export default function VerifiableCredentialList() {
if (selected) {
return (
<VerifiableCredentialDetails
className="verifiable-credential-list__card"
className="verifiable-credential-wrapper__card"
credential={selected.credential}
schema={selected.schema}
status={selected.status}
Expand All @@ -138,12 +138,12 @@ export default function VerifiableCredentialList() {
return (
<>
<Topbar title={t('topbar.list')} />
<div className="verifiable-credential-list">
<div className="verifiable-credential-wrapper">
{verifiableCredentials.value.map((credential) => {
return (
<VerifiableCredentialCardWithStatusFromChain
key={credential.id}
className="verifiable-credential-list__card"
className="verifiable-credential-wrapper__card"
credential={credential}
onClick={(
status: VerifiableCredentialStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const t: typeof en = {
},
menu: {
revoke: 'Ophæv',
details: 'Detaljer',
},
details: {
id: 'Legitimationholders ID',
validFrom: 'Gyldig fra',
validUntil: 'Gyldig indtil',
},
status: {
Active: 'Aktiv',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const t = {
},
menu: {
revoke: 'Revoke',
details: 'Details',
},
details: {
id: 'Credential holder ID',
validFrom: 'Valid from',
validUntil: 'Valid until',
},
status: {
Active: 'Active',
Expand Down
12 changes: 9 additions & 3 deletions packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import Button from '../Button/Button';

export interface PopupMenuItem {
title: string;
icon: JSX.Element;
icon?: JSX.Element;
onClick?: () => void;
}

interface PopupMenuProps {
items: PopupMenuItem[];
onClickOutside: () => void;
afterButtonClick: () => void;
}

export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) {
export default function PopupMenu({ items, afterButtonClick, onClickOutside }: PopupMenuProps) {
return (
<DetectClickOutside onClickOutside={onClickOutside}>
<div className="popup-menu">
Expand All @@ -24,7 +25,12 @@ export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) {
key={item.title}
clear
className={clsx('popup-menu__item', item.onClick ? null : 'popup-menu__item--disabled')}
onClick={item.onClick}
onClick={() => {
if (item.onClick) {
item.onClick();
}
afterButtonClick();
}}
>
<div className="popup-menu__item__title heading6">{item.title}</div>
<div className="popup-menu__item__icon">{item.icon}</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export default function Topbar({ title, onBackButtonClick, menuButton }: TopbarP
<MoreIcon className="topbar__icon-container__icon" />
</Button>
<div className={clsx('topbar__popup-menu', showPopupMenu && 'topbar__popup-menu__show')}>
<PopupMenu items={menuButton.items} onClickOutside={() => setShowPopupMenu(false)} />
<PopupMenu
items={menuButton.items}
onClickOutside={() => setShowPopupMenu(false)}
afterButtonClick={() => setShowPopupMenu(false)}
/>
</div>
</>
)}
Expand Down
6 changes: 6 additions & 0 deletions packages/browser-wallet/src/shared/utils/time-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export function secondsToDaysRoundedDown(seconds: bigint | undefined): bigint {
return seconds ? seconds / (60n * 60n * 24n) : 0n;
}

export const withDateAndTime = Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'medium',
hourCycle: 'h23',
}).format;

0 comments on commit a0eb36e

Please sign in to comment.