Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra details #338

Merged
merged 8 commits into from
Aug 17, 2023
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}
/>
shjortConcordium marked this conversation as resolved.
Show resolved Hide resolved
{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;
Loading