From 1630177323b51cf7390455e492ce669ecb9cccfe Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 8 Aug 2023 16:15:09 +0200 Subject: [PATCH 1/5] Add extra details view of credential --- .../VerifiableCredential.scss | 3 +- .../VerifiableCredentialCard.tsx | 24 +++-- .../VerifiableCredentialDetails.tsx | 88 +++++++++++++++++-- .../VerifiableCredentialList.tsx | 2 +- .../pages/VerifiableCredential/i18n/da.ts | 6 ++ .../pages/VerifiableCredential/i18n/en.ts | 6 ++ .../src/popup/shared/PopupMenu/PopupMenu.tsx | 12 ++- .../src/popup/shared/Topbar/Topbar.tsx | 6 +- .../src/shared/utils/time-helpers.ts | 6 ++ 9 files changed, 132 insertions(+), 21 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index ee76b9da0..e03078e06 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -1,4 +1,4 @@ -.verifiable-credential-list { +.verifiable-credential-wrapper { height: calc(100% - 56px); background-color: $color-bg; overflow-y: auto; @@ -88,6 +88,7 @@ } &-value { + overflow-wrap: break-word; display: block; font-size: rem(10px); font-weight: $font-weight-light; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index debac7695..3c4da39c4 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -25,7 +25,7 @@ function DisplayImage({ image }: { image: MetadataUrl }) { /** * Renders a verifiable credential attribute. */ -function DisplayAttribute({ +export function DisplayAttribute({ attributeKey, attributeValue, attributeTitle, @@ -99,6 +99,22 @@ function applySchema( }; } +export function VerifiableCredentialCardHeader({ + metadata, + credentialStatus, +}: { + metadata: VerifiableCredentialMetadata; + credentialStatus: VerifiableCredentialStatus; +}) { + return ( +
+ +
{metadata.title}
+ +
+ ); +} + export function VerifiableCredentialCard({ credential, schema, @@ -118,11 +134,7 @@ export function VerifiableCredentialCard({ return ( -
- -
{metadata.title}
- -
+ {metadata.image && }
{attributes && diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 5ffd21d0e..e021344ba 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -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'; @@ -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, @@ -17,11 +18,63 @@ import { getRevokeTransactionExecutionEnergyEstimate, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; +import { TimeStampUnit, dateFromTimestamp } 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, +}: { + credentialEntry: CredentialQueryResponse; + status: VerifiableCredentialStatus; + metadata: VerifiableCredentialMetadata; +}) { + 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 ( +
+
+ +
+ + + {credentialEntry.credentialInfo.validUntil !== undefined && ( + + )} +
+
+
+ ); +} export default function VerifiableCredentialDetails({ credential, @@ -42,6 +95,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) { @@ -98,6 +152,10 @@ export default function VerifiableCredentialDetails({ icon: , onClick: goToConfirmPage, }, + { + title: t('menu.details'), + onClick: () => setShowExtraDetails(true), + }, ], }; }, [credentialEntry, goToConfirmPage]); @@ -112,17 +170,29 @@ export default function VerifiableCredentialDetails({ <> (showExtraDetails ? setShowExtraDetails(false) : backButtonOnClick()), + }} menuButton={menuButton} /> -
- -
+ )} + {!showExtraDetails && ( +
+ +
+ )} ); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 69bb65059..764130948 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -118,7 +118,7 @@ export default function VerifiableCredentialList() { return ( <> -
+
{verifiableCredentials.map((credential) => { return ( void; } interface PopupMenuProps { items: PopupMenuItem[]; onClickOutside: () => void; + onButtonClick: () => void; } -export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) { +export default function PopupMenu({ items, onButtonClick, onClickOutside }: PopupMenuProps) { return (
@@ -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(); + } + onButtonClick(); + }} >
{item.title}
{item.icon}
diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index 0cf61a640..9de2a1cf3 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -64,7 +64,11 @@ export default function Topbar({
- setShowPopupMenu(false)} /> + setShowPopupMenu(false)} + onButtonClick={() => setShowPopupMenu(false)} + />
)} diff --git a/packages/browser-wallet/src/shared/utils/time-helpers.ts b/packages/browser-wallet/src/shared/utils/time-helpers.ts index 3f946dd2f..3faed8c40 100644 --- a/packages/browser-wallet/src/shared/utils/time-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/time-helpers.ts @@ -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; From 5e390a5a8ba1778660859e83de91500b9155585e Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 16 Aug 2023 11:47:07 +0200 Subject: [PATCH 2/5] Show details button even when not revocable --- .../VerifiableCredentialDetails.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 5a7b8f077..f2fa30381 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -148,29 +148,29 @@ 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; } + const detailsButton = { + title: t('menu.details'), + onClick: () => setShowExtraDetails(true), + }; + + let revokeButton; + if (credentialEntry?.credentialInfo.holderRevocable && status !== VerifiableCredentialStatus.Revoked) { + revokeButton = { + title: t('menu.revoke'), + icon: , + onClick: goToConfirmPage, + }; + } + return { type: ButtonTypes.More, - items: [ - { - title: t('menu.revoke'), - icon: , - onClick: goToConfirmPage, - }, - { - title: t('menu.details'), - onClick: () => setShowExtraDetails(true), - }, - ], + items: revokeButton ? [detailsButton, revokeButton] : [detailsButton], }; - }, [credentialEntry, goToConfirmPage]); + }, [credentialEntry?.credentialInfo.holderRevocable, goToConfirmPage]); // Wait for the credential entry to be loaded from the chain, and for the HdWallet // to be loaded to be ready to derive keys. From a3ec363ad7d88a8535966d78051c16cfc4e57308 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 17 Aug 2023 13:52:08 +0200 Subject: [PATCH 3/5] Refactor according to review comments --- .../src/popup/pages/VerifiableCredential/i18n/da.ts | 2 +- .../browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx | 6 +++--- packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts index 5280c91ef..75341d4ed 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -10,7 +10,7 @@ const t: typeof en = { details: 'Detaljer', }, details: { - id: 'Legimitationholders ID', + id: 'Legitimationholders ID', validFrom: 'Gyldig fra', validUntil: 'Gyldig indtil', }, diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index 963896e2f..025b14c9c 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -12,10 +12,10 @@ export interface PopupMenuItem { interface PopupMenuProps { items: PopupMenuItem[]; onClickOutside: () => void; - onButtonClick: () => void; + afterButtonClick: () => void; } -export default function PopupMenu({ items, onButtonClick, onClickOutside }: PopupMenuProps) { +export default function PopupMenu({ items, afterButtonClick, onClickOutside }: PopupMenuProps) { return (
@@ -29,7 +29,7 @@ export default function PopupMenu({ items, onButtonClick, onClickOutside }: Popu if (item.onClick) { item.onClick(); } - onButtonClick(); + afterButtonClick(); }} >
{item.title}
diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index 9de2a1cf3..416c3c2e2 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -67,7 +67,7 @@ export default function Topbar({ setShowPopupMenu(false)} - onButtonClick={() => setShowPopupMenu(false)} + afterButtonClick={() => setShowPopupMenu(false)} />
From a480c5961d5d06745e4d3cbafd7acc5c6eec41b6 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 17 Aug 2023 14:16:16 +0200 Subject: [PATCH 4/5] Change cursor when hovering clickable credential --- .../pages/VerifiableCredential/VerifiableCredential.scss | 6 ++++++ .../pages/VerifiableCredential/VerifiableCredentialCard.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index a50992b9a..eda5c20f2 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -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; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 792b59cd7..f13449d85 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -49,7 +49,7 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className if (onClick) { return (
{ From 494cb0e01aba83392aec72216a29ea06f29fe350 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 17 Aug 2023 14:16:28 +0200 Subject: [PATCH 5/5] Hide details menu button when already viewing details --- .../VerifiableCredentialDetails.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index f2fa30381..493e34928 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -152,25 +152,33 @@ export default function VerifiableCredentialDetails({ return undefined; } - const detailsButton = { - title: t('menu.details'), - onClick: () => setShowExtraDetails(true), - }; + const menuButtons = []; - let revokeButton; if (credentialEntry?.credentialInfo.holderRevocable && status !== VerifiableCredentialStatus.Revoked) { - revokeButton = { + const revokeButton = { title: t('menu.revoke'), icon: , onClick: goToConfirmPage, }; + menuButtons.push(revokeButton); } - return { - type: ButtonTypes.More, - items: revokeButton ? [detailsButton, revokeButton] : [detailsButton], - }; - }, [credentialEntry?.credentialInfo.holderRevocable, goToConfirmPage]); + 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.