From 50b5995fa1dd0876b54d703bba65f58f0a08ebe3 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 9 May 2023 12:38:26 +0200 Subject: [PATCH 001/231] Add verifiable credentials page --- .../src/popup/constants/routes.ts | 3 ++ .../page-layouts/MainLayout/Header/Header.tsx | 12 +++++ .../popup/page-layouts/MainLayout/i18n/da.ts | 1 + .../popup/page-layouts/MainLayout/i18n/en.ts | 1 + .../VerifiableCredential.scss | 46 +++++++++++++++++++ .../VerifiableCredentialCard.tsx | 43 +++++++++++++++++ .../VerifiableCredentialList.tsx | 37 +++++++++++++++ .../popup/pages/VerifiableCredential/index.ts | 1 + .../browser-wallet/src/popup/shell/Routes.tsx | 2 + .../browser-wallet/src/popup/store/utils.ts | 5 +- .../src/popup/store/verifiable-credential.ts | 7 +++ .../src/popup/styles/_components.scss | 1 + .../src/shared/storage/access.ts | 5 ++ .../src/shared/storage/types.ts | 14 ++++++ 14 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/index.ts create mode 100644 packages/browser-wallet/src/popup/store/verifiable-credential.ts diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index b178771b..ad09fed6 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -18,6 +18,9 @@ export const relativeRoutes = { path: 'identities', add: { path: 'add' }, }, + verifiableCredentials: { + path: 'verifiable-credentials', + }, settings: { path: 'settings', passcode: { diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx index a8d469a6..4d94e5be 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx @@ -54,6 +54,7 @@ const transitionVariants: Variants = { enum Section { Account, Id, + VerifiableCredentials, Settings, } @@ -67,6 +68,8 @@ function getTitle(section: Section, pathname: string) { return 'header.accounts'; case Section.Id: return 'header.ids'; + case Section.VerifiableCredentials: + return 'header.verifiableCredentials'; case Section.Settings: { if (pathname.startsWith(absoluteRoutes.home.settings.recovery.path)) { return 'header.settings.recovery'; @@ -91,6 +94,9 @@ function getSection(pathname: string): Section { if (pathname.startsWith(absoluteRoutes.home.identities.path)) { return Section.Id; } + if (pathname.startsWith(absoluteRoutes.home.verifiableCredentials.path)) { + return Section.VerifiableCredentials; + } if (pathname.startsWith(absoluteRoutes.home.settings.path)) { return Section.Settings; } @@ -187,6 +193,12 @@ export default function Header({ onToggle, className }: Props) { setNavOpen(false)} to={absoluteRoutes.home.identities.path}> {t('header.ids')} + setNavOpen(false)} + to={absoluteRoutes.home.verifiableCredentials.path} + > + {t('header.verifiableCredentials')} + setNavOpen(false)} to={absoluteRoutes.home.settings.path}> {t('header.settings.main')} diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index 59924a47..9ac5c3fa 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -9,6 +9,7 @@ const t: typeof en = { header: { accounts: 'Konti', ids: 'ID kort', + verifiableCredentials: 'Legitimationsoplysninger', settings: { main: 'Wallet indstillinger', recovery: 'Genskabning af din wallet', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index cf53bc16..954441e2 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -7,6 +7,7 @@ const t = { header: { accounts: 'Accounts', ids: 'ID cards', + verifiableCredentials: 'Verifiable credentials', settings: { main: 'Wallet settings', recovery: 'Restoring your wallet', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss new file mode 100644 index 00000000..1afe0358 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -0,0 +1,46 @@ +.verifiable-credential { + border-radius: rem(8px); + background: #f4bb5c; + padding: rem(10px); + margin: rem(10px); + box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); + position: relative; + + &__header-logo { + position: absolute; + left: rem(10px); + top: rem(10px); + width: rem(20px); + } + + &__header-title { + position: absolute; + left: rem(35px); + top: rem(10px); + width: rem(100px); + } + + &__header-status { + position: absolute; + right: rem(10px); + top: rem(10px); + } + + &__body-attributes { + margin-top: rem(30px); + + &-row { + margin-bottom: rem(6px); + + label { + font-variant: small-caps; + display: block; + } + + &-value { + display: block; + font-weight: $font-weight-bold; + } + } + } +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx new file mode 100644 index 00000000..414b5a8d --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import CcdIcon from '@assets/svg/concordium.svg'; +import { VerifiableCredential, VerifiableCredentialStatus } from '../../../shared/storage/types'; + +function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { + switch (status) { + case VerifiableCredentialStatus.Active: + case VerifiableCredentialStatus.Revoked: + case VerifiableCredentialStatus.Expired: + case VerifiableCredentialStatus.NotActivated: + default: + return
{VerifiableCredentialStatus[status]}
; + } +} + +function Logo() { + return ( +
+ +
+ ); +} + +export function VerifiableCredentialCard({ credential }: { credential: VerifiableCredential }) { + return ( +
+
+ +
{credential.title}
+ +
+
+ {credential.attributes && + Object.entries(credential.attributes).map((value) => ( +
+ +
{value[1]}
+
+ ))} +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx new file mode 100644 index 00000000..7f05bfc5 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential'; +import { useAtomValue } from 'jotai'; +import { VerifiableCredentialCard } from './VerifiableCredentialCard'; + +/** + * Component to display when there are no verifiable credentials in the wallet. + */ +function NoVerifiableCredentials() { + return ( +
+

You do not have any verifiable credentials in your wallet.

+
+ ); +} + +/** + * Renders all verifiable credentials that are in the wallet. The credentials + * are selectable by clicking them, which will move the user to a view containing + * a single credential. + */ +export default function VerifiableCredentialList() { + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + + if (!verifiableCredentials) { + return ; + } + + return ( + <> + {verifiableCredentials.map((credential, index) => { + // eslint-disable-next-line react/no-array-index-key + return ; + })} + + ); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/index.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/index.ts new file mode 100644 index 00000000..658eefc9 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/index.ts @@ -0,0 +1 @@ +export { default } from './VerifiableCredentialList'; diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 8862e117..47bb30a6 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -32,6 +32,7 @@ import RecoveryFinish from '@popup/pages/Recovery/RecoveryFinish'; import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; +import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; type PromptKey = keyof Omit; @@ -172,6 +173,7 @@ export default function Routes() { path={`${relativePath(relativeRoutes.home.path, absoluteRoutes.home.identities.add.path)}/*`} /> } path={relativeRoutes.home.identities.path} /> + } path={relativeRoutes.home.verifiableCredentials.path} /> } /> } path={relativeRoutes.home.settings.passcode.path} /> diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 1e6ba1bc..7b325925 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -26,11 +26,13 @@ import { sessionCookie, sessionOpenPrompt, storedAcceptedTerms, + storedVerifiableCredentials, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; -const accessorMap = { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const accessorMap: Record> = { [ChromeStorageKey.Identities]: useIndexedStorage(storedIdentities, getGenesisHash), [ChromeStorageKey.SelectedIdentity]: storedSelectedIdentity, [ChromeStorageKey.ConnectedSites]: storedConnectedSites, @@ -55,6 +57,7 @@ const accessorMap = { [ChromeStorageKey.Cookie]: useIndexedStorage(sessionCookie, getGenesisHash), [ChromeStorageKey.OpenPrompt]: sessionOpenPrompt, [ChromeStorageKey.AcceptedTerms]: storedAcceptedTerms, + [ChromeStorageKey.VerifiableCredentials]: useIndexedStorage(storedVerifiableCredentials, getGenesisHash), }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts new file mode 100644 index 00000000..29b66333 --- /dev/null +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -0,0 +1,7 @@ +import { ChromeStorageKey, VerifiableCredential } from '@shared/storage/types'; +import { atomWithChromeStorage } from './utils'; + +export const storedVerifiableCredentialsAtom = atomWithChromeStorage( + ChromeStorageKey.VerifiableCredentials, + [] +); diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index 8c0d104e..2764f5d5 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -30,6 +30,7 @@ @import '../pages/Settings/Settings'; @import '../pages/Identity/Identity'; @import '../pages/IdentityIssuance/IdentityIssuance'; +@import '../pages/VerifiableCredential/VerifiableCredential'; @import '../pages/About/About'; @import '../pages/Login/Login'; @import '../pages/AddAccount/AddAccount'; diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index bd9ee3bd..c2809a14 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -11,6 +11,7 @@ import { TokenMetadata, TokenStorage, AcceptedTermsState, + VerifiableCredential, } from './types'; export type StorageAccessor = { @@ -145,6 +146,10 @@ export const storedTokenMetadata = makeStorageAccessor('local', ChromeStorageKey.AcceptedTerms); +export const storedVerifiableCredentials = makeIndexedStorageAccessor( + 'local', + ChromeStorageKey.VerifiableCredentials +); export const sessionOpenPrompt = makeStorageAccessor('session', ChromeStorageKey.OpenPrompt); export const sessionPasscode = makeStorageAccessor('session', ChromeStorageKey.Passcode); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 84377e30..6cbb93c4 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -25,6 +25,7 @@ export enum ChromeStorageKey { Cookie = 'cookie', OpenPrompt = 'openPrompt', AcceptedTerms = 'acceptedTerms', + VerifiableCredentials = 'verifiableCredentials', } export enum Theme { @@ -251,3 +252,16 @@ export type AcceptedTermsState = { accepted: boolean; version: string; }; + +export enum VerifiableCredentialStatus { + Active, + Revoked, + Expired, + NotActivated, +} + +export interface VerifiableCredential { + title: string; + status: VerifiableCredentialStatus; + attributes: Record; +} From c6154d5c8b1c193aa03fbeebd016da0624a56c4a Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 9 May 2023 13:41:08 +0200 Subject: [PATCH 002/231] Make credentials selectable --- .../VerifiableCredentialCard.tsx | 29 +++++++++++++++++-- .../VerifiableCredentialList.tsx | 17 +++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 414b5a8d..5b66cafe 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -21,9 +21,34 @@ function Logo() { ); } -export function VerifiableCredentialCard({ credential }: { credential: VerifiableCredential }) { +/** + * Provide the required clickable properties if onClick is defined. + */ +function clickableProperties(onClick?: () => void) { + if (onClick) { + return { + onClick, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && onClick) { + onClick(); + } + }, + role: 'button', + tabIndex: 0, + }; + } + return {}; +} + +export function VerifiableCredentialCard({ + credential, + onClick, +}: { + credential: VerifiableCredential; + onClick?: () => void; +}) { return ( -
+
{credential.title}
diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 7f05bfc5..4e1ef046 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; +import { VerifiableCredential } from '@shared/storage/types'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; /** @@ -21,16 +22,26 @@ function NoVerifiableCredentials() { */ export default function VerifiableCredentialList() { const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const [selected, setSelected] = useState(); if (!verifiableCredentials) { return ; } + if (selected) { + return ; + } return ( <> {verifiableCredentials.map((credential, index) => { - // eslint-disable-next-line react/no-array-index-key - return ; + return ( + setSelected(credential)} + /> + ); })} ); From 30378e92a4247391c7691292978e200b09a7d8e7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 10 May 2023 13:58:51 +0200 Subject: [PATCH 003/231] Use schema to render verifiable credentials --- .../VerifiableCredentialCard.tsx | 95 ++++++++++++++----- .../browser-wallet/src/popup/store/utils.ts | 2 + .../src/popup/store/verifiable-credential.ts | 8 +- .../src/shared/storage/access.ts | 5 + .../src/shared/storage/types.ts | 49 +++++++++- 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 5b66cafe..b5832dd0 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -1,6 +1,12 @@ -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import CcdIcon from '@assets/svg/concordium.svg'; -import { VerifiableCredential, VerifiableCredentialStatus } from '../../../shared/storage/types'; +import { useAtomValue } from 'jotai'; +import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; +import { + VerifiableCredential, + VerifiableCredentialStatus, + VerifiableCredentialSchema, +} from '../../../shared/storage/types'; function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { switch (status) { @@ -22,22 +28,53 @@ function Logo() { } /** - * Provide the required clickable properties if onClick is defined. + * Renders a credential using the applied schema. The schema is used to determine the correct + * label for the attribute value. + * @param credential the credential to render + * @param schema the schema for the credential + * @throws if there is a mismatch in fields between the credential and the schema, i.e. the schema is invalid. */ -function clickableProperties(onClick?: () => void) { +function DisplayAttribute({ + attributeKey, + attributeValue, + schema, +}: { + attributeKey: string; + attributeValue: string | number; + schema: VerifiableCredentialSchema; +}) { + const attributeSchema = schema.schema.properties.credentialSubject.properties[attributeKey]; + return ( +
+ +
{attributeValue}
+
+ ); +} + +/** + * Wraps children components in a verifiable credential card that is clickable if onClick + * is defined. + */ +function ClickableVerifiableCredential({ children, onClick }: PropsWithChildren<{ onClick?: () => void }>) { if (onClick) { - return { - onClick, - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && onClick) { - onClick(); - } - }, - role: 'button', - tabIndex: 0, - }; + return ( +
{ + if (e.key === 'Enter') { + onClick(); + } + }} + role="button" + tabIndex={0} + > + {children} +
+ ); } - return {}; + return
{children}
; } export function VerifiableCredentialCard({ @@ -47,22 +84,30 @@ export function VerifiableCredentialCard({ credential: VerifiableCredential; onClick?: () => void; }) { + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + if (schemas.loading || !Object.keys(schemas.value).length) { + return null; + } + const schema = schemas.value[credential.credentialSchema.id]; + return ( -
+
-
{credential.title}
- +
{credential.type[0]}
+
- {credential.attributes && - Object.entries(credential.attributes).map((value) => ( -
- -
{value[1]}
-
+ {credential.credentialSubject && + Object.entries(credential.credentialSubject).map((value) => ( + ))}
-
+ ); } diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 7b325925..bc54cb84 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -27,6 +27,7 @@ import { sessionOpenPrompt, storedAcceptedTerms, storedVerifiableCredentials, + storedVerifiableCredentialSchemas, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -58,6 +59,7 @@ const accessorMap: Record> = { [ChromeStorageKey.OpenPrompt]: sessionOpenPrompt, [ChromeStorageKey.AcceptedTerms]: storedAcceptedTerms, [ChromeStorageKey.VerifiableCredentials]: useIndexedStorage(storedVerifiableCredentials, getGenesisHash), + [ChromeStorageKey.VerifiableCredentialSchemas]: storedVerifiableCredentialSchemas, }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts index 29b66333..a11f17d8 100644 --- a/packages/browser-wallet/src/popup/store/verifiable-credential.ts +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -1,7 +1,13 @@ -import { ChromeStorageKey, VerifiableCredential } from '@shared/storage/types'; +import { ChromeStorageKey, VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; import { atomWithChromeStorage } from './utils'; export const storedVerifiableCredentialsAtom = atomWithChromeStorage( ChromeStorageKey.VerifiableCredentials, [] ); + +export const storedVerifiableCredentialSchemasAtom = atomWithChromeStorage>( + ChromeStorageKey.VerifiableCredentialSchemas, + {}, + true +); diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index c2809a14..df2c08f1 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -12,6 +12,7 @@ import { TokenStorage, AcceptedTermsState, VerifiableCredential, + VerifiableCredentialSchema, } from './types'; export type StorageAccessor = { @@ -150,6 +151,10 @@ export const storedVerifiableCredentials = makeIndexedStorageAccessor>( + 'local', + ChromeStorageKey.VerifiableCredentialSchemas +); export const sessionOpenPrompt = makeStorageAccessor('session', ChromeStorageKey.OpenPrompt); export const sessionPasscode = makeStorageAccessor('session', ChromeStorageKey.Passcode); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 6cbb93c4..a7749196 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -26,6 +26,7 @@ export enum ChromeStorageKey { OpenPrompt = 'openPrompt', AcceptedTerms = 'acceptedTerms', VerifiableCredentials = 'verifiableCredentials', + VerifiableCredentialSchemas = 'verifiableCredentialSchemas', } export enum Theme { @@ -253,6 +254,8 @@ export type AcceptedTermsState = { version: string; }; +// TODO[orhoj]: The types are incomplete as the final schemas are not ready. +// The types used here are taken from the draft documents available. export enum VerifiableCredentialStatus { Active, Revoked, @@ -260,8 +263,50 @@ export enum VerifiableCredentialStatus { NotActivated, } +interface CredentialSchema { + id: string; + type: string; +} + +export type CredentialSubject = { id: string } & Record; + export interface VerifiableCredential { + context: string[]; + id: string; + type: string[]; + issuer: string; + issuanceDate: string; + credentialSubject: CredentialSubject; + credentialSchema: CredentialSchema; +} + +interface CredentialSchemaProperty { title: string; - status: VerifiableCredentialStatus; - attributes: Record; + type: 'string' | 'number'; + description: string; + index: string; +} + +interface CredentialSchemaSubject { + properties: { id: CredentialSchemaProperty } & Record; +} + +export interface SchemaProperties { + credentialSubject: CredentialSchemaSubject; +} + +export interface VerifiableCredentialSchemaSchema { + // credentialSubject: CredentialSchemaSubject; + properties: SchemaProperties; +} + +export interface VerifiableCredentialSchema { + type: string; + version: string; + id: string; + name: string; + author: string; + authored: string; + schema: VerifiableCredentialSchemaSchema; + proof: string; } From aa74fd7afaba7c8a5f4ff82dc71afd0d423fcf6c Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 10 May 2023 14:52:17 +0200 Subject: [PATCH 004/231] Throw error on missing schema --- .../VerifiableCredential/VerifiableCredentialCard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index b5832dd0..1a064559 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -85,9 +85,14 @@ export function VerifiableCredentialCard({ onClick?: () => void; }) { const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); - if (schemas.loading || !Object.keys(schemas.value).length) { + if (schemas.loading) { return null; } + if (!Object.keys(schemas.value).length) { + throw new Error('Attempted to render a verifiable credential, but no schemas were found.'); + } else if (!schemas.value[credential.credentialSchema.id]) { + throw new Error(`No schema with id: ${credential.credentialSchema.id}`); + } const schema = schemas.value[credential.credentialSchema.id]; return ( From 5ff191f0c47368bf95ff02e18ba0d218bdb14bc7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 10 May 2023 16:23:46 +0200 Subject: [PATCH 005/231] Sort verifiable credentials and refactor validation --- .../VerifiableCredentialCard.tsx | 67 +++++++++++-------- .../VerifiableCredentialList.tsx | 22 +++++- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 1a064559..467df9db 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -1,7 +1,5 @@ import React, { PropsWithChildren } from 'react'; import CcdIcon from '@assets/svg/concordium.svg'; -import { useAtomValue } from 'jotai'; -import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { VerifiableCredential, VerifiableCredentialStatus, @@ -28,25 +26,20 @@ function Logo() { } /** - * Renders a credential using the applied schema. The schema is used to determine the correct - * label for the attribute value. - * @param credential the credential to render - * @param schema the schema for the credential - * @throws if there is a mismatch in fields between the credential and the schema, i.e. the schema is invalid. + * Renders a verifiable credential attribute. */ function DisplayAttribute({ attributeKey, attributeValue, - schema, + attributeTitle, }: { attributeKey: string; attributeValue: string | number; - schema: VerifiableCredentialSchema; + attributeTitle: string; }) { - const attributeSchema = schema.schema.properties.credentialSubject.properties[attributeKey]; return (
- +
{attributeValue}
); @@ -77,23 +70,43 @@ function ClickableVerifiableCredential({ children, onClick }: PropsWithChildren< return
{children}
; } +/** + * Apply the schema to an attribute, adding the index and title from the schema. The index + * should be used for sorting, and the title should be displayed to a user. + * @param schema the schema to apply + * @returns the attribute together with its index and title. + * @throws if there is a mismatch in fields between the credential and the schema, i.e. the schema is invalid. + */ +function applySchema( + schema: VerifiableCredentialSchema +): (value: [string, string | number]) => { index: number; title: string; key: string; value: string | number } { + return (value: [string, string | number]) => { + const attributeSchema = schema.schema.properties.credentialSubject.properties[value[0]]; + if (!attributeSchema) { + throw new Error(`Missing attribute schema for key: ${value[0]}`); + } + return { + index: Number(attributeSchema.index), + title: attributeSchema.title, + key: value[0], + value: value[1], + }; + }; +} + export function VerifiableCredentialCard({ credential, + schema, onClick, }: { credential: VerifiableCredential; + schema: VerifiableCredentialSchema; onClick?: () => void; }) { - const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); - if (schemas.loading) { - return null; - } - if (!Object.keys(schemas.value).length) { - throw new Error('Attempted to render a verifiable credential, but no schemas were found.'); - } else if (!schemas.value[credential.credentialSchema.id]) { - throw new Error(`No schema with id: ${credential.credentialSchema.id}`); - } - const schema = schemas.value[credential.credentialSchema.id]; + const attributes = Object.entries(credential.credentialSubject) + .filter((val) => val[0] !== 'id') + .map(applySchema(schema)) + .sort((a, b) => a.index - b.index); return ( @@ -103,13 +116,13 @@ export function VerifiableCredentialCard({
- {credential.credentialSubject && - Object.entries(credential.credentialSubject).map((value) => ( + {attributes && + attributes.map((attribute) => ( ))}
diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 4e1ef046..0844b69f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -1,5 +1,8 @@ import React, { useState } from 'react'; -import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential'; +import { + storedVerifiableCredentialSchemasAtom, + storedVerifiableCredentialsAtom, +} from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; @@ -22,13 +25,27 @@ function NoVerifiableCredentials() { */ export default function VerifiableCredentialList() { const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const [selected, setSelected] = useState(); + if (schemas.loading) { + return null; + } if (!verifiableCredentials) { return ; } + if (!Object.keys(schemas.value).length) { + throw new Error('Attempted to render verifiable credentials, but no schemas were found.'); + } else { + for (const verifiableCredential of verifiableCredentials) { + if (!Object.keys(schemas.value).includes(verifiableCredential.credentialSchema.id)) { + throw new Error(`A credential did not have a corresponding schema: ${verifiableCredential.id}`); + } + } + } + if (selected) { - return ; + return ; } return ( @@ -39,6 +56,7 @@ export default function VerifiableCredentialList() { // eslint-disable-next-line react/no-array-index-key key={index} credential={credential} + schema={schemas.value[credential.credentialSchema.id]} onClick={() => setSelected(credential)} /> ); From 13cf0ad775d356e5fb13a7498c63da0d5bc67da3 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 12 May 2023 14:52:02 +0200 Subject: [PATCH 006/231] Fix issue when checking existence of credentials --- .../pages/VerifiableCredential/VerifiableCredentialList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 0844b69f..cf1a6adf 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -31,7 +31,7 @@ export default function VerifiableCredentialList() { if (schemas.loading) { return null; } - if (!verifiableCredentials) { + if (!verifiableCredentials || !verifiableCredentials.length) { return ; } if (!Object.keys(schemas.value).length) { From d05008d1b5b67430ede6e8582c94f2b9c6b4057f Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 23 May 2023 14:01:39 +0200 Subject: [PATCH 007/231] Initial new connection work --- examples/two-step-transfer/index.html | 8 ++ .../src/wallet-api-types.ts | 2 + packages/browser-wallet-api/src/wallet-api.ts | 10 ++ .../browser-wallet-message-hub/src/message.ts | 2 + .../browser-wallet/src/background/index.ts | 8 ++ .../src/popup/constants/routes.ts | 3 + .../ExternalRequestLayout.scss | 1 + .../ExternalRequestLayout.tsx | 3 + .../popup/page-layouts/MainLayout/i18n/da.ts | 1 + .../popup/page-layouts/MainLayout/i18n/en.ts | 1 + .../ConnectAccountsRequest.scss | 24 ++++ .../ConnectAccountsRequest.tsx | 108 ++++++++++++++++++ .../pages/ConnectAccountsRequest/i18n/da.ts | 14 +++ .../pages/ConnectAccountsRequest/i18n/en.ts | 13 +++ .../pages/ConnectAccountsRequest/index.ts | 1 + .../browser-wallet/src/popup/shell/Routes.tsx | 12 ++ .../src/popup/styles/_components.scss | 1 + 17 files changed, 212 insertions(+) create mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss create mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx create mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts create mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/index.ts diff --git a/examples/two-step-transfer/index.html b/examples/two-step-transfer/index.html index 6cee526b..1363693b 100644 --- a/examples/two-step-transfer/index.html +++ b/examples/two-step-transfer/index.html @@ -36,6 +36,13 @@ document.getElementById('accountAddress').innerHTML = accountAddress; }); }); + + document.getElementById('requestAccounts').addEventListener('click', () => { + provider.requestAccounts().then((accountAddresses) => { + currentAccountAddress = accountAddresses[0]; + document.getElementById('accountAddress').innerHTML = accountAddress; + }); + }); document.getElementById('sendTransfer').addEventListener('click', () => provider .sendTransaction(currentAccountAddress, concordiumSDK.AccountTransactionType.Transfer, { @@ -224,6 +231,7 @@

Account address:

+ ModuleSource (base64): diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 78649cca..684c04f5 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -108,6 +108,8 @@ interface MainWalletApi { */ connect(): Promise; + requestAccounts(): Promise; + /** * Returns some connected account, prioritizing the most recently selected. Resolves with account address or undefined if there are no connected account. */ diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 7f48ea4c..a162fb5a 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -94,6 +94,16 @@ class WalletApi extends EventEmitter implements IWalletApi { return response.result; } + public async requestAccounts(): Promise { + const response = await this.messageHandler.sendMessage>(MessageType.ConnectAccounts); + console.log(response); + if (!response.success) { + throw new Error(response.message); + } + + return response.result; + } + /** * Returns some connected account, prioritizing the most recently selected. Resolves with account address or undefined if there are no connected account. */ diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index e5c0dce4..20745541 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -16,6 +16,7 @@ export enum MessageType { GrpcRequest = 'M_GrpcRequest', AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', + ConnectAccounts = 'M_ConnectAccounts', } /** @@ -37,6 +38,7 @@ export enum InternalMessageType { AddTokens = 'I_AddTokens', IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', + ConnectAccounts = 'I_ConnectAccounts', } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 6e6b81b2..eb1cd00d 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -470,6 +470,14 @@ forwardToPopup( handleConnectionResponse, withPromptEnd ); +forwardToPopup( + MessageType.ConnectAccounts, + InternalMessageType.ConnectAccounts, + runConditionComposer(runIfNotWhitelisted, withPromptStart), + handleConnectMessage, + handleConnectionResponse, + withPromptEnd +); forwardToPopup( MessageType.SendTransaction, InternalMessageType.SendTransaction, diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index b178771b..c20a3219 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -39,6 +39,9 @@ export const relativeRoutes = { connectionRequest: { path: 'connection-request', }, + connectAccountsRequest: { + path: 'connect-accounts-request', + }, signMessage: { path: 'sign-message', }, diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss index e22e894d..e4d26bdb 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss @@ -1,6 +1,7 @@ .external-request-layout { position: relative; height: calc(100% - $main-layout-header-height); + background-color: $color-bg; &__main { padding: rem(10px); diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index a6126ac3..63976236 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -16,6 +16,9 @@ function Header() { if (pathname.startsWith(absoluteRoutes.prompt.connectionRequest.path)) { return t('header.connect'); } + if (pathname.startsWith(absoluteRoutes.prompt.connectAccountsRequest.path)) { + return t('header.connectAccountsRequest'); + } if (pathname.startsWith(absoluteRoutes.prompt.addTokens.path)) { return t('header.addTokens'); } diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index 59924a47..bda88e32 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -20,6 +20,7 @@ const t: typeof en = { idProof: 'Bevis for identitet', request: 'Anmodning om Signatur', connect: 'Ny Forbindelse', + connectAccountsRequest: 'Anmodning om tilladelse', }, entityList: { searchPlaceholder: 'Søg', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index cf53bc16..e65be61d 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -18,6 +18,7 @@ const t = { idProof: 'Proof of identity', request: 'Signature Request', connect: 'New connection', + connectAccountsRequest: 'Allowlisting request', }, entityList: { searchPlaceholder: 'Search', diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss new file mode 100644 index 00000000..1b85261a --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss @@ -0,0 +1,24 @@ +.connect-accounts-request { + margin-top: rem(8px); + padding: rem(10px); + + background: #EAF0F5; + border: 1px solid #DAE4EE; + border-radius: 10px 10px 0px 0px; +} + +.connect-accounts-request-accounts { + padding: rem(10px); + width: 100%; + margin-left: auto; + margin-right: auto; + + background: $color-input-bg; + border: 1px solid #DAE4EE; + border-top: none; + border-radius: 0px 0px 10px 10px; + + .item:not(:last-of-type) & { + border-bottom: 1px dotted $color-petroleum; + } +} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx new file mode 100644 index 00000000..58e39cb9 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -0,0 +1,108 @@ +import { absoluteRoutes } from '@popup/constants/routes'; +import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; +import { credentialsAtom, selectedAccountAtom, storedConnectedSitesAtom } from '@popup/store/account'; +import { sessionPasscodeAtom } from '@popup/store/settings'; +import { useAtom, useAtomValue } from 'jotai'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, Navigate } from 'react-router-dom'; +import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; +import Button from '@popup/shared/Button'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import AccountList from '@popup/page-layouts/MainLayout/AccountList/AccountList'; +import { WalletCredential } from '@shared/storage/types'; +import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import AccountInfoListenerContextProvider from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; +import CopyButton from '@popup/shared/CopyButton'; +import { displayAsCcd } from 'wallet-common-helpers'; + +type Props = { + onAllow(): void; + onReject(): void; +}; + +type ItemProps = { + account: WalletCredential; + checked: boolean; + selected: boolean; + identityName: string; +}; + +function AccountListItem({ account, checked, selected, identityName }: ItemProps) { + const accountInfo = useAccountInfo(account); + const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); + + return ( +
+
+
+ {displaySplitAddress(account.address)}{' '} +
+ e.stopPropagation()} + tabIndex={-1} + /> +
+
{identityName}
+
{displayAsCcd(totalBalance)}
+
+ ); +} + +export default function ConnectionRequest({ onAllow, onReject }: Props) { + const { state } = useLocation(); + const { t } = useTranslation('connectionRequest'); + const { onClose, withClose } = useContext(fullscreenPromptContext); + const selectedAccount = useAtomValue(selectedAccountAtom); + const [connectedSitesLoading, setConnectedSites] = useAtom(storedConnectedSitesAtom); + const connectedSites = connectedSitesLoading.value; + const passcode = useAtomValue(sessionPasscodeAtom); + const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); + const accounts = useAtomValue(credentialsAtom); + + + useEffect(() => onClose(onReject), [onClose, onReject]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { url } = (state as any).payload; + const urlDisplay = displayUrl(url); + + return ( + +
+
+

{urlDisplay} wants to be added to your allowlist. Do you want to proceed?

+
+
+

Allowlisting a service

+ Allowlisting a service means that it can request identity proofs and signatures from selected accounts. +
+
+ + {accounts.map((account) => { + return + })} + +
+
+ + +
+
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts new file mode 100644 index 00000000..10e1cf54 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts @@ -0,0 +1,14 @@ +import type en from './en'; + +const t: typeof en = { + title: 'Forbindelsesforespørgsel', + descriptionP1: 'Dette vil tillade {{dApp}} at se dine konti og foreslå transaktioner til at blive underskrevet.', + descriptionP2: 'Du bør kun forbinde dine konti med hjemmesider og tjenester du stoler på.', + waiting: 'Venter', + actions: { + cancel: 'Afvis', + connect: 'Forbind', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts new file mode 100644 index 00000000..a7aa8ada --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts @@ -0,0 +1,13 @@ +const t = { + title: 'Connect {{ account }} to {{dApp}}?', + descriptionP1: + 'This will allow {{dApp}} to see your account address, public balance, transaction history, and suggest transactions to sign.', + descriptionP2: 'You should only connect your account to websites and services you trust.', + waiting: 'Waiting', + actions: { + cancel: 'Cancel', + connect: 'Connect', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/index.ts b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/index.ts new file mode 100644 index 00000000..36fa8deb --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/index.ts @@ -0,0 +1 @@ +export { default } from './ConnectAccountsRequest'; diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 8862e117..fc3921cf 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -32,6 +32,7 @@ import RecoveryFinish from '@popup/pages/Recovery/RecoveryFinish'; import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; +import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; type PromptKey = keyof Omit; @@ -82,6 +83,8 @@ function usePrompt(type: InternalMessageType | MessageType, promptKey: PromptKey export default function Routes() { const handleConnectionResponse = useMessagePrompt(InternalMessageType.Connect, 'connectionRequest'); + const handleConnectAccountsResponse = useMessagePrompt(InternalMessageType.ConnectAccounts, 'connectAccountsRequest'); + const handleSendTransactionResponse = useMessagePrompt>( InternalMessageType.SendTransaction, 'sendTransaction' @@ -148,6 +151,15 @@ export default function Routes() { /> } /> + handleConnectAccountsResponse(true)} + onReject={() => handleConnectAccountsResponse(false)} + /> + } + /> Date: Tue, 23 May 2023 16:31:00 +0200 Subject: [PATCH 008/231] More styling of accounts list --- .../ConnectAccountsRequest.scss | 42 +++++++++++++++++-- .../ConnectAccountsRequest.tsx | 28 ++++++------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss index 1b85261a..a27c6699 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss @@ -1,6 +1,9 @@ .connect-accounts-request { + width: 100%; + margin-top: rem(8px); padding: rem(10px); + padding-top: rem(0px); background: #EAF0F5; border: 1px solid #DAE4EE; @@ -8,8 +11,10 @@ } .connect-accounts-request-accounts { - padding: rem(10px); + padding-left: rem(10px); + padding-right: rem(10px); width: 100%; + height: rem(207px); margin-left: auto; margin-right: auto; @@ -18,7 +23,38 @@ border-top: none; border-radius: 0px 0px 10px 10px; - .item:not(:last-of-type) & { - border-bottom: 1px dotted $color-petroleum; + scrollbar-width: thin; + overflow-y: auto; + + &__account-item { + padding-top: rem(5px); + padding-bottom: rem(5px); + font-weight: $font-weight-light; + + &:not(:last-child) { + border-bottom: rem(1px) dashed #DAE4EE;; + } + + &__primary { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + &__check-box { + position: absolute; + right: rem(0px); + bottom: rem(-12.5px); + width: rem(15px); + + input { + margin: 0; + } + } + + &__secondary { + font-size: rem(8px); + } } } diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 58e39cb9..ab13eb25 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,21 +1,19 @@ -import { absoluteRoutes } from '@popup/constants/routes'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; import { credentialsAtom, selectedAccountAtom, storedConnectedSitesAtom } from '@popup/store/account'; import { sessionPasscodeAtom } from '@popup/store/settings'; import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, Navigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; -import AccountList from '@popup/page-layouts/MainLayout/AccountList/AccountList'; import { WalletCredential } from '@shared/storage/types'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; import AccountInfoListenerContextProvider from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; -import CopyButton from '@popup/shared/CopyButton'; import { displayAsCcd } from 'wallet-common-helpers'; +import { Checkbox } from '@popup/shared/Form/Checkbox'; type Props = { onAllow(): void; @@ -34,20 +32,20 @@ function AccountListItem({ account, checked, selected, identityName }: ItemProps const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); return ( -
-
+
+
{displaySplitAddress(account.address)}{' '}
- e.stopPropagation()} - tabIndex={-1} + { + e.stopPropagation(); + }} />
-
{identityName}
-
{displayAsCcd(totalBalance)}
+
{identityName}
+
{displayAsCcd(totalBalance)}
); } @@ -61,8 +59,8 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const connectedSites = connectedSitesLoading.value; const passcode = useAtomValue(sessionPasscodeAtom); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); - const accounts = useAtomValue(credentialsAtom); - + const accountst = useAtomValue(credentialsAtom); + const accounts = accountst.concat(accountst).concat(accountst); useEffect(() => onClose(onReject), [onClose, onReject]); From 0ca28017a0bbd556b06c07ac902e90197df3b3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Thu, 25 May 2023 12:49:32 +0200 Subject: [PATCH 009/231] Improvements to the UI --- packages/browser-wallet-api/src/wallet-api.ts | 5 ++- .../ExternalRequestLayout.tsx | 3 +- .../ConnectAccountsRequest.scss | 25 +++++------ .../ConnectAccountsRequest.tsx | 45 ++++++++----------- .../browser-wallet/src/popup/shell/Routes.tsx | 5 ++- 5 files changed, 39 insertions(+), 44 deletions(-) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index a162fb5a..8de6b392 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -95,8 +95,9 @@ class WalletApi extends EventEmitter implements IWalletApi { } public async requestAccounts(): Promise { - const response = await this.messageHandler.sendMessage>(MessageType.ConnectAccounts); - console.log(response); + const response = await this.messageHandler.sendMessage>( + MessageType.ConnectAccounts + ); if (!response.success) { throw new Error(response.message); } diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index 63976236..287e03ea 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -7,6 +7,7 @@ import Toast from '@popup/shared/Toast/Toast'; import { useCredential } from '@popup/shared/utils/account-helpers'; import AccountDetails from '@popup/pages/Account/AccountDetails'; +import clsx from 'clsx'; function Header() { const { t } = useTranslation('mainLayout'); @@ -63,7 +64,7 @@ export default function ExternalRequestLayout({ children }: Props) {
{account && } -
{children}
+
{children}
diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss index a27c6699..5d45905b 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss @@ -1,28 +1,25 @@ .connect-accounts-request { width: 100%; - margin-top: rem(8px); padding: rem(10px); - padding-top: rem(0px); - - background: #EAF0F5; - border: 1px solid #DAE4EE; - border-radius: 10px 10px 0px 0px; + padding-top: rem(0); + background: #eaf0f5; + border: 1px solid #dae4ee; + border-radius: 10px 10px 0 0; } .connect-accounts-request-accounts { padding-left: rem(10px); padding-right: rem(10px); width: 100%; - height: rem(207px); + min-height: rem(180px); margin-left: auto; margin-right: auto; - + margin-bottom: rem(15px); background: $color-input-bg; - border: 1px solid #DAE4EE; + border: 1px solid #dae4ee; border-top: none; - border-radius: 0px 0px 10px 10px; - + border-radius: 0 0 10px 10px; scrollbar-width: thin; overflow-y: auto; @@ -32,7 +29,7 @@ font-weight: $font-weight-light; &:not(:last-child) { - border-bottom: rem(1px) dashed #DAE4EE;; + border-bottom: rem(1px) dashed #dae4ee; } &__primary { @@ -44,10 +41,10 @@ &__check-box { position: absolute; - right: rem(0px); + right: rem(0); bottom: rem(-12.5px); width: rem(15px); - + input { margin: 0; } diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index ab13eb25..48b4ab60 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,7 +1,6 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { credentialsAtom, selectedAccountAtom, storedConnectedSitesAtom } from '@popup/store/account'; -import { sessionPasscodeAtom } from '@popup/store/settings'; -import { useAtom, useAtomValue } from 'jotai'; +import { credentialsAtom } from '@popup/store/account'; +import { useAtomValue } from 'jotai'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -22,21 +21,17 @@ type Props = { type ItemProps = { account: WalletCredential; - checked: boolean; - selected: boolean; identityName: string; }; -function AccountListItem({ account, checked, selected, identityName }: ItemProps) { +function AccountListItem({ account, identityName }: ItemProps) { const accountInfo = useAccountInfo(account); const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); return ( -
+
-
- {displaySplitAddress(account.address)}{' '} -
+
{displaySplitAddress(account.address)}
{ @@ -45,7 +40,9 @@ function AccountListItem({ account, checked, selected, identityName }: ItemProps />
{identityName}
-
{displayAsCcd(totalBalance)}
+
+ {displayAsCcd(totalBalance)} +
); } @@ -54,13 +51,8 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const { state } = useLocation(); const { t } = useTranslation('connectionRequest'); const { onClose, withClose } = useContext(fullscreenPromptContext); - const selectedAccount = useAtomValue(selectedAccountAtom); - const [connectedSitesLoading, setConnectedSites] = useAtom(storedConnectedSitesAtom); - const connectedSites = connectedSitesLoading.value; - const passcode = useAtomValue(sessionPasscodeAtom); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); - const accountst = useAtomValue(credentialsAtom); - const accounts = accountst.concat(accountst).concat(accountst); + const accounts = useAtomValue(credentialsAtom); useEffect(() => onClose(onReject), [onClose, onReject]); @@ -71,17 +63,18 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { return (
-
-

{urlDisplay} wants to be added to your allowlist. Do you want to proceed?

+
+

{urlDisplay} wants to be added to your allowlist. Do you want to proceed?

-
+

Allowlisting a service

- Allowlisting a service means that it can request identity proofs and signatures from selected accounts. + Allowlisting a service means that it can request identity proofs and signatures from selected + accounts.
-
+
{accounts.map((account) => { - return + return ; })}
@@ -92,10 +85,10 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index fc3921cf..cbd0243b 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -83,7 +83,10 @@ function usePrompt(type: InternalMessageType | MessageType, promptKey: PromptKey export default function Routes() { const handleConnectionResponse = useMessagePrompt(InternalMessageType.Connect, 'connectionRequest'); - const handleConnectAccountsResponse = useMessagePrompt(InternalMessageType.ConnectAccounts, 'connectAccountsRequest'); + const handleConnectAccountsResponse = useMessagePrompt( + InternalMessageType.ConnectAccounts, + 'connectAccountsRequest' + ); const handleSendTransactionResponse = useMessagePrompt>( InternalMessageType.SendTransaction, From 9718edd330108a63fdf629414ee0d327258ca195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Thu, 25 May 2023 17:01:25 +0200 Subject: [PATCH 010/231] Save connection in storage --- .../ConnectAccountsRequest.tsx | 44 ++++++++++++++++--- .../browser-wallet/src/popup/store/account.ts | 6 +++ .../browser-wallet/src/popup/store/utils.ts | 2 + .../src/shared/storage/access.ts | 5 +++ .../src/shared/storage/types.ts | 5 +++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 48b4ab60..e5b94f0c 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,6 +1,6 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { credentialsAtom } from '@popup/store/account'; -import { useAtomValue } from 'jotai'; +import { credentialsAtom, storedAllowListAtom } from '@popup/store/account'; +import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -22,9 +22,10 @@ type Props = { type ItemProps = { account: WalletCredential; identityName: string; + onToggleChecked: () => void; }; -function AccountListItem({ account, identityName }: ItemProps) { +function AccountListItem({ account, identityName, onToggleChecked }: ItemProps) { const accountInfo = useAccountInfo(account); const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); @@ -37,6 +38,7 @@ function AccountListItem({ account, identityName }: ItemProps) { onClick={(e) => { e.stopPropagation(); }} + onChange={onToggleChecked} />
{identityName}
@@ -53,6 +55,9 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const { onClose, withClose } = useContext(fullscreenPromptContext); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); const accounts = useAtomValue(credentialsAtom); + const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); + + const [accountsToAdd, setAccountsToAdd] = useState([]); useEffect(() => onClose(onReject), [onClose, onReject]); @@ -60,6 +65,30 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const { url } = (state as any).payload; const urlDisplay = displayUrl(url); + /** + * Update the local state containing the list of account addresses + * to be stored in the allowlist for the requesting dApp. If the address + * is already present, then it is removed, if not present then it is added. + * @param accountAddress the account address to add or remove from the list. + */ + function updateAccountAddressChecked(accountAddress: string) { + if (accountsToAdd.includes(accountAddress)) { + const updatedAccounts = accountsToAdd.filter((acc) => acc !== accountAddress); + setAccountsToAdd(updatedAccounts); + } else { + setAccountsToAdd([...accountsToAdd, accountAddress]); + } + } + + async function updateAllowList(url: string) { + const updatedAllowList = { + ...allowListLoading.value + }; + + updatedAllowList[url] = accountsToAdd; + setAllowList(updatedAllowList); + } + return (
@@ -74,7 +103,8 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) {
{accounts.map((account) => { - return ; + return updateAccountAddressChecked(account.address)} + />; })}
@@ -85,10 +115,10 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { diff --git a/packages/browser-wallet/src/popup/store/account.ts b/packages/browser-wallet/src/popup/store/account.ts index 32a94b50..40899b4f 100644 --- a/packages/browser-wallet/src/popup/store/account.ts +++ b/packages/browser-wallet/src/popup/store/account.ts @@ -19,6 +19,12 @@ export const storedConnectedSitesAtom = atomWithChromeStorage>( + ChromeStorageKey.AllowList, + {}, + true +); + const storedAccountAtom = atomWithChromeStorage(ChromeStorageKey.SelectedAccount, undefined); export const selectedAccountAtom = atom>( (get) => get(storedAccountAtom), diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 1e6ba1bc..6d9de2f3 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -26,6 +26,7 @@ import { sessionCookie, sessionOpenPrompt, storedAcceptedTerms, + storedAllowList, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -55,6 +56,7 @@ const accessorMap = { [ChromeStorageKey.Cookie]: useIndexedStorage(sessionCookie, getGenesisHash), [ChromeStorageKey.OpenPrompt]: sessionOpenPrompt, [ChromeStorageKey.AcceptedTerms]: storedAcceptedTerms, + [ChromeStorageKey.AllowList]: storedAllowList, }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index bd9ee3bd..4e10fc7e 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -145,6 +145,11 @@ export const storedTokenMetadata = makeStorageAccessor('local', ChromeStorageKey.AcceptedTerms); +const indexedStoredAllowList = makeIndexedStorageAccessor>( + 'local', + ChromeStorageKey.AllowList +); +export const storedAllowList = useIndexedStorage(indexedStoredAllowList, getGenesisHash); export const sessionOpenPrompt = makeStorageAccessor('session', ChromeStorageKey.OpenPrompt); export const sessionPasscode = makeStorageAccessor('session', ChromeStorageKey.Passcode); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 3d997627..8cfd2221 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -25,6 +25,7 @@ export enum ChromeStorageKey { Cookie = 'cookie', OpenPrompt = 'openPrompt', AcceptedTerms = 'acceptedTerms', + AllowList = 'allowList', } export enum Theme { @@ -253,3 +254,7 @@ export type AcceptedTermsState = { version: string; url?: string; }; + +export interface AllowList { + +} \ No newline at end of file From 0c747d4abf3130b632335da7ed72247f772a4d32 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 26 May 2023 13:35:38 +0200 Subject: [PATCH 011/231] Add settings page for allowlist --- .../browser-wallet/src/assets/svg/link.svg | 3 ++ .../src/popup/constants/routes.ts | 3 ++ .../page-layouts/MainLayout/Header/Header.tsx | 3 ++ .../popup/page-layouts/MainLayout/i18n/da.ts | 1 + .../popup/page-layouts/MainLayout/i18n/en.ts | 1 + .../src/popup/pages/Allowlist/Allowlist.scss | 19 +++++++++++ .../src/popup/pages/Allowlist/Allowlist.tsx | 32 +++++++++++++++++++ .../src/popup/pages/Allowlist/index.ts | 1 + .../ConnectAccountsRequest.scss | 10 +++--- .../ConnectAccountsRequest.tsx | 21 +++++++----- .../src/popup/pages/Settings/Settings.tsx | 3 ++ .../src/popup/pages/Settings/i18n/da.ts | 1 + .../src/popup/pages/Settings/i18n/en.ts | 1 + .../browser-wallet/src/popup/shell/Routes.tsx | 2 ++ .../src/popup/styles/_components.scss | 1 + .../src/shared/storage/types.ts | 4 --- 16 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/link.svg create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/index.ts diff --git a/packages/browser-wallet/src/assets/svg/link.svg b/packages/browser-wallet/src/assets/svg/link.svg new file mode 100644 index 00000000..608b65d2 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index c20a3219..d12a7084 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -20,6 +20,9 @@ export const relativeRoutes = { }, settings: { path: 'settings', + allowList: { + path: 'allowlist', + }, passcode: { path: 'passcode', }, diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx index a8d469a6..e4e38111 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx @@ -68,6 +68,9 @@ function getTitle(section: Section, pathname: string) { case Section.Id: return 'header.ids'; case Section.Settings: { + if (pathname.startsWith(absoluteRoutes.home.settings.allowList.path)) { + return 'header.settings.allowlist'; + } if (pathname.startsWith(absoluteRoutes.home.settings.recovery.path)) { return 'header.settings.recovery'; } diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index bda88e32..91ae920a 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -11,6 +11,7 @@ const t: typeof en = { ids: 'ID kort', settings: { main: 'Wallet indstillinger', + allowlist: 'Tilladelsesliste', recovery: 'Genskabning af din wallet', network: 'Netværksindstillinger', passcode: 'Skift adgangskode', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index e65be61d..673646bf 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -9,6 +9,7 @@ const t = { ids: 'ID cards', settings: { main: 'Wallet settings', + allowlist: 'Allowlist', recovery: 'Restoring your wallet', network: 'Network settings', passcode: 'Change passcode', diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss new file mode 100644 index 00000000..ad70dee8 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -0,0 +1,19 @@ +.allowlist-settings-page { + height: 100%; + width: 100%; + background-color: $color-bg; + padding: 0 rem(10px); + + &__link-icon { + width: rem(20px); + height: rem(10px); + } + + &__list-item { + padding: rem(10px) 0; + + &:not(:last-child) { + border-bottom: rem(1px) dashed $color-petroleum; + } + } +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx new file mode 100644 index 00000000..881820d3 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import SidedRow from '@popup/shared/SidedRow/SidedRow'; +import { storedAllowListAtom } from '@popup/store/account'; +import { useAtomValue } from 'jotai'; +import LinkIcon from '@assets/svg/link.svg'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; + +function EmptyAllowlistPage() { + return
; +} + +export default function Allowlist() { + const allowlistWithLoading = useAtomValue(storedAllowListAtom); + const allowlist = allowlistWithLoading.value; + + if (allowlistWithLoading.loading) { + return ; + } + + return ( +
+ {Object.keys(allowlist).map((serviceName) => ( +
+ } + /> +
+ ))} +
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/index.ts b/packages/browser-wallet/src/popup/pages/Allowlist/index.ts new file mode 100644 index 00000000..d6aaaa62 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/index.ts @@ -0,0 +1 @@ +export { default } from './Allowlist'; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss index 5d45905b..4dac754b 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss @@ -4,22 +4,22 @@ padding: rem(10px); padding-top: rem(0); background: #eaf0f5; - border: 1px solid #dae4ee; - border-radius: 10px 10px 0 0; + border: rem(1px) solid #dae4ee; + border-radius: rem(10px) rem(10px) 0 0; } .connect-accounts-request-accounts { padding-left: rem(10px); padding-right: rem(10px); width: 100%; - min-height: rem(180px); + min-height: rem(175px); margin-left: auto; margin-right: auto; margin-bottom: rem(15px); background: $color-input-bg; - border: 1px solid #dae4ee; + border: rem(1px) solid #dae4ee; border-top: none; - border-radius: 0 0 10px 10px; + border-radius: 0 0 rem(10px) rem(10px); scrollbar-width: thin; overflow-y: auto; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index e5b94f0c..04de5695 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -80,13 +80,12 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { } } - async function updateAllowList(url: string) { - const updatedAllowList = { - ...allowListLoading.value + async function updateAllowList(urlToUpdate: string) { + const updatedAllowList: Record = { + ...allowListLoading.value, }; - - updatedAllowList[url] = accountsToAdd; - setAllowList(updatedAllowList); + updatedAllowList[urlToUpdate] = accountsToAdd; + await setAllowList(updatedAllowList); } return ( @@ -103,8 +102,14 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) {
{accounts.map((account) => { - return updateAccountAddressChecked(account.address)} - />; + return ( + updateAccountAddressChecked(account.address)} + /> + ); })}
diff --git a/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx b/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx index 035de1ce..b87f6c6b 100644 --- a/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx +++ b/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx @@ -32,6 +32,9 @@ export default function Settings() { return (
+ + {t('allowlist')} + {t('passcode')} diff --git a/packages/browser-wallet/src/popup/pages/Settings/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Settings/i18n/da.ts index d3373438..96f5c6da 100644 --- a/packages/browser-wallet/src/popup/pages/Settings/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Settings/i18n/da.ts @@ -1,6 +1,7 @@ import type en from './en'; const t: typeof en = { + allowlist: 'Tilladelsesliste', passcode: 'Skift adgangskode', network: 'Netværksindstillinger', recover: 'Genskab ID & konti', diff --git a/packages/browser-wallet/src/popup/pages/Settings/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Settings/i18n/en.ts index cbf92436..020de0b3 100644 --- a/packages/browser-wallet/src/popup/pages/Settings/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Settings/i18n/en.ts @@ -1,4 +1,5 @@ const t = { + allowlist: 'Allowlist', passcode: 'Change passcode', network: 'Network settings', recover: 'Restore IDs & accounts', diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index cbd0243b..1a9bfd87 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -33,6 +33,7 @@ import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; +import Allowlist from '@popup/pages/Allowlist'; type PromptKey = keyof Omit; @@ -189,6 +190,7 @@ export default function Routes() { } path={relativeRoutes.home.identities.path} /> } /> + } path={relativeRoutes.home.settings.allowList.path} /> } path={relativeRoutes.home.settings.passcode.path} /> } path={relativeRoutes.home.settings.network.path} /> } path={relativeRoutes.home.settings.recovery.path} /> diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index 52f84d6d..8c12c48a 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -36,6 +36,7 @@ @import '../pages/AddAccount/AddAccount'; @import '../pages/NetworkSettings/NetworkSettings'; @import '../pages/ChangePasscode/ChangePasscode'; +@import '../pages/Allowlist/Allowlist'; @import '../pages/Recovery/Recovery'; @import '../pages/ExternalAddTokens/ExternalAddTokens'; @import '../pages/TermsAndConditions/TermsAndConditions'; diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 8cfd2221..90fc869c 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -254,7 +254,3 @@ export type AcceptedTermsState = { version: string; url?: string; }; - -export interface AllowList { - -} \ No newline at end of file From 5624cc0642fef88cf5114cb87bc2d5c9cc45114a Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 26 May 2023 14:51:37 +0200 Subject: [PATCH 012/231] Refactor allowlist editor --- .../src/popup/pages/Allowlist/Allowlist.scss | 49 +++++++++++ .../popup/pages/Allowlist/AllowlistEditor.tsx | 85 +++++++++++++++++++ .../ConnectAccountsRequest.scss | 48 ----------- .../ConnectAccountsRequest.tsx | 84 ++---------------- 4 files changed, 141 insertions(+), 125 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index ad70dee8..6bb700fa 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -17,3 +17,52 @@ } } } + +.connect-accounts-request-accounts { + padding-left: rem(10px); + padding-right: rem(10px); + width: 100%; + min-height: rem(175px); + height: rem(175px); + margin-left: auto; + margin-right: auto; + margin-bottom: rem(15px); + background: $color-input-bg; + border: rem(1px) solid #dae4ee; + border-top: none; + border-radius: 0 0 rem(10px) rem(10px); + scrollbar-width: thin; + overflow-y: auto; + + &__account-item { + padding-top: rem(5px); + padding-bottom: rem(5px); + font-weight: $font-weight-light; + + &:not(:last-child) { + border-bottom: rem(1px) dashed #dae4ee; + } + + &__primary { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + &__check-box { + position: absolute; + right: rem(0); + bottom: rem(-12.5px); + width: rem(15px); + + input { + margin: 0; + } + } + + &__secondary { + font-size: rem(8px); + } + } +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx new file mode 100644 index 00000000..b806d1d9 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -0,0 +1,85 @@ +import AccountInfoListenerContextProvider, { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import React, { useMemo } from 'react'; +import { Checkbox } from '@popup/shared/Form/Checkbox'; +import { WalletCredential } from '@shared/storage/types'; +import { displayAsCcd } from 'wallet-common-helpers'; +import { useAtomValue } from 'jotai'; +import { credentialsAtom } from '@popup/store/account'; + +type ItemProps = { + account: WalletCredential; + identityName: string; + onToggleChecked: () => void; +}; + +function AccountListItem({ account, identityName, onToggleChecked }: ItemProps) { + const accountInfo = useAccountInfo(account); + const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); + + return ( +
+
+
{displaySplitAddress(account.address)}
+ { + e.stopPropagation(); + }} + onChange={onToggleChecked} + /> +
+
{identityName}
+
+ {displayAsCcd(totalBalance)} +
+
+ ); +} + +export default function AllowlistEditor({ + selectedAccounts, + setSelectedAccounts, +}: { + selectedAccounts: string[]; + setSelectedAccounts: React.Dispatch>; +}) { + const accounts = useAtomValue(credentialsAtom); + + /** + * Update the local state containing the list of account addresses. If the address + * is already present, then it is removed, if not present then it is added. + * @param accountAddress the account address to add or remove from the list. + */ + function updateAccountAddressChecked(accountAddress: string) { + if (selectedAccounts.includes(accountAddress)) { + const updatedAccounts = selectedAccounts.filter((acc) => acc !== accountAddress); + setSelectedAccounts(updatedAccounts); + } else { + setSelectedAccounts([...selectedAccounts, accountAddress]); + } + } + + return ( +
+
+

Allowlisting a service

+ Allowlisting a service means that it can request identity proofs and signatures from selected accounts. +
+
+ + {accounts.map((account) => { + return ( + updateAccountAddressChecked(account.address)} + /> + ); + })} + +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss index 4dac754b..9eaa9e71 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss @@ -7,51 +7,3 @@ border: rem(1px) solid #dae4ee; border-radius: rem(10px) rem(10px) 0 0; } - -.connect-accounts-request-accounts { - padding-left: rem(10px); - padding-right: rem(10px); - width: 100%; - min-height: rem(175px); - margin-left: auto; - margin-right: auto; - margin-bottom: rem(15px); - background: $color-input-bg; - border: rem(1px) solid #dae4ee; - border-top: none; - border-radius: 0 0 rem(10px) rem(10px); - scrollbar-width: thin; - overflow-y: auto; - - &__account-item { - padding-top: rem(5px); - padding-bottom: rem(5px); - font-weight: $font-weight-light; - - &:not(:last-child) { - border-bottom: rem(1px) dashed #dae4ee; - } - - &__primary { - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - } - - &__check-box { - position: absolute; - right: rem(0); - bottom: rem(-12.5px); - width: rem(15px); - - input { - margin: 0; - } - } - - &__secondary { - font-size: rem(8px); - } - } -} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 04de5695..fabc45fb 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,85 +1,33 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { credentialsAtom, storedAllowListAtom } from '@popup/store/account'; -import { useAtom, useAtomValue } from 'jotai'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { storedAllowListAtom } from '@popup/store/account'; +import { useAtom } from 'jotai'; +import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; -import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; -import { WalletCredential } from '@shared/storage/types'; -import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; -import AccountInfoListenerContextProvider from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; -import { displayAsCcd } from 'wallet-common-helpers'; -import { Checkbox } from '@popup/shared/Form/Checkbox'; +import AllowlistEditor from '../Allowlist/AllowlistEditor'; type Props = { onAllow(): void; onReject(): void; }; -type ItemProps = { - account: WalletCredential; - identityName: string; - onToggleChecked: () => void; -}; - -function AccountListItem({ account, identityName, onToggleChecked }: ItemProps) { - const accountInfo = useAccountInfo(account); - const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); - - return ( -
-
-
{displaySplitAddress(account.address)}
- { - e.stopPropagation(); - }} - onChange={onToggleChecked} - /> -
-
{identityName}
-
- {displayAsCcd(totalBalance)} -
-
- ); -} - export default function ConnectionRequest({ onAllow, onReject }: Props) { const { state } = useLocation(); const { t } = useTranslation('connectionRequest'); const { onClose, withClose } = useContext(fullscreenPromptContext); + const [accountsToAdd, setAccountsToAdd] = useState([]); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); - const accounts = useAtomValue(credentialsAtom); const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); - const [accountsToAdd, setAccountsToAdd] = useState([]); - useEffect(() => onClose(onReject), [onClose, onReject]); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { url } = (state as any).payload; const urlDisplay = displayUrl(url); - /** - * Update the local state containing the list of account addresses - * to be stored in the allowlist for the requesting dApp. If the address - * is already present, then it is removed, if not present then it is added. - * @param accountAddress the account address to add or remove from the list. - */ - function updateAccountAddressChecked(accountAddress: string) { - if (accountsToAdd.includes(accountAddress)) { - const updatedAccounts = accountsToAdd.filter((acc) => acc !== accountAddress); - setAccountsToAdd(updatedAccounts); - } else { - setAccountsToAdd([...accountsToAdd, accountAddress]); - } - } - async function updateAllowList(urlToUpdate: string) { const updatedAllowList: Record = { ...allowListLoading.value, @@ -92,27 +40,9 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) {
-

{urlDisplay} wants to be added to your allowlist. Do you want to proceed?

+

{urlDisplay} wants to be added to your allowlist.

-
-

Allowlisting a service

- Allowlisting a service means that it can request identity proofs and signatures from selected - accounts. -
-
- - {accounts.map((account) => { - return ( - updateAccountAddressChecked(account.address)} - /> - ); - })} - -
+
+ +
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index 6bb700fa..95f8e3b8 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -1,3 +1,14 @@ +.empty-allowlist-settings-page { + height: 100%; + width: 100%; + background-color: $color-bg; + padding: 0 rem(10px); + display: flex; + align-items: center; + text-align: center; + color: $color-text-faded; +} + .allowlist-settings-page { height: 100%; width: 100%; @@ -66,3 +77,12 @@ } } } + +.allow-list-edit-page { + height: 100%; + padding: rem(10px); + background-color: $color-bg; + display: flex; + flex-direction: column; + justify-content: space-between; +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index 881820d3..e4dcc084 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -4,29 +4,59 @@ import { storedAllowListAtom } from '@popup/store/account'; import { useAtomValue } from 'jotai'; import LinkIcon from '@assets/svg/link.svg'; import { displayUrl } from '@popup/shared/utils/string-helpers'; +import Button from '@popup/shared/Button'; +import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; +import { allowlistRoutes } from './routes'; +import AllowListView from './AllowListView'; -function EmptyAllowlistPage() { +function LoadingAllowlistPage() { return
; } -export default function Allowlist() { +function EmptyAllowlistPage() { + return ( +
You do not currently have any services in your allowlist.
+ ); +} + +function Allowlist() { + const nav = useNavigate(); const allowlistWithLoading = useAtomValue(storedAllowListAtom); const allowlist = allowlistWithLoading.value; if (allowlistWithLoading.loading) { + return ; + } + if (Object.entries(allowlist).length === 0) { return ; } return (
- {Object.keys(allowlist).map((serviceName) => ( -
- } - /> -
- ))} + {Object.keys(allowlist).map((serviceName) => { + const path = generatePath(allowlistRoutes.edit, { serviceName: encodeURIComponent(serviceName) }); + return ( +
+ nav(path)}> + + + } + /> +
+ ); + })}
); } + +export default function AllowListRoutes() { + return ( + + } /> + } /> + + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index b806d1d9..ad5ce00f 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -10,10 +10,11 @@ import { credentialsAtom } from '@popup/store/account'; type ItemProps = { account: WalletCredential; identityName: string; + checked: boolean; onToggleChecked: () => void; }; -function AccountListItem({ account, identityName, onToggleChecked }: ItemProps) { +function AccountListItem({ account, identityName, checked, onToggleChecked }: ItemProps) { const accountInfo = useAccountInfo(account); const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); @@ -26,6 +27,7 @@ function AccountListItem({ account, identityName, onToggleChecked }: ItemProps) onClick={(e) => { e.stopPropagation(); }} + checked={checked} onChange={onToggleChecked} />
@@ -74,6 +76,7 @@ export default function AllowlistEditor({ key={account.address} account={account} identityName="SomeName" + checked={selectedAccounts.includes(account.address)} onToggleChecked={() => updateAccountAddressChecked(account.address)} /> ); diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/routes.ts b/packages/browser-wallet/src/popup/pages/Allowlist/routes.ts new file mode 100644 index 00000000..dcd62945 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/routes.ts @@ -0,0 +1,7 @@ +/** + * Used for outlet on the account page. + */ +export const allowlistRoutes = { + index: '/', + edit: 'edit/:serviceName/*', +}; diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/util.ts b/packages/browser-wallet/src/popup/pages/Allowlist/util.ts new file mode 100644 index 00000000..d246caf2 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/util.ts @@ -0,0 +1,12 @@ +export async function updateAllowList( + urlToUpdate: string, + allowlist: Record, + accountsToAdd: string[], + setAllowList: (update: Record) => Promise +) { + const updatedAllowList: Record = { + ...allowlist, + }; + updatedAllowList[urlToUpdate] = accountsToAdd; + await setAllowList(updatedAllowList); +} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index fabc45fb..b8667f1b 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -8,6 +8,7 @@ import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import AllowlistEditor from '../Allowlist/AllowlistEditor'; +import { updateAllowList } from '../Allowlist/util'; type Props = { onAllow(): void; @@ -28,14 +29,6 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const { url } = (state as any).payload; const urlDisplay = displayUrl(url); - async function updateAllowList(urlToUpdate: string) { - const updatedAllowList: Record = { - ...allowListLoading.value, - }; - updatedAllowList[urlToUpdate] = accountsToAdd; - await setAllowList(updatedAllowList); - } - return (
@@ -52,7 +45,12 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { disabled={connectButtonDisabled} onClick={() => { setConnectButtonDisabled(true); - updateAllowList(new URL(url).origin).then(withClose(onAllow)); + updateAllowList( + new URL(url).origin, + allowListLoading.value, + accountsToAdd, + setAllowList + ).then(withClose(onAllow)); }} > {t('actions.connect')} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 1a9bfd87..f3ac46dd 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -33,7 +33,7 @@ import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; -import Allowlist from '@popup/pages/Allowlist'; +import AllowListRoutes from '@popup/pages/Allowlist'; type PromptKey = keyof Omit; @@ -190,7 +190,7 @@ export default function Routes() { } path={relativeRoutes.home.identities.path} /> } /> - } path={relativeRoutes.home.settings.allowList.path} /> + } path={`${relativeRoutes.home.settings.allowList.path}/*`} /> } path={relativeRoutes.home.settings.passcode.path} /> } path={relativeRoutes.home.settings.network.path} /> } path={relativeRoutes.home.settings.recovery.path} /> From 79c03351c0fa7beed4ea2f13e74d28f5f4c91321 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 30 May 2023 12:55:14 +0200 Subject: [PATCH 014/231] Add translations --- .../src/popup/pages/Allowlist/AllowListView.tsx | 6 ++++-- .../src/popup/pages/Allowlist/Allowlist.tsx | 6 +++--- .../popup/pages/Allowlist/AllowlistEditor.tsx | 6 ++++-- .../src/popup/pages/Allowlist/i18n/da.ts | 16 ++++++++++++++++ .../src/popup/pages/Allowlist/i18n/en.ts | 14 ++++++++++++++ .../ConnectAccountsRequest.tsx | 4 ++-- .../pages/ConnectAccountsRequest/i18n/da.ts | 9 +++------ .../pages/ConnectAccountsRequest/i18n/en.ts | 6 +----- .../src/popup/shell/i18n/locales/da.ts | 4 ++++ .../src/popup/shell/i18n/locales/en.ts | 4 ++++ 10 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx index 9b1b4e7f..1e2f24aa 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx @@ -4,11 +4,13 @@ import Button from '@popup/shared/Button/Button'; import { storedAllowListAtom } from '@popup/store/account'; import { useAtom } from 'jotai'; import { absoluteRoutes } from '@popup/constants/routes'; +import { useTranslation } from 'react-i18next'; import AllowlistEditor from './AllowlistEditor'; import { updateAllowList } from './util'; export default function AllowListView() { const nav = useNavigate(); + const { t } = useTranslation('allowlist', { keyPrefix: 'view' }); const [selectedAccounts, setSelectedAccounts] = useState([]); const { serviceName } = useParams<{ serviceName: string }>(); const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); @@ -47,7 +49,7 @@ export default function AllowListView() { ) } > - Update allowlist + {t('update')}
); diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index e4dcc084..1b10e81e 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -6,6 +6,7 @@ import LinkIcon from '@assets/svg/link.svg'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import Button from '@popup/shared/Button'; import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { allowlistRoutes } from './routes'; import AllowListView from './AllowListView'; @@ -14,9 +15,8 @@ function LoadingAllowlistPage() { } function EmptyAllowlistPage() { - return ( -
You do not currently have any services in your allowlist.
- ); + const { t } = useTranslation('allowlist'); + return
{t('empty')}
; } function Allowlist() { diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index ad5ce00f..77ef6acc 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -6,6 +6,7 @@ import { WalletCredential } from '@shared/storage/types'; import { displayAsCcd } from 'wallet-common-helpers'; import { useAtomValue } from 'jotai'; import { credentialsAtom } from '@popup/store/account'; +import { useTranslation } from 'react-i18next'; type ItemProps = { account: WalletCredential; @@ -46,6 +47,7 @@ export default function AllowlistEditor({ selectedAccounts: string[]; setSelectedAccounts: React.Dispatch>; }) { + const { t } = useTranslation('allowlist', { keyPrefix: 'editor' }); const accounts = useAtomValue(credentialsAtom); /** @@ -65,8 +67,8 @@ export default function AllowlistEditor({ return (
-

Allowlisting a service

- Allowlisting a service means that it can request identity proofs and signatures from selected accounts. +

{t('header')}

+ {t('description')}
diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts new file mode 100644 index 00000000..0ea0e863 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts @@ -0,0 +1,16 @@ +import type en from './en'; + +const t: typeof en = { + empty: 'Du har på nuværende tidspunkt ikke allowlisted nogen service.', + editor: { + header: 'Allowlisting af en service', + description: + 'At allowliste en service betyder at den kan anmode om identitetsbeviser, og signaturer fra de valgte konti.', + }, + view: { + remove: 'Fjern service fra allowlisten', + update: 'Opdater allowlist', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts new file mode 100644 index 00000000..1ed34607 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts @@ -0,0 +1,14 @@ +const t = { + empty: 'You do not currently have any services in your allowlist.', + editor: { + header: 'Allowlisting a service', + description: + 'Allowlisting a service means that it can request identity proofs and signatures from selected accounts.', + }, + view: { + remove: 'Remove service from allowlist', + update: 'Update allowlist', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index b8667f1b..301fa43a 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -17,7 +17,7 @@ type Props = { export default function ConnectionRequest({ onAllow, onReject }: Props) { const { state } = useLocation(); - const { t } = useTranslation('connectionRequest'); + const { t } = useTranslation('connectAccountsRequest'); const { onClose, withClose } = useContext(fullscreenPromptContext); const [accountsToAdd, setAccountsToAdd] = useState([]); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); @@ -33,7 +33,7 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) {
-

{urlDisplay} wants to be added to your allowlist.

+

{t('header', { url: urlDisplay })}

diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts index 10e1cf54..2d000f43 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/da.ts @@ -1,13 +1,10 @@ import type en from './en'; const t: typeof en = { - title: 'Forbindelsesforespørgsel', - descriptionP1: 'Dette vil tillade {{dApp}} at se dine konti og foreslå transaktioner til at blive underskrevet.', - descriptionP2: 'Du bør kun forbinde dine konti med hjemmesider og tjenester du stoler på.', - waiting: 'Venter', + header: '{{ url }} vil gerne tilføjes til din allowlist', actions: { - cancel: 'Afvis', - connect: 'Forbind', + cancel: 'Cancel', + connect: 'Connect', }, }; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts index a7aa8ada..76c85713 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/i18n/en.ts @@ -1,9 +1,5 @@ const t = { - title: 'Connect {{ account }} to {{dApp}}?', - descriptionP1: - 'This will allow {{dApp}} to see your account address, public balance, transaction history, and suggest transactions to sign.', - descriptionP2: 'You should only connect your account to websites and services you trust.', - waiting: 'Waiting', + header: '{{ url }} wants to be added to your allowlist', actions: { cancel: 'Cancel', connect: 'Connect', diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 6d3d09bf..1f9eba3a 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -18,6 +18,8 @@ import changePasscode from '@popup/pages/ChangePasscode/i18n/da'; import externalAddTokens from '@popup/pages/ExternalAddTokens/i18n/da'; import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/da'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; +import allowlist from '@popup/pages/Allowlist/i18n/da'; +import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; import type en from './en'; @@ -42,6 +44,8 @@ const t: typeof en = { externalAddTokens, termsAndConditions, idProofRequest, + allowlist, + connectAccountsRequest, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 731a5f8e..ae78022e 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -18,6 +18,8 @@ import changePasscode from '@popup/pages/ChangePasscode/i18n/en'; import externalAddTokens from '@popup/pages/ExternalAddTokens/i18n/en'; import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/en'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; +import allowlist from '@popup/pages/Allowlist/i18n/en'; +import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; const t = { shared, @@ -40,6 +42,8 @@ const t = { externalAddTokens, termsAndConditions, idProofRequest, + allowlist, + connectAccountsRequest, }; export default t; From 65c364001ce8912f291bb78dbd6b7506e30dae7b Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 30 May 2023 13:32:28 +0200 Subject: [PATCH 015/231] Create hook for getting identity name --- .../MainLayout/AccountList/AccountList.tsx | 17 ++++------------- .../Account/AccountDetails/AccountDetails.tsx | 11 +++-------- .../popup/pages/Allowlist/AllowlistEditor.tsx | 7 +++---- .../src/popup/shared/utils/account-helpers.ts | 17 ++++++++++++++++- .../browser-wallet/src/popup/store/identity.ts | 13 +------------ 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/AccountList/AccountList.tsx b/packages/browser-wallet/src/popup/page-layouts/MainLayout/AccountList/AccountList.tsx index 79f56c7d..fa0600a4 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/AccountList/AccountList.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/AccountList/AccountList.tsx @@ -9,9 +9,8 @@ import BakerIcon from '@assets/svg/baker.svg'; import DelegationIcon from '@assets/svg/delegation.svg'; import { absoluteRoutes } from '@popup/constants/routes'; import { credentialsAtom, selectedAccountAtom } from '@popup/store/account'; -import { identityNamesAtom } from '@popup/store/identity'; import { useTranslation } from 'react-i18next'; -import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; import { WalletCredential } from '@shared/storage/types'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; import { isDelegatorAccount, isBakerAccount, AccountInfo } from '@concordium/web-sdk'; @@ -23,7 +22,6 @@ type ItemProps = { account: WalletCredential; checked: boolean; selected: boolean; - identityName: string; }; function BakerOrDelegatorIcon({ @@ -44,9 +42,10 @@ function BakerOrDelegatorIcon({ return null; } -function AccountListItem({ account, checked, selected, identityName }: ItemProps) { +function AccountListItem({ account, checked, selected }: ItemProps) { const accountInfo = useAccountInfo(account); const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); + const identityName = useIdentityName(account, 'Unknown'); return (
@@ -79,7 +78,6 @@ const AccountList = forwardRef(({ className, onSelect }, const [selectedAccount, setSelectedAccount] = useAtom(selectedAccountAtom); const nav = useNavigate(); const { t } = useTranslation('mainLayout'); - const identityNames = useAtomValue(identityNamesAtom); return ( @@ -95,14 +93,7 @@ const AccountList = forwardRef(({ className, onSelect }, ref={ref} searchableKeys={['address']} > - {(a, checked) => ( - - )} + {(a, checked) => } ); }); diff --git a/packages/browser-wallet/src/popup/pages/Account/AccountDetails/AccountDetails.tsx b/packages/browser-wallet/src/popup/pages/Account/AccountDetails/AccountDetails.tsx index 981fd1a2..85c5501b 100644 --- a/packages/browser-wallet/src/popup/pages/Account/AccountDetails/AccountDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/AccountDetails/AccountDetails.tsx @@ -2,10 +2,7 @@ import clsx from 'clsx'; import React, { useEffect, useState } from 'react'; import { displayAsCcd, getPublicAccountAmounts, PublicAccountAmounts } from 'wallet-common-helpers'; import { useTranslation } from 'react-i18next'; -import { useAtomValue } from 'jotai'; -import { identityNamesAtom } from '@popup/store/identity'; - -import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; import VerifiedIcon from '@assets/svg/verified-stamp.svg'; import { CreationStatus, WalletCredential } from '@shared/storage/types'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; @@ -40,7 +37,7 @@ const zeroBalance: Omit = { export default function AccountDetails({ expanded, account, className }: Props) { const { t } = useTranslation('account', { keyPrefix: 'details' }); const [balances, setBalances] = useState>(zeroBalance); - const identityNames = useAtomValue(identityNamesAtom); + const identityName = useIdentityName(account); const accountInfo = useAccountInfo(account); useEffect(() => { @@ -72,9 +69,7 @@ export default function AccountDetails({ expanded, account, className }: Props) return (
{displaySplitAddress(account.address)}
-
- {identityNames?.[account.providerIndex]?.[account.identityIndex]} -
+
{identityName}
diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index 77ef6acc..381709b4 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -1,5 +1,5 @@ import AccountInfoListenerContextProvider, { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; -import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; import React, { useMemo } from 'react'; import { Checkbox } from '@popup/shared/Form/Checkbox'; import { WalletCredential } from '@shared/storage/types'; @@ -10,13 +10,13 @@ import { useTranslation } from 'react-i18next'; type ItemProps = { account: WalletCredential; - identityName: string; checked: boolean; onToggleChecked: () => void; }; -function AccountListItem({ account, identityName, checked, onToggleChecked }: ItemProps) { +function AccountListItem({ account, checked, onToggleChecked }: ItemProps) { const accountInfo = useAccountInfo(account); + const identityName = useIdentityName(account, 'Identity'); const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); return ( @@ -77,7 +77,6 @@ export default function AllowlistEditor({ updateAccountAddressChecked(account.address)} /> diff --git a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts index e6ed407a..cd700276 100644 --- a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts @@ -1,7 +1,7 @@ import { credentialsAtom, selectedAccountAtom } from '@popup/store/account'; import { networkConfigurationAtom } from '@popup/store/settings'; import { useAtomValue } from 'jotai'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { identitiesAtom } from '@popup/store/identity'; import { AccountInfo, ConcordiumHdWallet } from '@concordium/web-sdk'; import { WalletCredential } from '@shared/storage/types'; @@ -25,6 +25,21 @@ export function useIdentityOf(cred?: WalletCredential) { return identity; } +export function useIdentityName(credential: WalletCredential, fallback?: string) { + const [identityName, setIdentityName] = useState(); + const identity = useIdentityOf(credential); + + useEffect(() => { + if (identity) { + setIdentityName(identity.name); + } else if (fallback !== undefined) { + setIdentityName(fallback); + } + }, [identity]); + + return identityName; +} + export function useCredential(accountAddress?: string) { const credentials = useAtomValue(credentialsAtom); diff --git a/packages/browser-wallet/src/popup/store/identity.ts b/packages/browser-wallet/src/popup/store/identity.ts index 733023c8..7f220302 100644 --- a/packages/browser-wallet/src/popup/store/identity.ts +++ b/packages/browser-wallet/src/popup/store/identity.ts @@ -9,7 +9,7 @@ import { SessionPendingIdentity, } from '@shared/storage/types'; import { Atom, atom, WritableAtom } from 'jotai'; -import { atomFamily, selectAtom } from 'jotai/utils'; +import { atomFamily } from 'jotai/utils'; import { credentialsAtomWithLoading } from './account'; import { AsyncWrapper, atomWithChromeStorage } from './utils'; @@ -44,17 +44,6 @@ export const selectedIdentityAtom = atom(ChromeStorageKey.IdentityProviders, []); -export const identityNamesAtom = selectAtom(identitiesAtom, (identities) => { - const map = {} as Record>; - identities.forEach((identity) => { - if (!map[identity.providerIndex]) { - map[identity.providerIndex] = {} as Record; - } - map[identity.providerIndex][identity.index] = identity.name; - }); - return map; -}); - export const isRecoveringAtom = atomWithChromeStorage(ChromeStorageKey.IsRecovering, false, true); const recoveryStatusAtom = atomWithChromeStorage( ChromeStorageKey.RecoveryStatus, From 3ca5e39de929972f1319e02ca52c8a150132e02e Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 30 May 2023 13:44:54 +0200 Subject: [PATCH 016/231] Fix go back issue --- .../src/popup/pages/Allowlist/AllowListView.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx index 1e2f24aa..a06fc86a 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx @@ -3,7 +3,6 @@ import { useNavigate, useParams } from 'react-router-dom'; import Button from '@popup/shared/Button/Button'; import { storedAllowListAtom } from '@popup/store/account'; import { useAtom } from 'jotai'; -import { absoluteRoutes } from '@popup/constants/routes'; import { useTranslation } from 'react-i18next'; import AllowlistEditor from './AllowlistEditor'; import { updateAllowList } from './util'; @@ -45,7 +44,7 @@ export default function AllowListView() { width="wide" onClick={() => updateAllowList(decodedServiceName, allowListLoading.value, selectedAccounts, setAllowList).then( - () => nav(absoluteRoutes.home.settings.allowList.path) + () => nav(-1) ) } > @@ -54,11 +53,7 @@ export default function AllowListView() { From a656992a3ac2b39b14d33963ffc7ef424eab7b21 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 30 May 2023 14:40:16 +0200 Subject: [PATCH 017/231] Refactor and cleanup --- .../popup/pages/Allowlist/AllowListView.tsx | 62 -------- .../src/popup/pages/Allowlist/Allowlist.scss | 115 ++++++++------- .../src/popup/pages/Allowlist/Allowlist.tsx | 12 +- .../popup/pages/Allowlist/AllowlistEditor.tsx | 136 ++++++++---------- .../pages/Allowlist/AllowlistEntryView.tsx | 94 ++++++++++++ .../src/popup/pages/Allowlist/i18n/da.ts | 5 +- .../src/popup/pages/Allowlist/i18n/en.ts | 9 +- .../ConnectAccountsRequest.scss | 9 -- .../ConnectAccountsRequest.tsx | 10 +- .../src/popup/styles/_components.scss | 1 - 10 files changed, 243 insertions(+), 210 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx delete mode 100644 packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx deleted file mode 100644 index a06fc86a..00000000 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowListView.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import Button from '@popup/shared/Button/Button'; -import { storedAllowListAtom } from '@popup/store/account'; -import { useAtom } from 'jotai'; -import { useTranslation } from 'react-i18next'; -import AllowlistEditor from './AllowlistEditor'; -import { updateAllowList } from './util'; - -export default function AllowListView() { - const nav = useNavigate(); - const { t } = useTranslation('allowlist', { keyPrefix: 'view' }); - const [selectedAccounts, setSelectedAccounts] = useState([]); - const { serviceName } = useParams<{ serviceName: string }>(); - const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); - - if (!serviceName) { - throw new Error('Invalid URL - the service name should be part of the URL.'); - } - - const decodedServiceName = decodeURIComponent(serviceName); - - useEffect(() => { - if (!allowListLoading.loading) { - setSelectedAccounts(allowListLoading.value[serviceName]); - } - }, [allowListLoading]); - - async function removeService(serviceNameToRemove: string, allowlist: Record) { - const updatedAllowlist = { ...allowlist }; - delete updatedAllowlist[serviceNameToRemove]; - await setAllowList(updatedAllowlist); - } - - if (allowListLoading.loading) { - return
; - } - - return ( -
- - - -
- ); -} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index 95f8e3b8..ad86fb1a 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -1,20 +1,12 @@ -.empty-allowlist-settings-page { +.allowlist-page, +.allowlist-empty { height: 100%; width: 100%; background-color: $color-bg; padding: 0 rem(10px); - display: flex; - align-items: center; - text-align: center; - color: $color-text-faded; } -.allowlist-settings-page { - height: 100%; - width: 100%; - background-color: $color-bg; - padding: 0 rem(10px); - +.allowlist-page { &__link-icon { width: rem(20px); height: rem(10px); @@ -29,56 +21,79 @@ } } -.connect-accounts-request-accounts { - padding-left: rem(10px); - padding-right: rem(10px); - width: 100%; - min-height: rem(175px); - height: rem(175px); - margin-left: auto; - margin-right: auto; - margin-bottom: rem(15px); - background: $color-input-bg; - border: rem(1px) solid #dae4ee; - border-top: none; - border-radius: 0 0 rem(10px) rem(10px); - scrollbar-width: thin; - overflow-y: auto; +.allowlist-empty { + display: flex; + align-items: center; + text-align: center; + color: $color-text-faded; +} - &__account-item { - padding-top: rem(5px); - padding-bottom: rem(5px); - font-weight: $font-weight-light; +.allowlist-entry-view { + &__header { + width: 100%; + margin-top: rem(8px); + padding: rem(10px); + padding-top: rem(0); + background: #eaf0f5; + border: rem(1px) solid #dae4ee; + border-radius: rem(10px) rem(10px) 0 0; + } - &:not(:last-child) { - border-bottom: rem(1px) dashed #dae4ee; - } + &__mode-description { + margin-top: rem(10px); + } - &__primary { - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - } + &__accounts { + width: 100%; + min-height: rem(160px); + height: rem(160px); + padding-left: rem(10px); + padding-right: rem(10px); + margin-left: auto; + margin-right: auto; + margin-bottom: rem(15px); + background: $color-input-bg; + border: rem(1px) solid #dae4ee; + border-top: none; + border-radius: 0 0 rem(10px) rem(10px); + scrollbar-width: thin; + overflow-y: auto; - &__check-box { - position: absolute; - right: rem(0); - bottom: rem(-12.5px); - width: rem(15px); + &__item { + padding-top: rem(5px); + padding-bottom: rem(5px); + font-weight: $font-weight-light; - input { - margin: 0; + &:not(:last-child) { + border-bottom: rem(1px) dashed #dae4ee; } - } - &__secondary { - font-size: rem(8px); + &__primary { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + &__check-box { + position: absolute; + right: rem(0); + bottom: rem(-12.5px); + width: rem(15px); + + input { + margin: 0; + } + } + + &__secondary { + font-size: rem(8px); + } } } } -.allow-list-edit-page { +.allow-list-editor { height: 100%; padding: rem(10px); background-color: $color-bg; diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index 1b10e81e..1538fa07 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -8,15 +8,15 @@ import Button from '@popup/shared/Button'; import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { allowlistRoutes } from './routes'; -import AllowListView from './AllowListView'; +import AllowListView from './AllowlistEditor'; function LoadingAllowlistPage() { - return
; + return
; } function EmptyAllowlistPage() { const { t } = useTranslation('allowlist'); - return
{t('empty')}
; + return
{t('empty')}
; } function Allowlist() { @@ -32,16 +32,16 @@ function Allowlist() { } return ( -
+
{Object.keys(allowlist).map((serviceName) => { const path = generatePath(allowlistRoutes.edit, { serviceName: encodeURIComponent(serviceName) }); return ( -
+
nav(path)}> - + } /> diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index 381709b4..d041673e 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -1,88 +1,74 @@ -import AccountInfoListenerContextProvider, { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; -import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; -import React, { useMemo } from 'react'; -import { Checkbox } from '@popup/shared/Form/Checkbox'; -import { WalletCredential } from '@shared/storage/types'; -import { displayAsCcd } from 'wallet-common-helpers'; -import { useAtomValue } from 'jotai'; -import { credentialsAtom } from '@popup/store/account'; +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import Button from '@popup/shared/Button/Button'; +import { storedAllowListAtom } from '@popup/store/account'; +import { useAtom } from 'jotai'; import { useTranslation } from 'react-i18next'; +import AllowlistEntryView, { AllowlistMode } from './AllowlistEntryView'; +import { updateAllowList } from './util'; -type ItemProps = { - account: WalletCredential; - checked: boolean; - onToggleChecked: () => void; -}; +function LoadingAllowlistEditor() { + return
; +} -function AccountListItem({ account, checked, onToggleChecked }: ItemProps) { - const accountInfo = useAccountInfo(account); - const identityName = useIdentityName(account, 'Identity'); - const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); +export default function AllowlistEditor() { + const nav = useNavigate(); + const { t } = useTranslation('allowlist', { keyPrefix: 'view' }); + const [selectedAccounts, setSelectedAccounts] = useState([]); + const { serviceName } = useParams<{ serviceName: string }>(); + const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); - return ( -
-
-
{displaySplitAddress(account.address)}
- { - e.stopPropagation(); - }} - checked={checked} - onChange={onToggleChecked} - /> -
-
{identityName}
-
- {displayAsCcd(totalBalance)} -
-
- ); -} + if (!serviceName) { + throw new Error('Invalid URL - the service name should be part of the URL.'); + } -export default function AllowlistEditor({ - selectedAccounts, - setSelectedAccounts, -}: { - selectedAccounts: string[]; - setSelectedAccounts: React.Dispatch>; -}) { - const { t } = useTranslation('allowlist', { keyPrefix: 'editor' }); - const accounts = useAtomValue(credentialsAtom); + const decodedServiceName = decodeURIComponent(serviceName); - /** - * Update the local state containing the list of account addresses. If the address - * is already present, then it is removed, if not present then it is added. - * @param accountAddress the account address to add or remove from the list. - */ - function updateAccountAddressChecked(accountAddress: string) { - if (selectedAccounts.includes(accountAddress)) { - const updatedAccounts = selectedAccounts.filter((acc) => acc !== accountAddress); - setSelectedAccounts(updatedAccounts); - } else { - setSelectedAccounts([...selectedAccounts, accountAddress]); + useEffect(() => { + if (!allowListLoading.loading) { + setSelectedAccounts(allowListLoading.value[serviceName]); } + }, [allowListLoading]); + + async function removeService(serviceNameToRemove: string, allowlist: Record) { + const updatedAllowlist = { ...allowlist }; + delete updatedAllowlist[serviceNameToRemove]; + await setAllowList(updatedAllowlist); + } + + if (allowListLoading.loading) { + return ; } return ( -
-
-

{t('header')}

- {t('description')} -
-
- - {accounts.map((account) => { - return ( - updateAccountAddressChecked(account.address)} - /> - ); - })} - +
+ +
+ +
); diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx new file mode 100644 index 00000000..2ffdf1ff --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx @@ -0,0 +1,94 @@ +import AccountInfoListenerContextProvider, { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; +import React, { useMemo } from 'react'; +import { Checkbox } from '@popup/shared/Form/Checkbox'; +import { WalletCredential } from '@shared/storage/types'; +import { displayAsCcd } from 'wallet-common-helpers'; +import { useAtomValue } from 'jotai'; +import { credentialsAtom } from '@popup/store/account'; +import { useTranslation } from 'react-i18next'; + +type ItemProps = { + account: WalletCredential; + checked: boolean; + onToggleChecked: () => void; +}; + +function AccountListItem({ account, checked, onToggleChecked }: ItemProps) { + const accountInfo = useAccountInfo(account); + const identityName = useIdentityName(account, 'Identity'); + const totalBalance = useMemo(() => accountInfo?.accountAmount || 0n, [accountInfo?.accountAmount]); + + return ( +
+
+
{displaySplitAddress(account.address)}
+ { + e.stopPropagation(); + }} + checked={checked} + onChange={onToggleChecked} + /> +
+
{identityName}
+
{displayAsCcd(totalBalance)}
+
+ ); +} + +export enum AllowlistMode { + Add, + Modify, +} + +export interface Props { + selectedAccounts: string[]; + setSelectedAccounts: React.Dispatch>; + mode: AllowlistMode; +} + +export default function AllowlistEntryView({ selectedAccounts, setSelectedAccounts, mode }: Props) { + const { t } = useTranslation('allowlist', { keyPrefix: 'entry' }); + const accounts = useAtomValue(credentialsAtom); + + /** + * Update the local state containing the list of account addresses. If the address + * is already present, then it is removed, if not present then it is added. + * @param accountAddress the account address to add or remove from the list. + */ + function updateAccountAddressChecked(accountAddress: string) { + if (selectedAccounts.includes(accountAddress)) { + const updatedAccounts = selectedAccounts.filter((acc) => acc !== accountAddress); + setSelectedAccounts(updatedAccounts); + } else { + setSelectedAccounts([...selectedAccounts, accountAddress]); + } + } + + return ( + +
+

{t('header')}

+ {t('description')} +
+ {mode === AllowlistMode.Add ?
{t('addDescription')}
: null} + {mode === AllowlistMode.Modify ?
{t('modifyDescription')}
: null} +
+
+
+ {accounts.map((account) => { + return ( + updateAccountAddressChecked(account.address)} + /> + ); + })} +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts index 0ea0e863..e492f760 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts @@ -2,10 +2,13 @@ import type en from './en'; const t: typeof en = { empty: 'Du har på nuværende tidspunkt ikke allowlisted nogen service.', - editor: { + entry: { header: 'Allowlisting af en service', description: 'At allowliste en service betyder at den kan anmode om identitetsbeviser, og signaturer fra de valgte konti.', + addDescription: 'Vælg hvilke konti der skal deles med servicen nedenfor.', + modifyDescription: + 'Du kan ændre i hvilke konti der er tilgængelige for servicen nedenfor. En allowlisted service kan altid anmode om proofs.', }, view: { remove: 'Fjern service fra allowlisten', diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts index 1ed34607..11fb6eff 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts @@ -1,13 +1,16 @@ const t = { empty: 'You do not currently have any services in your allowlist.', - editor: { + entry: { header: 'Allowlisting a service', description: 'Allowlisting a service means that it can request identity proofs and signatures from selected accounts.', + addDescription: 'Select accounts to share with the service below.', + modifyDescription: + 'You can modify which accounts are accessible to the service below. Any allowlisted service can request proofs.', }, view: { - remove: 'Remove service from allowlist', - update: 'Update allowlist', + remove: 'Remove', + update: 'Update', }, }; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss deleted file mode 100644 index 9eaa9e71..00000000 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.scss +++ /dev/null @@ -1,9 +0,0 @@ -.connect-accounts-request { - width: 100%; - margin-top: rem(8px); - padding: rem(10px); - padding-top: rem(0); - background: #eaf0f5; - border: rem(1px) solid #dae4ee; - border-radius: rem(10px) rem(10px) 0 0; -} diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 301fa43a..51502e5e 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -7,7 +7,7 @@ import { useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; -import AllowlistEditor from '../Allowlist/AllowlistEditor'; +import AllowlistEntryView, { AllowlistMode } from '../Allowlist/AllowlistEntryView'; import { updateAllowList } from '../Allowlist/util'; type Props = { @@ -15,7 +15,7 @@ type Props = { onReject(): void; }; -export default function ConnectionRequest({ onAllow, onReject }: Props) { +export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { const { state } = useLocation(); const { t } = useTranslation('connectAccountsRequest'); const { onClose, withClose } = useContext(fullscreenPromptContext); @@ -35,7 +35,11 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) {

{t('header', { url: urlDisplay })}

- +
From dfd1099eaf48ec99ba55e6eabb31af102ef8fa7e Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 31 May 2023 15:06:28 +0200 Subject: [PATCH 019/231] Return adresses and handle locked wallet --- .../browser-wallet/src/background/index.ts | 67 ++++++++++++++----- .../ExternalRequestLayout.tsx | 2 +- .../ConnectAccountsRequest.tsx | 33 +++++++-- .../browser-wallet/src/popup/shell/Routes.tsx | 13 +++- 4 files changed, 91 insertions(+), 24 deletions(-) diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index eb1cd00d..1a40095b 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -14,6 +14,7 @@ import { sessionOpenPrompt, storedAcceptedTerms, getGenesisHash, + storedAllowList, } from '@shared/storage/access'; import JSONBig from 'json-bigint'; @@ -345,6 +346,38 @@ async function findPrioritizedAccountConnectedToSite(url: string): Promise> = async (_msg, sender) => { + if (!sender.url) { + throw new Error('Expected URL to be available for sender.'); + } + + const locked = await isWalletLocked(); + if (locked) { + return { run: true }; + } + + const allowlist = await storedAllowList.get(); + if (!allowlist) { + return { run: true }; + } + + const allowlistedAccounts = allowlist[new URL(sender.url).origin]; + if (allowlistedAccounts !== undefined) { + return { run: false, response: { success: true, result: allowlistedAccounts } }; + } + + // The URL has not been allowlisted yet, so run the handler. + return { run: true }; +}; + /** * Run condition that runs the handler if the wallet is non-empty (an account exists), and no * account in the wallet is connected to the sender URL. @@ -448,15 +481,17 @@ const getSelectedChainHandler: ExtensionMessageHandler = (_msg, sender, respond) bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GetSelectedChain), getSelectedChainHandler); -const withPromptStart: RunCondition> = async () => { - const isPromptOpen = await sessionOpenPrompt.get(); - const isOpen = await testPopupOpen(); - if (isPromptOpen && isOpen) { - return { run: false, response: { success: false, message: 'Another prompt is already open' } }; - } - sessionOpenPrompt.set(true); - return { run: true }; -}; +function withPromptStart(): RunCondition> { + return async () => { + const isPromptOpen = await sessionOpenPrompt.get(); + const isOpen = await testPopupOpen(); + if (isPromptOpen && isOpen) { + return { run: false, response: { success: false, message: 'Another prompt is already open' } }; + } + sessionOpenPrompt.set(true); + return { run: true }; + }; +} function withPromptEnd() { sessionOpenPrompt.set(false); @@ -465,7 +500,7 @@ function withPromptEnd() { forwardToPopup( MessageType.Connect, InternalMessageType.Connect, - runConditionComposer(runIfNotWhitelisted, withPromptStart), + runConditionComposer(runIfNotWhitelisted, withPromptStart()), handleConnectMessage, handleConnectionResponse, withPromptEnd @@ -473,15 +508,15 @@ forwardToPopup( forwardToPopup( MessageType.ConnectAccounts, InternalMessageType.ConnectAccounts, - runConditionComposer(runIfNotWhitelisted, withPromptStart), + runConditionComposer(runIfNotAllowlisted, withPromptStart()), handleConnectMessage, - handleConnectionResponse, + undefined, withPromptEnd ); forwardToPopup( MessageType.SendTransaction, InternalMessageType.SendTransaction, - runConditionComposer(runIfWhitelisted, ensureTransactionPayloadParse, withPromptStart), + runConditionComposer(runIfWhitelisted, ensureTransactionPayloadParse, withPromptStart()), appendUrlToPayload, undefined, withPromptEnd @@ -489,7 +524,7 @@ forwardToPopup( forwardToPopup( MessageType.SignMessage, InternalMessageType.SignMessage, - runConditionComposer(runIfWhitelisted, ensureMessageWithSchemaParse, withPromptStart), + runConditionComposer(runIfWhitelisted, ensureMessageWithSchemaParse, withPromptStart()), appendUrlToPayload, undefined, withPromptEnd @@ -497,7 +532,7 @@ forwardToPopup( forwardToPopup( MessageType.AddTokens, InternalMessageType.AddTokens, - runConditionComposer(runIfWhitelisted, withPromptStart), + runConditionComposer(runIfWhitelisted, withPromptStart()), appendUrlToPayload, undefined, withPromptEnd @@ -505,7 +540,7 @@ forwardToPopup( forwardToPopup( MessageType.IdProof, InternalMessageType.IdProof, - runConditionComposer(runIfWhitelisted, runIfValidProof, withPromptStart), + runConditionComposer(runIfWhitelisted, runIfValidProof, withPromptStart()), appendUrlToPayload, undefined, withPromptEnd diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index 287e03ea..bf72c7f1 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -52,7 +52,7 @@ interface Location { } interface Props { - children: ReactNode; + children?: ReactNode; } export default function ExternalRequestLayout({ children }: Props) { diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 3cf65951..6d3fa178 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,20 +1,26 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; import { storedAllowListAtom } from '@popup/store/account'; -import { useAtom } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { sessionPasscodeAtom } from '@popup/store/settings'; +import { absoluteRoutes } from '@popup/constants/routes'; import AllowlistEntryView, { AllowlistMode } from '../Allowlist/AllowlistEntryView'; import { updateAllowList } from '../Allowlist/util'; type Props = { - onAllow(): void; + onAllow(accountAdresses: string[]): void; onReject(): void; }; +function LoadingConnectAccountsRequest() { + return ; +} + export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { const { state } = useLocation(); const { t } = useTranslation('connectAccountsRequest'); @@ -22,9 +28,24 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { const [accountsToAdd, setAccountsToAdd] = useState([]); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); + const passcode = useAtomValue(sessionPasscodeAtom); useEffect(() => onClose(onReject), [onClose, onReject]); + if (allowListLoading.loading || passcode.loading) { + return ; + } + + // The wallet is locked, so prompt the user to unlock the wallet before connecting. + if (!passcode.value) { + return ( + + ); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { url } = (state as any).payload; const urlDisplay = displayUrl(url); @@ -54,7 +75,11 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { allowListLoading.value, accountsToAdd, setAllowList - ).then(withClose(onAllow)); + ).then( + withClose(() => { + onAllow(accountsToAdd); + }) + ); }} > {t('actions.connect')} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index f3ac46dd..93b64398 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -84,7 +84,7 @@ function usePrompt(type: InternalMessageType | MessageType, promptKey: PromptKey export default function Routes() { const handleConnectionResponse = useMessagePrompt(InternalMessageType.Connect, 'connectionRequest'); - const handleConnectAccountsResponse = useMessagePrompt( + const handleConnectAccountsResponse = useMessagePrompt>( InternalMessageType.ConnectAccounts, 'connectAccountsRequest' ); @@ -159,8 +159,15 @@ export default function Routes() { path={relativeRoutes.prompt.connectAccountsRequest.path} element={ handleConnectAccountsResponse(true)} - onReject={() => handleConnectAccountsResponse(false)} + onAllow={(accountAddresses: string[]) => + handleConnectAccountsResponse({ success: true, result: accountAddresses }) + } + onReject={() => + handleConnectAccountsResponse({ + success: false, + message: 'Request was rejected', + }) + } /> } /> From c6ba81c305cba3a433aba64d82e5550d7f76920f Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 1 Jun 2023 14:03:40 +0200 Subject: [PATCH 020/231] Remove usage of connected sites --- .../src/handlers.ts | 13 +-- .../browser-wallet/src/background/index.ts | 59 +++++++------ .../src/background/message-handler.ts | 4 +- .../src/popup/pages/Account/Account.tsx | 8 +- .../AccountSettings/AccountSettings.scss | 29 ------ .../AccountSettings/AccountSettings.tsx | 5 -- .../AccountSettings/ConnectedSites.tsx | 88 ------------------- .../pages/Account/AccountSettings/routes.ts | 1 - .../src/popup/pages/Account/ConnectedBox.tsx | 39 +++----- .../src/popup/pages/Account/i18n/da.ts | 6 -- .../src/popup/pages/Account/i18n/en.ts | 6 -- .../src/popup/pages/Allowlist/Allowlist.tsx | 4 +- .../popup/pages/Allowlist/AllowlistEditor.tsx | 21 +++-- .../src/popup/pages/Allowlist/util.ts | 32 +++++-- .../ConnectAccountsRequest.tsx | 8 +- .../ConnectionRequest/ConnectionRequest.tsx | 19 ++-- .../src/popup/shared/message-handler.ts | 4 +- .../browser-wallet/src/popup/store/account.ts | 2 +- .../src/popup/store/settings.ts | 23 ++--- .../browser-wallet/src/popup/store/utils.ts | 4 +- .../src/shared/storage/access.ts | 4 +- 21 files changed, 117 insertions(+), 262 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/pages/Account/AccountSettings/ConnectedSites.tsx diff --git a/packages/browser-wallet-message-hub/src/handlers.ts b/packages/browser-wallet-message-hub/src/handlers.ts index 160e8383..49c4ad6f 100644 --- a/packages/browser-wallet-message-hub/src/handlers.ts +++ b/packages/browser-wallet-message-hub/src/handlers.ts @@ -194,7 +194,7 @@ const defaultBroadcastOptions: BroadcastOptions = { requireWhitelist: true }; export class ExtensionsMessageHandler extends BaseMessageHandler { constructor( - private connectedSites: { get(): Promise | undefined> }, + private allowlist: { get(): Promise | undefined> }, private selectedAccount: { get(): Promise } ) { super(); @@ -217,7 +217,8 @@ export class ExtensionsMessageHandler extends BaseMessageHandler /** * Broadcast an event of a specific type, with an optional payload, to all currently - * open and whitelisted (connected to the selected account) tabs. + * open and allowlisted (connected to the selected account) tabs. + * * By specifying options, it's possible to disable the whitelist by `{requireWhitelist: false}` * and also declare a callback to be called for tabs not included in the whitelist through `{nonWhitelistedTabCallback: (t: chrome.tabs.Tab) => ...}` * Default options are {requireWhitelist: true} @@ -291,12 +292,14 @@ export class ExtensionsMessageHandler extends BaseMessageHandler } private async getWhitelistedTabs(tabs: chrome.tabs.Tab[]): Promise { - const connectedSites = await this.connectedSites.get(); + const allowlist = await this.allowlist.get(); const selectedAccount = await this.selectedAccount.get(); let whitelistedUrls: string[] = []; - if (selectedAccount && connectedSites) { - whitelistedUrls = connectedSites[selectedAccount] ?? []; + if (selectedAccount && allowlist) { + whitelistedUrls = Object.entries(allowlist) + .filter((entry) => entry[1].includes(selectedAccount)) + .map((val) => val[0]); } return tabs.filter(({ url }) => url !== undefined && whitelistedUrls?.includes(new URL(url).origin)); diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 1a40095b..28856309 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -7,14 +7,13 @@ import { } from '@concordium/browser-wallet-message-hub'; import { deserializeTypeValue, HttpProvider } from '@concordium/web-sdk'; import { - storedConnectedSites, storedSelectedAccount, storedCurrentNetwork, sessionPasscode, sessionOpenPrompt, storedAcceptedTerms, getGenesisHash, - storedAllowList, + storedAllowlist, } from '@shared/storage/access'; import JSONBig from 'json-bigint'; @@ -49,13 +48,13 @@ async function isWalletLocked(): Promise { } /** - * Determines whether the given url has been whitelisted by any account. + * Determines whether the given URL has been allowlisted. */ -async function isWhiteListedForAnyAccount(url: string): Promise { +async function isAllowlisted(url: string): Promise { const urlOrigin = new URL(url).origin; - const connectedSites = await storedConnectedSites.get(); - if (connectedSites) { - return Object.values(connectedSites).some((sites) => sites.includes(urlOrigin)); + const allowlist = await storedAllowlist.get(); + if (allowlist) { + return Object.keys(allowlist).includes(urlOrigin); } return false; } @@ -72,7 +71,7 @@ async function performRpcCall( onFailure(walletLockedMessage); } - const isWhiteListed = await isWhiteListedForAnyAccount(senderUrl); + const isWhiteListed = await isAllowlisted(senderUrl); if (isWhiteListed) { const url = (await storedCurrentNetwork.get())?.jsonRpcUrl; if (!url) { @@ -102,7 +101,7 @@ async function exportGRPCLocation( onSuccess: (response: string | undefined) => void, onFailure: (response: string) => void ): Promise { - const isWhiteListed = await isWhiteListedForAnyAccount(callerUrl); + const isWhiteListed = await isAllowlisted(callerUrl); if (!isWhiteListed) { return onFailure(rpcCallNotAllowedMessage); } @@ -256,20 +255,23 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.Creat const NOT_WHITELISTED = 'Site is not whitelisted'; /** - * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. + * Run condition that ensures that a handler is only run if the URL is in the allowlist + * of the provided account. */ -const runIfWhitelisted: RunCondition> = async (msg, sender) => { +const runIfAccountIsAllowlisted: RunCondition> = async (msg, sender) => { const { accountAddress } = msg.payload; - const connectedSites = await storedConnectedSites.get(); + const allowlist = await storedAllowlist.get(); const locked = await isWalletLocked(); - if (!accountAddress || connectedSites === undefined || locked) { + if (!accountAddress || allowlist === undefined || locked) { return { run: false, response: { success: false, message: NOT_WHITELISTED } }; } - const accountConnectedSites = connectedSites[accountAddress] ?? []; - if (sender.url !== undefined && accountConnectedSites.includes(new URL(sender.url).origin)) { - return { run: true }; + if (sender.url !== undefined) { + const allowlistedAccounts = allowlist[new URL(sender.url).origin]; + if (allowlistedAccounts.includes(accountAddress)) { + return { run: true }; + } } return { run: false, response: { success: false, message: NOT_WHITELISTED } }; @@ -320,27 +322,26 @@ const ensureTransactionPayloadParse: RunCondition { const urlOrigin = new URL(url).origin; const selectedAccount = await storedSelectedAccount.get(); - const connectedSites = await storedConnectedSites.get(); + const allowlist = await storedAllowlist.get(); - if (!selectedAccount || !connectedSites) { + if (!selectedAccount || !allowlist) { return undefined; } - const selectedAccountConnectedSites = connectedSites[selectedAccount] ?? []; - if (selectedAccountConnectedSites.includes(urlOrigin)) { + const connectedAccounts = allowlist[urlOrigin] ?? []; + if (connectedAccounts.includes(selectedAccount)) { return selectedAccount; } - const connectedAccount = Object.entries(connectedSites).find((item) => item[1] && item[1].includes(urlOrigin)); - if (connectedAccount) { - return connectedAccount[0]; + if (connectedAccounts.length > 0) { + return connectedAccounts[0]; } return undefined; @@ -364,7 +365,7 @@ const runIfNotAllowlisted: RunCondition setDetailsExpanded((o) => !o)} /> - setDetailsExpanded(false)} - accountAddress={selectedCred.address} - /> +
{isConfirmed && } diff --git a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.scss b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.scss index 3e2dadce..e3e08173 100644 --- a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.scss +++ b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.scss @@ -1,5 +1,4 @@ .account-settings-page, -.connected-sites-list, .export-private-key-page, .account-statement-page { min-height: 100%; @@ -27,34 +26,6 @@ } } -.connected-sites-list { - padding-left: rem(14px); - padding-right: rem(14px); - overflow: overlay; - - &__element { - display: flex; - flex-direction: row; - justify-content: space-between; - padding-top: rem(10px); - padding-bottom: rem(10px); - - &__connect { - cursor: pointer; - color: $color-dark-green; - } - - &__disconnect { - cursor: pointer; - color: $color-blue; - } - } - - & > *:not(:last-child) { - border-bottom: rem(1px) solid $color-petroleum; - } -} - .export-private-key-page { padding-left: rem(19px); padding-right: rem(19px); diff --git a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.tsx b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.tsx index faa50938..3147dc18 100644 --- a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/AccountSettings.tsx @@ -3,7 +3,6 @@ import NavList from '@popup/shared/NavList'; import { Link, Route, Routes } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { accountSettingsRoutes } from './routes'; -import ConnectedSites from './ConnectedSites'; import ExportPrivateKey from './ExportPrivateKey'; import AccountStatement from './AccountStatement'; import { accountPageContext } from '../utils'; @@ -17,9 +16,6 @@ export function AccountSettings() { return ( - - {t('connectedSites.title')} - {t('exportPrivateKey.title')} @@ -34,7 +30,6 @@ export default function AccountSettingRoutes() { return ( } /> - } /> } /> } /> diff --git a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/ConnectedSites.tsx b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/ConnectedSites.tsx deleted file mode 100644 index ecf13377..00000000 --- a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/ConnectedSites.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { useAtom, useAtomValue } from 'jotai'; -import { selectedAccountAtom, storedConnectedSitesAtom } from '@popup/store/account'; -import Button from '@popup/shared/Button'; -import { useTranslation } from 'react-i18next'; -import { popupMessageHandler } from '@popup/shared/message-handler'; -import { EventType } from '@concordium/browser-wallet-api-helpers'; -import { useCurrentOpenTabUrl } from '@popup/shared/utils/tabs'; -import { displayUrl } from '@popup/shared/utils/string-helpers'; - -export default function ConnectedSites() { - const { t } = useTranslation('account', { keyPrefix: 'settings.connectedSites' }); - const selectedAccount = useAtomValue(selectedAccountAtom); - const [connectedSitesLoading, setConnectedSites] = useAtom(storedConnectedSitesAtom); - const connectedSites = connectedSitesLoading.value; - const openTabUrl = useCurrentOpenTabUrl(); - - if (!selectedAccount || !openTabUrl) { - return null; - } - - const localSites = connectedSites[selectedAccount] ?? []; - if (!openTabUrl && !connectedSitesLoading.loading && localSites.length === 0) { - return ( -
-
{t('noConnected')}
-
- ); - } - - function updateConnectedSites(updatedConnectedSitesForAccount: string[], account: string) { - const connectedSitesWithSiteAdded = { - ...connectedSites, - }; - connectedSitesWithSiteAdded[account] = updatedConnectedSitesForAccount; - setConnectedSites(connectedSitesWithSiteAdded); - } - - function connectSite(site: string, account: string) { - const currentConnectedSitesForAccount = connectedSites[account] ?? []; - if (currentConnectedSitesForAccount.includes(site)) { - return; - } - - const updatedConnectedSitesForAccount = [site]; - updatedConnectedSitesForAccount.push(...currentConnectedSitesForAccount); - updateConnectedSites(updatedConnectedSitesForAccount, account); - popupMessageHandler.broadcastToUrl(EventType.AccountChanged, site, account); - } - - function removeConnectedSite(site: string, account: string) { - const currentConnectedSitesForAccount = connectedSites[account] ?? []; - const updatedConnectedSitesForAccount = currentConnectedSitesForAccount.filter((v) => v !== site); - updateConnectedSites(updatedConnectedSitesForAccount, account); - popupMessageHandler.broadcastToUrl(EventType.AccountDisconnected, site, account); - } - - return ( -
- {openTabUrl && !localSites.includes(openTabUrl) && ( -
-
{displayUrl(openTabUrl)}
- -
- )} - {localSites.map((site) => { - return ( -
-
{displayUrl(site)}
- -
- ); - })} -
- ); -} diff --git a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/routes.ts b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/routes.ts index 39ea897c..aa201ab1 100644 --- a/packages/browser-wallet/src/popup/pages/Account/AccountSettings/routes.ts +++ b/packages/browser-wallet/src/popup/pages/Account/AccountSettings/routes.ts @@ -1,5 +1,4 @@ export const accountSettingsRoutes = { - connectedSites: 'connected-sites', exportPrivateKey: 'export-private-key', accountStatement: 'account-statement', }; diff --git a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx index b1d5b9a6..ae79436e 100644 --- a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx @@ -1,48 +1,29 @@ import { useAtomValue } from 'jotai'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { storedConnectedSitesAtom } from '@popup/store/account'; +import { storedAllowlistAtom } from '@popup/store/account'; import clsx from 'clsx'; type Props = { accountAddress?: string; url?: string; -} & ( - | { link: string; onNavigate?: () => void } - | { - link?: undefined; - onNavigate?: undefined; - } -); +}; -export default function ConnectedBox({ accountAddress, url, link, onNavigate }: Props) { +export default function ConnectedBox({ accountAddress, url }: Props) { const { t } = useTranslation('account'); - const connectedSites = useAtomValue(storedConnectedSitesAtom); const [isConnectedToSite, setIsConnectedToSite] = useState(); + const allowlist = useAtomValue(storedAllowlistAtom); useMemo(() => { - if (accountAddress && !connectedSites.loading && url) { - const connectedSitesForAccount = connectedSites.value[accountAddress] ?? []; - setIsConnectedToSite(connectedSitesForAccount.includes(url)); + if (accountAddress && !allowlist.loading && url) { + const connectedAccountsForUrl = allowlist.value[url] ?? []; + setIsConnectedToSite(connectedAccountsForUrl.includes(accountAddress)); } - }, [accountAddress, connectedSites, url]); - - if (!link) { - return ( -
- {isConnectedToSite ? t('siteConnected') : t('siteNotConnected')} -
- ); - } + }, [accountAddress, allowlist, url]); return ( - +
{isConnectedToSite ? t('siteConnected') : t('siteNotConnected')} - +
); } diff --git a/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts index cd92dcff..7a20ce1c 100644 --- a/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts @@ -25,12 +25,6 @@ const t: typeof en = { passiveDelegation: 'Passiv delegation', }, settings: { - connectedSites: { - title: 'Forbundne hjemmesider', - noConnected: 'Den valgte konto er ikke forbundet til nogen hjemmeside.', - connect: 'Forbind', - disconnect: 'Fjern', - }, exportPrivateKey: { title: 'Eksportér privatnøgle', description: 'Indtast venligst din adgangskode for at vise din private nøgle.', diff --git a/packages/browser-wallet/src/popup/pages/Account/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Account/i18n/en.ts index da56858f..4f21f5f1 100644 --- a/packages/browser-wallet/src/popup/pages/Account/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Account/i18n/en.ts @@ -23,12 +23,6 @@ const t = { passiveDelegation: 'Passive delegation', }, settings: { - connectedSites: { - title: 'Connected sites', - noConnected: 'The selected account is not connected to any sites.', - connect: 'Connect', - disconnect: 'Disconnect', - }, exportPrivateKey: { title: 'Export private key', description: 'Please enter your passcode to show the private key.', diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index 1538fa07..6e7fd984 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -1,6 +1,6 @@ import React from 'react'; import SidedRow from '@popup/shared/SidedRow/SidedRow'; -import { storedAllowListAtom } from '@popup/store/account'; +import { storedAllowlistAtom } from '@popup/store/account'; import { useAtomValue } from 'jotai'; import LinkIcon from '@assets/svg/link.svg'; import { displayUrl } from '@popup/shared/utils/string-helpers'; @@ -21,7 +21,7 @@ function EmptyAllowlistPage() { function Allowlist() { const nav = useNavigate(); - const allowlistWithLoading = useAtomValue(storedAllowListAtom); + const allowlistWithLoading = useAtomValue(storedAllowlistAtom); const allowlist = allowlistWithLoading.value; if (allowlistWithLoading.loading) { diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index e1a4e514..1e6048e9 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Button from '@popup/shared/Button/Button'; -import { storedAllowListAtom } from '@popup/store/account'; -import { useAtom } from 'jotai'; +import { selectedAccountAtom, storedAllowlistAtom } from '@popup/store/account'; +import { useAtom, useAtomValue } from 'jotai'; import { useTranslation } from 'react-i18next'; +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { EventType } from '@concordium/browser-wallet-api-helpers'; import AllowlistEntryView, { AllowlistMode } from './AllowlistEntryView'; -import { updateAllowList } from './util'; +import { handleAllowlistEntryUpdate } from './util'; function LoadingAllowlistEditor() { return
; @@ -16,7 +18,8 @@ export default function AllowlistEditor() { const { t } = useTranslation('allowlist', { keyPrefix: 'view' }); const [selectedAccounts, setSelectedAccounts] = useState([]); const { serviceName } = useParams<{ serviceName: string }>(); - const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); + const [allowListLoading, setAllowList] = useAtom(storedAllowlistAtom); + const selectedAccount = useAtomValue(selectedAccountAtom); if (!serviceName) { throw new Error('Invalid URL - the service name should be part of the URL.'); @@ -32,8 +35,13 @@ export default function AllowlistEditor() { async function removeService(serviceNameToRemove: string, allowlist: Record) { const updatedAllowlist = { ...allowlist }; + const disconnectedAccounts = [...updatedAllowlist[serviceNameToRemove]]; delete updatedAllowlist[serviceNameToRemove]; await setAllowList(updatedAllowlist); + + for (const account of disconnectedAccounts) { + popupMessageHandler.broadcastToUrl(EventType.AccountDisconnected, serviceNameToRemove, account); + } } if (allowListLoading.loading) { @@ -52,11 +60,12 @@ export default function AllowlistEditor() { className="m-r-10" width="narrow" onClick={() => - updateAllowList( + handleAllowlistEntryUpdate( decodedServiceName, allowListLoading.value, selectedAccounts, - setAllowList + setAllowList, + selectedAccount ).then(() => nav(-1)) } > diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/util.ts b/packages/browser-wallet/src/popup/pages/Allowlist/util.ts index d246caf2..f76a43f8 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/util.ts +++ b/packages/browser-wallet/src/popup/pages/Allowlist/util.ts @@ -1,12 +1,34 @@ -export async function updateAllowList( +import { EventType } from '@concordium/browser-wallet-api-helpers'; +import { popupMessageHandler } from '@popup/shared/message-handler'; + +async function broadcastAccountDisconnectedEvents(disconnectedAccounts: string[], tabUrl: string) { + for (const disconnectedAccount of disconnectedAccounts) { + popupMessageHandler.broadcastToUrl(EventType.AccountDisconnected, tabUrl, disconnectedAccount); + } +} + +export async function handleAllowlistEntryUpdate( urlToUpdate: string, allowlist: Record, accountsToAdd: string[], - setAllowList: (update: Record) => Promise + setAllowlist: (update: Record) => Promise, + selectedAccount?: string ) { - const updatedAllowList: Record = { + const updatedAllowlist: Record = { ...allowlist, }; - updatedAllowList[urlToUpdate] = accountsToAdd; - await setAllowList(updatedAllowList); + updatedAllowlist[urlToUpdate] = accountsToAdd; + await setAllowlist(updatedAllowlist); + + const currentAllowlistedAccounts = allowlist[urlToUpdate] ?? []; + const removedAccounts = currentAllowlistedAccounts.filter((acc) => !updatedAllowlist[urlToUpdate].includes(acc)); + broadcastAccountDisconnectedEvents(removedAccounts, urlToUpdate); + + if ( + selectedAccount && + !currentAllowlistedAccounts.includes(selectedAccount) && + accountsToAdd.includes(selectedAccount) + ) { + popupMessageHandler.broadcastToUrl(EventType.AccountChanged, urlToUpdate, selectedAccount); + } } diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 6d3fa178..84d43129 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -1,5 +1,5 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { storedAllowListAtom } from '@popup/store/account'; +import { storedAllowlistAtom } from '@popup/store/account'; import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { displayUrl } from '@popup/shared/utils/string-helpers'; import { sessionPasscodeAtom } from '@popup/store/settings'; import { absoluteRoutes } from '@popup/constants/routes'; import AllowlistEntryView, { AllowlistMode } from '../Allowlist/AllowlistEntryView'; -import { updateAllowList } from '../Allowlist/util'; +import { handleAllowlistEntryUpdate } from '../Allowlist/util'; type Props = { onAllow(accountAdresses: string[]): void; @@ -27,7 +27,7 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { const { onClose, withClose } = useContext(fullscreenPromptContext); const [accountsToAdd, setAccountsToAdd] = useState([]); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); - const [allowListLoading, setAllowList] = useAtom(storedAllowListAtom); + const [allowListLoading, setAllowList] = useAtom(storedAllowlistAtom); const passcode = useAtomValue(sessionPasscodeAtom); useEffect(() => onClose(onReject), [onClose, onReject]); @@ -70,7 +70,7 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { disabled={connectButtonDisabled} onClick={() => { setConnectButtonDisabled(true); - updateAllowList( + handleAllowlistEntryUpdate( new URL(url).origin, allowListLoading.value, accountsToAdd, diff --git a/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx index 5b78c257..b60b8a7b 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx @@ -1,6 +1,6 @@ import { absoluteRoutes } from '@popup/constants/routes'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { selectedAccountAtom, storedConnectedSitesAtom } from '@popup/store/account'; +import { selectedAccountAtom, storedAllowlistAtom } from '@popup/store/account'; import { sessionPasscodeAtom } from '@popup/store/settings'; import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useState } from 'react'; @@ -10,6 +10,7 @@ import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { handleAllowlistEntryUpdate } from '../Allowlist/util'; type Props = { onAllow(): void; @@ -21,14 +22,14 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const { t } = useTranslation('connectionRequest'); const { onClose, withClose } = useContext(fullscreenPromptContext); const selectedAccount = useAtomValue(selectedAccountAtom); - const [connectedSitesLoading, setConnectedSites] = useAtom(storedConnectedSitesAtom); - const connectedSites = connectedSitesLoading.value; + const [allowlistLoading, setAllowlist] = useAtom(storedAllowlistAtom); + const allowlist = allowlistLoading.value; const passcode = useAtomValue(sessionPasscodeAtom); const [connectButtonDisabled, setConnectButtonDisabled] = useState(false); useEffect(() => onClose(onReject), [onClose, onReject]); - if (!selectedAccount || connectedSitesLoading.loading || passcode.loading) { + if (!selectedAccount || allowlistLoading.loading || passcode.loading) { return null; } @@ -43,15 +44,7 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { } async function connectAccount(account: string, url: string) { - const updatedConnectedSites = { - ...connectedSites, - }; - - const connectedSitesForAccount = connectedSites[account] ?? []; - connectedSitesForAccount.push(url); - updatedConnectedSites[account] = connectedSitesForAccount; - - await setConnectedSites(updatedConnectedSites); + await handleAllowlistEntryUpdate(url, allowlist, [account], setAllowlist); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/browser-wallet/src/popup/shared/message-handler.ts b/packages/browser-wallet/src/popup/shared/message-handler.ts index 9c2b0ee6..b3602c7b 100644 --- a/packages/browser-wallet/src/popup/shared/message-handler.ts +++ b/packages/browser-wallet/src/popup/shared/message-handler.ts @@ -1,4 +1,4 @@ import { ExtensionsMessageHandler } from '@concordium/browser-wallet-message-hub'; -import { storedConnectedSites, storedSelectedAccount } from '@shared/storage/access'; +import { storedAllowlist, storedSelectedAccount } from '@shared/storage/access'; -export const popupMessageHandler = new ExtensionsMessageHandler(storedConnectedSites, storedSelectedAccount); +export const popupMessageHandler = new ExtensionsMessageHandler(storedAllowlist, storedSelectedAccount); diff --git a/packages/browser-wallet/src/popup/store/account.ts b/packages/browser-wallet/src/popup/store/account.ts index 40899b4f..a1d4215a 100644 --- a/packages/browser-wallet/src/popup/store/account.ts +++ b/packages/browser-wallet/src/popup/store/account.ts @@ -19,7 +19,7 @@ export const storedConnectedSitesAtom = atomWithChromeStorage>( +export const storedAllowlistAtom = atomWithChromeStorage>( ChromeStorageKey.AllowList, {}, true diff --git a/packages/browser-wallet/src/popup/store/settings.ts b/packages/browser-wallet/src/popup/store/settings.ts index fe5d17cb..377e2d44 100644 --- a/packages/browser-wallet/src/popup/store/settings.ts +++ b/packages/browser-wallet/src/popup/store/settings.ts @@ -9,7 +9,7 @@ import { atom } from 'jotai'; import { EventType } from '@concordium/browser-wallet-api-helpers'; import { popupMessageHandler } from '@popup/shared/message-handler'; import { ConcordiumGRPCClient, createConcordiumClient } from '@concordium/web-sdk'; -import { storedConnectedSites, storedCredentials } from '@shared/storage/access'; +import { storedAllowlist, storedCredentials } from '@shared/storage/access'; import { GRPCTIMEOUT, mainnet } from '@shared/constants/networkConfiguration'; import { atomWithChromeStorage } from './utils'; import { selectedAccountAtom } from './account'; @@ -32,37 +32,24 @@ export const networkConfigurationAtom = atom { const networkPromise = set(storedNetworkConfigurationAtom, networkConfiguration); const identityPromise = set(selectedIdentityIndexAtom, 0); - const credentials = await storedCredentials.get(networkConfiguration.genesisHash); + const selectedAccount = credentials?.length ? credentials[0]?.address : undefined; const accountPromise = set(selectedAccountAtom, selectedAccount); // Wait for all the derived state of a network change to be done before broadcasting await Promise.all([networkPromise, identityPromise, accountPromise]); - const connectedSites = await storedConnectedSites.get(); - const sortedConnectedSites = connectedSites - ? Object.entries(connectedSites).sort(([accountA], [accountB]) => { - if (credentials === undefined) { - return 0; - } - return ( - credentials.findIndex((c) => c.address === accountA) - - credentials.findIndex((c) => c.address === accountB) - ); - }) - : undefined; - + const allowlist = await storedAllowlist.get(); popupMessageHandler.broadcast(EventType.ChainChanged, networkConfiguration.genesisHash, { requireWhitelist: false, nonWhitelistedTabCallback: ({ url }) => { - if (!url) { + if (!url || !allowlist || !allowlist[url]) { return; } // If tab has any account connected, send account changed event, otherwise account disconnected. - const filtered = sortedConnectedSites?.filter(([, sites]) => sites.some((s) => url.startsWith(s))); - const firstConnectedAccount = filtered?.[0]?.[0]; + const firstConnectedAccount = allowlist[url].length > 0 ? allowlist[url][0] : undefined; if (firstConnectedAccount) { popupMessageHandler.broadcastToUrl(EventType.AccountChanged, url, firstConnectedAccount); } else { diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 6d9de2f3..700e3fdd 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -26,7 +26,7 @@ import { sessionCookie, sessionOpenPrompt, storedAcceptedTerms, - storedAllowList, + storedAllowlist, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -56,7 +56,7 @@ const accessorMap = { [ChromeStorageKey.Cookie]: useIndexedStorage(sessionCookie, getGenesisHash), [ChromeStorageKey.OpenPrompt]: sessionOpenPrompt, [ChromeStorageKey.AcceptedTerms]: storedAcceptedTerms, - [ChromeStorageKey.AllowList]: storedAllowList, + [ChromeStorageKey.AllowList]: storedAllowlist, }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 4e10fc7e..210f547e 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -145,11 +145,11 @@ export const storedTokenMetadata = makeStorageAccessor('local', ChromeStorageKey.AcceptedTerms); -const indexedStoredAllowList = makeIndexedStorageAccessor>( +const indexedStoredAllowlist = makeIndexedStorageAccessor>( 'local', ChromeStorageKey.AllowList ); -export const storedAllowList = useIndexedStorage(indexedStoredAllowList, getGenesisHash); +export const storedAllowlist = useIndexedStorage(indexedStoredAllowlist, getGenesisHash); export const sessionOpenPrompt = makeStorageAccessor('session', ChromeStorageKey.OpenPrompt); export const sessionPasscode = makeStorageAccessor('session', ChromeStorageKey.Passcode); From 414ecd592f5f49f2089651cce18ca3b1e5dbad63 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 1 Jun 2023 14:30:33 +0200 Subject: [PATCH 021/231] Refactor and document --- packages/browser-wallet-api-helpers/CHANGELOG.md | 6 ++++++ packages/browser-wallet-api-helpers/README.md | 11 +++++++++++ .../src/wallet-api-types.ts | 8 ++++++++ packages/browser-wallet-api/src/wallet-api.ts | 4 ++++ packages/browser-wallet-message-hub/src/handlers.ts | 4 ++-- packages/browser-wallet/src/popup/constants/routes.ts | 2 +- .../popup/page-layouts/MainLayout/Header/Header.tsx | 2 +- .../src/popup/pages/Settings/Settings.tsx | 2 +- packages/browser-wallet/src/popup/shell/Routes.tsx | 2 +- packages/browser-wallet/src/shared/storage/types.ts | 2 +- 10 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/browser-wallet-api-helpers/CHANGELOG.md b/packages/browser-wallet-api-helpers/CHANGELOG.md index 62c15ef4..c1ad1290 100644 --- a/packages/browser-wallet-api-helpers/CHANGELOG.md +++ b/packages/browser-wallet-api-helpers/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- `requestAccounts` entrypoint to connect to a list of accounts. + ## 2.5.0 ### Added diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index 8896d270..097c988a 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -74,6 +74,17 @@ if (accountAddress) { } ``` +### requestAccounts + +To request a connection to the wallet from the user, the `requestAccounts` method has to be invoked. The method returns a `Promise` resolving with the list of accounts +that the user has chosen to connect with. The list may be empty. Alternatively the promise will be rejecting if the request is rejected in the wallet. If the wallet is locked, +then this call prompts the user to first unlock the wallet before accepting or rejecting the request. + +```typescript +const provider = await detectConcordiumProvider(); +const accountAddresses = await provider.requestAccounts(); +``` + ### getSelectedChain This can be invoked to get the genesis hash of the chain selected in the wallet. The method returns a `Promise`, resolving with the genesis hash (as a hex string) of the selected chain, or undefined if the wallet is locked or has not been set up by the user. diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 684c04f5..8a6aadd8 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -102,12 +102,20 @@ interface MainWalletApi { * @param message message to be signed. Note that the wallet will prepend some bytes to ensure the message cannot be a transaction. The message should either be a utf8 string or { @link SignMessageObject }. */ signMessage(accountAddress: string, message: string | SignMessageObject): Promise; + /** * Requests a connection to the Concordium wallet, prompting the user to either accept or reject the request. * If a connection has already been accepted for the url once the returned promise will resolve without prompting the user. + * @deprecated use {@link requestAccounts} instead */ connect(): Promise; + /** + * Requests a connection to the Concordium wallet, prompting the user to either accept or reject the request. The + * user will be prompted to select which accounts should be connected. The list of connected accounts is returned + * to the caller. If a connection has already been accepted previously, then the returned promise will resolve + * with the list of connected accounts. Note that the list of accounts may be empty. + */ requestAccounts(): Promise; /** diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 8de6b392..ceb2feb0 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -94,6 +94,10 @@ class WalletApi extends EventEmitter implements IWalletApi { return response.result; } + /** + * Request a connection to the wallet. Resolves with a list of accounts that the user has accepted + * to connect with. The list of accounts may be empty. + */ public async requestAccounts(): Promise { const response = await this.messageHandler.sendMessage>( MessageType.ConnectAccounts diff --git a/packages/browser-wallet-message-hub/src/handlers.ts b/packages/browser-wallet-message-hub/src/handlers.ts index 49c4ad6f..b26c2e57 100644 --- a/packages/browser-wallet-message-hub/src/handlers.ts +++ b/packages/browser-wallet-message-hub/src/handlers.ts @@ -298,8 +298,8 @@ export class ExtensionsMessageHandler extends BaseMessageHandler let whitelistedUrls: string[] = []; if (selectedAccount && allowlist) { whitelistedUrls = Object.entries(allowlist) - .filter((entry) => entry[1].includes(selectedAccount)) - .map((val) => val[0]); + .filter(([, accounts]) => accounts.includes(selectedAccount)) + .map(([url]) => url); } return tabs.filter(({ url }) => url !== undefined && whitelistedUrls?.includes(new URL(url).origin)); diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index d12a7084..0aa71a5e 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -20,7 +20,7 @@ export const relativeRoutes = { }, settings: { path: 'settings', - allowList: { + allowlist: { path: 'allowlist', }, passcode: { diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx index e4e38111..f9198671 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/Header/Header.tsx @@ -68,7 +68,7 @@ function getTitle(section: Section, pathname: string) { case Section.Id: return 'header.ids'; case Section.Settings: { - if (pathname.startsWith(absoluteRoutes.home.settings.allowList.path)) { + if (pathname.startsWith(absoluteRoutes.home.settings.allowlist.path)) { return 'header.settings.allowlist'; } if (pathname.startsWith(absoluteRoutes.home.settings.recovery.path)) { diff --git a/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx b/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx index b87f6c6b..b5432273 100644 --- a/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx +++ b/packages/browser-wallet/src/popup/pages/Settings/Settings.tsx @@ -32,7 +32,7 @@ export default function Settings() { return (
- + {t('allowlist')} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 93b64398..85c30f07 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -197,7 +197,7 @@ export default function Routes() { } path={relativeRoutes.home.identities.path} /> } /> - } path={`${relativeRoutes.home.settings.allowList.path}/*`} /> + } path={`${relativeRoutes.home.settings.allowlist.path}/*`} /> } path={relativeRoutes.home.settings.passcode.path} /> } path={relativeRoutes.home.settings.network.path} /> } path={relativeRoutes.home.settings.recovery.path} /> diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 90fc869c..0be06a1c 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -25,7 +25,7 @@ export enum ChromeStorageKey { Cookie = 'cookie', OpenPrompt = 'openPrompt', AcceptedTerms = 'acceptedTerms', - AllowList = 'allowList', + AllowList = 'allowlist', } export enum Theme { From 495e8dfb2987a35569d0667fc920b29bb9f900f9 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 2 Jun 2023 11:37:36 +0200 Subject: [PATCH 022/231] UI improvements from UX discussions --- .../src/popup/pages/Account/Account.tsx | 12 ++- .../src/popup/pages/Account/ConnectedBox.tsx | 43 ++++++++--- .../src/popup/pages/Allowlist/Allowlist.scss | 12 +++ .../src/popup/pages/Allowlist/Allowlist.tsx | 4 +- .../popup/pages/Allowlist/AllowlistEditor.tsx | 74 +++++++++++-------- .../pages/Allowlist/AllowlistEntryView.tsx | 23 ++++-- .../src/popup/pages/Allowlist/i18n/da.ts | 12 ++- .../src/popup/pages/Allowlist/i18n/en.ts | 12 ++- .../ConnectAccountsRequest.tsx | 4 +- 9 files changed, 134 insertions(+), 62 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Account/Account.tsx b/packages/browser-wallet/src/popup/pages/Account/Account.tsx index bb1104b6..34bf2630 100644 --- a/packages/browser-wallet/src/popup/pages/Account/Account.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/Account.tsx @@ -1,7 +1,7 @@ import { useAtomValue } from 'jotai'; import React, { useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Navigate, Outlet, Route, Routes, useNavigate } from 'react-router-dom'; +import { Navigate, Outlet, Route, Routes, generatePath, useNavigate } from 'react-router-dom'; import { accountsAtom } from '@popup/store/account'; import MenuButton from '@popup/shared/MenuButton'; import { useSelectedCredential } from '@popup/shared/utils/account-helpers'; @@ -9,6 +9,7 @@ import { CreationStatus } from '@shared/storage/types'; import { useCurrentOpenTabUrl } from '@popup/shared/utils/tabs'; import Button from '@popup/shared/Button'; import { absoluteRoutes } from '@popup/constants/routes'; +import { allowlistRoutes } from '@popup/pages/Allowlist/routes'; import { accountRoutes } from './routes'; import AccountActions from './AccountActions'; import DisplayAddress from './DisplayAddress'; @@ -57,7 +58,14 @@ function Account() { onClick={() => setDetailsExpanded((o) => !o)} /> - +
{isConfirmed && } diff --git a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx index ae79436e..2f67c20c 100644 --- a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx @@ -1,29 +1,50 @@ import { useAtomValue } from 'jotai'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { storedAllowlistAtom } from '@popup/store/account'; import clsx from 'clsx'; +import { Link } from 'react-router-dom'; -type Props = { +interface Props { accountAddress?: string; url?: string; -}; + link?: string; +} -export default function ConnectedBox({ accountAddress, url }: Props) { +export default function ConnectedBox({ accountAddress, url, link }: Props) { const { t } = useTranslation('account'); - const [isConnectedToSite, setIsConnectedToSite] = useState(); + const [isWalletConnectedToSite, setWalletConnectedToSite] = useState(); + const [isAccountConnectedToSite, setAccountConnectedToSite] = useState(); const allowlist = useAtomValue(storedAllowlistAtom); - useMemo(() => { + useEffect(() => { if (accountAddress && !allowlist.loading && url) { - const connectedAccountsForUrl = allowlist.value[url] ?? []; - setIsConnectedToSite(connectedAccountsForUrl.includes(accountAddress)); + const allowlistForUrl = allowlist.value[url]; + const connectedAccountsForUrl = allowlistForUrl ?? []; + setAccountConnectedToSite(connectedAccountsForUrl.includes(accountAddress)); + setWalletConnectedToSite(allowlistForUrl !== undefined); } }, [accountAddress, allowlist, url]); + if (!link || (!isWalletConnectedToSite && !isAccountConnectedToSite)) { + return ( +
+ {isAccountConnectedToSite ? t('siteConnected') : t('siteNotConnected')} +
+ ); + } + return ( -
- {isConnectedToSite ? t('siteConnected') : t('siteNotConnected')} -
+ + {isAccountConnectedToSite ? t('siteConnected') : t('siteNotConnected')} + ); } diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index 56100acc..95f90fc7 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -100,4 +100,16 @@ flex-direction: column; justify-content: space-between; align-items: center; + + &__remove-wrapper { + width: 100%; + display: flex; + justify-content: center; + margin-top: rem(10px); + padding-top: rem(10px); + } + + &__remove-button { + color: $color-danger; + } } diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index 6e7fd984..fb9ec2ef 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -8,7 +8,7 @@ import Button from '@popup/shared/Button'; import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { allowlistRoutes } from './routes'; -import AllowListView from './AllowlistEditor'; +import AllowlistEditor from './AllowlistEditor'; function LoadingAllowlistPage() { return
; @@ -56,7 +56,7 @@ export default function AllowListRoutes() { return ( } /> - } /> + } /> ); } diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx index 1e6048e9..5c6ae92b 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEditor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Button from '@popup/shared/Button/Button'; import { selectedAccountAtom, storedAllowlistAtom } from '@popup/store/account'; @@ -6,6 +6,8 @@ import { useAtom, useAtomValue } from 'jotai'; import { useTranslation } from 'react-i18next'; import { popupMessageHandler } from '@popup/shared/message-handler'; import { EventType } from '@concordium/browser-wallet-api-helpers'; +import Modal from '@popup/shared/Modal/Modal'; +import ButtonGroup from '@popup/shared/ButtonGroup'; import AllowlistEntryView, { AllowlistMode } from './AllowlistEntryView'; import { handleAllowlistEntryUpdate } from './util'; @@ -15,8 +17,8 @@ function LoadingAllowlistEditor() { export default function AllowlistEditor() { const nav = useNavigate(); - const { t } = useTranslation('allowlist', { keyPrefix: 'view' }); - const [selectedAccounts, setSelectedAccounts] = useState([]); + const { t } = useTranslation('allowlist', { keyPrefix: 'editor' }); + const [showPrompt, setShowPrompt] = useState(false); const { serviceName } = useParams<{ serviceName: string }>(); const [allowListLoading, setAllowList] = useAtom(storedAllowlistAtom); const selectedAccount = useAtomValue(selectedAccountAtom); @@ -27,12 +29,6 @@ export default function AllowlistEditor() { const decodedServiceName = decodeURIComponent(serviceName); - useEffect(() => { - if (!allowListLoading.loading) { - setSelectedAccounts(allowListLoading.value[serviceName]); - } - }, [allowListLoading]); - async function removeService(serviceNameToRemove: string, allowlist: Record) { const updatedAllowlist = { ...allowlist }; const disconnectedAccounts = [...updatedAllowlist[serviceNameToRemove]]; @@ -48,35 +44,51 @@ export default function AllowlistEditor() { return ; } + const trigger = ( + + ); + return (
+ handleAllowlistEntryUpdate( + decodedServiceName, + allowListLoading.value, + accounts, + setAllowList, + selectedAccount + ) + } />
- - +

{t('modal.header')}

+

{t('modal.description')}

+ + + + +
); diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx index 1b15c7c6..77232021 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/AllowlistEntryView.tsx @@ -1,6 +1,6 @@ import AccountInfoListenerContextProvider, { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; import { displaySplitAddress, useIdentityName } from '@popup/shared/utils/account-helpers'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Checkbox } from '@popup/shared/Form/Checkbox'; import { WalletCredential } from '@shared/storage/types'; import { displayAsCcd } from 'wallet-common-helpers'; @@ -44,26 +44,33 @@ export enum AllowlistMode { } export interface Props { - selectedAccounts: string[]; - setSelectedAccounts: React.Dispatch>; + initialSelectedAccounts: string[]; mode: AllowlistMode; + onChange?: (selectedAccounts: string[]) => void; } -export default function AllowlistEntryView({ selectedAccounts, setSelectedAccounts, mode }: Props) { +export default function AllowlistEntryView({ initialSelectedAccounts, mode, onChange }: Props) { const { t } = useTranslation('allowlist', { keyPrefix: 'entry' }); + const [selectedAccounts, setSelectedAccounts] = useState(initialSelectedAccounts); const accounts = useAtomValue(credentialsAtom); /** * Update the local state containing the list of account addresses. If the address - * is already present, then it is removed, if not present then it is added. + * is already present, then it is removed, if not present then it is added. The onChange + * callback is called with the new list of selected account addresses. * @param accountAddress the account address to add or remove from the list. */ function updateAccountAddressChecked(accountAddress: string) { + let updatedSelectedAccounts = []; if (selectedAccounts.includes(accountAddress)) { - const updatedAccounts = selectedAccounts.filter((acc) => acc !== accountAddress); - setSelectedAccounts(updatedAccounts); + updatedSelectedAccounts = selectedAccounts.filter((acc) => acc !== accountAddress); } else { - setSelectedAccounts([...selectedAccounts, accountAddress]); + updatedSelectedAccounts = [...selectedAccounts, accountAddress]; + } + + setSelectedAccounts(updatedSelectedAccounts); + if (onChange) { + onChange(updatedSelectedAccounts); } } diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts index e492f760..bfd47dab 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/da.ts @@ -10,9 +10,15 @@ const t: typeof en = { modifyDescription: 'Du kan ændre i hvilke konti der er tilgængelige for servicen nedenfor. En allowlisted service kan altid anmode om proofs.', }, - view: { - remove: 'Fjern service fra allowlisten', - update: 'Opdater allowlist', + editor: { + removeButton: 'Fjern service fra allowlisten', + modal: { + header: 'Fjern service?', + description: + 'Når du fjerner en service betyder det, at den ikke længere kan anmode om credential proofs, eller anmode dig om at signere transkationer.', + keep: 'Behold', + remove: 'Fjern', + }, }, }; diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts index 11fb6eff..32efd3c8 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Allowlist/i18n/en.ts @@ -8,9 +8,15 @@ const t = { modifyDescription: 'You can modify which accounts are accessible to the service below. Any allowlisted service can request proofs.', }, - view: { - remove: 'Remove', - update: 'Update', + editor: { + removeButton: 'Remove service from allowlist', + modal: { + header: 'Remove service?', + description: + 'Removing a service means that it cannot request credential proofs, or ask you to sign transactions anymore.', + keep: 'Keep', + remove: 'Remove', + }, }, }; diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 84d43129..271f3ad9 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -57,9 +57,9 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) {

{t('header', { url: urlDisplay })}

setAccountsToAdd(selectedAccounts)} />
); From 851f59b0d2e833fd3dcd1d60ac3d1615f65a12b9 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 15 Jun 2023 14:52:27 +0200 Subject: [PATCH 024/231] Exchange allowlist icon --- packages/browser-wallet/src/assets/svg/arrow-forward.svg | 3 +++ .../src/popup/pages/Allowlist/Allowlist.scss | 9 +++++++-- .../src/popup/pages/Allowlist/Allowlist.tsx | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/arrow-forward.svg diff --git a/packages/browser-wallet/src/assets/svg/arrow-forward.svg b/packages/browser-wallet/src/assets/svg/arrow-forward.svg new file mode 100644 index 00000000..5e271fb3 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/arrow-forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index 34b7be8b..11b8908a 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -8,8 +8,13 @@ .allowlist-page { &__link-icon { - width: rem(20px); - height: rem(10px); + height: rem(12px); + display: block; + stroke: $color-cta; + + path { + fill: $color-cta; + } } &__list-item { diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index fb9ec2ef..365cf854 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -2,7 +2,7 @@ import React from 'react'; import SidedRow from '@popup/shared/SidedRow/SidedRow'; import { storedAllowlistAtom } from '@popup/store/account'; import { useAtomValue } from 'jotai'; -import LinkIcon from '@assets/svg/link.svg'; +import ForwardArrowIcon from '@assets/svg/arrow-forward.svg'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import Button from '@popup/shared/Button'; import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; @@ -41,7 +41,7 @@ function Allowlist() { left={displayUrl(serviceName)} right={ } /> From 04022269640931030e293925c88a5ce7c8d7b0a1 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 19 Jun 2023 16:30:58 +0200 Subject: [PATCH 025/231] Improve UI and add status icons --- .../browser-wallet/src/assets/svg/block.svg | 3 + .../browser-wallet/src/assets/svg/pending.svg | 4 + .../browser-wallet/src/assets/svg/revoked.svg | 5 + .../src/assets/svg/verified.svg | 4 + .../src/popup/constants/dimensions.ts | 2 +- .../VerifiableCredential.scss | 77 +++++++--- .../VerifiableCredentialCard.stories.tsx | 138 ++++++++++++++++++ .../VerifiableCredentialCard.tsx | 34 ++++- .../VerifiableCredentialHooks.tsx | 13 ++ .../VerifiableCredentialList.tsx | 4 +- .../src/shared/storage/types.ts | 8 +- 11 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/block.svg create mode 100644 packages/browser-wallet/src/assets/svg/pending.svg create mode 100644 packages/browser-wallet/src/assets/svg/revoked.svg create mode 100644 packages/browser-wallet/src/assets/svg/verified.svg create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx diff --git a/packages/browser-wallet/src/assets/svg/block.svg b/packages/browser-wallet/src/assets/svg/block.svg new file mode 100644 index 00000000..38530dd6 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/block.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/assets/svg/pending.svg b/packages/browser-wallet/src/assets/svg/pending.svg new file mode 100644 index 00000000..81bbf1e9 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/pending.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/browser-wallet/src/assets/svg/revoked.svg b/packages/browser-wallet/src/assets/svg/revoked.svg new file mode 100644 index 00000000..63fa1f8c --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/revoked.svg @@ -0,0 +1,5 @@ + + + + diff --git a/packages/browser-wallet/src/assets/svg/verified.svg b/packages/browser-wallet/src/assets/svg/verified.svg new file mode 100644 index 00000000..22c3cc1f --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/browser-wallet/src/popup/constants/dimensions.ts b/packages/browser-wallet/src/popup/constants/dimensions.ts index 8dada543..dbf774cd 100644 --- a/packages/browser-wallet/src/popup/constants/dimensions.ts +++ b/packages/browser-wallet/src/popup/constants/dimensions.ts @@ -17,6 +17,6 @@ export const medium: Dimensions = { // >=1440p export const large: Dimensions = { - width: 354, + width: 375, height: 600, // Max allowed }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 1afe0358..9cef5704 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -1,45 +1,76 @@ +.verifiable-credential-list { + overflow-y: auto; +} + .verifiable-credential { - border-radius: rem(8px); - background: #f4bb5c; - padding: rem(10px); - margin: rem(10px); + color: $color-white; + border-radius: rem(16px); + background: #148f9d; + margin: rem(16px); box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); position: relative; - &__header-logo { - position: absolute; - left: rem(10px); - top: rem(10px); - width: rem(20px); - } + &__header { + display: flex; + align-items: center; + height: 62px; - &__header-title { - position: absolute; - left: rem(35px); - top: rem(10px); - width: rem(100px); - } + &__logo { + flex-shrink: 0; + margin-left: rem(11px); + width: rem(28px); + height: rem(28px); - &__header-status { - position: absolute; - right: rem(10px); - top: rem(10px); + svg { + path { + fill: $color-white; + } + } + } + + &__title { + font-size: rem(10px); + margin-left: rem(7px); + } + + &__status { + display: flex; + font-size: rem(8px); + color: $color-white; + margin-right: rem(11px); + margin-left: auto; + align-items: center; + + svg { + margin-left: rem(5px); + } + + path { + fill: $color-white; + } + } } &__body-attributes { - margin-top: rem(30px); + padding: rem(11px); &-row { - margin-bottom: rem(6px); + &:not(:last-child) { + margin-bottom: rem(10px); + } label { + color: $color-white; + opacity: 0.5; + font-size: rem(9px); font-variant: small-caps; display: block; } &-value { display: block; - font-weight: $font-weight-bold; + font-size: rem(10px); + font-weight: $font-weight-light; } } } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx new file mode 100644 index 00000000..252c2fb3 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -0,0 +1,138 @@ +/* eslint-disable react/function-component-definition, react/destructuring-assignment */ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { VerifiableCredentialCard } from './VerifiableCredentialCard'; + +export default { + title: 'VerifiableCredential/VerifiableCredentialCard', + component: VerifiableCredentialCard, +} as ComponentMeta; + +const schema = { + type: 'https://w3c-ccg.github.io/vc-json-schemas/', + version: '1.0', + id: 'https://example-university.com/certificates/simple-education-certificate.json', + name: 'UniversityDegreeCredential', + author: 'did:ccd:mainnet:acc:...', + authored: '2023-01-01T00:00:00+00:00', + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'https://example-university.com/certificates/simple-education-certificate.json', + title: 'Education certificate', + description: 'Simple representation of an education certificate.', + type: 'object', + required: ['@context', 'type', 'issuer', 'issuanceDate', 'credentialSubject'], + properties: { + '@context': { + type: ['string', 'array', 'object'], + }, + id: { + type: 'string', + format: 'uri', + }, + type: { + type: ['string', 'array'], + items: { + type: 'string', + }, + }, + issuer: { + type: ['string', 'object'], + format: 'uri', + required: ['id'], + properties: { + id: { + type: 'string', + format: 'uri', + }, + }, + }, + issuanceDate: { + type: 'string', + format: 'date-time', + }, + expirationDate: { + type: 'string', + format: 'date-time', + }, + validFrom: { + type: 'string', + format: 'date-time', + }, + validUntil: { + type: 'string', + format: 'date-time', + }, + credentialSubject: { + type: 'object', + properties: { + id: { + title: 'id', + type: 'string', + description: 'Credential subject identifier', + }, + degreeType: { + title: 'Degree type', + type: 'string', + description: 'Degree type', + index: '0', + }, + degreeName: { + title: 'Degree name', + type: 'string', + description: 'Degree name', + index: '1', + }, + graduationDate: { + type: 'string', + format: 'date-time', + description: 'Graduation date', + index: '2', + title: 'Graduation date', + }, + }, + required: ['id', 'degreeType', 'degreeName', 'graduationDate'], + }, + credentialSchema: { + type: 'object', + required: ['id', 'type'], + properties: { + id: { + type: 'string', + format: 'uri', + }, + type: { + type: 'string', + }, + }, + }, + }, + }, + proof: '', +}; + +const verifiableCredential = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'Concordium VC URI'], + id: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/ff4aa77af80b4d72973ccb957d180746', + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/issuer', + issuanceDate: '2010-01-01T00:00:00Z', + credentialSubject: { + id: 'did:ccd:pkc:ebfeb1f712ebc6f1c276e12ec21', + degreeType: 'Bachelor degree', + degreeName: 'Bachelor of Science and Arts', + graduationDate: '2010-06-01T00:00:00Z', + }, + credentialSchema: { + id: 'https://example-university.com/certificates/simple-education-certificate.json', + type: 'CredentialSchema2022', // the same for all schemas + }, +}; + +export const Primary: ComponentStory = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 467df9db..3c31d29f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -1,25 +1,47 @@ import React, { PropsWithChildren } from 'react'; import CcdIcon from '@assets/svg/concordium.svg'; +import RevokedIcon from '@assets/svg/revoked.svg'; +import ActiveIcon from '@assets/svg/verified.svg'; +import ExpiredIcon from '@assets/svg/block.svg'; +import PendingIcon from '@assets/svg/pending.svg'; import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema, } from '../../../shared/storage/types'; +import { useCredentialStatus } from './VerifiableCredentialHooks'; function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { + let icon = null; switch (status) { case VerifiableCredentialStatus.Active: + icon = ; + break; case VerifiableCredentialStatus.Revoked: + icon = ; + break; case VerifiableCredentialStatus.Expired: + icon = ; + break; case VerifiableCredentialStatus.NotActivated: + icon = ; + break; default: - return
{VerifiableCredentialStatus[status]}
; + icon = null; + break; } + + return ( +
+ {VerifiableCredentialStatus[status]} + {icon} +
+ ); } function Logo() { return ( -
+
); @@ -103,6 +125,8 @@ export function VerifiableCredentialCard({ schema: VerifiableCredentialSchema; onClick?: () => void; }) { + const credentialStatus = useCredentialStatus(); + const attributes = Object.entries(credential.credentialSubject) .filter((val) => val[0] !== 'id') .map(applySchema(schema)) @@ -110,10 +134,10 @@ export function VerifiableCredentialCard({ return ( -
+
-
{credential.type[0]}
- +
Concordium Employment
+
{attributes && diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx new file mode 100644 index 00000000..08945ff5 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -0,0 +1,13 @@ +import { VerifiableCredentialStatus } from '@shared/storage/types'; +import { useEffect, useState } from 'react'; + +// TODO This function should look up the status in the registry contract. +export function useCredentialStatus() { + const [status, setStatus] = useState(VerifiableCredentialStatus.Unknown); + + useEffect(() => { + setStatus(VerifiableCredentialStatus.Active); + }, []); + + return status; +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index cf1a6adf..5d46de0b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -49,7 +49,7 @@ export default function VerifiableCredentialList() { } return ( - <> +
{verifiableCredentials.map((credential, index) => { return ( ); })} - +
); } diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index a7749196..ad957e76 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -261,6 +261,7 @@ export enum VerifiableCredentialStatus { Revoked, Expired, NotActivated, + Unknown, } interface CredentialSchema { @@ -271,7 +272,7 @@ interface CredentialSchema { export type CredentialSubject = { id: string } & Record; export interface VerifiableCredential { - context: string[]; + '@context': string[]; id: string; type: string[]; issuer: string; @@ -280,11 +281,12 @@ export interface VerifiableCredential { credentialSchema: CredentialSchema; } +// TODO Type this so that the id property does not require an index. interface CredentialSchemaProperty { title: string; - type: 'string' | 'number'; + type: 'string' | 'number' | string; description: string; - index: string; + index?: string; } interface CredentialSchemaSubject { From b2744c9d361d00c1e27d778c250ae709f86fdbea Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 23 Jun 2023 09:35:45 +0200 Subject: [PATCH 026/231] Make full line in allowlist clickable --- .../src/popup/pages/Allowlist/Allowlist.scss | 17 ++++++++++---- .../src/popup/pages/Allowlist/Allowlist.tsx | 22 ++++++++----------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index 11b8908a..ed0458f4 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -7,21 +7,30 @@ } .allowlist-page { + &__link { + display: block; + + &:not(:last-child) { + border-bottom: rem(1px) dashed $color-petroleum; + } + } + &__link-icon { height: rem(12px); display: block; - stroke: $color-cta; path { - fill: $color-cta; + fill: $color-text; } } &__list-item { padding: rem(10px) 0; - &:not(:last-child) { - border-bottom: rem(1px) dashed $color-petroleum; + &:hover { + path { + fill: $color-cta; + } } } } diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx index 365cf854..4253d122 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.tsx @@ -4,8 +4,7 @@ import { storedAllowlistAtom } from '@popup/store/account'; import { useAtomValue } from 'jotai'; import ForwardArrowIcon from '@assets/svg/arrow-forward.svg'; import { displayUrl } from '@popup/shared/utils/string-helpers'; -import Button from '@popup/shared/Button'; -import { Route, Routes, generatePath, useNavigate } from 'react-router-dom'; +import { Link, Route, Routes, generatePath } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { allowlistRoutes } from './routes'; import AllowlistEditor from './AllowlistEditor'; @@ -20,7 +19,6 @@ function EmptyAllowlistPage() { } function Allowlist() { - const nav = useNavigate(); const allowlistWithLoading = useAtomValue(storedAllowlistAtom); const allowlist = allowlistWithLoading.value; @@ -36,16 +34,14 @@ function Allowlist() { {Object.keys(allowlist).map((serviceName) => { const path = generatePath(allowlistRoutes.edit, { serviceName: encodeURIComponent(serviceName) }); return ( -
- nav(path)}> - - - } - /> -
+ +
+ } + /> +
+ ); })}
From 315b94e16ecc7c4cae26ae674126e72cd6416851 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 23 Jun 2023 12:59:37 +0200 Subject: [PATCH 027/231] Fix review suggestions --- packages/browser-wallet-api/src/wallet-api.ts | 5 +++-- .../src/popup/page-layouts/MainLayout/i18n/da.ts | 2 +- .../src/popup/page-layouts/MainLayout/i18n/en.ts | 2 +- .../browser-wallet/src/popup/pages/Account/ConnectedBox.tsx | 3 +-- packages/browser-wallet/src/popup/store/account.ts | 2 +- packages/browser-wallet/src/popup/store/utils.ts | 2 +- packages/browser-wallet/src/shared/storage/access.ts | 2 +- packages/browser-wallet/src/shared/storage/types.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index ceb2feb0..850986c7 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -96,9 +96,10 @@ class WalletApi extends EventEmitter implements IWalletApi { /** * Request a connection to the wallet. Resolves with a list of accounts that the user has accepted - * to connect with. The list of accounts may be empty. + * to connect with. The list of accounts may be empty. It rejects if the request is rejected or closed + * by the user in the wallet. */ - public async requestAccounts(): Promise { + public async requestAccounts(): Promise { const response = await this.messageHandler.sendMessage>( MessageType.ConnectAccounts ); diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index 91ae920a..fa6da79a 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -21,7 +21,7 @@ const t: typeof en = { idProof: 'Bevis for identitet', request: 'Anmodning om Signatur', connect: 'Ny Forbindelse', - connectAccountsRequest: 'Anmodning om tilladelse', + allowlistingRequest: 'Anmodning om tilladelse', }, entityList: { searchPlaceholder: 'Søg', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index 673646bf..7d42196c 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -19,7 +19,7 @@ const t = { idProof: 'Proof of identity', request: 'Signature Request', connect: 'New connection', - connectAccountsRequest: 'Allowlisting request', + allowlistingRequest: 'Allowlisting request', }, entityList: { searchPlaceholder: 'Search', diff --git a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx index 2f67c20c..808450ff 100644 --- a/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/ConnectedBox.tsx @@ -20,8 +20,7 @@ export default function ConnectedBox({ accountAddress, url, link }: Props) { useEffect(() => { if (accountAddress && !allowlist.loading && url) { const allowlistForUrl = allowlist.value[url]; - const connectedAccountsForUrl = allowlistForUrl ?? []; - setAccountConnectedToSite(connectedAccountsForUrl.includes(accountAddress)); + setAccountConnectedToSite(allowlistForUrl?.includes(accountAddress) ?? false); setWalletConnectedToSite(allowlistForUrl !== undefined); } }, [accountAddress, allowlist, url]); diff --git a/packages/browser-wallet/src/popup/store/account.ts b/packages/browser-wallet/src/popup/store/account.ts index a1d4215a..aa21acf2 100644 --- a/packages/browser-wallet/src/popup/store/account.ts +++ b/packages/browser-wallet/src/popup/store/account.ts @@ -20,7 +20,7 @@ export const storedConnectedSitesAtom = atomWithChromeStorage>( - ChromeStorageKey.AllowList, + ChromeStorageKey.Allowlist, {}, true ); diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 700e3fdd..ebf80b1f 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -56,7 +56,7 @@ const accessorMap = { [ChromeStorageKey.Cookie]: useIndexedStorage(sessionCookie, getGenesisHash), [ChromeStorageKey.OpenPrompt]: sessionOpenPrompt, [ChromeStorageKey.AcceptedTerms]: storedAcceptedTerms, - [ChromeStorageKey.AllowList]: storedAllowlist, + [ChromeStorageKey.Allowlist]: storedAllowlist, }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 210f547e..33de0f93 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -147,7 +147,7 @@ export const storedTokenMetadata = makeStorageAccessor('local', ChromeStorageKey.AcceptedTerms); const indexedStoredAllowlist = makeIndexedStorageAccessor>( 'local', - ChromeStorageKey.AllowList + ChromeStorageKey.Allowlist ); export const storedAllowlist = useIndexedStorage(indexedStoredAllowlist, getGenesisHash); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 0be06a1c..1e8f9a43 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -25,7 +25,7 @@ export enum ChromeStorageKey { Cookie = 'cookie', OpenPrompt = 'openPrompt', AcceptedTerms = 'acceptedTerms', - AllowList = 'allowlist', + Allowlist = 'allowlist', } export enum Theme { From 31b564a2104efbbec96fbfc946b9872ccfd32d8a Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 27 Jun 2023 14:36:24 +0200 Subject: [PATCH 028/231] Get credential status from registry --- .../VerifiableCredentialCard.tsx | 2 +- .../VerifiableCredentialHooks.tsx | 26 ++++-- .../utils/verifiable-credential-helpers.ts | 83 +++++++++++++++++++ .../verifiable-credential-helpers.test.ts | 34 ++++++++ 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts create mode 100644 packages/browser-wallet/test/verifiable-credential-helpers.test.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 3c31d29f..a464ce1b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -125,7 +125,7 @@ export function VerifiableCredentialCard({ schema: VerifiableCredentialSchema; onClick?: () => void; }) { - const credentialStatus = useCredentialStatus(); + const credentialStatus = useCredentialStatus(credential); const attributes = Object.entries(credential.credentialSubject) .filter((val) => val[0] !== 'id') diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 08945ff5..5a56e0a8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,13 +1,29 @@ -import { VerifiableCredentialStatus } from '@shared/storage/types'; +import { grpcClientAtom } from '@popup/store/settings'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; +import { + getCredentialHolderId, + getCredentialRegistryContractAddress, + getVerifiableCredentialStatus, +} from '@shared/utils/verifiable-credential-helpers'; +import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; -// TODO This function should look up the status in the registry contract. -export function useCredentialStatus() { +/** + * Retrieve the on-chain credential status for a verifiable credential in a registry contract. + * @param credential the verifiable credential to lookup the status for + * @returns the status for the given credential + */ +export function useCredentialStatus(credential: VerifiableCredential) { const [status, setStatus] = useState(VerifiableCredentialStatus.Unknown); + const client = useAtomValue(grpcClientAtom); useEffect(() => { - setStatus(VerifiableCredentialStatus.Active); - }, []); + const credentialHolderId = getCredentialHolderId(credential.id); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getVerifiableCredentialStatus(client, registryContractAddress, credentialHolderId).then((credentialStatus) => { + setStatus(credentialStatus); + }); + }, [credential.id, client]); return status; } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts new file mode 100644 index 00000000..956a5942 --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -0,0 +1,83 @@ +import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; +import { VerifiableCredentialStatus } from '@shared/storage/types'; +import { Buffer } from 'buffer/'; + +/** + * Extracts the credential holder id from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the credential holder id + */ +export function getCredentialHolderId(credentialId: string): string { + const splitted = credentialId.split('/'); + const credentialHolderId = splitted[splitted.length - 1]; + + if (credentialHolderId.length !== 64) { + throw new Error(`Invalid credential holder id found from: ${credentialId}`); + } + + return credentialHolderId; +} + +/** + * Extracts the credential registry contract addres from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the contract address of the issuing contract of the provided credential id + */ +export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { + const splitted = credentialId.split(':'); + const index = BigInt(splitted[4]); + const subindex = BigInt(splitted[5].split('/')[0]); + return { index, subindex }; +} + +function deserializeCredentialStatus(serializedCredentialStatus: string): VerifiableCredentialStatus { + const buff = Buffer.from(serializedCredentialStatus, 'hex'); + switch (buff.readUInt8(0)) { + case 0: + return VerifiableCredentialStatus.Active; + case 1: + return VerifiableCredentialStatus.Revoked; + case 2: + return VerifiableCredentialStatus.Expired; + case 3: + return VerifiableCredentialStatus.NotActivated; + default: + throw new Error(`Received an invalid credential status: ${serializedCredentialStatus}`); + } +} + +/** + * Get the status of a verifiable credential in a CIS-4 contract. + * @param client the GRPC client for accessing a node + * @param contractAddress the address of a CIS-4 contract + * @param credentialHolderId the public key for the credential holder + * @throws an error if the invoke contract call fails, or if no return value is available + * @returns the status of the verifiable credential, the status will be unknown if the contract is not found + */ +export async function getVerifiableCredentialStatus( + client: ConcordiumGRPCClient, + contractAddress: ContractAddress, + credentialHolderId: string +) { + const instanceInfo = await client.getInstanceInfo(contractAddress); + if (instanceInfo === undefined) { + return VerifiableCredentialStatus.Unknown; + } + + const result = await client.invokeContract({ + contract: contractAddress, + method: `${instanceInfo.name.substring(5)}.credentialStatus`, + parameter: Buffer.from(credentialHolderId, 'hex'), + }); + + if (result.tag !== 'success') { + throw new Error(result.reason.tag); + } + + const { returnValue } = result; + if (returnValue === undefined) { + throw new Error(`Return value is missing from credentialStatus result in CIS-4 contract: ${contractAddress}`); + } + + return deserializeCredentialStatus(returnValue); +} diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts new file mode 100644 index 00000000..fe8a57d3 --- /dev/null +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -0,0 +1,34 @@ +import { + getCredentialHolderId, + getCredentialRegistryContractAddress, +} from '../src/shared/utils/verifiable-credential-helpers'; + +test('credential holder id is extracted from verifiable credential id field', () => { + const id = + 'did:ccd:mainnet:sci:4718:0/credentialEntry/2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106'; + expect(getCredentialHolderId(id)).toEqual('2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106'); +}); + +test('an error is thrown if credential holder id has invalid length', () => { + const id = + 'did:ccd:mainnet:sci:4718:0/credentialEntry/2eec102b173118dda466411fc7df88093788a34c3e4b0a8891f5c671a9d106'; + expect(() => getCredentialHolderId(id)).toThrow(Error); +}); + +test('credentialHolderId: an error is thrown if the credential id is invalid', () => { + const id = + 'did:ccd:mainnet:sci:4718:0credentialEntry2eec102b173118dda466411fc7df88093788a34c3e4b0a8891f5c671a9d106'; + expect(() => getCredentialHolderId(id)).toThrow(Error); +}); + +test('registry contract address is extracted from verifiable credential id field', () => { + const id = + 'did:ccd:mainnet:sci:4718:0/credentialEntry/2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106'; + expect(getCredentialRegistryContractAddress(id)).toEqual({ index: BigInt(4718), subindex: BigInt(0) }); +}); + +test('credentialRegistryContractAddress: an error is thrown if the credential id is invalid', () => { + const id = + 'did:ccd:mainnet:sci:4718:0credentialEntry2eec102b173118dda466411fc7df88093788a34c3e4b0a8891f5c671a9d106'; + expect(() => getCredentialRegistryContractAddress(id)).toThrow(Error); +}); From 85953b57d6285d8d38f21c39e8022bb0c68ea0c1 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 29 Jun 2023 11:37:50 +0200 Subject: [PATCH 029/231] Use constructor injection for credential status hook --- .../VerifiableCredentialCard.stories.tsx | 7 ++++++- .../VerifiableCredential/VerifiableCredentialCard.tsx | 4 ++-- .../VerifiableCredential/VerifiableCredentialList.tsx | 10 +++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index 252c2fb3..92057c44 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/function-component-definition, react/destructuring-assignment */ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { VerifiableCredentialStatus } from '@shared/storage/types'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; export default { @@ -132,7 +133,11 @@ const verifiableCredential = { export const Primary: ComponentStory = () => { return (
- + VerifiableCredentialStatus.Active} + />
); }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index a464ce1b..17eda68d 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -9,7 +9,6 @@ import { VerifiableCredentialStatus, VerifiableCredentialSchema, } from '../../../shared/storage/types'; -import { useCredentialStatus } from './VerifiableCredentialHooks'; function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { let icon = null; @@ -120,13 +119,14 @@ export function VerifiableCredentialCard({ credential, schema, onClick, + useCredentialStatus, }: { credential: VerifiableCredential; schema: VerifiableCredentialSchema; onClick?: () => void; + useCredentialStatus: (cred: VerifiableCredential) => VerifiableCredentialStatus; }) { const credentialStatus = useCredentialStatus(credential); - const attributes = Object.entries(credential.credentialSubject) .filter((val) => val[0] !== 'id') .map(applySchema(schema)) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 5d46de0b..6769b691 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -6,6 +6,7 @@ import { import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; +import { useCredentialStatus } from './VerifiableCredentialHooks'; /** * Component to display when there are no verifiable credentials in the wallet. @@ -45,7 +46,13 @@ export default function VerifiableCredentialList() { } if (selected) { - return ; + return ( + useCredentialStatus(cred)} + /> + ); } return ( @@ -58,6 +65,7 @@ export default function VerifiableCredentialList() { credential={credential} schema={schemas.value[credential.credentialSchema.id]} onClick={() => setSelected(credential)} + useCredentialStatus={(cred) => useCredentialStatus(cred)} /> ); })} From 74c4992f83b18bd6cf275e1fb9c34e5327873989 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 29 Jun 2023 16:01:25 +0200 Subject: [PATCH 030/231] Add Topbar component and use it --- .../src/assets/svg/back-icon.svg | 15 +++++ .../browser-wallet/src/assets/svg/more.svg | 4 ++ .../VerifiableCredential.scss | 11 +++- .../VerifiableCredentialList.tsx | 51 +++++++++------ .../src/popup/shared/Topbar/Topbar.scss | 33 ++++++++++ .../popup/shared/Topbar/Topbar.stories.tsx | 44 +++++++++++++ .../src/popup/shared/Topbar/Topbar.tsx | 63 +++++++++++++++++++ .../src/popup/shared/Topbar/index.ts | 1 + .../src/popup/styles/_components.scss | 1 + .../src/popup/styles/config/_typography.scss | 8 +++ 10 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/back-icon.svg create mode 100644 packages/browser-wallet/src/assets/svg/more.svg create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/index.ts diff --git a/packages/browser-wallet/src/assets/svg/back-icon.svg b/packages/browser-wallet/src/assets/svg/back-icon.svg new file mode 100644 index 00000000..c8fa3bd8 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/back-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/browser-wallet/src/assets/svg/more.svg b/packages/browser-wallet/src/assets/svg/more.svg new file mode 100644 index 00000000..0abc2086 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/more.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 9cef5704..47e42223 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -1,4 +1,6 @@ .verifiable-credential-list { + height: calc(100% - 56px); + background-color: $color-bg; overflow-y: auto; } @@ -6,7 +8,14 @@ color: $color-white; border-radius: rem(16px); background: #148f9d; - margin: rem(16px); + margin-left: rem(16px); + margin-right: rem(16px); + margin-bottom: rem(16px); + + &:not(:first-child) { + margin-top: rem(16px); + } + box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); position: relative; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 6769b691..157167fe 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -5,6 +5,7 @@ import { } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; +import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; @@ -47,28 +48,40 @@ export default function VerifiableCredentialList() { if (selected) { return ( - useCredentialStatus(cred)} - /> + <> + setSelected(undefined) }} + menuButton={{ type: ButtonTypes.More, onClick: () => {} }} + /> +
+ useCredentialStatus(cred)} + /> +
+ ); } return ( -
- {verifiableCredentials.map((credential, index) => { - return ( - setSelected(credential)} - useCredentialStatus={(cred) => useCredentialStatus(cred)} - /> - ); - })} -
+ <> + +
+ {verifiableCredentials.map((credential, index) => { + return ( + setSelected(credential)} + useCredentialStatus={(cred) => useCredentialStatus(cred)} + /> + ); + })} +
+ ); } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss new file mode 100644 index 00000000..0bd3da8b --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss @@ -0,0 +1,33 @@ +.topbar { + height: 56px; + background-color: $color-bg; + color: $color-cta; + padding-left: 16px; + padding-right: 16px; + display: flex; + align-items: center; + justify-content: center; + + &__title { + text-align: center; + width: 100%; + } + + &__icon-container { + height: 24px; + width: 24px; + flex-shrink: 0; + + &__icon { + display: flex; + align-items: center; + height: 24px; + width: 24px; + stroke-width: 0; + + path { + fill: $color-cta; + } + } + } +} diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx new file mode 100644 index 00000000..0df3dcaf --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx @@ -0,0 +1,44 @@ +/* eslint-disable react/function-component-definition */ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; +import Topbar, { ButtonTypes } from './Topbar'; + +export default { + title: 'Shared/Topbar', + component: Topbar, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + return ( + +
+ +
+
+ ); +}; + +export const WithBackButton = Template.bind({}); +WithBackButton.args = { + title: 'Page Navigation Title', +}; + +export const WithoutBackButton = Template.bind({}); +WithoutBackButton.args = { + title: 'Page Navigation Title', + backButton: { show: false }, +}; + +export const WithMoreMenuButton = Template.bind({}); +WithMoreMenuButton.args = { + title: 'Page Navigation Title', + backButton: { show: false }, + menuButton: { type: ButtonTypes.More, onClick: () => {} }, +}; + +export const WithBackAndMoreMenuButton = Template.bind({}); +WithBackAndMoreMenuButton.args = { + title: 'Page Navigation Title', + menuButton: { type: ButtonTypes.More, onClick: () => {} }, +}; diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx new file mode 100644 index 00000000..30ec1789 --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import BackIcon from '@assets/svg/back-icon.svg'; +import MoreIcon from '@assets/svg/more.svg'; +import { useNavigate } from 'react-router-dom'; +import Button from '../Button'; + +export enum ButtonTypes { + More, +} + +interface NoBackButton { + show: false; +} + +interface ShowBackButton { + show: true; + onClick: () => void; +} + +type BackButton = ShowBackButton | NoBackButton; + +interface MenuButton { + type: ButtonTypes; + onClick: () => void; +} + +interface TopbarProps { + title: string; + backButton?: BackButton; + menuButton?: MenuButton; +} + +export default function Topbar({ + title, + backButton = { + show: true, + onClick: () => { + const nav = useNavigate(); + return nav(-1); + }, + }, + menuButton, +}: TopbarProps) { + return ( +
+
+ {backButton.show && ( + + )} +
+
{title}
+
+ {menuButton && ( + + )} +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/shared/Topbar/index.ts b/packages/browser-wallet/src/popup/shared/Topbar/index.ts new file mode 100644 index 00000000..91156873 --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/index.ts @@ -0,0 +1 @@ +export { default } from './Topbar'; diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index d3845a5d..948dbc1d 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -21,6 +21,7 @@ @import '../shared/TokenDetails/TokenDetails'; @import '../shared/ContractTokenLine/ContractTokenLine'; @import '../shared/DisabledAmountInput/DisabledAmountInput'; +@import '../shared/Topbar/Topbar'; // Pages @import '../pages/Account'; diff --git a/packages/browser-wallet/src/popup/styles/config/_typography.scss b/packages/browser-wallet/src/popup/styles/config/_typography.scss index 132c6e82..e1ee33ac 100644 --- a/packages/browser-wallet/src/popup/styles/config/_typography.scss +++ b/packages/browser-wallet/src/popup/styles/config/_typography.scss @@ -1,6 +1,7 @@ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@200;300;400;500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@600&display=swap'); $font-family-roboto: 'Roboto', 'Inter', sans-serif; $font-family-mono: 'Roboto Mono', 'Inter', sans-serif; @@ -8,3 +9,10 @@ $font-weight-extra-light: 200; $font-weight-light: 300; $font-weight-regular: 400; $font-weight-bold: 500; + +.display6 { + font-family: 'Work Sans', sans-serif; + font-weight: 600; + font-size: 14px; + line-height: 17px; +} From 4dc891b25bc688c28730074ff7dd9a46bc9f9e34 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 30 Jun 2023 09:03:19 +0200 Subject: [PATCH 031/231] Add popup menu component --- .../browser-wallet/src/assets/svg/archive.svg | 15 +++++++ .../browser-wallet/src/assets/svg/revoke.svg | 3 ++ .../src/popup/shared/PopupMenu/PopupMenu.scss | 41 +++++++++++++++++++ .../shared/PopupMenu/PopupMenu.stories.tsx | 35 ++++++++++++++++ .../src/popup/shared/PopupMenu/PopupMenu.tsx | 27 ++++++++++++ .../src/popup/shared/PopupMenu/index.ts | 1 + .../src/popup/styles/_components.scss | 1 + .../src/popup/styles/config/_typography.scss | 7 ++++ 8 files changed, 130 insertions(+) create mode 100644 packages/browser-wallet/src/assets/svg/archive.svg create mode 100644 packages/browser-wallet/src/assets/svg/revoke.svg create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/index.ts diff --git a/packages/browser-wallet/src/assets/svg/archive.svg b/packages/browser-wallet/src/assets/svg/archive.svg new file mode 100644 index 00000000..f9dbe4ce --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/archive.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/browser-wallet/src/assets/svg/revoke.svg b/packages/browser-wallet/src/assets/svg/revoke.svg new file mode 100644 index 00000000..15142a8f --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/revoke.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss new file mode 100644 index 00000000..c1926c9b --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -0,0 +1,41 @@ +.popup-menu { + width: 200px; + box-shadow: 0 0 20px rgb(0 0 0 / 15%); + border-radius: 16px; + display: flex; + flex-direction: column; + + &__item { + &:hover { + background-color: lightgray; + } + + &:first-child { + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } + + &:last-child { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } + + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + height: 44px; + + &:not(:last-child) { + border-bottom: 1px solid lightgray; + } + + &__icon { + height: 20px; + width: 20px; + display: flex; + align-items: center; + justify-content: center; + } + } +} diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx new file mode 100644 index 00000000..864aa61f --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx @@ -0,0 +1,35 @@ +/* eslint-disable react/function-component-definition */ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; +import RevokeIcon from '@assets/svg/revoke.svg'; +import ArchiveIcon from '@assets/svg/archive.svg'; +import PopupMenu from './PopupMenu'; + +export default { + title: 'Shared/PopupMenu', + component: PopupMenu, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + return ( + +
+ +
+
+ ); +}; + +export const WithSingleItem = Template.bind({}); +WithSingleItem.args = { + items: [{ title: 'Revoke', icon: }], +}; + +export const WithTwoItems = Template.bind({}); +WithTwoItems.args = { + items: [ + { title: 'Revoke', icon: }, + { title: 'Archive', icon: }, + ], +}; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx new file mode 100644 index 00000000..b4136bdc --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Button from '../Button/Button'; + +interface PopupMenuItem { + title: string; + icon: JSX.Element; + onClick?: () => void; +} + +interface PopupMenuProps { + items: PopupMenuItem[]; +} + +export default function PopupMenu({ items }: PopupMenuProps) { + return ( +
+ {items.map((item) => { + return ( + + ); + })} +
+ ); +} diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts b/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts new file mode 100644 index 00000000..be13378e --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts @@ -0,0 +1 @@ +export { default } from './PopupMenu'; diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index 948dbc1d..8c580ac3 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -22,6 +22,7 @@ @import '../shared/ContractTokenLine/ContractTokenLine'; @import '../shared/DisabledAmountInput/DisabledAmountInput'; @import '../shared/Topbar/Topbar'; +@import '../shared/PopupMenu/PopupMenu'; // Pages @import '../pages/Account'; diff --git a/packages/browser-wallet/src/popup/styles/config/_typography.scss b/packages/browser-wallet/src/popup/styles/config/_typography.scss index e1ee33ac..ac33a567 100644 --- a/packages/browser-wallet/src/popup/styles/config/_typography.scss +++ b/packages/browser-wallet/src/popup/styles/config/_typography.scss @@ -16,3 +16,10 @@ $font-weight-bold: 500; font-size: 14px; line-height: 17px; } + +.heading6 { + font-family: $font-family-roboto; + font-weight: 500; + font-size: 14px; + line-height: 18px; +} From 46c9d1c9ced2cf973f402ec2d926489d2426e945 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 30 Jun 2023 10:25:24 +0200 Subject: [PATCH 032/231] Add revoke button to credential details view --- .../VerifiableCredentialList.tsx | 16 +++++++---- .../pages/VerifiableCredential/i18n/da.ts | 13 +++++++++ .../pages/VerifiableCredential/i18n/en.ts | 11 ++++++++ .../src/popup/shared/PopupMenu/PopupMenu.scss | 1 + .../src/popup/shared/PopupMenu/PopupMenu.tsx | 28 +++++++++++-------- .../src/popup/shared/Topbar/Topbar.scss | 12 ++++++++ .../popup/shared/Topbar/Topbar.stories.tsx | 4 +-- .../src/popup/shared/Topbar/Topbar.tsx | 19 +++++++++---- .../src/popup/shell/i18n/locales/da.ts | 2 ++ .../src/popup/shell/i18n/locales/en.ts | 2 ++ 10 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 157167fe..9a60af8a 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -6,8 +6,10 @@ import { import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; +import { useTranslation } from 'react-i18next'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; +import RevokeIcon from '../../../assets/svg/revoke.svg'; /** * Component to display when there are no verifiable credentials in the wallet. @@ -29,6 +31,7 @@ export default function VerifiableCredentialList() { const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const [selected, setSelected] = useState(); + const { t } = useTranslation('verifiableCredential'); if (schemas.loading) { return null; @@ -47,12 +50,14 @@ export default function VerifiableCredentialList() { } if (selected) { + const menuButton = { type: ButtonTypes.More, items: [{ title: t('menu.revoke'), icon: }] }; + return ( <> setSelected(undefined) }} - menuButton={{ type: ButtonTypes.More, onClick: () => {} }} + menuButton={menuButton} />
- +
- {verifiableCredentials.map((credential, index) => { + {verifiableCredentials.map((credential) => { return ( setSelected(credential)} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts new file mode 100644 index 00000000..df90dc40 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -0,0 +1,13 @@ +import type en from './en'; + +const t: typeof en = { + topbar: { + details: 'Kortdetaljer', + list: 'Web3 ID Kort', + }, + menu: { + revoke: 'Ophæv', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts new file mode 100644 index 00000000..3931b04d --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -0,0 +1,11 @@ +const t = { + topbar: { + details: 'Credential Details', + list: 'Web3 ID Credentials', + }, + menu: { + revoke: 'Revoke', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss index c1926c9b..4ff9c03c 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -4,6 +4,7 @@ border-radius: 16px; display: flex; flex-direction: column; + background-color: $color-white; &__item { &:hover { diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index b4136bdc..90c67d67 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { DetectClickOutside } from 'wallet-common-helpers'; import Button from '../Button/Button'; -interface PopupMenuItem { +export interface PopupMenuItem { title: string; icon: JSX.Element; onClick?: () => void; @@ -9,19 +10,22 @@ interface PopupMenuItem { interface PopupMenuProps { items: PopupMenuItem[]; + onClickOutside: () => void; } -export default function PopupMenu({ items }: PopupMenuProps) { +export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) { return ( -
- {items.map((item) => { - return ( - - ); - })} -
+ +
+ {items.map((item) => { + return ( + + ); + })} +
+
); } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss index 0bd3da8b..8ad3eefc 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss @@ -1,4 +1,5 @@ .topbar { + position: relative; height: 56px; background-color: $color-bg; color: $color-cta; @@ -30,4 +31,15 @@ } } } + + &__popup-menu { + display: none; + position: absolute; + right: 10px; + + &__show { + z-index: 1; + display: block; + } + } } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx index 0df3dcaf..4afbd4e9 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx @@ -34,11 +34,11 @@ export const WithMoreMenuButton = Template.bind({}); WithMoreMenuButton.args = { title: 'Page Navigation Title', backButton: { show: false }, - menuButton: { type: ButtonTypes.More, onClick: () => {} }, + menuButton: { type: ButtonTypes.More, items: [{ title: 'Revoke', icon:
Test
}] }, }; export const WithBackAndMoreMenuButton = Template.bind({}); WithBackAndMoreMenuButton.args = { title: 'Page Navigation Title', - menuButton: { type: ButtonTypes.More, onClick: () => {} }, + menuButton: { type: ButtonTypes.More, items: [{ title: 'Revoke', icon:
Test
}] }, }; diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index 30ec1789..fe636a96 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import BackIcon from '@assets/svg/back-icon.svg'; import MoreIcon from '@assets/svg/more.svg'; import { useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; import Button from '../Button'; +import PopupMenu, { PopupMenuItem } from '../PopupMenu/PopupMenu'; export enum ButtonTypes { More, @@ -19,11 +21,13 @@ interface ShowBackButton { type BackButton = ShowBackButton | NoBackButton; -interface MenuButton { - type: ButtonTypes; - onClick: () => void; +interface MoreMenuButton { + type: ButtonTypes.More; + items: PopupMenuItem[]; } +type MenuButton = MoreMenuButton; + interface TopbarProps { title: string; backButton?: BackButton; @@ -41,6 +45,8 @@ export default function Topbar({ }, menuButton, }: TopbarProps) { + const [showPopupMenu, setShowPopupMenu] = useState(false); + return (
@@ -53,8 +59,11 @@ export default function Topbar({
{title}
{menuButton && ( - )}
diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 1f9eba3a..a1fc8d17 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -20,6 +20,7 @@ import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/da'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; import allowlist from '@popup/pages/Allowlist/i18n/da'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/da'; import type en from './en'; @@ -46,6 +47,7 @@ const t: typeof en = { idProofRequest, allowlist, connectAccountsRequest, + verifiableCredential, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index ae78022e..107a2621 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -20,6 +20,7 @@ import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/en'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/en'; const t = { shared, @@ -44,6 +45,7 @@ const t = { idProofRequest, allowlist, connectAccountsRequest, + verifiableCredential, }; export default t; From d45ba7091e76dfa82ae6f2f69a4b405647402a8f Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 3 Jul 2023 10:16:43 +0200 Subject: [PATCH 033/231] Initial revoke UI and methods --- packages/browser-wallet/package.json | 1 + .../VerifiableCredentialDetails.tsx | 128 ++++++++++++++++++ .../VerifiableCredentialList.tsx | 23 +--- .../src/popup/shared/Topbar/Topbar.tsx | 2 +- .../utils/verifiable-credential-helpers.ts | 54 ++++++++ yarn.lock | 8 ++ 6 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e7c2c7ea..3db7aa34 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -20,6 +20,7 @@ "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", "@concordium/web-sdk": "^4.0.1", + "@noble/ed25519": "^2.0.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx new file mode 100644 index 00000000..5437fd67 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Buffer } from 'buffer/'; +import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; +import { useAtomValue } from 'jotai'; +import { VerifiableCredential } from '@shared/storage/types'; +import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar'; +import { useTranslation } from 'react-i18next'; +import { AccountTransactionType, CcdAmount, ConcordiumGRPCClient, UpdateContractPayload } from '@concordium/web-sdk'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + RevocationDataHolder, + RevokeCredentialHolderParam, + SigningData, + getCredentialHolderId, + getCredentialRegistryContractAddress, + serializeRevokeCredentialHolderParam, + sign, +} from '@shared/utils/verifiable-credential-helpers'; +import { fetchContractName } from '@shared/utils/token-helpers'; +import { grpcClientAtom } from '@popup/store/settings'; +import { absoluteRoutes } from '@popup/constants/routes'; +import { selectedAccountAtom } from '@popup/store/account'; +import { usePrivateKey } from '@popup/shared/utils/account-helpers'; +import { accountRoutes } from '../Account/routes'; +import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer'; +import RevokeIcon from '../../../assets/svg/revoke.svg'; +import { useCredentialStatus } from './VerifiableCredentialHooks'; +import { VerifiableCredentialCard } from './VerifiableCredentialCard'; + +const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; + +async function buildRevokeTransaction( + client: ConcordiumGRPCClient, + credential: VerifiableCredential, + privateKey: string +): Promise { + const address = getCredentialRegistryContractAddress(credential.id); + const contractName = await fetchContractName(client, address.index, address.subindex); + + // TODO Get the correct nonce. + + const signingData: SigningData = { + contractAddress: address, + entryPoint: 'revokeCredentialHolder', + nonce: BigInt(1), + timestamp: BigInt(Date.now() + 60000), + }; + + const data: RevocationDataHolder = { + credentialId: getCredentialHolderId(credential.id), + signingData, + }; + + const signature = await sign(Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), privateKey); + const parameter: RevokeCredentialHolderParam = { + signature, + data, + }; + + // Get better NRG estimate. How to do that? + return { + address, + amount: new CcdAmount(0n), + receiveName: `${contractName}.revokeCredentialHolder`, + maxContractExecutionEnergy: BigInt(5000), + message: serializeRevokeCredentialHolderParam(parameter), + }; +} + +export default function VerifiableCredentialDetails({ + credential, + backButtonOnClick, +}: { + credential: VerifiableCredential; + backButtonOnClick: () => void; +}) { + const nav = useNavigate(); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const { t } = useTranslation('verifiableCredential'); + + const client = useAtomValue(grpcClientAtom); + const { pathname } = useLocation(); + + const selectedAccount = useAtomValue(selectedAccountAtom); + const key = usePrivateKey(selectedAccount); + + const goToConfirm = () => { + // TODO Fix this... Basically we need to wait for these values before displaying the card. + if (!selectedAccount || !key) { + return; + } + + buildRevokeTransaction(client, credential, key).then((payload) => { + const confirmTransferState: ConfirmGenericTransferState = { + payload, + type: AccountTransactionType.Update, + }; + + // Override current router entry with stateful version + nav(pathname, { replace: true, state: true }); + nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { + state: confirmTransferState, + }); + }); + }; + + const menuButton: MenuButton = { + type: ButtonTypes.More, + items: [{ title: t('menu.revoke'), icon: , onClick: () => goToConfirm() }], + }; + + return ( + <> + +
+ useCredentialStatus(cred)} + /> +
+ + ); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 9a60af8a..98ed9dd8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -5,11 +5,11 @@ import { } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; -import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; +import Topbar from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; -import RevokeIcon from '../../../assets/svg/revoke.svg'; +import VerifiableCredentialDetails from './VerifiableCredentialDetails'; /** * Component to display when there are no verifiable credentials in the wallet. @@ -50,24 +50,7 @@ export default function VerifiableCredentialList() { } if (selected) { - const menuButton = { type: ButtonTypes.More, items: [{ title: t('menu.revoke'), icon: }] }; - - return ( - <> - setSelected(undefined) }} - menuButton={menuButton} - /> -
- useCredentialStatus(cred)} - /> -
- - ); + return setSelected(undefined)} />; } return ( diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index fe636a96..ca8e9aa8 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -26,7 +26,7 @@ interface MoreMenuButton { items: PopupMenuItem[]; } -type MenuButton = MoreMenuButton; +export type MenuButton = MoreMenuButton; interface TopbarProps { title: string; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 956a5942..b54e5398 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,6 +1,60 @@ import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; +import * as ed from '@noble/ed25519'; + +export async function sign(digest: Buffer, privateKey: string) { + return Buffer.from(await ed.signAsync(digest, privateKey)).toString('hex'); +} + +export interface SigningData { + contractAddress: ContractAddress; + entryPoint: string; + nonce: bigint; + timestamp: bigint; +} + +export interface RevocationDataHolder { + /** The public key identifying the credential holder */ + credentialId: string; + /** Metadata of the signature */ + signingData: SigningData; +} + +export interface RevokeCredentialHolderParam { + /** Ed25519 signature on the revocation message as a hex string */ + signature: string; + /** Revocation data */ + data: RevocationDataHolder; +} + +export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { + let buffer = Buffer.from(parameter.signature, 'hex'); + buffer = Buffer.concat([buffer, Buffer.from(parameter.data.credentialId, 'hex')]); + + // TODO Fix BigInt issue here. + const addressBuffer = Buffer.alloc(16); + addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.index, 0); + addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.subindex, 8); + + const serializedEntryPointName = Buffer.from(parameter.data.signingData.entryPoint, 'utf-8'); + let entryPointSerialization = Buffer.alloc(2); + entryPointSerialization.writeUInt16LE(serializedEntryPointName.length, 0); + entryPointSerialization = Buffer.concat([entryPointSerialization, serializedEntryPointName]); + + const finalBuffer = Buffer.alloc(16); + finalBuffer.writeBigUInt64LE(parameter.data.signingData.nonce, 0); + finalBuffer.writeBigUInt64LE(parameter.data.signingData.timestamp, 8); + + return Buffer.concat([buffer, addressBuffer, entryPointSerialization, finalBuffer, Buffer.of(0)]); +} + +/** + * contractAddress: ContractAddress; + entryPoint: string; + nonce: BigInt; + timestamp: BigInt; + */ /** * Extracts the credential holder id from a verifiable credential id (did). diff --git a/yarn.lock b/yarn.lock index 07a81131..1441dc1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,6 +1961,7 @@ __metadata: "@concordium/web-sdk": ^4.0.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 + "@noble/ed25519": ^2.0.0 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 "@storybook/addon-actions": ^6.5.7 @@ -3057,6 +3058,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^2.0.0": + version: 2.0.0 + resolution: "@noble/ed25519@npm:2.0.0" + checksum: 4404deb3ca4f7a07863a362d697dc624ff0e82083ca4e41f976f76ec6fe879363b4722529389aa1ae280fe62558d9f229c8403b0a077add8b5f1ec1290d6e2d7 + languageName: node + linkType: hard + "@noble/hashes@npm:~1.1.1": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" From 746312a2f0532a83da4321f667ba7e4af3bbfd21 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 10:36:55 +0200 Subject: [PATCH 034/231] Revocation improvements and testing --- packages/browser-wallet/package.json | 2 +- .../VerifiableCredentialDetails.tsx | 142 ++++----- .../VerifiableCredentialHooks.tsx | 24 +- .../src/popup/shared/PopupMenu/PopupMenu.scss | 4 + .../src/popup/shared/PopupMenu/PopupMenu.tsx | 7 +- .../src/popup/shared/Topbar/Topbar.tsx | 8 +- .../utils/verifiable-credential-helpers.ts | 292 +++++++++++++++--- .../verifiable-credential-helpers.test.ts | 50 +++ yarn.lock | 16 +- 9 files changed, 412 insertions(+), 133 deletions(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 3db7aa34..52521c4a 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -20,7 +20,7 @@ "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", "@concordium/web-sdk": "^4.0.1", - "@noble/ed25519": "^2.0.0", + "@noble/ed25519": "^1.7.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 5437fd67..dbe1990f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -1,70 +1,33 @@ -import React from 'react'; -import { Buffer } from 'buffer/'; +import React, { useCallback, useMemo } from 'react'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; -import { AccountTransactionType, CcdAmount, ConcordiumGRPCClient, UpdateContractPayload } from '@concordium/web-sdk'; +import { AccountTransactionType } from '@concordium/web-sdk'; import { useLocation, useNavigate } from 'react-router-dom'; +import { grpcClientAtom } from '@popup/store/settings'; +import { absoluteRoutes } from '@popup/constants/routes'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { - RevocationDataHolder, - RevokeCredentialHolderParam, - SigningData, + buildRevokeTransaction, getCredentialHolderId, getCredentialRegistryContractAddress, - serializeRevokeCredentialHolderParam, - sign, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; -import { grpcClientAtom } from '@popup/store/settings'; -import { absoluteRoutes } from '@popup/constants/routes'; -import { selectedAccountAtom } from '@popup/store/account'; -import { usePrivateKey } from '@popup/shared/utils/account-helpers'; import { accountRoutes } from '../Account/routes'; import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer'; import RevokeIcon from '../../../assets/svg/revoke.svg'; -import { useCredentialStatus } from './VerifiableCredentialHooks'; +import { useCredentialEntry, useCredentialStatus } from './VerifiableCredentialHooks'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; -const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; - -async function buildRevokeTransaction( - client: ConcordiumGRPCClient, - credential: VerifiableCredential, - privateKey: string -): Promise { - const address = getCredentialRegistryContractAddress(credential.id); - const contractName = await fetchContractName(client, address.index, address.subindex); - - // TODO Get the correct nonce. - - const signingData: SigningData = { - contractAddress: address, - entryPoint: 'revokeCredentialHolder', - nonce: BigInt(1), - timestamp: BigInt(Date.now() + 60000), - }; - - const data: RevocationDataHolder = { - credentialId: getCredentialHolderId(credential.id), - signingData, - }; - - const signature = await sign(Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), privateKey); - const parameter: RevokeCredentialHolderParam = { - signature, - data, - }; - - // Get better NRG estimate. How to do that? - return { - address, - amount: new CcdAmount(0n), - receiveName: `${contractName}.revokeCredentialHolder`, - maxContractExecutionEnergy: BigInt(5000), - message: serializeRevokeCredentialHolderParam(parameter), - }; +/** + * Calculates the next revocation nonce based on the current revocation nonce. + * @param nonce the current nonce returned in the credential entry + * @returns the next nonce to use for a holder revocation update + */ +function getNextRevocationNonce(nonce: bigint) { + return nonce + 1n; } export default function VerifiableCredentialDetails({ @@ -75,39 +38,68 @@ export default function VerifiableCredentialDetails({ backButtonOnClick: () => void; }) { const nav = useNavigate(); - const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const { pathname } = useLocation(); const { t } = useTranslation('verifiableCredential'); - + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const client = useAtomValue(grpcClientAtom); - const { pathname } = useLocation(); - - const selectedAccount = useAtomValue(selectedAccountAtom); - const key = usePrivateKey(selectedAccount); + const hdWallet = useHdWallet(); + const credentialEntry = useCredentialEntry(credential); - const goToConfirm = () => { - // TODO Fix this... Basically we need to wait for these values before displaying the card. - if (!selectedAccount || !key) { + const goToConfirmPage = useCallback(async () => { + if (credentialEntry === undefined || hdWallet === undefined) { return; } - buildRevokeTransaction(client, credential, key).then((payload) => { - const confirmTransferState: ConfirmGenericTransferState = { - payload, - type: AccountTransactionType.Update, - }; + const contractAddress = getCredentialRegistryContractAddress(credential.id); + const credentialId = getCredentialHolderId(credential.id); + const contractName = await fetchContractName(client, contractAddress.index, contractAddress.subindex); + if (contractName === undefined) { + throw new Error(`Unable to find contract name for address: ${contractAddress}`); + } + const revocationNonce = getNextRevocationNonce(credentialEntry.revocationNonce); + const signingKey = hdWallet.getVerifiableCredentialSigningKey(0).toString('hex'); + const payload = await buildRevokeTransaction( + contractAddress, + contractName, + credentialId, + revocationNonce, + signingKey + ); + + const confirmTransferState: ConfirmGenericTransferState = { + payload, + type: AccountTransactionType.Update, + }; - // Override current router entry with stateful version - nav(pathname, { replace: true, state: true }); - nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { - state: confirmTransferState, - }); + // Override current router entry with stateful version + nav(pathname, { replace: true, state: true }); + nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { + state: confirmTransferState, }); - }; + }, [client, credential, hdWallet, credentialEntry, nav, pathname]); + + const menuButton: MenuButton | undefined = useMemo(() => { + if (credentialEntry === undefined) { + return undefined; + } + + return { + type: ButtonTypes.More, + items: [ + { + title: t('menu.revoke'), + icon: , + onClick: credentialEntry.holderRevocable ? () => goToConfirmPage() : undefined, + }, + ], + }; + }, [credentialEntry, goToConfirmPage]); - const menuButton: MenuButton = { - type: ButtonTypes.More, - items: [{ title: t('menu.revoke'), icon: , onClick: () => goToConfirm() }], - }; + // Wait for the credential entry to be loaded from the chain, and for the HdWallet + // to be loaded to be ready to derive keys. + if (credentialEntry === undefined || hdWallet === undefined) { + return null; + } return ( <> diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 5a56e0a8..3d207c30 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,15 +1,37 @@ import { grpcClientAtom } from '@popup/store/settings'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { + CredentialEntry, getCredentialHolderId, getCredentialRegistryContractAddress, + getVerifiableCredentialEntry, getVerifiableCredentialStatus, } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; /** - * Retrieve the on-chain credential status for a verifiable credential in a registry contract. + * Retrieve the on-chain credential entry for a verifiable credential in a CIS-4 credential registry contract. + * @param credential the verifiable credential to retrieve the credential entry for + * @returns the credential entry for the given credential, undefined if one is not found yet + */ +export function useCredentialEntry(credential: VerifiableCredential) { + const [credentialEntry, setCredentialEntry] = useState(); + const client = useAtomValue(grpcClientAtom); + + useEffect(() => { + const credentialHolderId = getCredentialHolderId(credential.id); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { + setCredentialEntry(entry); + }); + }, [credential.id, client]); + + return credentialEntry; +} + +/** + * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. * @param credential the verifiable credential to lookup the status for * @returns the status for the given credential */ diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss index 4ff9c03c..097c20b7 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -31,6 +31,10 @@ border-bottom: 1px solid lightgray; } + &--disabled { + opacity: 0.2; + } + &__icon { height: 20px; width: 20px; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index 90c67d67..d7f158e2 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { DetectClickOutside } from 'wallet-common-helpers'; +import clsx from 'clsx'; import Button from '../Button/Button'; export interface PopupMenuItem { @@ -19,7 +20,11 @@ export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) {
{items.map((item) => { return ( - diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index ca8e9aa8..0cf61a64 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -59,12 +59,14 @@ export default function Topbar({
{title}
{menuButton && ( -
setShowPopupMenu(false)} />
- + )}
diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index b54e5398..25087906 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,10 +1,49 @@ -import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; +import { CcdAmount, ConcordiumGRPCClient, ContractAddress, UpdateContractPayload } from '@concordium/web-sdk'; import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; import * as ed from '@noble/ed25519'; +/** + * Extracts the credential holder id from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the credential holder id + */ +export function getCredentialHolderId(credentialId: string): string { + const splitted = credentialId.split('/'); + const credentialHolderId = splitted[splitted.length - 1]; + + if (credentialHolderId.length !== 64) { + throw new Error(`Invalid credential holder id found from: ${credentialId}`); + } + + return credentialHolderId; +} + +/** + * Extracts the credential registry contract addres from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the contract address of the issuing contract of the provided credential id + */ +export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { + const splitted = credentialId.split(':'); + const index = BigInt(splitted[4]); + const subindex = BigInt(splitted[5].split('/')[0]); + return { index, subindex }; +} + export async function sign(digest: Buffer, privateKey: string) { - return Buffer.from(await ed.signAsync(digest, privateKey)).toString('hex'); + return Buffer.from(await ed.sign(digest, privateKey)).toString('hex'); +} + +// TODO This function is a copy from the node-sdk. Remove the duplicate here and export it in the SDK to use instead. +export function encodeWord64LE(value: bigint): Buffer { + if (value > 18446744073709551615n || value < 0n) { + throw new Error(`The input has to be a 64 bit unsigned integer but it was: ${value}`); + } + const arr = new ArrayBuffer(8); + const view = new DataView(arr); + view.setBigUint64(0, value, true); + return Buffer.from(new Uint8Array(arr)); } export interface SigningData { @@ -28,60 +67,93 @@ export interface RevokeCredentialHolderParam { data: RevocationDataHolder; } -export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { - let buffer = Buffer.from(parameter.signature, 'hex'); - buffer = Buffer.concat([buffer, Buffer.from(parameter.data.credentialId, 'hex')]); +/** + * Serializes the revocation data holder. This is the data that is signed, together with a prefix, to authorize + * the revocation of a held credential. + * @param data the revocation data context to serialize + * @returns the serialized revocation data holder + */ +function serializeRevocationDataHolder(data: RevocationDataHolder) { + const credentialId = Buffer.from(data.credentialId, 'hex'); + + const contractIndex = encodeWord64LE(data.signingData.contractAddress.index); + const contractSubindex = encodeWord64LE(data.signingData.contractAddress.subindex); - // TODO Fix BigInt issue here. - const addressBuffer = Buffer.alloc(16); - addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.index, 0); - addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.subindex, 8); + const entrypointName = Buffer.from(data.signingData.entryPoint, 'utf-8'); + const entrypointLength = Buffer.alloc(2); + entrypointLength.writeUInt16LE(entrypointName.length, 0); - const serializedEntryPointName = Buffer.from(parameter.data.signingData.entryPoint, 'utf-8'); - let entryPointSerialization = Buffer.alloc(2); - entryPointSerialization.writeUInt16LE(serializedEntryPointName.length, 0); - entryPointSerialization = Buffer.concat([entryPointSerialization, serializedEntryPointName]); + const nonce = encodeWord64LE(data.signingData.nonce); + const timestamp = encodeWord64LE(data.signingData.timestamp); - const finalBuffer = Buffer.alloc(16); - finalBuffer.writeBigUInt64LE(parameter.data.signingData.nonce, 0); - finalBuffer.writeBigUInt64LE(parameter.data.signingData.timestamp, 8); + const optionalReason = Buffer.of(0); - return Buffer.concat([buffer, addressBuffer, entryPointSerialization, finalBuffer, Buffer.of(0)]); + return Buffer.concat([ + credentialId, + contractIndex, + contractSubindex, + entrypointLength, + entrypointName, + nonce, + timestamp, + optionalReason, + ]); } -/** - * contractAddress: ContractAddress; - entryPoint: string; - nonce: BigInt; - timestamp: BigInt; - */ +export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { + const signature = Buffer.from(parameter.signature, 'hex'); + const data = serializeRevocationDataHolder(parameter.data); + return Buffer.concat([signature, data]); +} /** - * Extracts the credential holder id from a verifiable credential id (did). - * @param credentialId the did for a credential - * @returns the credential holder id + * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. + * @param address the address of the contract that the credential to revoke is registered in + * @param contractName the name of the contract at {@link address} + * @param credentialId the id of the credential to revoke + * @param nonce the revocation nonce + * @param signingKey the signing key associated with the credential (the private key to the {@link credentialId}) + * @returns an update contract transaction for revoking the credential with id {@link credentialId} */ -export function getCredentialHolderId(credentialId: string): string { - const splitted = credentialId.split('/'); - const credentialHolderId = splitted[splitted.length - 1]; +export async function buildRevokeTransaction( + address: ContractAddress, + contractName: string, + credentialId: string, + nonce: bigint, + signingKey: string +): Promise { + const signingData: SigningData = { + contractAddress: address, + entryPoint: 'revokeCredentialHolder', + nonce, + timestamp: BigInt(Date.now() + 5 * 60000), + }; - if (credentialHolderId.length !== 64) { - throw new Error(`Invalid credential holder id found from: ${credentialId}`); - } + const data: RevocationDataHolder = { + credentialId, + signingData, + }; - return credentialHolderId; -} + const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; + const serializedData = serializeRevocationDataHolder(data); + const signature = await sign( + Buffer.concat([Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), serializedData]), + signingKey + ); -/** - * Extracts the credential registry contract addres from a verifiable credential id (did). - * @param credentialId the did for a credential - * @returns the contract address of the issuing contract of the provided credential id - */ -export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { - const splitted = credentialId.split(':'); - const index = BigInt(splitted[4]); - const subindex = BigInt(splitted[5].split('/')[0]); - return { index, subindex }; + const parameter: RevokeCredentialHolderParam = { + signature, + data, + }; + + // Get better NRG estimate. How to do that? Invoke the contract and use that. + return { + address, + amount: new CcdAmount(0n), + receiveName: `${contractName}.revokeCredentialHolder`, + maxContractExecutionEnergy: BigInt(50000), + message: serializeRevokeCredentialHolderParam(parameter), + }; } function deserializeCredentialStatus(serializedCredentialStatus: string): VerifiableCredentialStatus { @@ -135,3 +207,135 @@ export async function getVerifiableCredentialStatus( return deserializeCredentialStatus(returnValue); } + +export interface CredentialEntry { + credentialHolderId: string; + holderRevocable: boolean; + validFrom: bigint; + validUntil?: bigint; + credentialType: string; + metadataUrl: string; + metadataChecksum?: string; + schemaUrl: string; + schemaChecksum?: string; + revocationNonce: bigint; +} + +function deserializeUrlChecksumPair(buffer: Buffer, offset: number) { + let localOffset = offset; + const urlLength = buffer.readUInt16LE(localOffset); + localOffset += 2; + + const url = buffer.toString('utf-8', localOffset, localOffset + urlLength); + localOffset += urlLength; + + const containsChecksum = buffer.readUInt8(localOffset); + localOffset += 1; + + let checksum: string | undefined; + if (containsChecksum) { + checksum = buffer.toString('hex', localOffset, localOffset + 32); + localOffset += 32; + } + + return { + url, + checksum, + offset: localOffset, + }; +} + +/** + * Deserializes a CredentialEntry according to the CIS-4 specification. + * @param serializedCredentialEntry a serialized credential entry as a hex string + */ +export function deserializeCredentialEntry(serializedCredentialEntry: string): CredentialEntry { + const buffer = Buffer.from(serializedCredentialEntry, 'hex'); + let offset = 0; + + const credentialHolderId = buffer.toString('hex', offset, offset + 32); + offset += 32; + + const holderRevocable = Boolean(buffer.readUInt8(offset)); + offset += 1; + + // TODO Remove this as the commitments are moving out of the contract. + const commitmentLength = buffer.readUInt16LE(offset); + offset += 2 + commitmentLength; + // TODO Remove until here. + + const validFrom = buffer.readBigUInt64LE(offset) as bigint; + offset += 8; + + const containsValidUntil = Boolean(buffer.readUInt8(offset)); + offset += 1; + + let validUntil: bigint | undefined; + if (containsValidUntil) { + validUntil = buffer.readBigUInt64LE(offset) as bigint; + offset += 8; + } + + const credentialTypeLength = buffer.readUInt8(offset); + offset += 1; + const credentialType = buffer.toString('utf-8', offset, offset + credentialTypeLength); + offset += credentialTypeLength; + + const metadata = deserializeUrlChecksumPair(buffer, offset); + offset = metadata.offset; + + const schema = deserializeUrlChecksumPair(buffer, offset); + offset = schema.offset; + + const revocationNonce = buffer.readBigInt64LE(offset) as bigint; + offset += 8; + + return { + credentialHolderId, + holderRevocable, + validFrom, + validUntil, + credentialType, + metadataUrl: metadata.url, + metadataChecksum: metadata.checksum, + schemaUrl: schema.url, + schemaChecksum: schema.checksum, + revocationNonce, + }; +} + +/** + * Get a Credential Entry from a CIS-4 contract. + * @param client the GRPC client for accessing a node + * @param contractAddress the address of a CIS-4 contract + * @param credentialHolderId the public key for the credential holder of the entry to retrieve + * @throws an error if the invoke contract call fails, or if no return value is available + * @returns the credential entry which contains data about the credential, undefined if the contract instance is not found + */ +export async function getVerifiableCredentialEntry( + client: ConcordiumGRPCClient, + contractAddress: ContractAddress, + credentialHolderId: string +) { + const instanceInfo = await client.getInstanceInfo(contractAddress); + if (instanceInfo === undefined) { + return undefined; + } + + const result = await client.invokeContract({ + contract: contractAddress, + method: `${instanceInfo.name.substring(5)}.credentialEntry`, + parameter: Buffer.from(credentialHolderId, 'hex'), + }); + + if (result.tag !== 'success') { + throw new Error(result.reason.tag); + } + + const { returnValue } = result; + if (returnValue === undefined) { + throw new Error(`Return value is missing from credentialEntry result in CIS-4 contract: ${contractAddress}`); + } + + return deserializeCredentialEntry(returnValue); +} diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index fe8a57d3..bc271dd3 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -1,8 +1,58 @@ import { + CredentialEntry, + RevocationDataHolder, + RevokeCredentialHolderParam, + SigningData, + deserializeCredentialEntry, getCredentialHolderId, getCredentialRegistryContractAddress, + serializeRevokeCredentialHolderParam, } from '../src/shared/utils/verifiable-credential-helpers'; +test('serializing a revoke credential holder parameter', () => { + const signingData: SigningData = { + contractAddress: { index: 4718n, subindex: 0n }, + entryPoint: 'revokeCredentialHolder', + nonce: 0n, + timestamp: 1688542350309n, + }; + + const data: RevocationDataHolder = { + credentialId: '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106', + signingData, + }; + + const revokeCredentialHolderParam: RevokeCredentialHolderParam = { + signature: + 'a70b2b7987a2835726bc6166da1e4d223b9f215962e20726a39ea95afaf9d10d13d0f093761f2f2a4c34d37f081ea501fed8ab74fb565b87822ff0aec6071309', + data, + }; + + expect(serializeRevokeCredentialHolderParam(revokeCredentialHolderParam).toString('hex')).toEqual( + 'a70b2b7987a2835726bc6166da1e4d223b9f215962e20726a39ea95afaf9d10d13d0f093761f2f2a4c34d37f081ea501fed8ab74fb565b87822ff0aec60713092eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d1066e12000000000000000000000000000016007265766f6b6543726564656e7469616c486f6c6465720000000000000000e58bf7248901000000' + ); +}); + +test('deserializing a credential entry', () => { + const serializedCredentialEntry = + '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d10601300099cc213d3bda6677df0663ec8ec54a2c05ccae0e8b99ba6130f6931ee85c3de04f4d42f0e4bb3f541e5592ade117b0fe26f5c17688010000000c4d7943726564656e7469616c0300666f6f001100687474703a2f2f736368656d612e6f7267000000000000000000'; + + const expected: CredentialEntry = { + credentialHolderId: '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106', + credentialType: 'MyCredential', + holderRevocable: true, + metadataChecksum: undefined, + metadataUrl: 'foo', + revocationNonce: 0n, + schemaChecksum: undefined, + schemaUrl: 'http://schema.org', + validFrom: 1685619602726n, + validUntil: undefined, + }; + + expect(deserializeCredentialEntry(serializedCredentialEntry)).toEqual(expected); +}); + test('credential holder id is extracted from verifiable credential id field', () => { const id = 'did:ccd:mainnet:sci:4718:0/credentialEntry/2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106'; diff --git a/yarn.lock b/yarn.lock index 1441dc1f..ebb16bf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,7 +1961,7 @@ __metadata: "@concordium/web-sdk": ^4.0.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 - "@noble/ed25519": ^2.0.0 + "@noble/ed25519": ^1.7.0 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 "@storybook/addon-actions": ^6.5.7 @@ -3051,6 +3051,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^1.7.0": + version: 1.7.3 + resolution: "@noble/ed25519@npm:1.7.3" + checksum: 45169927d51de513e47bbeebff3a603433c4ac7579e1b8c5034c380a0afedbe85e6959be3d69584a7a5ed6828d638f8f28879003b9bb2fb5f22d8aa2d88fd5fe + languageName: node + linkType: hard + "@noble/ed25519@npm:^1.7.1": version: 1.7.1 resolution: "@noble/ed25519@npm:1.7.1" @@ -3058,13 +3065,6 @@ __metadata: languageName: node linkType: hard -"@noble/ed25519@npm:^2.0.0": - version: 2.0.0 - resolution: "@noble/ed25519@npm:2.0.0" - checksum: 4404deb3ca4f7a07863a362d697dc624ff0e82083ca4e41f976f76ec6fe879363b4722529389aa1ae280fe62558d9f229c8403b0a077add8b5f1ec1290d6e2d7 - languageName: node - linkType: hard - "@noble/hashes@npm:~1.1.1": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" From 6a7c4b344827762d7be356cb8737abd00e8aadfa Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 11:24:32 +0200 Subject: [PATCH 035/231] Estimate execution cost of revocation --- .../VerifiableCredentialDetails.tsx | 14 ++++- .../utils/verifiable-credential-helpers.ts | 58 +++++++++++++++---- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index dbe1990f..7b1da0fa 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -11,8 +11,10 @@ import { absoluteRoutes } from '@popup/constants/routes'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { buildRevokeTransaction, + buildRevokeTransactionParameters, getCredentialHolderId, getCredentialRegistryContractAddress, + getRevokeTransactionExecutionEnergyEstimate, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; import { accountRoutes } from '../Account/routes'; @@ -58,13 +60,21 @@ export default function VerifiableCredentialDetails({ } const revocationNonce = getNextRevocationNonce(credentialEntry.revocationNonce); const signingKey = hdWallet.getVerifiableCredentialSigningKey(0).toString('hex'); - const payload = await buildRevokeTransaction( + + const parameters = await buildRevokeTransactionParameters( contractAddress, - contractName, credentialId, revocationNonce, signingKey ); + const maxExecutionEnergy = await getRevokeTransactionExecutionEnergyEstimate(client, contractName, parameters); + const payload = await buildRevokeTransaction( + contractAddress, + contractName, + credentialId, + maxExecutionEnergy, + parameters + ); const confirmTransferState: ConfirmGenericTransferState = { payload, diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 25087906..e115f0c2 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -107,21 +107,19 @@ export function serializeRevokeCredentialHolderParam(parameter: RevokeCredential } /** - * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. - * @param address the address of the contract that the credential to revoke is registered in - * @param contractName the name of the contract at {@link address} - * @param credentialId the id of the credential to revoke + * Builds the parameters used for a holder revocation transaction to revoke a given credential in a CIS-4 contract. + * @param address the address of the contract that hte credential to revoke is registered in + * @param credentialId the id of the credential to revoke (this is a derived public key) * @param nonce the revocation nonce * @param signingKey the signing key associated with the credential (the private key to the {@link credentialId}) - * @returns an update contract transaction for revoking the credential with id {@link credentialId} + * @returns the unserialized parameters for a holder revocation transaction */ -export async function buildRevokeTransaction( +export async function buildRevokeTransactionParameters( address: ContractAddress, - contractName: string, credentialId: string, nonce: bigint, signingKey: string -): Promise { +) { const signingData: SigningData = { contractAddress: address, entryPoint: 'revokeCredentialHolder', @@ -146,13 +144,30 @@ export async function buildRevokeTransaction( data, }; - // Get better NRG estimate. How to do that? Invoke the contract and use that. + return parameter; +} + +/** + * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. + * @param address the address of the contract that the credential to revoke is registered in + * @param contractName the name of the contract at {@link address} + * @param credentialId the id of the credential to revoke + * @param maxContractExecutionEnergy the maximum contract execution energy + * @returns an update contract transaction for revoking the credential with id {@link credentialId} + */ +export async function buildRevokeTransaction( + address: ContractAddress, + contractName: string, + credentialId: string, + maxContractExecutionEnergy: bigint, + parameters: RevokeCredentialHolderParam +): Promise { return { address, amount: new CcdAmount(0n), receiveName: `${contractName}.revokeCredentialHolder`, - maxContractExecutionEnergy: BigInt(50000), - message: serializeRevokeCredentialHolderParam(parameter), + maxContractExecutionEnergy, + message: serializeRevokeCredentialHolderParam(parameters), }; } @@ -172,6 +187,27 @@ function deserializeCredentialStatus(serializedCredentialStatus: string): Verifi } } +/** + * Estimates the cost of a holder revocation transaction. + * @param client the GRPC client for accessing a node + * @param contractName the name of the contract to invoke to get the estimate + * @param parameters the unserialized parameters for a holder revocation transaction + * @returns an estimate of the execution cost of the holder revocation transaction + */ +export async function getRevokeTransactionExecutionEnergyEstimate( + client: ConcordiumGRPCClient, + contractName: string, + parameters: RevokeCredentialHolderParam +) { + const invokeResult = await client.invokeContract({ + contract: parameters.data.signingData.contractAddress, + method: `${contractName}.${parameters.data.signingData.entryPoint}`, + parameter: serializeRevokeCredentialHolderParam(parameters), + }); + // TODO Use a shared function for the buffer calculation. Also used in token-helpers. + return (invokeResult.usedEnergy * 12n) / 10n; +} + /** * Get the status of a verifiable credential in a CIS-4 contract. * @param client the GRPC client for accessing a node From 6985da6cead1ba70e2d588dd2e40fc64e3ec40f5 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 11:40:46 +0200 Subject: [PATCH 036/231] Refactor NRG buffer calculation --- .../src/shared/utils/contract-helpers.ts | 9 +++++++++ .../browser-wallet/src/shared/utils/token-helpers.ts | 5 +++-- .../src/shared/utils/verifiable-credential-helpers.ts | 5 +++-- packages/browser-wallet/test/contract-helpers.test.ts | 11 +++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/browser-wallet/src/shared/utils/contract-helpers.ts create mode 100644 packages/browser-wallet/test/contract-helpers.test.ts diff --git a/packages/browser-wallet/src/shared/utils/contract-helpers.ts b/packages/browser-wallet/src/shared/utils/contract-helpers.ts new file mode 100644 index 00000000..a2916559 --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/contract-helpers.ts @@ -0,0 +1,9 @@ +/** + * Applies a buffer of 20% to an estimated execution energy amount. The goal is to prevent transactions + * from running into insufficient energy errors. + * @param estimatedExecutionEnergy the estimated execution cost for an update transaction, should be retrieved by invoking the contract + * @returns returns the estimated execution energy with an additional buffer added to prevent running into insufficient energy errors + */ +export function applyExecutionNRGBuffer(estimatedExecutionEnergy: bigint) { + return (estimatedExecutionEnergy * 12n) / 10n; +} diff --git a/packages/browser-wallet/src/shared/utils/token-helpers.ts b/packages/browser-wallet/src/shared/utils/token-helpers.ts index 5fa3a707..ff3f63d1 100644 --- a/packages/browser-wallet/src/shared/utils/token-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/token-helpers.ts @@ -15,6 +15,7 @@ import { CIS2_SCHEMA_CONTRACT_NAME, CIS2_SCHEMA } from '@popup/constants/schema' import i18n from '@popup/shell/i18n'; import { SmartContractParameters } from '@concordium/browser-wallet-api-helpers'; import { determineUpdatePayloadSize } from './energy-helpers'; +import { applyExecutionNRGBuffer } from './contract-helpers'; export interface ContractDetails { contractName: string; @@ -347,8 +348,8 @@ async function getTokenTransferExecutionEnergyEstimate( if (!res || res.tag === 'failure') { throw new Error(res?.reason?.tag || 'no response'); } - // TODO: determine the "safety ratio" - return (res.usedEnergy * 12n) / 10n; + + return applyExecutionNRGBuffer(res.usedEnergy); } function getContractName(instanceInfo: InstanceInfo): string | undefined { diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index e115f0c2..42cea93e 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -2,6 +2,7 @@ import { CcdAmount, ConcordiumGRPCClient, ContractAddress, UpdateContractPayload import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; import * as ed from '@noble/ed25519'; +import { applyExecutionNRGBuffer } from './contract-helpers'; /** * Extracts the credential holder id from a verifiable credential id (did). @@ -204,8 +205,8 @@ export async function getRevokeTransactionExecutionEnergyEstimate( method: `${contractName}.${parameters.data.signingData.entryPoint}`, parameter: serializeRevokeCredentialHolderParam(parameters), }); - // TODO Use a shared function for the buffer calculation. Also used in token-helpers. - return (invokeResult.usedEnergy * 12n) / 10n; + + return applyExecutionNRGBuffer(invokeResult.usedEnergy); } /** diff --git a/packages/browser-wallet/test/contract-helpers.test.ts b/packages/browser-wallet/test/contract-helpers.test.ts new file mode 100644 index 00000000..f41c42c7 --- /dev/null +++ b/packages/browser-wallet/test/contract-helpers.test.ts @@ -0,0 +1,11 @@ +import { applyExecutionNRGBuffer } from '../src/shared/utils/contract-helpers'; + +test('NRG buffer added is 20 percent', () => { + const estimatedExecutionEnergy = 500n; + expect(applyExecutionNRGBuffer(estimatedExecutionEnergy)).toEqual(600n); +}); + +test('NRG buffer returns 0 for 0 input', () => { + const estimatedExecutionEnergy = 0n; + expect(applyExecutionNRGBuffer(estimatedExecutionEnergy)).toEqual(0n); +}); From 34b368adb063ff3b1c40e3a6ad151477bff335a9 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 7 Jul 2023 13:41:12 +0200 Subject: [PATCH 037/231] Update credential entry to match updated contract --- .../VerifiableCredentialDetails.tsx | 2 +- .../VerifiableCredentialHooks.tsx | 4 +- .../src/popup/shared/PopupMenu/PopupMenu.tsx | 1 + .../utils/verifiable-credential-helpers.ts | 53 ++++++++++--------- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 7b1da0fa..3b186578 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -99,7 +99,7 @@ export default function VerifiableCredentialDetails({ { title: t('menu.revoke'), icon: , - onClick: credentialEntry.holderRevocable ? () => goToConfirmPage() : undefined, + onClick: credentialEntry.credentialInfo.holderRevocable ? () => goToConfirmPage() : undefined, }, ], }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 3d207c30..a395511b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,7 +1,7 @@ import { grpcClientAtom } from '@popup/store/settings'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { - CredentialEntry, + CredentialQueryResponse, getCredentialHolderId, getCredentialRegistryContractAddress, getVerifiableCredentialEntry, @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; * @returns the credential entry for the given credential, undefined if one is not found yet */ export function useCredentialEntry(credential: VerifiableCredential) { - const [credentialEntry, setCredentialEntry] = useState(); + const [credentialEntry, setCredentialEntry] = useState(); const client = useAtomValue(grpcClientAtom); useEffect(() => { diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index d7f158e2..8173b94f 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -21,6 +21,7 @@ export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) { {items.map((item) => { return (
+ {metadata.image && ( +
+ +
+ )}
{attributes && attributes.map((attribute) => ( diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index ab6931d9..6230ce55 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -228,8 +228,8 @@ export async function getCredentialSchema( export interface VerifiableCredentialMetadata { title: string; logo: MetadataUrl; - backgroundColor: string; - image: MetadataUrl; + background_color: string; + image?: MetadataUrl; localization?: Record; } From f952bd23f9e04175713129b18f38f0d698a82308 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 14 Jul 2023 14:51:29 +0200 Subject: [PATCH 043/231] Refactor and cleanup --- .../VerifiableCredentialCard.tsx | 50 +--- .../VerifiableCredentialHooks.tsx | 42 ++++ .../VerifiableCredentialList.tsx | 179 ++------------ .../VerifiableCredentialStatus.tsx | 37 +++ .../src/shared/storage/types.ts | 2 - .../utils/verifiable-credential-helpers.ts | 231 +++++++++++++----- 6 files changed, 278 insertions(+), 263 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index a6e28839..1d68aac7 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -1,8 +1,5 @@ import React, { PropsWithChildren } from 'react'; -import RevokedIcon from '@assets/svg/revoked.svg'; -import ActiveIcon from '@assets/svg/verified.svg'; -import ExpiredIcon from '@assets/svg/block.svg'; -import PendingIcon from '@assets/svg/pending.svg'; + import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import Img from '@popup/shared/Img'; import { @@ -11,41 +8,18 @@ import { VerifiableCredentialSchema, MetadataUrl, } from '../../../shared/storage/types'; - -function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { - let icon = null; - switch (status) { - case VerifiableCredentialStatus.Active: - icon = ; - break; - case VerifiableCredentialStatus.Revoked: - icon = ; - break; - case VerifiableCredentialStatus.Expired: - icon = ; - break; - case VerifiableCredentialStatus.NotActivated: - icon = ; - break; - default: - icon = null; - break; - } - - return ( -
- {VerifiableCredentialStatus[status]} - {icon} -
- ); -} +import StatusIcon from './VerifiableCredentialStatus'; function Logo({ logo }: { logo: MetadataUrl }) { return ; } function DisplayImage({ image }: { image: MetadataUrl }) { - return ; + return ( +
+ +
+ ); } /** @@ -129,15 +103,15 @@ function applySchema( export function VerifiableCredentialCard({ credential, schema, - onClick, credentialStatus, metadata, + onClick, }: { credential: VerifiableCredential; schema: VerifiableCredentialSchema; credentialStatus: VerifiableCredentialStatus; - onClick?: () => void; metadata: VerifiableCredentialMetadata; + onClick?: () => void; }) { const attributes = Object.entries(credential.credentialSubject) .filter((val) => val[0] !== 'id') @@ -151,11 +125,7 @@ export function VerifiableCredentialCard({
{metadata.title}
- {metadata.image && ( -
- -
- )} + {metadata.image && }
{attributes && attributes.map((attribute) => ( diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index cdd2320d..29e52b8e 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -14,6 +14,8 @@ import { storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; +import { AsyncWrapper } from '@popup/store/utils'; +import { ConcordiumGRPCClient } from '@concordium/web-sdk'; /** * Retrieve the on-chain credential status for a verifiable credential in a registry contract. @@ -103,3 +105,43 @@ export function useCredentialMetadata(credential: VerifiableCredential) { return metadata; } + +/** + * Retrieves data and uses the provided data setter to update chrome.storage with the changes found. + * The dataFetcher is responsible for delivering the exact updated picture that should be set. + * @param credentials the credentials to fetch up to date data for + * @param storedData the current stored data (that is to be updated) + * @param setStoredData setter for setting the stored data, should be an atom setter connected to chrome.storage + * @param dataFetcher the function that fetches updated data + */ +export function useFetchingEffect( + credentials: VerifiableCredential[] | undefined, + storedData: AsyncWrapper>, + setStoredData: (update: Record) => Promise, + dataFetcher: ( + credentials: VerifiableCredential[], + client: ConcordiumGRPCClient, + abortControllers: AbortController[], + storedData: Record + ) => Promise> +) { + const client = useAtomValue(grpcClientAtom); + + useEffect(() => { + let isCancelled = false; + const abortControllers: AbortController[] = []; + + if (credentials && !storedData.loading) { + dataFetcher(credentials, client, abortControllers, storedData.value).then((result) => { + if (!isCancelled) { + setStoredData(result); + } + }); + } + + return () => { + isCancelled = true; + abortControllers.forEach((controller) => controller.abort()); + }; + }, [storedData.loading, credentials, client]); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 6fa2999d..d363078e 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -1,28 +1,22 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialSchemasAtom, storedVerifiableCredentialsAtom, } from '@popup/store/verifiable-credential'; import { useAtom, useAtomValue } from 'jotai'; -import { - MetadataUrl, - VerifiableCredential, - VerifiableCredentialSchema, - VerifiableCredentialStatus, -} from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; import { VerifiableCredentialMetadata, - getCredentialHolderId, - getCredentialMetadata, - getCredentialRegistryContractAddress, - getCredentialRegistryMetadata, - getCredentialSchema, - getVerifiableCredentialEntry, + getChangesToCredentialMetadata, + getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; -import { grpcClientAtom } from '@popup/store/settings'; -import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; -import { useCredentialMetadata, useCredentialSchema, useCredentialStatus } from './VerifiableCredentialHooks'; +import { + useCredentialMetadata, + useCredentialSchema, + useCredentialStatus, + useFetchingEffect, +} from './VerifiableCredentialHooks'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; /** @@ -51,9 +45,8 @@ function VerifiableCredentialCardWithStatusFromChain({ const schema = useCredentialSchema(credential); const metadata = useCredentialMetadata(credential); - // TODO Improve this so that a card can render without a schema in some temporary - // shape or form (e.g. an empty card, or just the raw attributes) - if (!schema || !metadata) { + // Render nothing until all the required data is available. + if (!schema || !metadata || status === VerifiableCredentialStatus.Unknown) { return null; } @@ -72,75 +65,6 @@ function VerifiableCredentialCardWithStatusFromChain({ ); } -/** - * Finds all the unique contract addresses for the issuers in a list of - * verifiable credentials, i.e. taking all the contract addresses and removing - * any duplicates. - * @param verifiableCredentials the credentials to find the contract addresses for - */ -function findIssuerContractAddresses(verifiableCredentials: VerifiableCredential[]) { - const allContractAddresses = verifiableCredentials.map((vc) => getCredentialRegistryContractAddress(vc.id)); - return [...new Set(allContractAddresses)]; -} - -async function getCredentialSchemas( - issuerContractAddresses: ContractAddress[], - abortControllers: AbortController[], - client: ConcordiumGRPCClient -) { - const onChainSchemas: VerifiableCredentialSchema[] = []; - - for (const contractAddress of issuerContractAddresses) { - const registryMetadata = await getCredentialRegistryMetadata(client, contractAddress); - - if (registryMetadata) { - const controller = new AbortController(); - abortControllers.push(controller); - try { - const credentialSchema = await getCredentialSchema( - registryMetadata.credentialSchema.schema, - controller - ); - onChainSchemas.push(credentialSchema); - } catch { - // TODO An error is thrown here if the controller aborts which can be ignored. - // TODO An error can also be thrown if the fetching of the credential schema goes haywire. - } - } - } - - return onChainSchemas; -} - -async function getCredentialMetadataBulk( - client: ConcordiumGRPCClient, - credentials: VerifiableCredential[], - abortControllers: AbortController[] -) { - const metadataUrls: MetadataUrl[] = []; - for (const vc of credentials) { - const entry = await getVerifiableCredentialEntry( - client, - getCredentialRegistryContractAddress(vc.id), - getCredentialHolderId(vc.id) - ); - if (entry) { - metadataUrls.push(entry.credentialInfo.metadataUrl); - } - } - const uniqueMetadataUrls = [...new Map(metadataUrls.map((item) => [item.url, item])).values()]; - - const metadataList: { metadata: VerifiableCredentialMetadata; url: string }[] = []; - for (const metadataUrl of uniqueMetadataUrls) { - const controller = new AbortController(); - abortControllers.push(controller); - const metadata = await getCredentialMetadata(metadataUrl, controller); - metadataList.push({ metadata, url: metadataUrl.url }); - } - - return metadataList; -} - /** * Renders all verifiable credentials that are in the wallet. The credentials * are selectable by clicking them, which will move the user to a view containing @@ -154,75 +78,22 @@ export default function VerifiableCredentialList() { schema: VerifiableCredentialSchema; metadata: VerifiableCredentialMetadata; }>(); - const client = useAtomValue(grpcClientAtom); const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); - useEffect(() => { - let isCancelled = false; - const abortControllers: AbortController[] = []; - - if (verifiableCredentials && !storedMetadata.loading) { - getCredentialMetadataBulk(client, verifiableCredentials, abortControllers).then((metadataList) => { - let updatedStoredMetadata = { ...storedMetadata.value }; - - // TODO Verify that something has actually changed before making the update. - for (const updatedMetadata of metadataList) { - if (storedMetadata.value === undefined) { - updatedStoredMetadata = { - [updatedMetadata.url]: updatedMetadata.metadata, - }; - } else { - updatedStoredMetadata[updatedMetadata.url] = updatedMetadata.metadata; - } - } - - if (!isCancelled) { - setStoredMetadata(updatedStoredMetadata); - } - }); - } - - return () => { - isCancelled = true; - abortControllers.forEach((controller) => controller.abort()); - }; - }, [storedMetadata.loading, verifiableCredentials, client]); - - useEffect(() => { - let isCancelled = false; - const abortControllers: AbortController[] = []; - - if (verifiableCredentials) { - const issuerContractAddresses = findIssuerContractAddresses(verifiableCredentials); - - if (!schemas.loading) { - getCredentialSchemas(issuerContractAddresses, abortControllers, client).then((upToDateSchemas) => { - let updatedSchemasInStorage = { ...schemas.value }; - - // TODO Verify that something has actually changed before making the update. - for (const updatedSchema of upToDateSchemas) { - if (schemas.value === undefined) { - updatedSchemasInStorage = { - [updatedSchema.$id]: updatedSchema, - }; - } else { - updatedSchemasInStorage[updatedSchema.$id] = updatedSchema; - } - } - - if (!isCancelled) { - setSchemas(updatedSchemasInStorage); - } - }); - } - } - - return () => { - isCancelled = true; - abortControllers.forEach((controller) => controller.abort()); - }; - }, [schemas.loading, verifiableCredentials, client]); + // Hooks that update the stored credential schemas and stored credential metadata. + useFetchingEffect( + verifiableCredentials, + storedMetadata, + setStoredMetadata, + getChangesToCredentialMetadata + ); + useFetchingEffect( + verifiableCredentials, + schemas, + setSchemas, + getChangesToCredentialSchemas + ); if (!verifiableCredentials || !verifiableCredentials.length) { return ; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx new file mode 100644 index 00000000..7155220f --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx @@ -0,0 +1,37 @@ +import { VerifiableCredentialStatus } from '@shared/storage/types'; +import React from 'react'; +import RevokedIcon from '@assets/svg/revoked.svg'; +import ActiveIcon from '@assets/svg/verified.svg'; +import ExpiredIcon from '@assets/svg/block.svg'; +import PendingIcon from '@assets/svg/pending.svg'; + +/** + * Component for displaying the status of a verifiable credential. + */ +export default function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { + let icon = null; + switch (status) { + case VerifiableCredentialStatus.Active: + icon = ; + break; + case VerifiableCredentialStatus.Revoked: + icon = ; + break; + case VerifiableCredentialStatus.Expired: + icon = ; + break; + case VerifiableCredentialStatus.NotActivated: + icon = ; + break; + default: + icon = null; + break; + } + + return ( +
+ {VerifiableCredentialStatus[status]} + {icon} +
+ ); +} diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 9d8734c0..190cfa03 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -258,8 +258,6 @@ export type AcceptedTermsState = { url?: string; }; -// TODO[orhoj]: The types are incomplete as the final schemas are not ready. -// The types used here are taken from the draft documents available. export enum VerifiableCredentialStatus { Active, Revoked, diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 6230ce55..7d9a7abc 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,5 +1,10 @@ import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; -import { MetadataUrl, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { + MetadataUrl, + VerifiableCredential, + VerifiableCredentialSchema, + VerifiableCredentialStatus, +} from '@shared/storage/types'; import { Buffer } from 'buffer/'; import { getContractName } from './contract-helpers'; @@ -63,7 +68,7 @@ export async function getVerifiableCredentialStatus(client: ConcordiumGRPCClient const result = await client.invokeContract({ contract: contractAddress, - method: `${instanceInfo.name.substring(5)}.credentialStatus`, + method: `${getContractName(instanceInfo)}.credentialStatus`, parameter: Buffer.from(getCredentialHolderId(credentialId), 'hex'), }); @@ -171,60 +176,6 @@ export async function getCredentialRegistryMetadata(client: ConcordiumGRPCClient return deserializeRegistryMetadata(returnValue); } -/** - * Retrieves a credential schema from the specified URL. - * @param metadata containing the URL and optionally the checksum of the JSON content served at the URL. - * @throws if the URL is unavailable - * @throws if the content served at the URL is not valid JSON - * @throws if the JSON served at the URL is not a valid verifiable credential schema - * @returns a credential schema - */ -export async function getCredentialSchema( - metadata: MetadataUrl, - abortController: AbortController -): Promise { - const response = await fetch(metadata.url, { - headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), - mode: 'cors', - signal: abortController.signal, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch the schema at: ${metadata.url}`); - } - - // TODO Validate the checksum here. - const body = Buffer.from(await response.arrayBuffer()); - let bodyAsString; - try { - bodyAsString = body.toString(); - const schema = JSON.parse(bodyAsString); - - // TODO Validate that the expected fields are available. - return schema; - } catch { - throw new Error(`Failed to parse JSON: ${bodyAsString}`); - } -} - -// { -// "title": "Concordium Employment", -// "logo" : { -// "url": "https://concordium.com/wp-content/uploads/2022/07/Concordium-1.png", -// "hash": "1c74f7eb1b3343a5834e60e9a8fce277f2c7553112accd42e63fae7a09e0caf8" -// } -// "background_color": "#000000", -// "image": { -// "url": "https://concordium.com/employment/vc-background.png", -// } -// "localization": { -// "da-DK": { -// "url": "https://location.of/the/danish/metadata.json", -// "hash": "624a1a7e51f7a87effbf8261426cb7d436cf597be327ebbf113e62cb7814a34b" -// } -// } -// } - export interface VerifiableCredentialMetadata { title: string; logo: MetadataUrl; @@ -317,7 +268,7 @@ export async function getVerifiableCredentialEntry( const result = await client.invokeContract({ contract: contractAddress, - method: `${instanceInfo.name.substring(5)}.credentialEntry`, + method: `${getContractName(instanceInfo)}.credentialEntry`, parameter: Buffer.from(credentialHolderId, 'hex'), }); @@ -333,15 +284,11 @@ export async function getVerifiableCredentialEntry( return deserializeCredentialEntry(returnValue); } -// TODO This method is almost identical to getting credential schema. Share -// the code instead. Only the verification of the type differs. /** - * Retrieves credential metadata from the specified URL. + * Retrieves data from the from the specified URL. */ -export async function getCredentialMetadata( - metadata: MetadataUrl, - abortController: AbortController -): Promise { +// TODO This can be merged with the equivalent function in token-helpers. +async function fetchDataFromUrl(metadata: MetadataUrl, abortController: AbortController): Promise { const response = await fetch(metadata.url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), mode: 'cors', @@ -352,16 +299,166 @@ export async function getCredentialMetadata( throw new Error(`Failed to fetch the schema at: ${metadata.url}`); } - // TODO Validate the checksum here. + // TODO Validate the checksum here (if available). const body = Buffer.from(await response.arrayBuffer()); let bodyAsString; try { bodyAsString = body.toString(); - const schema = JSON.parse(bodyAsString); - // TODO Validate that the expected fields are available. + const schema = JSON.parse(bodyAsString) as T; return schema; } catch { throw new Error(`Failed to parse JSON: ${bodyAsString}`); } } + +/** + * Retrieves a credential schema from the specified URL. + */ +export async function fetchCredentialSchema( + metadata: MetadataUrl, + abortController: AbortController +): Promise { + return fetchDataFromUrl(metadata, abortController); +} + +/** + * Retrieves credential metadata from the specified URL. + */ +export async function fetchCredentialMetadata( + metadata: MetadataUrl, + abortController: AbortController +): Promise { + return fetchDataFromUrl(metadata, abortController); +} + +/** + * Retrieves credential schemas for each of the provided credentials. The method ensures + * that duplicate schemas are not fetched multiple times, by only fetching once per + * contract. + * @param credentials the verifiable credentials to get schemas for + * @param client the GRPC client for accessing a node + * @param abortControllers controllers to enable aborting the fetching if needed + * @returns a list of verifiable credential schemas + */ +export async function getCredentialSchemas( + credentials: VerifiableCredential[], + abortControllers: AbortController[], + client: ConcordiumGRPCClient +) { + const onChainSchemas: VerifiableCredentialSchema[] = []; + + const allContractAddresses = credentials.map((vc) => getCredentialRegistryContractAddress(vc.id)); + const issuerContractAddresses = [...new Set(allContractAddresses)]; + + for (const contractAddress of issuerContractAddresses) { + // TODO Add error handling for the call below. + const registryMetadata = await getCredentialRegistryMetadata(client, contractAddress); + + if (registryMetadata) { + const controller = new AbortController(); + abortControllers.push(controller); + try { + const credentialSchema = await fetchCredentialSchema( + registryMetadata.credentialSchema.schema, + controller + ); + onChainSchemas.push(credentialSchema); + } catch { + // TODO An error is thrown here if the controller aborts which can be ignored. + // TODO An error can also be thrown if the fetching of the credential schema goes haywire. + } + } + } + + return onChainSchemas; +} + +/** + * Retrieves credential metadata for each of the provided credentials. The method ensures + * that duplicate metadata (metadata hosted at the same URL) is not fetched multiple + * times. + * @param client the GRPC client for accessing a node + * @param credentials the verifiable credentials to get metadata for + * @param abortControllers controllers to enable aborting the fetching if needed + * @returns a list of pairs of verifiable credential metadata and the URL they were fetched from (their key) + */ +export async function getCredentialMetadata( + credentials: VerifiableCredential[], + client: ConcordiumGRPCClient, + abortControllers: AbortController[] +) { + const metadataUrls: MetadataUrl[] = []; + for (const vc of credentials) { + // TODO Add error handling for the call below. + const entry = await getVerifiableCredentialEntry( + client, + getCredentialRegistryContractAddress(vc.id), + getCredentialHolderId(vc.id) + ); + if (entry) { + metadataUrls.push(entry.credentialInfo.metadataUrl); + } + } + const uniqueMetadataUrls = [...new Map(metadataUrls.map((item) => [item.url, item])).values()]; + + const metadataList: { metadata: VerifiableCredentialMetadata; url: string }[] = []; + for (const metadataUrl of uniqueMetadataUrls) { + const controller = new AbortController(); + abortControllers.push(controller); + try { + const metadata = await fetchCredentialMetadata(metadataUrl, controller); + metadataList.push({ metadata, url: metadataUrl.url }); + } catch { + // TODO An error is thrown here if the controller aborts which can be ignored. + // TODO An error can also be thrown if the fetching of the credential schema goes haywire. + } + } + + return metadataList; +} + +export async function getChangesToCredentialMetadata( + credentials: VerifiableCredential[], + client: ConcordiumGRPCClient, + abortControllers: AbortController[], + storedMetadata: Record +) { + const upToDateCredentialMetadata = await getCredentialMetadata(credentials, client, abortControllers); + let updatedStoredMetadata = { ...storedMetadata }; + + // TODO Verify that something has actually changed before making the update. + for (const updatedMetadata of upToDateCredentialMetadata) { + if (storedMetadata.value === undefined) { + updatedStoredMetadata = { + [updatedMetadata.url]: updatedMetadata.metadata, + }; + } else { + updatedStoredMetadata[updatedMetadata.url] = updatedMetadata.metadata; + } + } + + return updatedStoredMetadata; +} + +export async function getChangesToCredentialSchemas( + credentials: VerifiableCredential[], + client: ConcordiumGRPCClient, + abortControllers: AbortController[], + storedSchemas: Record +) { + const upToDateSchemas = await getCredentialSchemas(credentials, abortControllers, client); + let updatedSchemasInStorage = { ...storedSchemas }; + + // TODO Verify that something has actually changed before making the update. + for (const updatedSchema of upToDateSchemas) { + if (storedSchemas === undefined) { + updatedSchemasInStorage = { + [updatedSchema.$id]: updatedSchema, + }; + } else { + updatedSchemasInStorage[updatedSchema.$id] = updatedSchema; + } + } + return updatedSchemasInStorage; +} From 4d32132cd660dda7282cb92b2c383b30fa1fab6f Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 14 Jul 2023 15:18:37 +0200 Subject: [PATCH 044/231] Validate checksum when fetching --- .../utils/verifiable-credential-helpers.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 7d9a7abc..c5e24d57 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,4 +1,4 @@ -import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; +import { ConcordiumGRPCClient, ContractAddress, sha256 } from '@concordium/web-sdk'; import { MetadataUrl, VerifiableCredential, @@ -288,19 +288,22 @@ export async function getVerifiableCredentialEntry( * Retrieves data from the from the specified URL. */ // TODO This can be merged with the equivalent function in token-helpers. -async function fetchDataFromUrl(metadata: MetadataUrl, abortController: AbortController): Promise { - const response = await fetch(metadata.url, { +async function fetchDataFromUrl({ url, hash }: MetadataUrl, abortController: AbortController): Promise { + const response = await fetch(url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), mode: 'cors', signal: abortController.signal, }); if (!response.ok) { - throw new Error(`Failed to fetch the schema at: ${metadata.url}`); + throw new Error(`Failed to fetch the schema at: ${url}`); } - // TODO Validate the checksum here (if available). const body = Buffer.from(await response.arrayBuffer()); + if (hash && sha256([body]).toString('hex') !== hash) { + throw new Error(`The content at URL ${url} did not match the provided checksum: ${hash}`); + } + let bodyAsString; try { bodyAsString = body.toString(); @@ -364,9 +367,11 @@ export async function getCredentialSchemas( controller ); onChainSchemas.push(credentialSchema); - } catch { - // TODO An error is thrown here if the controller aborts which can be ignored. - // TODO An error can also be thrown if the fetching of the credential schema goes haywire. + } catch (e) { + // Ignore errors that occur because we aborted, as that is expected to happen. + if (!controller.signal.aborted) { + // TODO Does it make sense to show errors to the user? + } } } } @@ -409,9 +414,11 @@ export async function getCredentialMetadata( try { const metadata = await fetchCredentialMetadata(metadataUrl, controller); metadataList.push({ metadata, url: metadataUrl.url }); - } catch { - // TODO An error is thrown here if the controller aborts which can be ignored. - // TODO An error can also be thrown if the fetching of the credential schema goes haywire. + } catch (e) { + // Ignore errors that occur because we aborted, as that is expected to happen. + if (!controller.signal.aborted) { + // TODO Does it make sense to show errors to the user? + } } } From d46519f9dca2aa5cd26893639f3888f8f3511f55 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 7 Aug 2023 10:49:57 +0200 Subject: [PATCH 045/231] Remove index from schema subject --- packages/browser-wallet/src/shared/storage/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 190cfa03..c0424716 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -293,7 +293,7 @@ interface CredentialSchemaProperty { interface CredentialSchemaSubject { type: string; required: string[]; - properties: { id: Exclude } & Record; + properties: { id: CredentialSchemaProperty } & Record; } export interface SchemaProperties { From 65184eaf46189719b917b9686d7c0e26ed8eb77d Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 7 Aug 2023 14:36:56 +0200 Subject: [PATCH 046/231] Add JSON schema validation of fetched data --- packages/browser-wallet/package.json | 1 + .../VerifiableCredentialCard.tsx | 6 +- .../utils/verifiable-credential-helpers.ts | 168 +++++++++++++++++- yarn.lock | 8 + 4 files changed, 170 insertions(+), 13 deletions(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e7c2c7ea..7a611142 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -33,6 +33,7 @@ "i18next-browser-languagedetector": "^6.1.4", "jotai": "^1.6.5", "json-bigint": "^1.0.0", + "jsonschema": "^1.4.1", "leb128": "^0.0.5", "lodash.debounce": "^4.0.8", "lodash.groupby": "^4.6.0", diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 1d68aac7..d0327452 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -85,14 +85,13 @@ function ClickableVerifiableCredential({ */ function applySchema( schema: VerifiableCredentialSchema -): (value: [string, string | number]) => { index: number; title: string; key: string; value: string | number } { +): (value: [string, string | number]) => { title: string; key: string; value: string | number } { return (value: [string, string | number]) => { const attributeSchema = schema.properties.credentialSubject.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); } return { - index: Number(attributeSchema.index), title: attributeSchema.title, key: value[0], value: value[1], @@ -115,8 +114,7 @@ export function VerifiableCredentialCard({ }) { const attributes = Object.entries(credential.credentialSubject) .filter((val) => val[0] !== 'id') - .map(applySchema(schema)) - .sort((a, b) => a.index - b.index); + .map(applySchema(schema)); return ( diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index c5e24d57..999d1dc9 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -6,6 +6,7 @@ import { VerifiableCredentialStatus, } from '@shared/storage/types'; import { Buffer } from 'buffer/'; +import jsonschema from 'jsonschema'; import { getContractName } from './contract-helpers'; /** @@ -176,6 +177,142 @@ export async function getCredentialRegistryMetadata(client: ConcordiumGRPCClient return deserializeRegistryMetadata(returnValue); } +// The schemas have been generated using ts-json-schema-generator and their +// corresponding type definitions. +const verifiableCredentialSchemaSchema = { + $ref: '#/definitions/VerifiableCredentialSchema', + $schema: 'http://json-schema.org/draft-07/schema#', + definitions: { + SchemaProperties: { + additionalProperties: false, + properties: { + credentialSubject: { + additionalProperties: false, + properties: { + properties: { + additionalProperties: { + type: 'object', + }, + properties: { + id: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + title: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['title', 'type', 'description'], + type: 'object', + }, + }, + required: ['id'], + type: 'object', + }, + required: { + items: { + type: 'string', + }, + type: 'array', + }, + type: { + type: 'string', + }, + }, + required: ['type', 'required', 'properties'], + type: 'object', + }, + }, + required: ['credentialSubject'], + type: 'object', + }, + VerifiableCredentialSchema: { + additionalProperties: false, + properties: { + $id: { + type: 'string', + }, + $schema: { + type: 'string', + }, + description: { + type: 'string', + }, + name: { + type: 'string', + }, + properties: { + $ref: '#/definitions/SchemaProperties', + }, + required: { + items: { + type: 'string', + }, + type: 'array', + }, + type: { + type: 'string', + }, + }, + required: ['$id', '$schema', 'name', 'description', 'type', 'properties', 'required'], + type: 'object', + }, + }, +}; + +const verifiableCredentialMetadataSchema = { + $ref: '#/definitions/VerifiableCredentialMetadata', + $schema: 'http://json-schema.org/draft-07/schema#', + definitions: { + HexString: { + type: 'string', + }, + MetadataUrl: { + additionalProperties: false, + properties: { + hash: { + $ref: '#/definitions/HexString', + }, + url: { + type: 'string', + }, + }, + required: ['url'], + type: 'object', + }, + VerifiableCredentialMetadata: { + additionalProperties: false, + properties: { + background_color: { + type: 'string', + }, + image: { + $ref: '#/definitions/MetadataUrl', + }, + localization: { + additionalProperties: { + $ref: '#/definitions/MetadataUrl', + }, + type: 'object', + }, + logo: { + $ref: '#/definitions/MetadataUrl', + }, + title: { + type: 'string', + }, + }, + required: ['title', 'logo', 'background_color'], + type: 'object', + }, + }, +}; + export interface VerifiableCredentialMetadata { title: string; logo: MetadataUrl; @@ -285,10 +422,14 @@ export async function getVerifiableCredentialEntry( } /** - * Retrieves data from the from the specified URL. + * Retrieves data from the from the specified URL. The result is validated according + * to the supplied JSON schema. */ -// TODO This can be merged with the equivalent function in token-helpers. -async function fetchDataFromUrl({ url, hash }: MetadataUrl, abortController: AbortController): Promise { +async function fetchDataFromUrl( + { url, hash }: MetadataUrl, + abortController: AbortController, + jsonSchema: typeof verifiableCredentialSchemaSchema | typeof verifiableCredentialSchemaSchema +): Promise { const response = await fetch(url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), mode: 'cors', @@ -304,15 +445,24 @@ async function fetchDataFromUrl({ url, hash }: MetadataUrl, abortController: throw new Error(`The content at URL ${url} did not match the provided checksum: ${hash}`); } + let bodyAsObject; let bodyAsString; try { bodyAsString = body.toString(); - // TODO Validate that the expected fields are available. - const schema = JSON.parse(bodyAsString) as T; - return schema; - } catch { + bodyAsObject = JSON.parse(bodyAsString); + } catch (e) { throw new Error(`Failed to parse JSON: ${bodyAsString}`); } + + const validator = new jsonschema.Validator(); + const validationResult = validator.validate(bodyAsObject, jsonSchema); + if (!validationResult.valid) { + throw new Error( + `The received JSON [${bodyAsString}] did not validate according to the schema: ${validationResult.errors}` + ); + } + + return bodyAsObject as T; } /** @@ -322,7 +472,7 @@ export async function fetchCredentialSchema( metadata: MetadataUrl, abortController: AbortController ): Promise { - return fetchDataFromUrl(metadata, abortController); + return fetchDataFromUrl(metadata, abortController, verifiableCredentialSchemaSchema); } /** @@ -332,7 +482,7 @@ export async function fetchCredentialMetadata( metadata: MetadataUrl, abortController: AbortController ): Promise { - return fetchDataFromUrl(metadata, abortController); + return fetchDataFromUrl(metadata, abortController, verifiableCredentialMetadataSchema); } /** diff --git a/yarn.lock b/yarn.lock index 07a81131..9bf6430f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2001,6 +2001,7 @@ __metadata: jest-chrome: ^0.8.0 jotai: ^1.6.5 json-bigint: ^1.0.0 + jsonschema: ^1.4.1 leb128: ^0.0.5 lodash.debounce: ^4.0.8 lodash.groupby: ^4.6.0 @@ -15306,6 +15307,13 @@ __metadata: languageName: node linkType: hard +"jsonschema@npm:^1.4.1": + version: 1.4.1 + resolution: "jsonschema@npm:1.4.1" + checksum: 1ef02a6cd9bc32241ec86bbf1300bdbc3b5f2d8df6eb795517cf7d1cd9909e7beba1e54fdf73990fd66be98a182bda9add9607296b0cb00b1348212988e424b2 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.2.1": version: 3.3.1 resolution: "jsx-ast-utils@npm:3.3.1" From d8df33b01f06c38fe6364a471c673be49bda624b Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 7 Aug 2023 14:37:43 +0200 Subject: [PATCH 047/231] Fix type error --- .../src/shared/utils/verifiable-credential-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 999d1dc9..0a947e86 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -428,7 +428,7 @@ export async function getVerifiableCredentialEntry( async function fetchDataFromUrl( { url, hash }: MetadataUrl, abortController: AbortController, - jsonSchema: typeof verifiableCredentialSchemaSchema | typeof verifiableCredentialSchemaSchema + jsonSchema: typeof verifiableCredentialMetadataSchema | typeof verifiableCredentialSchemaSchema ): Promise { const response = await fetch(url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), From 1de2014165ab5c2919bd7460a5f84d993ce5995f Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 7 Aug 2023 16:14:31 +0200 Subject: [PATCH 048/231] Refactoring --- .../VerifiableCredentialCard.stories.tsx | 147 ++++++------------ .../VerifiableCredentialCard.tsx | 12 +- .../VerifiableCredentialHooks.tsx | 8 +- .../VerifiableCredentialList.tsx | 7 +- .../src/shared/storage/types.ts | 2 - .../utils/verifiable-credential-helpers.ts | 32 ++-- 6 files changed, 86 insertions(+), 122 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index 92057c44..6262775a 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -1,7 +1,8 @@ /* eslint-disable react/function-component-definition, react/destructuring-assignment */ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; export default { @@ -9,107 +10,60 @@ export default { component: VerifiableCredentialCard, } as ComponentMeta; -const schema = { - type: 'https://w3c-ccg.github.io/vc-json-schemas/', - version: '1.0', - id: 'https://example-university.com/certificates/simple-education-certificate.json', - name: 'UniversityDegreeCredential', - author: 'did:ccd:mainnet:acc:...', - authored: '2023-01-01T00:00:00+00:00', - schema: { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'https://example-university.com/certificates/simple-education-certificate.json', - title: 'Education certificate', - description: 'Simple representation of an education certificate.', - type: 'object', - required: ['@context', 'type', 'issuer', 'issuanceDate', 'credentialSubject'], - properties: { - '@context': { - type: ['string', 'array', 'object'], - }, - id: { - type: 'string', - format: 'uri', - }, - type: { - type: ['string', 'array'], - items: { +const schema: VerifiableCredentialSchema = { + $id: 'https://example-university.com/certificates/JsonSchema2023-education-certificate.json', + $schema: 'https://json-schema.org/draft/2020-12/schema', + name: 'Education certificate', + description: 'Simple representation of an education certificate.', + type: 'object', + properties: { + credentialSubject: { + type: 'object', + properties: { + id: { + title: 'Credential subject id', type: 'string', + description: 'Credential subject identifier', }, - }, - issuer: { - type: ['string', 'object'], - format: 'uri', - required: ['id'], - properties: { - id: { - type: 'string', - format: 'uri', - }, + degreeType: { + title: 'Degree Hello', + type: 'string', + description: 'Degree type', }, - }, - issuanceDate: { - type: 'string', - format: 'date-time', - }, - expirationDate: { - type: 'string', - format: 'date-time', - }, - validFrom: { - type: 'string', - format: 'date-time', - }, - validUntil: { - type: 'string', - format: 'date-time', - }, - credentialSubject: { - type: 'object', - properties: { - id: { - title: 'id', - type: 'string', - description: 'Credential subject identifier', - }, - degreeType: { - title: 'Degree type', - type: 'string', - description: 'Degree type', - index: '0', - }, - degreeName: { - title: 'Degree name', - type: 'string', - description: 'Degree name', - index: '1', - }, - graduationDate: { - type: 'string', - format: 'date-time', - description: 'Graduation date', - index: '2', - title: 'Graduation date', - }, + degreeName: { + title: 'Degree name', + type: 'string', + description: 'Degree name', }, - required: ['id', 'degreeType', 'degreeName', 'graduationDate'], - }, - credentialSchema: { - type: 'object', - required: ['id', 'type'], - properties: { - id: { - type: 'string', - format: 'uri', - }, - type: { - type: 'string', - }, + graduationDate: { + title: 'Graduation date', + type: 'string', + format: 'date-time', + description: 'Graduation date', }, }, + required: ['id', 'degreeType', 'degreeName', 'graduationDate'], + }, + }, + required: ['credentialSubject'], +}; + +const metadata: VerifiableCredentialMetadata = { + title: 'Education Certificate v2', + logo: { + url: 'https://img.logoipsum.com/298.svg', + hash: '1c74f7eb1b3343a5834e60e9a8fce277f2c7553112accd42e63fae7a09e0caf8', + }, + background_color: '#003d73', + image: { + url: 'https://picsum.photos/327/120', + }, + localization: { + 'da-DK': { + url: 'https://location.of/the/danish/metadata.json', + hash: '624a1a7e51f7a87effbf8261426cb7d436cf597be327ebbf113e62cb7814a34b', }, }, - proof: '', }; const verifiableCredential = { @@ -132,11 +86,12 @@ const verifiableCredential = { export const Primary: ComponentStory = () => { return ( -
+
VerifiableCredentialStatus.Active} + credentialStatus={VerifiableCredentialStatus.Active} + metadata={metadata} />
); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index d0327452..debac769 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -48,9 +48,9 @@ function DisplayAttribute({ */ function ClickableVerifiableCredential({ children, - onClick, metadata, -}: PropsWithChildren<{ onClick?: () => void; metadata: VerifiableCredentialMetadata }>) { + onClick, +}: PropsWithChildren<{ metadata: VerifiableCredentialMetadata; onClick?: () => void }>) { if (onClick) { return (
+
{metadata.title}
diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 29e52b8e..27ed7622 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -23,7 +23,7 @@ import { ConcordiumGRPCClient } from '@concordium/web-sdk'; * @returns the status for the given credential */ export function useCredentialStatus(credential: VerifiableCredential) { - const [status, setStatus] = useState(VerifiableCredentialStatus.Unknown); + const [status, setStatus] = useState(); const client = useAtomValue(grpcClientAtom); useEffect(() => { @@ -123,7 +123,7 @@ export function useFetchingEffect( client: ConcordiumGRPCClient, abortControllers: AbortController[], storedData: Record - ) => Promise> + ) => Promise<{ data: Record; updateReceived: boolean }> ) { const client = useAtomValue(grpcClientAtom); @@ -133,8 +133,8 @@ export function useFetchingEffect( if (credentials && !storedData.loading) { dataFetcher(credentials, client, abortControllers, storedData.value).then((result) => { - if (!isCancelled) { - setStoredData(result); + if (!isCancelled && result.updateReceived) { + setStoredData(result.data); } }); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index d363078e..199cc5d1 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -46,7 +46,7 @@ function VerifiableCredentialCardWithStatusFromChain({ const metadata = useCredentialMetadata(credential); // Render nothing until all the required data is available. - if (!schema || !metadata || status === VerifiableCredentialStatus.Unknown) { + if (!schema || !metadata || status === undefined) { return null; } @@ -112,11 +112,10 @@ export default function VerifiableCredentialList() { return (
- {verifiableCredentials.map((credential, index) => { + {verifiableCredentials.map((credential) => { return ( Date: Tue, 8 Aug 2023 13:51:52 +0200 Subject: [PATCH 049/231] Fixes according to comments --- .../ExternalRequestLayout.tsx | 2 +- .../VerifiableCredentialHooks.tsx | 2 +- .../utils/verifiable-credential-helpers.ts | 18 +++++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index bf72c7f1..d51d9af4 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -18,7 +18,7 @@ function Header() { return t('header.connect'); } if (pathname.startsWith(absoluteRoutes.prompt.connectAccountsRequest.path)) { - return t('header.connectAccountsRequest'); + return t('header.allowlistingRequest'); } if (pathname.startsWith(absoluteRoutes.prompt.addTokens.path)) { return t('header.addTokens'); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 27ed7622..8b8d75f2 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -54,7 +54,7 @@ export function useCredentialSchema(credential: VerifiableCredential) { } setSchema(schemaValue); } - }, [schemas]); + }, [schemas.loading]); return schema; } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 078b5d95..1954af28 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -15,8 +15,8 @@ import { getContractName } from './contract-helpers'; * @returns the credential holder id */ export function getCredentialHolderId(credentialId: string): string { - const splitted = credentialId.split('/'); - const credentialHolderId = splitted[splitted.length - 1]; + const credentialIdParts = credentialId.split('/'); + const credentialHolderId = credentialIdParts[credentialIdParts.length - 1]; if (credentialHolderId.length !== 64) { throw new Error(`Invalid credential holder id found from: ${credentialId}`); @@ -31,9 +31,9 @@ export function getCredentialHolderId(credentialId: string): string { * @returns the contract address of the issuing contract of the provided credential id */ export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { - const splitted = credentialId.split(':'); - const index = BigInt(splitted[4]); - const subindex = BigInt(splitted[5].split('/')[0]); + const credentialIdParts = credentialId.split(':'); + const index = BigInt(credentialIdParts[4]); + const subindex = BigInt(credentialIdParts[5].split('/')[0]); return { index, subindex }; } @@ -502,7 +502,7 @@ export async function getCredentialSchemas( const onChainSchemas: VerifiableCredentialSchema[] = []; const allContractAddresses = credentials.map((vc) => getCredentialRegistryContractAddress(vc.id)); - const issuerContractAddresses = [...new Set(allContractAddresses)]; + const issuerContractAddresses = new Set(allContractAddresses); for (const contractAddress of issuerContractAddresses) { let registryMetadata: MetadataResponse | undefined; @@ -558,6 +558,11 @@ export async function getCredentialMetadata( metadataUrls.push(entry.credentialInfo.metadataUrl); } } + + // We filter any duplicate URLs. Note that there could be metadata pairs (url, hash) with the + // same URL but separate hashes that are thrown away here. This is done intentionally for now, + // as the assumption is that that would be a rare situation. This means that the first instance + // of the URL is the one used for gathering the credential metadata. const uniqueMetadataUrls = [...new Map(metadataUrls.map((item) => [item.url, item])).values()]; const metadataList: { metadata: VerifiableCredentialMetadata; url: string }[] = []; @@ -615,7 +620,6 @@ export async function getChangesToCredentialSchemas( let updatedSchemasInStorage = { ...storedSchemas }; let updateReceived = false; - // TODO Verify that something has actually changed before making the update. for (const updatedSchema of upToDateSchemas) { if (storedSchemas === undefined) { updatedSchemasInStorage = { From 1630177323b51cf7390455e492ce669ecb9cccfe Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 8 Aug 2023 16:15:09 +0200 Subject: [PATCH 050/231] 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 ee76b9da..e03078e0 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 debac769..3c4da39c 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 5ffd21d0..e021344b 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 69bb6505..76413094 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 0cf61a64..9de2a1cf 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 3f946dd2..3faed8c4 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 52c8d10cf83d93d3afd0608f15efcc7782878432 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 9 Aug 2023 10:52:27 +0200 Subject: [PATCH 051/231] Update schema and credential types --- .../VerifiableCredentialCard.stories.tsx | 72 ++++++++++++------- .../VerifiableCredentialCard.tsx | 4 +- .../src/shared/storage/types.ts | 14 ++-- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index 6262775a..926475f7 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -25,21 +25,28 @@ const schema: VerifiableCredentialSchema = { type: 'string', description: 'Credential subject identifier', }, - degreeType: { - title: 'Degree Hello', - type: 'string', - description: 'Degree type', - }, - degreeName: { - title: 'Degree name', - type: 'string', - description: 'Degree name', - }, - graduationDate: { - title: 'Graduation date', - type: 'string', - format: 'date-time', - description: 'Graduation date', + attributes: { + title: 'Attributes', + description: 'Credential attributes', + type: 'object', + properties: { + degreeType: { + title: 'Degree Hello', + type: 'string', + description: 'Degree type', + }, + degreeName: { + title: 'Degree name', + type: 'string', + description: 'Degree name', + }, + graduationDate: { + title: 'Graduation date', + type: 'string', + format: 'date-time', + description: 'Graduation date', + }, + }, }, }, required: ['id', 'degreeType', 'degreeName', 'graduationDate'], @@ -67,20 +74,35 @@ const metadata: VerifiableCredentialMetadata = { }; const verifiableCredential = { - '@context': ['https://www.w3.org/2018/credentials/v1', 'Concordium VC URI'], - id: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/ff4aa77af80b4d72973ccb957d180746', - type: ['VerifiableCredential', 'UniversityDegreeCredential'], + $schema: './JsonSchema2023-education-certificate.json', + id: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d', + type: ['VerifiableCredential', 'ConcordiumVerifiableCredential', 'UniversityDegreeCredential'], issuer: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/issuer', - issuanceDate: '2010-01-01T00:00:00Z', + validFrom: '2010-01-01T00:00:00Z', + validUntil: '2030-01-01T00:00:00Z', credentialSubject: { - id: 'did:ccd:pkc:ebfeb1f712ebc6f1c276e12ec21', - degreeType: 'Bachelor degree', - degreeName: 'Bachelor of Science and Arts', - graduationDate: '2010-06-01T00:00:00Z', + id: 'did:ccd:pkc:76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d', + attributes: { + degreeType: 'BachelorDegree', + degreeName: 'Bachelor of Science and Arts', + graduationDate: '2010-06-01T00:00:00Z', + }, }, credentialSchema: { - id: 'https://example-university.com/certificates/simple-education-certificate.json', - type: 'CredentialSchema2022', // the same for all schemas + id: 'https://example-university.com/certificates/JsonSchema2023-education-certificate.json', + type: 'JsonSchema2023', + }, + randomness: { + degreeType: '6490531ea308a2e661f62c4678e00bb87c9f602be7a053e910f8e44609bc5adb', + degreeName: '29b439aa58324b2be5c5a3ceb7ba23b48397ba1d1d9081869f56ff1c96a2b32f', + graduationDate: '2f5e0279c8ff6bcb004024dd4ba4f3e29d30ec91e3e4583855c2dae35ae83f8d', + }, + proof: { + type: 'Ed25519Signature2020', + verificationMethod: 'did:ccd:pkc:12345678ad3b62d3040f693722f94d61efa4fdd6ee797dd1e8aa5a651a0c4ac1', + proofPurpose: 'assertionMethod', + proofValue: + 'facdb03a1d054a55808875864abc85cc41d2c32290929bbb361a710b0fda5e7f333ac33abdb1b5f0ebb5662335c34410b8e96ca6730df7eb100f814f223d0b07', }, }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index debac769..aac2dfa4 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -87,7 +87,7 @@ function applySchema( schema: VerifiableCredentialSchema ): (value: [string, string | number]) => { title: string; key: string; value: string | number } { return (value: [string, string | number]) => { - const attributeSchema = schema.properties.credentialSubject.properties[value[0]]; + const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); } @@ -112,7 +112,7 @@ export function VerifiableCredentialCard({ metadata: VerifiableCredentialMetadata; onClick?: () => void; }) { - const attributes = Object.entries(credential.credentialSubject) + const attributes = Object.entries(credential.credentialSubject.attributes) .filter((val) => val[0] !== 'id') .map(applySchema(schema)); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 1f8fb3ca..39e11210 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -270,14 +270,15 @@ interface CredentialSchema { type: string; } -export type CredentialSubject = { id: string } & Record; +export type CredentialSubject = { + id: string; + attributes: Record; +}; export interface VerifiableCredential { - '@context': string[]; id: string; type: string[]; issuer: string; - issuanceDate: string; credentialSubject: CredentialSubject; credentialSchema: CredentialSchema; } @@ -286,12 +287,17 @@ interface CredentialSchemaProperty { title: string; type: 'string' | 'number' | string; description: string; + format?: string; } +type CredentialSchemaAttributes = { + properties: Record; +} & CredentialSchemaProperty; + interface CredentialSchemaSubject { type: string; + properties: { id: CredentialSchemaProperty } & { attributes: CredentialSchemaAttributes }; required: string[]; - properties: { id: CredentialSchemaProperty } & Record; } export interface SchemaProperties { From eb0324cc3cc77ed698fe35e2221b13d954e6355d Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 9 Aug 2023 11:43:26 +0200 Subject: [PATCH 052/231] More fixes to the types --- .../pages/VerifiableCredential/VerifiableCredentialCard.tsx | 4 +--- packages/browser-wallet/src/shared/storage/types.ts | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index aac2dfa4..b094e32f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -112,9 +112,7 @@ export function VerifiableCredentialCard({ metadata: VerifiableCredentialMetadata; onClick?: () => void; }) { - const attributes = Object.entries(credential.credentialSubject.attributes) - .filter((val) => val[0] !== 'id') - .map(applySchema(schema)); + const attributes = Object.entries(credential.credentialSubject.attributes).map(applySchema(schema)); return ( diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 39e11210..bea60ee2 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -292,11 +292,15 @@ interface CredentialSchemaProperty { type CredentialSchemaAttributes = { properties: Record; + required: string[]; } & CredentialSchemaProperty; interface CredentialSchemaSubject { type: string; - properties: { id: CredentialSchemaProperty } & { attributes: CredentialSchemaAttributes }; + properties: { + id: CredentialSchemaProperty; + attributes: CredentialSchemaAttributes; + }; required: string[]; } From 32325e5452c49ae97d7d7c8d7bee880b2c8157e3 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 8 Aug 2023 13:24:55 +0200 Subject: [PATCH 053/231] Endpoint to add web3Id credentials --- examples/add-example-Web3Id/README.md | 18 ++ examples/add-example-Web3Id/index.html | 110 +++++++++++ examples/add-example-Web3Id/package.json | 13 ++ examples/eSealing/package.json | 2 +- examples/nft-minting/package.json | 2 +- examples/piggybank/package.json | 2 +- examples/two-step-transfer/package.json | 2 +- examples/voting/package.json | 2 +- examples/wCCD/package.json | 2 +- packages/browser-wallet-api-helpers/README.md | 19 ++ .../browser-wallet-api-helpers/package.json | 2 +- .../src/wallet-api-types.ts | 42 +++++ packages/browser-wallet-api/package.json | 2 +- packages/browser-wallet-api/src/wallet-api.ts | 44 ++++- .../browser-wallet-message-hub/src/message.ts | 3 + packages/browser-wallet/package.json | 4 +- .../browser-wallet/src/background/index.ts | 91 ++++++++- .../src/popup/constants/routes.ts | 3 + .../ExternalRequestLayout.tsx | 3 + .../FullscreenPromptLayout.tsx | 2 +- .../popup/page-layouts/MainLayout/i18n/da.ts | 2 + .../popup/page-layouts/MainLayout/i18n/en.ts | 2 + .../pages/Account/Earn/Baking/Baking.tsx | 9 +- .../pages/Account/Earn/Delegate/Delegate.tsx | 9 +- .../TransactionLog/TransactionElement.tsx | 4 +- .../popup/pages/AddAccount/ChooseIdentity.tsx | 18 +- .../AddWeb3IdCredential.scss | 5 + .../AddWeb3IdCredential.tsx | 176 ++++++++++++++++++ .../pages/AddWeb3IdCredential/i18n/da.ts | 7 + .../pages/AddWeb3IdCredential/i18n/en.ts | 7 + .../VerifiableCredential.scss | 5 +- .../VerifiableCredentialCard.stories.tsx | 43 +---- .../VerifiableCredentialCard.tsx | 59 +++--- .../VerifiableCredentialHooks.tsx | 2 +- .../VerifiableCredentialList.tsx | 13 +- .../src/popup/shared/utils/account-helpers.ts | 11 ++ .../popup/shared/utils/identity-helpers.ts | 13 ++ .../src/popup/shared/utils/wallet-proxy.ts | 2 +- .../browser-wallet/src/popup/shell/Routes.tsx | 20 +- .../src/popup/shell/i18n/locales/da.ts | 2 + .../src/popup/shell/i18n/locales/en.ts | 2 + .../browser-wallet/src/popup/store/utils.ts | 9 + .../src/popup/store/verifiable-credential.ts | 4 + .../src/popup/styles/_components.scss | 1 + .../src/shared/storage/access.ts | 3 + .../src/shared/storage/types.ts | 13 +- .../src/shared/storage/update.ts | 1 + .../test/transaction-helpers.test.ts | 2 +- yarn.lock | 98 ++++------ 49 files changed, 744 insertions(+), 166 deletions(-) create mode 100644 examples/add-example-Web3Id/README.md create mode 100644 examples/add-example-Web3Id/index.html create mode 100644 examples/add-example-Web3Id/package.json create mode 100644 packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.scss create mode 100644 packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx create mode 100644 packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts diff --git a/examples/add-example-Web3Id/README.md b/examples/add-example-Web3Id/README.md new file mode 100644 index 00000000..4df27829 --- /dev/null +++ b/examples/add-example-Web3Id/README.md @@ -0,0 +1,18 @@ +# Add example Web3Id web page + +The example project included in this repository, serves as a working example of how to interact with web3Id parts of the Concordium browser wallet. + +## Prerequisites + +- Browser wallet extension must be installed in google chrome. +- A web3Id-issuer running ( https://github.com/Concordium/concordium-web3id/tree/web3id-revision/services/web3id-issuer ) + +## Installing + +- Run `yarn` in package root. + q- Build concordium helpers by running `yarn build:api-helpers`. + +## Running the example + +- Run `yarn start` in a terminal +- Open URL logged in console (typically http://127.0.0.1:8080) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html new file mode 100644 index 00000000..b1c511e1 --- /dev/null +++ b/examples/add-example-Web3Id/index.html @@ -0,0 +1,110 @@ + + + + Example Web3Id + + + + + + + + +
+ +

Account address:

+
+
+ Web3IdIssuer Endpoint: +
+ Web3Id MetadataUrl: + +
+

Attribute values:

+ degreeType: +
+ degreeName: +
+ graduationDate: +
+
+ + + diff --git a/examples/add-example-Web3Id/package.json b/examples/add-example-Web3Id/package.json new file mode 100644 index 00000000..02c3eacd --- /dev/null +++ b/examples/add-example-Web3Id/package.json @@ -0,0 +1,13 @@ +{ + "name": "add-example-web3-id", + "packageManager": "yarn@3.2.0", + "devDependencies": { + "live-server": "^1.2.2" + }, + "scripts": { + "start": "live-server ./index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" + }, + "dependencies": { + "@concordium/web-sdk": "^6.1.0-alpha" + } +} diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index ac158834..7e3b7251 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^3.3.1", + "@concordium/web-sdk": "^6.1.0-alpha", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/examples/nft-minting/package.json b/examples/nft-minting/package.json index 9ab4613b..4d6ac214 100644 --- a/examples/nft-minting/package.json +++ b/examples/nft-minting/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^3.3.1", + "@concordium/web-sdk": "^6.1.0-alpha", "cors": "^2.8.5", "express": "^4.18.1", "express-fileupload": "^1.4.0", diff --git a/examples/piggybank/package.json b/examples/piggybank/package.json index f5c45209..7c001590 100644 --- a/examples/piggybank/package.json +++ b/examples/piggybank/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^3.3.1", + "@concordium/web-sdk": "^6.1.0-alpha", "react": "^18.1.0", "react-dom": "^18.1.0" }, diff --git a/examples/two-step-transfer/package.json b/examples/two-step-transfer/package.json index 3489c356..523a7cf3 100644 --- a/examples/two-step-transfer/package.json +++ b/examples/two-step-transfer/package.json @@ -8,6 +8,6 @@ "start": "live-server ../two-step-transfer/index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^3.3.1" + "@concordium/web-sdk": "^6.1.0-alpha" } } diff --git a/examples/voting/package.json b/examples/voting/package.json index e4c415df..5d463a53 100644 --- a/examples/voting/package.json +++ b/examples/voting/package.json @@ -4,7 +4,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "^2.0.0", - "@concordium/web-sdk": "^3.3.1", + "@concordium/web-sdk": "^6.1.0-alpha", "bootstrap": "^5.2.1", "cross-env": "^7.0.3", "moment": "^2.29.4", diff --git a/examples/wCCD/package.json b/examples/wCCD/package.json index 1c6431ae..597e4957 100644 --- a/examples/wCCD/package.json +++ b/examples/wCCD/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^3.3.1", + "@concordium/web-sdk": "^6.1.0-alpha", "@thi.ng/leb128": "^2.1.18", "@walletconnect/types": "^2.1.4", "mathjs": "^11.4.0", diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index 097c988a..ba9866f2 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -215,6 +215,8 @@ const provider = await detectConcordiumProvider(); await provider.addCIS2Tokens('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0'); ``` +// TODO Remove this (because the function is deprecated) + ### Prove ID statement It is possible to request a proof for a given ID statement on a specific account. The function takes 3 arguments. The statement to be proved, a challenge to ensure that the proof was not generated for a different context, and the account that should prove that statement. @@ -224,6 +226,8 @@ If the wallet is locked, or you have not connected with the wallet (or previousl The following exemplifies requesting a proof for a statement name myIdStatement (To see how to create a statement check out [our documentation](https://developer.concordium.software/en/mainnet/net/guides/create-proofs.html)) with a challenge of "12346789ABCD" id, for the account `2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk`. +// TODO Fix this example. + ```typescript const statement = myIdStatement; const challenge = '12346789ABCD'; @@ -231,6 +235,21 @@ const provider = await detectConcordiumProvider(); await provider.requestIdProof('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0'); ``` +### Add WebId Credentials + +To add a Web3IdCredential, use the `addWeb3IdCredential` endpoint. +The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet. + +// TODO Add example. + +### Request Verifiable Presentation + +It is possible to request a veriable presentation on a number statements about accounts and web3IdCredentials. The function takes 2 arguments. The statements to be proved and a challenge to ensure that the proof was not generated for a different context. + +To build a statement, the Web3StatementBuilder, from the web-sdk, can be used. (// TODO link to it) + +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects proving the statement, the `Promise` will reject. + ## Events ### Account changed diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index 4359eb42..d707a658 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -19,7 +19,7 @@ "url": "https://concordium.com" }, "dependencies": { - "@concordium/web-sdk": "^4.0.1" + "@concordium/web-sdk": "^6.1.0-alpha" }, "devDependencies": { "@babel/core": "^7.17.10", diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 1d939d80..e7635f65 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -9,8 +9,34 @@ import type { IdStatement, IdProofOutput, ConcordiumGRPCClient, + CredentialSubject, + HexString, } from '@concordium/web-sdk'; +export interface MetadataUrl { + url: string; + hash?: string; +} + +export interface APIVerifiableCredential { + $schema: string; + type: string[]; + issuer: string; + issuanceDate: string; + credentialSubject: Omit; + credentialSchema: { + id: string; + type: string; + }; +} + +export interface CredentialProof { + proofPurpose: 'assertionMethod'; + proofValue: HexString; + type: 'Ed25519Signature2020'; + verificationMethod: string; +} + export type SendTransactionPayload = | Exclude | Omit @@ -168,6 +194,22 @@ interface MainWalletApi { * @returns The id proof and the id of the credential used to prove it. */ requestIdProof(accountAddress: string, statement: IdStatement, challenge: string): Promise; + + /** + * Requests that a web3IdCredential is added to the wallet. + * Note that this will throw an error if the dApp is not allowlisted, locked, or if the user rejects adding the credential. + * @param credential the web3IdCredential that should be added to the wallet + * @param metadataUrl the url where the metadata, to display the credential, is located. + * @param createSignature a callback function, which takes the credentialId as input and must return the randomness used for the commitment of the values and signature on the commitments and credentialId. + * @returns the credentialId, containing the publicKey that will be associated with the credential. + */ + addWeb3IdCredential( + credential: APIVerifiableCredential, + metadataUrl: MetadataUrl, + createSignature: ( + credentialId: string + ) => Promise<{ randomness: Record; proof: CredentialProof }> + ): Promise; } export type WalletApi = MainWalletApi & EventListeners; diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index aab6019c..ca70f58e 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/common-sdk": "^7.0.1", + "@concordium/common-sdk": "^9.1.0-alpha", "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3", diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 850986c7..300b40a7 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -21,11 +21,14 @@ import { SchemaType, SignMessageObject, SmartContractParameters, + APIVerifiableCredential, + MetadataUrl, + CredentialProof, } from '@concordium/browser-wallet-api-helpers'; import EventEmitter from 'events'; import type { JsonRpcRequest } from '@concordium/common-sdk/lib/providers/provider'; import { IdProofOutput, IdStatement } from '@concordium/common-sdk/lib/idProofTypes'; -import ConcordiumGRPCClient from '@concordium/common-sdk/lib/GRPCClient'; +import { ConcordiumGRPCClient } from '@concordium/common-sdk/lib/GRPCClient'; import JSONBig from 'json-bigint'; import { stringify } from './util'; import { BWGRPCTransport } from './gRPC-transport'; @@ -243,6 +246,45 @@ class WalletApi extends EventEmitter implements IWalletApi { return res.result; } + + public async addWeb3IdCredential( + credential: APIVerifiableCredential, + metadataUrl: MetadataUrl, + createSignature: ( + credentialId: string + ) => Promise<{ randomness: Record; proof: CredentialProof }> + ): Promise { + const res = await this.messageHandler.sendMessage>( + MessageType.AddWeb3IdCredential, + { + credential, + metadataUrl, + } + ); + + if (!res.success) { + throw new Error(res.message); + } + + const credentialId = res.result; + + const { proof, randomness } = await createSignature(credentialId); + + const saveSignatureResult = await this.messageHandler.sendMessage>( + MessageType.AddWeb3IdCredentialGiveSignature, + { + credentialId, + proof, + randomness, + } + ); + + if (!saveSignatureResult.success) { + throw new Error(saveSignatureResult.message); + } + + return credentialId; + } } export const walletApi = new WalletApi(); diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 20745541..5f37e832 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -17,6 +17,8 @@ export enum MessageType { AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', ConnectAccounts = 'M_ConnectAccounts', + AddWeb3IdCredential = 'M_AddWeb3IdCredential', + AddWeb3IdCredentialGiveSignature = 'M_AddWeb3IdCredentialGiveSignature', } /** @@ -39,6 +41,7 @@ export enum InternalMessageType { IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', ConnectAccounts = 'I_ConnectAccounts', + AddWeb3IdCredential = 'I_AddWeb3IdCredential', } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 7a611142..61292107 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.0.6", + "version": "1.1.0.0", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", @@ -19,7 +19,7 @@ "dependencies": { "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^4.0.1", + "@concordium/web-sdk": "^6.1.0-alpha", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 28856309..34ee5357 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -14,16 +14,21 @@ import { storedAcceptedTerms, getGenesisHash, storedAllowlist, + storedVerifiableCredentials, + sessionVerifiableCredentials, + useIndexedStorage, } from '@shared/storage/access'; import JSONBig from 'json-bigint'; -import { ChromeStorageKey, NetworkConfiguration } from '@shared/storage/types'; +import { ChromeStorageKey, NetworkConfiguration, VerifiableCredential } from '@shared/storage/types'; import { buildURLwithSearchParameters } from '@shared/utils/url-helpers'; import { getTermsAndConditionsConfig } from '@shared/utils/network-helpers'; import { Buffer } from 'buffer/'; import { BackgroundSendTransactionPayload } from '@shared/utils/types'; import { parsePayload } from '@shared/utils/payload-helpers'; import { mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration'; +import { addToList, web3IdCredentialLock } from '@shared/storage/update'; +import { CredentialProof } from '@concordium/browser-wallet-api-helpers'; import bgMessageHandler from './message-handler'; import { forwardToPopup, @@ -379,6 +384,24 @@ const runIfNotAllowlisted: RunCondition> = async (_msg, sender) => { + const allowlist = await storedAllowlist.get(); + const locked = await isWalletLocked(); + + if (allowlist === undefined || locked) { + return { run: false, response: { success: false, message: NOT_WHITELISTED } }; + } + + if (sender.url !== undefined && allowlist[new URL(sender.url).origin]) { + return { run: true }; + } + + return { run: false, response: { success: false, message: NOT_WHITELISTED } }; +}; + /** * Run condition that runs the handler if the wallet is non-empty (an account exists), and no * account in the wallet is connected to the sender URL. @@ -482,6 +505,63 @@ const getSelectedChainHandler: ExtensionMessageHandler = (_msg, sender, respond) bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GetSelectedChain), getSelectedChainHandler); +const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; +const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; + +async function web3IdAddSignatureHandler(input: { + credentialId: string; + proof: CredentialProof; + randomness: Record; +}): Promise { + const { credentialId, proof, randomness } = input; + + const genesisHash = await getGenesisHash(); + const tempCredentials = await sessionVerifiableCredentials.get(genesisHash); + + if (!tempCredentials) { + throw new Error(NO_CREDENTIALS_FIT); + } + + const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId); + + if (!saved) { + throw new Error(NO_CREDENTIALS_FIT); + } + + // TODO verify signature/randomness + if (!proof?.proofValue) { + throw new Error(INVALID_CREDENTIAL_PROOF); + } + + const credential: VerifiableCredential = { + ...saved, + signature: proof.proofValue, + randomness, + }; + + addToList( + web3IdCredentialLock, + credential, + useIndexedStorage(storedVerifiableCredentials, () => Promise.resolve(genesisHash)) + ); + // TODO remove temp in session +} + +bgMessageHandler.handleMessage( + createMessageTypeFilter(MessageType.AddWeb3IdCredentialGiveSignature), + (input, sender, respond) => { + if (!sender.url || !isAllowlisted(sender.url)) { + respond({ success: false, message: 'not allowlisted' }); + } + + web3IdAddSignatureHandler(input.payload) + .then(() => respond({ success: true })) + .catch((error) => respond({ success: false, message: error.message })); + + return true; + } +); + function withPromptStart(): RunCondition> { return async () => { const isPromptOpen = await sessionOpenPrompt.get(); @@ -546,3 +626,12 @@ forwardToPopup( undefined, withPromptEnd ); +forwardToPopup( + MessageType.AddWeb3IdCredential, + InternalMessageType.AddWeb3IdCredential, + // TODO Check stuff, particular that the ids have the same network, as the currently chosen one. + runConditionComposer(runIfAllowlisted, withPromptStart()), + appendUrlToPayload, + undefined, + withPromptEnd +); diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index e1a9b2dc..1ade89e4 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -45,6 +45,9 @@ export const relativeRoutes = { connectionRequest: { path: 'connection-request', }, + addWeb3IdCredential: { + path: 'add-web3id-credential', + }, connectAccountsRequest: { path: 'connect-accounts-request', }, diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index d51d9af4..bc17e688 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -23,6 +23,9 @@ function Header() { if (pathname.startsWith(absoluteRoutes.prompt.addTokens.path)) { return t('header.addTokens'); } + if (pathname.startsWith(absoluteRoutes.prompt.addWeb3IdCredential.path)) { + return t('header.addWeb3IdCredential'); + } if (pathname.startsWith(absoluteRoutes.prompt.idProof.path)) { return t('header.idProof'); } diff --git a/packages/browser-wallet/src/popup/page-layouts/FullscreenPromptLayout/FullscreenPromptLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/FullscreenPromptLayout/FullscreenPromptLayout.tsx index efafb3ca..a8d21bf1 100644 --- a/packages/browser-wallet/src/popup/page-layouts/FullscreenPromptLayout/FullscreenPromptLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/FullscreenPromptLayout/FullscreenPromptLayout.tsx @@ -8,7 +8,7 @@ type OnCloseHandler = () => void; type Unsubscribe = () => void; type OnClose = (handler: OnCloseHandler) => Unsubscribe; -type WithClose = void>(action: F) => (...args: A) => void; +type WithClose = (action: (...args: A) => void) => (...args: A) => void; type FullscreenPromptContext = { onClose: OnClose; diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index 33d10c2b..d698b0c9 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -18,6 +18,8 @@ const t: typeof en = { passcode: 'Skift adgangskode', about: 'Om', }, + addWeb3IdCredential: 'Tilføj Web3Id Credential', + connectAccountsRequest: 'Forbind konti', addTokens: 'Tilføj tokens', idProof: 'Bevis for identitet', request: 'Anmodning om Signatur', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index 466707a9..a19f0889 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -16,6 +16,8 @@ const t = { passcode: 'Change passcode', about: 'About', }, + addWeb3IdCredential: 'Add Web3Id Credential', + connectAccountsRequest: 'Connect accounts', addTokens: 'Add tokens', idProof: 'Proof of identity', request: 'Signature Request', diff --git a/packages/browser-wallet/src/popup/pages/Account/Earn/Baking/Baking.tsx b/packages/browser-wallet/src/popup/pages/Account/Earn/Baking/Baking.tsx index 30fae3bb..484d4d46 100644 --- a/packages/browser-wallet/src/popup/pages/Account/Earn/Baking/Baking.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/Earn/Baking/Baking.tsx @@ -4,6 +4,7 @@ import { Link, Route, Routes, useNavigate } from 'react-router-dom'; import { AccountInfoBakerV1, AccountTransactionType, + ConsensusStatusV0, isBakerAccountV1, StakePendingChange, StakePendingChangeType, @@ -66,7 +67,13 @@ function DisplayPendingChange({ pendingChange }: DisplayPendingChangeProps) { const effectiveTime = useMemo(() => { if (pendingChange) { - const date = dateFromStakePendingChange(pendingChange, consensusStatus, tokenomicsInfo, chainParameters); + // TODO fix type assertion + const date = dateFromStakePendingChange( + pendingChange, + consensusStatus as ConsensusStatusV0, + tokenomicsInfo, + chainParameters + ); if (date) { return getFormattedDateString(date); } diff --git a/packages/browser-wallet/src/popup/pages/Account/Earn/Delegate/Delegate.tsx b/packages/browser-wallet/src/popup/pages/Account/Earn/Delegate/Delegate.tsx index 49173b22..5693c935 100644 --- a/packages/browser-wallet/src/popup/pages/Account/Earn/Delegate/Delegate.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/Earn/Delegate/Delegate.tsx @@ -4,6 +4,7 @@ import { Link, Route, Routes, useNavigate } from 'react-router-dom'; import { AccountInfoDelegator, AccountTransactionType, + ConsensusStatusV0, DelegationTargetType, isDelegatorAccount, StakePendingChangeType, @@ -63,7 +64,13 @@ function DisplayPendingChange({ pendingChange }: DisplayPendingChangeProps) { const effectiveTime = useMemo(() => { if (pendingChange) { - const date = dateFromStakePendingChange(pendingChange, consensusStatus, tokenomicsInfo, chainParameters); + // TODO fix type assertion + const date = dateFromStakePendingChange( + pendingChange, + consensusStatus as ConsensusStatusV0, + tokenomicsInfo, + chainParameters + ); if (date) { return getFormattedDateString(date); } diff --git a/packages/browser-wallet/src/popup/pages/Account/TransactionLog/TransactionElement.tsx b/packages/browser-wallet/src/popup/pages/Account/TransactionLog/TransactionElement.tsx index 962d5f78..034f9f0d 100644 --- a/packages/browser-wallet/src/popup/pages/Account/TransactionLog/TransactionElement.tsx +++ b/packages/browser-wallet/src/popup/pages/Account/TransactionLog/TransactionElement.tsx @@ -68,7 +68,7 @@ function isTransferTransaction(type: AccountTransactionType) { case AccountTransactionType.TransferToEncrypted: case AccountTransactionType.TransferToPublic: case AccountTransactionType.TransferWithSchedule: - case AccountTransactionType.TransferWithScheduleWithMemo: + case AccountTransactionType.TransferWithScheduleAndMemo: case AccountTransactionType.EncryptedAmountTransfer: case AccountTransactionType.EncryptedAmountTransferWithMemo: return true; @@ -147,7 +147,7 @@ function mapTypeToText(type: AccountTransactionType | RewardType | SpecialTransa return 'Transfer'; case AccountTransactionType.EncryptedAmountTransferWithMemo: return 'Shielded transfer'; - case AccountTransactionType.TransferWithScheduleWithMemo: + case AccountTransactionType.TransferWithScheduleAndMemo: return 'Scheduled transfer'; case AccountTransactionType.ConfigureBaker: return 'Configure baker'; diff --git a/packages/browser-wallet/src/popup/pages/AddAccount/ChooseIdentity.tsx b/packages/browser-wallet/src/popup/pages/AddAccount/ChooseIdentity.tsx index 9bf5e561..a503e565 100644 --- a/packages/browser-wallet/src/popup/pages/AddAccount/ChooseIdentity.tsx +++ b/packages/browser-wallet/src/popup/pages/AddAccount/ChooseIdentity.tsx @@ -5,8 +5,8 @@ import { useAtomValue, useSetAtom } from 'jotai'; import IdentityProviderIcon from '@popup/shared/IdentityProviderIcon'; import IdCard from '@popup/shared/IdCard'; -import { identitiesAtom, selectedIdentityIndexAtom, identityProvidersAtom } from '@popup/store/identity'; -import { Identity, CreationStatus, WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; +import { selectedIdentityIndexAtom, identityProvidersAtom } from '@popup/store/identity'; +import { Identity, WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; import { absoluteRoutes } from '@popup/constants/routes'; import Button from '@popup/shared/Button'; import { credentialsAtom } from '@popup/store/account'; @@ -14,6 +14,7 @@ import { getMaxAccountsForIdentity, isIdentityOfCredential } from '@shared/utils import { getNextEmptyCredNumber } from '@popup/shared/utils/account-helpers'; import Modal from '@popup/shared/Modal'; import i18n from '@popup/shell/i18n'; +import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; function validateValidForAccountCreation(identity: ConfirmedIdentity, creds: WalletCredential[]): string | undefined { const nextCredNumber = getNextEmptyCredNumber(creds); @@ -25,7 +26,7 @@ function validateValidForAccountCreation(identity: ConfirmedIdentity, creds: Wal export default function ChooseIdentity() { const { t } = useTranslation('addAccount'); - const identities = useAtomValue(identitiesAtom); + const identities = useConfirmedIdentities(); const setSelectedIdentityIndex = useSetAtom(selectedIdentityIndexAtom); const nav = useNavigate(); const providers = useAtomValue(identityProvidersAtom); @@ -37,7 +38,11 @@ export default function ChooseIdentity() { [providers] ); - if (!identities.length) { + if (identities.loading) { + return null; + } + + if (!identities.value.length) { return (

{t('noIdentities')}

@@ -58,10 +63,7 @@ export default function ChooseIdentity() { {modalMessage}

{t('chooseIdentity')}

- {identities.map((identity, i) => { - if (identity.status !== CreationStatus.Confirmed) { - return null; - } + {identities.value.map((identity, i) => { const credsOfCurrentIdentity = credentials.filter(isIdentityOfCredential(identity)); const reason = validateValidForAccountCreation(identity, credsOfCurrentIdentity); diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.scss b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.scss new file mode 100644 index 00000000..2fe3b283 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.scss @@ -0,0 +1,5 @@ +.add-web3Id-credential { + &__card { + margin: rem(10px) rem(6px); + } +} diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx new file mode 100644 index 00000000..d2fb87a0 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -0,0 +1,176 @@ +import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; +import { selectedAccountAtom } from '@popup/store/account'; +import { useAtom, useAtomValue } from 'jotai'; +import React, { useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; +import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; +import Button from '@popup/shared/Button'; +import { + sessionTemporaryVerifiableCredentialsAtom, + storedVerifiableCredentialMetadataAtom, + storedVerifiableCredentialsAtom, + storedVerifiableCredentialSchemasAtom, +} from '@popup/store/verifiable-credential'; +import { NetworkConfiguration, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; +import { useAsyncMemo } from 'wallet-common-helpers'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; +import { ContractAddress } from '@concordium/web-sdk'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { + fetchCredentialMetadata, + getCredentialRegistryContractAddress, +} from '@shared/utils/verifiable-credential-helpers'; +import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; +import { networkConfigurationAtom } from '@popup/store/settings'; +import { getNet } from '@shared/utils/network-helpers'; +import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; +import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; + +type Props = { + onAllow(key: string): void; + onReject(): void; +}; + +interface Location { + state: { + payload: { + url: string; + credential: APIVerifiableCredential; + metadataUrl: MetadataUrl; + }; + }; +} + +function createCredentialSubjectId(credentialHolderId: string, network: NetworkConfiguration) { + return `did:ccd:${getNet(network).toLowerCase()}:pkc:${credentialHolderId}`; +} + +function createCredentialId(credentialHolderId: string, issuer: ContractAddress, network: NetworkConfiguration) { + return `did:ccd:${getNet(network).toLowerCase()}:sci:${issuer.index}:${ + issuer.subindex + }/credentialEntry/${credentialHolderId}`; +} + +export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { + const { state } = useLocation() as Location; + const { t } = useTranslation('addWeb3IdCredential'); + const { onClose, withClose } = useContext(fullscreenPromptContext); + const selectedAccount = useAtomValue(selectedAccountAtom); + const [acceptButtonDisabled, setAcceptButtonDisabled] = useState(false); + const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom); + const storedWeb3IdCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const [verifiableCredentialMetadata, setVerifiableCredentialMetadata] = useAtom( + storedVerifiableCredentialMetadataAtom + ); + const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); + const wallet = useHdWallet(); + const network = useAtomValue(networkConfigurationAtom); + + const { credential, url, metadataUrl } = state.payload; + + useEffect(() => onClose(onReject), [onClose, onReject]); + + const controller = new AbortController(); + + const metadata = useAsyncMemo( + () => { + return fetchCredentialMetadata(metadataUrl, controller); + }, + undefined, + [metadataUrl] + ); + useEffect(() => () => controller.abort(), [metadataUrl]); + + // TODO use Jakobs? + const schema = useAsyncMemo( + async () => { + if (schemas.loading) { + return undefined; + } + const schemaUrl = credential.credentialSchema.id; + if (schemaUrl in schemas.value) { + return schemas.value[schemaUrl]; + } + // TODO check checksum + const response = await fetch(schemaUrl); + return JSON.parse(await response.text()); + }, + undefined, + [schemas.loading] + ); + + async function addCredential(credentialSchema: VerifiableCredentialSchema) { + if (!wallet) { + throw new Error('unreachable'); + } + + const schemaUrl = credential.credentialSchema.id; + if (!Object.keys(schemas.value).includes(schemaUrl)) { + const updatedSchemas = { ...schemas.value }; + updatedSchemas[schemaUrl] = credentialSchema; + setSchemas(updatedSchemas); + } + // Find the next unused index (// TODO verify on chain) + const index = [...(web3IdCredentials || []), ...(storedWeb3IdCredentials || [])].reduce( + (best, cred) => (cred.issuer === credential.issuer ? Math.max(cred.index + 1, best) : best), + 0 + ); + + const issuer = getCredentialRegistryContractAddress(credential.issuer); + + const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); + const credentialSubjectId = createCredentialSubjectId(credentialHolderId, network); + const credentialSubject = { ...credential.credentialSubject, id: credentialSubjectId }; + + const fullCredential = { + ...credential, + credentialSubject, + id: createCredentialId(credentialHolderId, issuer, network), + index, + }; + await setWeb3IdCredentials([...(web3IdCredentials || []), fullCredential]); + if (metadata) { + const newMetadata = { ...verifiableCredentialMetadata.value }; + newMetadata[metadataUrl.url] = metadata; + await setVerifiableCredentialMetadata(newMetadata); + } + return credentialSubjectId; + } + + if (!selectedAccount || !schema || !wallet || !metadata) { + // TODO: loading screen? + return null; + } + + const urlDisplay = displayUrl(url); + return ( + +
+
{t('description', { dapp: urlDisplay })}
+ +
+ + +
+
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts new file mode 100644 index 00000000..08608b75 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts @@ -0,0 +1,7 @@ +const t = { + description: '{{ dapp }} anmoder at du tilføjer denne web3Id Credential til din wallet.', + accept: 'Tilføj credential', + reject: 'Annuller', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts new file mode 100644 index 00000000..77dac170 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -0,0 +1,7 @@ +const t = { + description: '{{ dapp }} requests that you add this web3Id Credential to the wallet.', + accept: 'Add credential', + reject: 'Cancel', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 9782d695..8353c456 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -1,11 +1,14 @@ .verifiable-credential-list { overflow-y: auto; + + &__card { + margin: rem(16px); + } } .verifiable-credential { color: $color-white; border-radius: rem(16px); - margin: rem(16px); box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); position: relative; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index 926475f7..a069831b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/function-component-definition, react/destructuring-assignment */ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; @@ -29,6 +29,7 @@ const schema: VerifiableCredentialSchema = { title: 'Attributes', description: 'Credential attributes', type: 'object', + required: ['degreeType', 'degreeName', 'graduationDate'], properties: { degreeType: { title: 'Degree Hello', @@ -49,7 +50,7 @@ const schema: VerifiableCredentialSchema = { }, }, }, - required: ['id', 'degreeType', 'degreeName', 'graduationDate'], + required: ['id'], }, }, required: ['credentialSubject'], @@ -73,36 +74,12 @@ const metadata: VerifiableCredentialMetadata = { }, }; -const verifiableCredential = { - $schema: './JsonSchema2023-education-certificate.json', - id: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d', - type: ['VerifiableCredential', 'ConcordiumVerifiableCredential', 'UniversityDegreeCredential'], - issuer: 'did:ccd:NETWORK:sci:INDEX:SUBINDEX/issuer', - validFrom: '2010-01-01T00:00:00Z', - validUntil: '2030-01-01T00:00:00Z', - credentialSubject: { - id: 'did:ccd:pkc:76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d', - attributes: { - degreeType: 'BachelorDegree', - degreeName: 'Bachelor of Science and Arts', - graduationDate: '2010-06-01T00:00:00Z', - }, - }, - credentialSchema: { - id: 'https://example-university.com/certificates/JsonSchema2023-education-certificate.json', - type: 'JsonSchema2023', - }, - randomness: { - degreeType: '6490531ea308a2e661f62c4678e00bb87c9f602be7a053e910f8e44609bc5adb', - degreeName: '29b439aa58324b2be5c5a3ceb7ba23b48397ba1d1d9081869f56ff1c96a2b32f', - graduationDate: '2f5e0279c8ff6bcb004024dd4ba4f3e29d30ec91e3e4583855c2dae35ae83f8d', - }, - proof: { - type: 'Ed25519Signature2020', - verificationMethod: 'did:ccd:pkc:12345678ad3b62d3040f693722f94d61efa4fdd6ee797dd1e8aa5a651a0c4ac1', - proofPurpose: 'assertionMethod', - proofValue: - 'facdb03a1d054a55808875864abc85cc41d2c32290929bbb361a710b0fda5e7f333ac33abdb1b5f0ebb5662335c34410b8e96ca6730df7eb100f814f223d0b07', +const credentialSubject = { + id: 'did:ccd:pkc:76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d', + attributes: { + degreeType: 'BachelorDegree', + degreeName: 'Bachelor of Science and Arts', + graduationDate: '2010-06-01T00:00:00Z', }, }; @@ -110,7 +87,7 @@ export const Primary: ComponentStory = () => { return (
-
{attributeValue}
+
{attributeValue.toString()}
); } +type ClickableProps = ClassName & PropsWithChildren<{ onClick?: () => void; metadata: VerifiableCredentialMetadata }>; + /** * Wraps children components in a verifiable credential card that is clickable if onClick * is defined. */ -function ClickableVerifiableCredential({ - children, - metadata, - onClick, -}: PropsWithChildren<{ metadata: VerifiableCredentialMetadata; onClick?: () => void }>) { +function ClickableVerifiableCredential({ children, onClick, metadata, className }: ClickableProps) { if (onClick) { return (
{ @@ -70,7 +65,10 @@ function ClickableVerifiableCredential({ ); } return ( -
+
{children}
); @@ -85,8 +83,8 @@ function ClickableVerifiableCredential({ */ function applySchema( schema: VerifiableCredentialSchema -): (value: [string, string | number]) => { title: string; key: string; value: string | number } { - return (value: [string, string | number]) => { +): (value: [string, string | bigint]) => { title: string; key: string; value: string | bigint } { + return (value: [string, string | bigint]) => { const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); @@ -99,23 +97,26 @@ function applySchema( }; } -export function VerifiableCredentialCard({ - credential, - schema, - credentialStatus, - metadata, - onClick, -}: { - credential: VerifiableCredential; +interface CardProps extends ClassName { + credentialSubject: Omit; schema: VerifiableCredentialSchema; credentialStatus: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; onClick?: () => void; -}) { - const attributes = Object.entries(credential.credentialSubject.attributes).map(applySchema(schema)); +} + +export function VerifiableCredentialCard({ + credentialSubject, + schema, + onClick, + credentialStatus, + metadata, + className, +}: CardProps) { + const attributes = Object.entries(credentialSubject.attributes).map(applySchema(schema)); return ( - +
{metadata.title}
diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 8b8d75f2..0d773132 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,5 +1,5 @@ import { grpcClientAtom } from '@popup/store/settings'; -import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; import { CredentialQueryResponse, VerifiableCredentialMetadata, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 199cc5d1..d5d73d7c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -5,7 +5,7 @@ import { storedVerifiableCredentialsAtom, } from '@popup/store/verifiable-credential'; import { useAtom, useAtomValue } from 'jotai'; -import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; import { VerifiableCredentialMetadata, getChangesToCredentialMetadata, @@ -33,8 +33,10 @@ function NoVerifiableCredentials() { function VerifiableCredentialCardWithStatusFromChain({ credential, onClick, + className, }: { credential: VerifiableCredential; + className: string; onClick?: ( status: VerifiableCredentialStatus, schema: VerifiableCredentialSchema, @@ -52,8 +54,9 @@ function VerifiableCredentialCardWithStatusFromChain({ return ( { if (onClick) { onClick(status, schema, metadata); @@ -102,8 +105,9 @@ export default function VerifiableCredentialList() { if (selected) { return ( @@ -116,6 +120,7 @@ export default function VerifiableCredentialList() { return ( { + if (!credId) { + return undefined; + } + return credentials.find((cred) => cred.credId === credId); + }, [credId, JSON.stringify(credentials)]); +} + export function useSelectedCredential() { const selectedAccount = useAtomValue(selectedAccountAtom); return useCredential(selectedAccount); diff --git a/packages/browser-wallet/src/popup/shared/utils/identity-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/identity-helpers.ts index eb9c2f9a..173b6c7e 100644 --- a/packages/browser-wallet/src/popup/shared/utils/identity-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/identity-helpers.ts @@ -1,3 +1,7 @@ +import { identitiesAtomWithLoading } from '@popup/store/identity'; +import { mapAsyncWrapper } from '@popup/store/utils'; +import { ConfirmedIdentity, CreationStatus, Identity } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; import { TFunction, useTranslation } from 'react-i18next'; import { formatDate } from 'wallet-common-helpers'; import sharedTranslations from '../i18n/en'; @@ -77,3 +81,12 @@ export function useDisplayAttributeValue() { } }; } + +export function isConfirmedIdentity(identity: Identity): identity is ConfirmedIdentity { + return identity.status === CreationStatus.Confirmed; +} + +export function useConfirmedIdentities() { + const identities = useAtomValue(identitiesAtomWithLoading); + return mapAsyncWrapper(identities, (ids) => ids.filter(isConfirmedIdentity)); +} diff --git a/packages/browser-wallet/src/popup/shared/utils/wallet-proxy.ts b/packages/browser-wallet/src/popup/shared/utils/wallet-proxy.ts index 047c099c..e91c29ce 100644 --- a/packages/browser-wallet/src/popup/shared/utils/wallet-proxy.ts +++ b/packages/browser-wallet/src/popup/shared/utils/wallet-proxy.ts @@ -97,7 +97,7 @@ function mapTransactionKindStringToTransactionType( case TransactionKindString.EncryptedAmountTransferWithMemo: return AccountTransactionType.EncryptedAmountTransferWithMemo; case TransactionKindString.TransferWithScheduleAndMemo: - return AccountTransactionType.TransferWithScheduleWithMemo; + return AccountTransactionType.TransferWithScheduleAndMemo; case TransactionKindString.ConfigureBaker: return AccountTransactionType.ConfigureBaker; case TransactionKindString.ConfigureDelegation: diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index a2d09fbc..c7ddd0b2 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -35,6 +35,7 @@ import IdProofRequest from '@popup/pages/IdProofRequest'; import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; +import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; type PromptKey = keyof Omit; @@ -89,7 +90,10 @@ export default function Routes() { InternalMessageType.ConnectAccounts, 'connectAccountsRequest' ); - + const handleAddWeb3IdCredentialResponse = useMessagePrompt>( + InternalMessageType.AddWeb3IdCredential, + 'addWeb3IdCredential' + ); const handleSendTransactionResponse = useMessagePrompt>( InternalMessageType.SendTransaction, 'sendTransaction' @@ -147,6 +151,20 @@ export default function Routes() { /> } /> + handleAddWeb3IdCredentialResponse({ success: true, result: key })} + onReject={() => + handleAddWeb3IdCredentialResponse({ + success: false, + message: 'Adding credential was rejected', + }) + } + /> + } + /> > = { [ChromeStorageKey.VerifiableCredentialSchemas]: storedVerifiableCredentialSchemas, [ChromeStorageKey.VerifiableCredentialMetadata]: storedVerifiableCredentialMetadata, [ChromeStorageKey.Allowlist]: storedAllowlist, + [ChromeStorageKey.TemporaryVerifiableCredentials]: useIndexedStorage(sessionVerifiableCredentials, getGenesisHash), }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { @@ -139,3 +141,10 @@ export function atomWithChromeStorage(key: ChromeStorageKey, fallback: V, wit return derived; } + +export function mapAsyncWrapper( + wrapper: AsyncWrapper, + map: (v: Value) => Mapped +): AsyncWrapper { + return { loading: wrapper.loading, value: map(wrapper.value) }; +} diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts index 3fe0647e..f69c4e00 100644 --- a/packages/browser-wallet/src/popup/store/verifiable-credential.ts +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -16,3 +16,7 @@ export const storedVerifiableCredentialSchemasAtom = atomWithChromeStorage >(ChromeStorageKey.VerifiableCredentialMetadata, {}, true); + +export const sessionTemporaryVerifiableCredentialsAtom = atomWithChromeStorage< + Omit[] | undefined +>(ChromeStorageKey.TemporaryVerifiableCredentials, []); diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index d3845a5d..f073dead 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -41,6 +41,7 @@ @import '../pages/ExternalAddTokens/ExternalAddTokens'; @import '../pages/TermsAndConditions/TermsAndConditions'; @import '../pages/IdProofRequest'; +@import '../pages/AddWeb3IdCredential/AddWeb3IdCredential'; // Layouts @import '../page-layouts/MainLayout'; diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 206323fc..3b543f6b 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -187,3 +187,6 @@ export const sessionPendingTransactions = makeIndexedStorageAccessor( 'session', ChromeStorageKey.PendingTransactions ); +export const sessionVerifiableCredentials = makeIndexedStorageAccessor< + Omit[] +>('session', ChromeStorageKey.TemporaryVerifiableCredentials); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index bea60ee2..2ab063df 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -1,3 +1,4 @@ +import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; import type { CryptographicParameters, HexString, IdentityObjectV1, Network, Versioned } from '@concordium/web-sdk'; export enum ChromeStorageKey { @@ -28,6 +29,7 @@ export enum ChromeStorageKey { VerifiableCredentials = 'verifiableCredentials', VerifiableCredentialSchemas = 'verifiableCredentialSchemas', VerifiableCredentialMetadata = 'verifiableCredentialMetadata', + TemporaryVerifiableCredentials = 'tempVerifiableCredentials', Allowlist = 'allowlist', } @@ -272,15 +274,16 @@ interface CredentialSchema { export type CredentialSubject = { id: string; - attributes: Record; + attributes: Record; }; -export interface VerifiableCredential { +export interface VerifiableCredential extends APIVerifiableCredential { id: string; - type: string[]; - issuer: string; - credentialSubject: CredentialSubject; + signature: string; + randomness: Record; credentialSchema: CredentialSchema; + credentialSubject: CredentialSubject; + index: number; } interface CredentialSchemaProperty { diff --git a/packages/browser-wallet/src/shared/storage/update.ts b/packages/browser-wallet/src/shared/storage/update.ts index 0765c625..48ada0f4 100644 --- a/packages/browser-wallet/src/shared/storage/update.ts +++ b/packages/browser-wallet/src/shared/storage/update.ts @@ -1,6 +1,7 @@ import { StorageAccessor } from './access'; export const accountInfoCacheLock = 'concordium_account_info_cache_lock'; +export const web3IdCredentialLock = 'concordium_web3IdCredential_lock'; /** * Safely updates a record in storage by first acquiring a lock, reading the current value diff --git a/packages/browser-wallet/test/transaction-helpers.test.ts b/packages/browser-wallet/test/transaction-helpers.test.ts index a65b99e0..7d95f611 100644 --- a/packages/browser-wallet/test/transaction-helpers.test.ts +++ b/packages/browser-wallet/test/transaction-helpers.test.ts @@ -1,7 +1,7 @@ import { validateTransferAmount } from '../src/popup/shared/utils/transaction-helpers'; import en from '../src/popup/shared/i18n/en'; -test('validateTransfer amount respects decimals paraimeter', () => { +test('validateTransfer amount respects decimals parameter', () => { const invalidMessage = en.utils.ccdAmount.invalid; expect(validateTransferAmount('1000', undefined, 0)).toBe(undefined); expect(validateTransferAmount('1000.1', undefined, 0)).toContain(invalidMessage); diff --git a/yarn.lock b/yarn.lock index 9bf6430f..331231de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^4.0.1 + "@concordium/web-sdk": ^6.1.0-alpha typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1930,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^7.0.1 + "@concordium/common-sdk": ^9.1.0-alpha "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -1958,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^4.0.1 + "@concordium/web-sdk": ^6.1.0-alpha "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@protobuf-ts/runtime-rpc": ^2.8.2 @@ -2026,11 +2026,11 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:6.3.0": - version: 6.3.0 - resolution: "@concordium/common-sdk@npm:6.3.0" +"@concordium/common-sdk@npm:9.1.0-alpha, @concordium/common-sdk@npm:^9.1.0-alpha": + version: 9.1.0-alpha + resolution: "@concordium/common-sdk@npm:9.1.0-alpha" dependencies: - "@concordium/rust-bindings": 0.10.0 + "@concordium/rust-bindings": 1.2.0-alpha "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/runtime-rpc": ^2.8.2 @@ -2042,27 +2042,7 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: 6ce6e130bce8f3011b00a37e7429a5e19d4b21c21918e3ba8fb0438af841ec9a78d9de5eb9aa277d42154d6784dd182aed9198f92826b3b124fc59dbf5d30078 - languageName: node - linkType: hard - -"@concordium/common-sdk@npm:7.0.1, @concordium/common-sdk@npm:^7.0.1": - version: 7.0.1 - resolution: "@concordium/common-sdk@npm:7.0.1" - dependencies: - "@concordium/rust-bindings": 0.12.0 - "@grpc/grpc-js": ^1.3.4 - "@noble/ed25519": ^1.7.1 - "@protobuf-ts/runtime-rpc": ^2.8.2 - "@scure/bip39": ^1.1.0 - bs58check: ^2.1.2 - buffer: ^6.0.3 - cross-fetch: 3.1.5 - hash.js: ^1.1.7 - iso-3166-1: ^2.1.1 - json-bigint: ^1.0.0 - uuid: ^8.3.2 - checksum: 96da061891eb4edb4e7ac1df0a19b5fe4930dfcb97040433b74e445acd51a749be1c08d30afd7f12673af9f3954be5b046c43e9741a85c59f19f57cd07450d80 + checksum: 5e900f3c65de4d196c3c9c54b95b3e11604567aa480ce79b252327f0322fb1ca9fbb1e374deff96e4b2f890d809491b46ddec1ee0b5c473abd01479ddd5ce5f9 languageName: node linkType: hard @@ -2097,13 +2077,6 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:0.10.0": - version: 0.10.0 - resolution: "@concordium/rust-bindings@npm:0.10.0" - checksum: f09c754080fe7e026308342e0139e18de72e68ced8228d80c34d873c065a93e2774015da33ee3f970f33f97916526a43f54a410e42451137cffa3ccbe7e660a3 - languageName: node - linkType: hard - "@concordium/rust-bindings@npm:0.11.0": version: 0.11.0 resolution: "@concordium/rust-bindings@npm:0.11.0" @@ -2111,10 +2084,10 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:0.12.0": - version: 0.12.0 - resolution: "@concordium/rust-bindings@npm:0.12.0" - checksum: 7ecfc4d89d6771df7d64320ce81f93bfef3b3cd5a6e5b3d80379ea7c2cd54ecb828a9f54f350ead0771957c5c2fb369a06fa47ffc1391276ccac84ae78c3c574 +"@concordium/rust-bindings@npm:1.2.0-alpha": + version: 1.2.0-alpha + resolution: "@concordium/rust-bindings@npm:1.2.0-alpha" + checksum: 907df900b4be8696042cb392696aee9894db80b3cdb02cacee01303f882b278af15245bce8b9499a8086bc8e7d33864a307e133fce70f318a57d6e5580b741f0 languageName: node linkType: hard @@ -2129,31 +2102,17 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^3.3.1": - version: 3.3.1 - resolution: "@concordium/web-sdk@npm:3.3.1" - dependencies: - "@concordium/common-sdk": 6.3.0 - "@concordium/rust-bindings": 0.10.0 - "@grpc/grpc-js": ^1.3.4 - "@protobuf-ts/grpcweb-transport": ^2.8.2 - buffer: ^6.0.3 - process: ^0.11.10 - checksum: a7be99af72d605ecbb874a7098b5cea2b5bd42b51899bc9a02ef6c95c4fd184898a19ed6b4745a8ea92deea41b998e529a0e1e83ade7c290fd697e4f7dc618b6 - languageName: node - linkType: hard - -"@concordium/web-sdk@npm:^4.0.1": - version: 4.0.1 - resolution: "@concordium/web-sdk@npm:4.0.1" +"@concordium/web-sdk@npm:^6.1.0-alpha": + version: 6.1.0-alpha + resolution: "@concordium/web-sdk@npm:6.1.0-alpha" dependencies: - "@concordium/common-sdk": 7.0.1 - "@concordium/rust-bindings": 0.12.0 + "@concordium/common-sdk": 9.1.0-alpha + "@concordium/rust-bindings": 1.2.0-alpha "@grpc/grpc-js": ^1.3.4 "@protobuf-ts/grpcweb-transport": ^2.8.2 buffer: ^6.0.3 process: ^0.11.10 - checksum: d61bc6d6b520c7d19778dd6fbe4fd4878825561cb2da9961dc43e6ceaaa186a7f5eff8b672550ed5e5d7d90137dffb56900eb980a502efbed51527670f1d7dad + checksum: 6f87653c893a7e30db84a442a78e4ae91acf34cc7bea148fca72ab605636c72c027c28bf62a4350b54b74674e5a5dd8ec8d406d6592c20bed95432e2c38b9b91 languageName: node linkType: hard @@ -6768,7 +6727,7 @@ __metadata: resolution: "NFT-Minting@workspace:examples/nft-minting" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -6907,6 +6866,15 @@ __metadata: languageName: node linkType: hard +"add-example-web3-id@workspace:examples/add-example-Web3Id": + version: 0.0.0-use.local + resolution: "add-example-web3-id@workspace:examples/add-example-Web3Id" + dependencies: + "@concordium/web-sdk": ^6.1.0-alpha + live-server: ^1.2.2 + languageName: unknown + linkType: soft + "address@npm:^1.0.1": version: 1.2.0 resolution: "address@npm:1.2.0" @@ -10211,7 +10179,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -17591,7 +17559,7 @@ __metadata: resolution: "piggybank@workspace:examples/piggybank" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -21508,7 +21476,7 @@ __metadata: version: 0.0.0-use.local resolution: "two-step-transfer@workspace:examples/two-step-transfer" dependencies: - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha live-server: ^1.2.2 languageName: unknown linkType: soft @@ -22206,7 +22174,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -22331,7 +22299,7 @@ __metadata: resolution: "wccd@workspace:examples/wCCD" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^3.3.1 + "@concordium/web-sdk": ^6.1.0-alpha "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 From f109bb5d57dda67bd66966c0e89f6d9487c98002 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 9 Aug 2023 16:14:37 +0200 Subject: [PATCH 054/231] Fix dependency array bug --- .../VerifiableCredentialCard.stories.tsx | 1 + .../VerifiableCredentialHooks.tsx | 8 ++++---- .../VerifiableCredentialList.tsx | 20 +++++++++++++++---- .../src/popup/store/verifiable-credential.ts | 5 +++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index 926475f7..7951d511 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -47,6 +47,7 @@ const schema: VerifiableCredentialSchema = { description: 'Graduation date', }, }, + required: [], }, }, required: ['id', 'degreeType', 'degreeName', 'graduationDate'], diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 8b8d75f2..a3f6f59f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -115,7 +115,7 @@ export function useCredentialMetadata(credential: VerifiableCredential) { * @param dataFetcher the function that fetches updated data */ export function useFetchingEffect( - credentials: VerifiableCredential[] | undefined, + credentials: AsyncWrapper, storedData: AsyncWrapper>, setStoredData: (update: Record) => Promise, dataFetcher: ( @@ -131,8 +131,8 @@ export function useFetchingEffect( let isCancelled = false; const abortControllers: AbortController[] = []; - if (credentials && !storedData.loading) { - dataFetcher(credentials, client, abortControllers, storedData.value).then((result) => { + if (!credentials.loading && credentials.value.length !== 0 && !storedData.loading) { + dataFetcher(credentials.value, client, abortControllers, storedData.value).then((result) => { if (!isCancelled && result.updateReceived) { setStoredData(result.data); } @@ -143,5 +143,5 @@ export function useFetchingEffect( isCancelled = true; abortControllers.forEach((controller) => controller.abort()); }; - }, [storedData.loading, credentials, client]); + }, [storedData.loading, credentials.loading]); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 199cc5d1..32b655e8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -19,13 +19,22 @@ import { } from './VerifiableCredentialHooks'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; +/** + * Component to display while loading verifiable credentials from storage. + */ +function LoadingVerifiableCredentials() { + return
; +} + /** * Component to display when there are no verifiable credentials in the wallet. */ function NoVerifiableCredentials() { return ( -
-

You do not have any verifiable credentials in your wallet.

+
+
+

You do not have any verifiable credentials in your wallet.

+
); } @@ -95,7 +104,10 @@ export default function VerifiableCredentialList() { getChangesToCredentialSchemas ); - if (!verifiableCredentials || !verifiableCredentials.length) { + if (verifiableCredentials.loading) { + return ; + } + if (verifiableCredentials.value.length === 0) { return ; } @@ -112,7 +124,7 @@ export default function VerifiableCredentialList() { return (
- {verifiableCredentials.map((credential) => { + {verifiableCredentials.value.map((credential) => { return ( ( +export const storedVerifiableCredentialsAtom = atomWithChromeStorage( ChromeStorageKey.VerifiableCredentials, - [] + [], + true ); export const storedVerifiableCredentialSchemasAtom = atomWithChromeStorage>( From 89bc9749ad27bd45a1a73a43c13d710804217ade Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 9 Aug 2023 17:05:52 +0200 Subject: [PATCH 055/231] Verify new web3IdCredential signature --- .../browser-wallet/src/background/index.ts | 39 +++++++++++++--- .../utils/verifiable-credential-helpers.ts | 44 +++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 34ee5357..ef00f1a1 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -5,7 +5,12 @@ import { ExtensionMessageHandler, MessageStatusWrapper, } from '@concordium/browser-wallet-message-hub'; -import { deserializeTypeValue, HttpProvider } from '@concordium/web-sdk'; +import { + createConcordiumClient, + deserializeTypeValue, + HttpProvider, + verifyWeb3IdCredentialSignature, +} from '@concordium/web-sdk'; import { storedSelectedAccount, storedCurrentNetwork, @@ -26,9 +31,14 @@ import { getTermsAndConditionsConfig } from '@shared/utils/network-helpers'; import { Buffer } from 'buffer/'; import { BackgroundSendTransactionPayload } from '@shared/utils/types'; import { parsePayload } from '@shared/utils/payload-helpers'; -import { mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration'; +import { GRPCTIMEOUT, mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration'; import { addToList, web3IdCredentialLock } from '@shared/storage/update'; import { CredentialProof } from '@concordium/browser-wallet-api-helpers'; +import { + getCredentialRegistryContractAddress, + getCredentialRegistryIssuerKey, + getPublicKeyfromPublicKeyIdentifierDID, +} from '@shared/utils/verifiable-credential-helpers'; import bgMessageHandler from './message-handler'; import { forwardToPopup, @@ -515,7 +525,13 @@ async function web3IdAddSignatureHandler(input: { }): Promise { const { credentialId, proof, randomness } = input; - const genesisHash = await getGenesisHash(); + const network = await storedCurrentNetwork.get(); + + if (!network) { + throw new Error('No network chosen'); + } + + const { genesisHash } = network; const tempCredentials = await sessionVerifiableCredentials.get(genesisHash); if (!tempCredentials) { @@ -528,8 +544,21 @@ async function web3IdAddSignatureHandler(input: { throw new Error(NO_CREDENTIALS_FIT); } - // TODO verify signature/randomness - if (!proof?.proofValue) { + const client = createConcordiumClient(network.grpcUrl, network.grpcPort, { timeout: GRPCTIMEOUT }); + const issuerContract = getCredentialRegistryContractAddress(saved.issuer); + + if ( + !proof?.proofValue || + !verifyWeb3IdCredentialSignature({ + globalContext: await client.getCryptographicParameters(), + signature: proof.proofValue, + randomness, + values: saved.credentialSubject.attributes, + issuerContract, + issuerPublicKey: await getCredentialRegistryIssuerKey(client, issuerContract), + holder: getPublicKeyfromPublicKeyIdentifierDID(saved.credentialSubject.id), + }) + ) { throw new Error(INVALID_CREDENTIAL_PROOF); } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 1954af28..ddd2fd40 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -25,6 +25,18 @@ export function getCredentialHolderId(credentialId: string): string { return credentialHolderId; } +/** Takes a PublicKey Identifier DID string and returns the public key. + * @param did a DID string on the form: "did:ccd:NETWORK:pkc:PUBLICKEY" + * @returns the publicKey PUBLICKEY + */ +export function getPublicKeyfromPublicKeyIdentifierDID(did: string) { + const didParts = did.split(':'); + if (!(didParts.length === 5 || didParts.length === 4) || didParts[didParts.length - 2] !== 'pkc') { + throw new Error(`Given DID was not a PublicKey Identifier: ${did}`); + } + return didParts[didParts.length - 1]; +} + /** * Extracts the credential registry contract addres from a verifiable credential id (did). * @param credentialId the did for a credential @@ -635,3 +647,35 @@ export async function getChangesToCredentialSchemas( } return { data: updatedSchemasInStorage, updateReceived }; } + +/** + * Get the registry issuer public key from a credential registry CIS-4 contract. + * @param client the GRPC client for accessing a node + * @param contractAddress the address of a CIS-4 contract + * @returns the registry public key for the contract + */ +export async function getCredentialRegistryIssuerKey( + client: ConcordiumGRPCClient, + contractAddress: ContractAddress +): Promise { + const instanceInfo = await client.getInstanceInfo(contractAddress); + if (instanceInfo === undefined) { + throw new Error('Given contract address was not a created instance'); + } + + const result = await client.invokeContract({ + contract: contractAddress, + method: `${getContractName(instanceInfo)}.issuer`, + }); + + if (result.tag !== 'success') { + throw new Error(result.reason.tag); + } + + const { returnValue } = result; + if (returnValue === undefined) { + throw new Error(`Return value is missing from issuer public key result in CIS-4 contract: ${contractAddress}`); + } + + return returnValue; +} From 0b26029d37941dbe75e1682531d8d187d27bfaa4 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 9 Aug 2023 17:45:06 +0200 Subject: [PATCH 056/231] Bump SDK version --- examples/add-example-Web3Id/package.json | 2 +- examples/eSealing/package.json | 2 +- examples/nft-minting/package.json | 2 +- examples/piggybank/package.json | 2 +- examples/two-step-transfer/package.json | 2 +- examples/voting/package.json | 2 +- examples/wCCD/package.json | 2 +- .../browser-wallet-api-helpers/package.json | 2 +- packages/browser-wallet-api/package.json | 2 +- packages/browser-wallet/package.json | 2 +- yarn.lock | 50 +++++++++---------- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/examples/add-example-Web3Id/package.json b/examples/add-example-Web3Id/package.json index 02c3eacd..cdeeaaec 100644 --- a/examples/add-example-Web3Id/package.json +++ b/examples/add-example-Web3Id/package.json @@ -8,6 +8,6 @@ "start": "live-server ./index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha" + "@concordium/web-sdk": "^6.1.0-alpha.1" } } diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index 7e3b7251..dfa8d916 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/examples/nft-minting/package.json b/examples/nft-minting/package.json index 4d6ac214..b8c6afb8 100644 --- a/examples/nft-minting/package.json +++ b/examples/nft-minting/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "cors": "^2.8.5", "express": "^4.18.1", "express-fileupload": "^1.4.0", diff --git a/examples/piggybank/package.json b/examples/piggybank/package.json index 7c001590..8b6a10f7 100644 --- a/examples/piggybank/package.json +++ b/examples/piggybank/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "react": "^18.1.0", "react-dom": "^18.1.0" }, diff --git a/examples/two-step-transfer/package.json b/examples/two-step-transfer/package.json index 523a7cf3..ed4b56dd 100644 --- a/examples/two-step-transfer/package.json +++ b/examples/two-step-transfer/package.json @@ -8,6 +8,6 @@ "start": "live-server ../two-step-transfer/index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha" + "@concordium/web-sdk": "^6.1.0-alpha.1" } } diff --git a/examples/voting/package.json b/examples/voting/package.json index 5d463a53..7d33b767 100644 --- a/examples/voting/package.json +++ b/examples/voting/package.json @@ -4,7 +4,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "^2.0.0", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "bootstrap": "^5.2.1", "cross-env": "^7.0.3", "moment": "^2.29.4", diff --git a/examples/wCCD/package.json b/examples/wCCD/package.json index 597e4957..e81b826d 100644 --- a/examples/wCCD/package.json +++ b/examples/wCCD/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "@thi.ng/leb128": "^2.1.18", "@walletconnect/types": "^2.1.4", "mathjs": "^11.4.0", diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index d707a658..5a1db886 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -19,7 +19,7 @@ "url": "https://concordium.com" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha" + "@concordium/web-sdk": "^6.1.0-alpha.1" }, "devDependencies": { "@babel/core": "^7.17.10", diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index ca70f58e..e8a163eb 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/common-sdk": "^9.1.0-alpha", + "@concordium/common-sdk": "^9.1.0-alpha.1", "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3", diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 61292107..cbeae286 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -19,7 +19,7 @@ "dependencies": { "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha", + "@concordium/web-sdk": "^6.1.0-alpha.1", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/yarn.lock b/yarn.lock index 331231de..14b85022 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1930,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^9.1.0-alpha + "@concordium/common-sdk": ^9.1.0-alpha.1 "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -1958,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@protobuf-ts/runtime-rpc": ^2.8.2 @@ -2026,11 +2026,11 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:9.1.0-alpha, @concordium/common-sdk@npm:^9.1.0-alpha": - version: 9.1.0-alpha - resolution: "@concordium/common-sdk@npm:9.1.0-alpha" +"@concordium/common-sdk@npm:9.1.0-alpha.1, @concordium/common-sdk@npm:^9.1.0-alpha.1": + version: 9.1.0-alpha.1 + resolution: "@concordium/common-sdk@npm:9.1.0-alpha.1" dependencies: - "@concordium/rust-bindings": 1.2.0-alpha + "@concordium/rust-bindings": 1.2.0-alpha.1 "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/runtime-rpc": ^2.8.2 @@ -2042,7 +2042,7 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: 5e900f3c65de4d196c3c9c54b95b3e11604567aa480ce79b252327f0322fb1ca9fbb1e374deff96e4b2f890d809491b46ddec1ee0b5c473abd01479ddd5ce5f9 + checksum: dd87f2e6fae7f9590ed167ad5f739d82b25bebb29cca14f871db2a8c9c5957f9e7d19cdf9d062a7191417968a7544d0a8678674f3cb06949055088ee4d44e443 languageName: node linkType: hard @@ -2084,10 +2084,10 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:1.2.0-alpha": - version: 1.2.0-alpha - resolution: "@concordium/rust-bindings@npm:1.2.0-alpha" - checksum: 907df900b4be8696042cb392696aee9894db80b3cdb02cacee01303f882b278af15245bce8b9499a8086bc8e7d33864a307e133fce70f318a57d6e5580b741f0 +"@concordium/rust-bindings@npm:1.2.0-alpha.1": + version: 1.2.0-alpha.1 + resolution: "@concordium/rust-bindings@npm:1.2.0-alpha.1" + checksum: c5f17b05a19eef4edcdcd0ef8372615662f5a05f8de062dff87c3c95b35583c556a5633d0394c91ac8680cd9bb7b06134b745cb7e850e9dcc94715d8cf72ef5f languageName: node linkType: hard @@ -2102,17 +2102,17 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^6.1.0-alpha": - version: 6.1.0-alpha - resolution: "@concordium/web-sdk@npm:6.1.0-alpha" +"@concordium/web-sdk@npm:^6.1.0-alpha.1": + version: 6.1.0-alpha.1 + resolution: "@concordium/web-sdk@npm:6.1.0-alpha.1" dependencies: - "@concordium/common-sdk": 9.1.0-alpha - "@concordium/rust-bindings": 1.2.0-alpha + "@concordium/common-sdk": 9.1.0-alpha.1 + "@concordium/rust-bindings": 1.2.0-alpha.1 "@grpc/grpc-js": ^1.3.4 "@protobuf-ts/grpcweb-transport": ^2.8.2 buffer: ^6.0.3 process: ^0.11.10 - checksum: 6f87653c893a7e30db84a442a78e4ae91acf34cc7bea148fca72ab605636c72c027c28bf62a4350b54b74674e5a5dd8ec8d406d6592c20bed95432e2c38b9b91 + checksum: 97590bae0e27fcb1157407aac12f4f3fd05c89580ea2e75007b3803e1fa32c142279558610597b263de84e8e28a3f3ca478c69a173c35578b3d215a3cc16cc1e languageName: node linkType: hard @@ -6727,7 +6727,7 @@ __metadata: resolution: "NFT-Minting@workspace:examples/nft-minting" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -6870,7 +6870,7 @@ __metadata: version: 0.0.0-use.local resolution: "add-example-web3-id@workspace:examples/add-example-Web3Id" dependencies: - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -10179,7 +10179,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -17559,7 +17559,7 @@ __metadata: resolution: "piggybank@workspace:examples/piggybank" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -21476,7 +21476,7 @@ __metadata: version: 0.0.0-use.local resolution: "two-step-transfer@workspace:examples/two-step-transfer" dependencies: - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -22174,7 +22174,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -22299,7 +22299,7 @@ __metadata: resolution: "wccd@workspace:examples/wCCD" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0-alpha + "@concordium/web-sdk": ^6.1.0-alpha.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 From a9459a8c4f99eddf0970b03d2c8236aa18315605 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 10 Aug 2023 10:17:01 +0200 Subject: [PATCH 057/231] Fixes to addWeb3IdCredentials --- examples/add-example-Web3Id/index.html | 4 +- packages/browser-wallet-api-helpers/README.md | 20 +-- .../src/wallet-api-types.ts | 22 ++-- packages/browser-wallet-api/src/wallet-api.ts | 12 +- .../browser-wallet-message-hub/src/message.ts | 2 +- packages/browser-wallet/package.json | 2 +- .../browser-wallet/src/background/index.ts | 117 ++++-------------- .../browser-wallet/src/background/web3Id.ts | 78 ++++++++++++ .../AddWeb3IdCredential.tsx | 44 +++---- .../src/shared/storage/types.ts | 11 +- .../utils/verifiable-credential-helpers.ts | 22 ++++ 11 files changed, 181 insertions(+), 153 deletions(-) create mode 100644 packages/browser-wallet/src/background/web3Id.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index b1c511e1..0ff7c91e 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -37,7 +37,7 @@ 'ConcordiumVerifiableCredential', 'UniversityDegreeCredential', ], - issuer: 'did:ccd:testnet:sci:5463:0/issuer', + issuer: 'did:ccd:testnet:sci:' + issuerIndex.value + ':0/issuer', credentialSubject: { attributes: values, }, @@ -97,6 +97,8 @@

Account address:

value="https://raw.githubusercontent.com/Concordium/concordium-web3id/credential-metadata-example/examples/json-schemas/metadata/credential-metadata.json" />
+ issuer Index: +

Attribute values:

degreeType:
diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index ba9866f2..b3270f18 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -235,20 +235,20 @@ const provider = await detectConcordiumProvider(); await provider.requestIdProof('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0'); ``` -### Add WebId Credentials +### Add Web3Id Credentials To add a Web3IdCredential, use the `addWeb3IdCredential` endpoint. -The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet. +The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet. -// TODO Add example. +Note that the id fields of the credential are omitted, and added by the wallet itself, as they require the credentialHolderId. -### Request Verifiable Presentation - -It is possible to request a veriable presentation on a number statements about accounts and web3IdCredentials. The function takes 2 arguments. The statements to be proved and a challenge to ensure that the proof was not generated for a different context. - -To build a statement, the Web3StatementBuilder, from the web-sdk, can be used. (// TODO link to it) - -If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects proving the statement, the `Promise` will reject. +```typescript +provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { + const randomness = createRandomness(attributes); // Choose some randomness for the attribute commitments. + const proof = createSignature(credential, id, randomness); // Create a signature to prove that the commitments are created by the issuer. + return { proof, randomness }; +}); +``` ## Events diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index e7635f65..59374799 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -18,18 +18,26 @@ export interface MetadataUrl { hash?: string; } +interface CredentialSchema { + id: string; + type: string; +} + +/** + * The expected form of a Web3IdCredential, with the id fields omitted. + */ export interface APIVerifiableCredential { $schema: string; type: string[]; issuer: string; issuanceDate: string; credentialSubject: Omit; - credentialSchema: { - id: string; - type: string; - }; + credentialSchema: CredentialSchema; } +/** + * Expected format for the proof that the Web3IdCredential's attribute commitments are valid + */ export interface CredentialProof { proofPurpose: 'assertionMethod'; proofValue: HexString; @@ -200,14 +208,14 @@ interface MainWalletApi { * Note that this will throw an error if the dApp is not allowlisted, locked, or if the user rejects adding the credential. * @param credential the web3IdCredential that should be added to the wallet * @param metadataUrl the url where the metadata, to display the credential, is located. - * @param createSignature a callback function, which takes the credentialId as input and must return the randomness used for the commitment of the values and signature on the commitments and credentialId. - * @returns the credentialId, containing the publicKey that will be associated with the credential. + * @param createSignature a callback function, which takes a DID identifier for the credentialHolderId as input and must return the randomness used for the commitment of the values and signature on the commitments and credentialId. + * @returns the DID identifier for the credentialHolderId, i.e. the publicKey that will be associated with the credential. */ addWeb3IdCredential( credential: APIVerifiableCredential, metadataUrl: MetadataUrl, createSignature: ( - credentialId: string + credentialHolderIdDID: string ) => Promise<{ randomness: Record; proof: CredentialProof }> ): Promise; } diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 300b40a7..752d1bb3 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -251,7 +251,7 @@ class WalletApi extends EventEmitter implements IWalletApi { credential: APIVerifiableCredential, metadataUrl: MetadataUrl, createSignature: ( - credentialId: string + credentialHolderIdDID: string ) => Promise<{ randomness: Record; proof: CredentialProof }> ): Promise { const res = await this.messageHandler.sendMessage>( @@ -266,14 +266,14 @@ class WalletApi extends EventEmitter implements IWalletApi { throw new Error(res.message); } - const credentialId = res.result; + const credentialHolderIdDID = res.result; - const { proof, randomness } = await createSignature(credentialId); + const { proof, randomness } = await createSignature(credentialHolderIdDID); const saveSignatureResult = await this.messageHandler.sendMessage>( - MessageType.AddWeb3IdCredentialGiveSignature, + MessageType.AddWeb3IdCredentialFinish, { - credentialId, + credentialHolderIdDID, proof, randomness, } @@ -283,7 +283,7 @@ class WalletApi extends EventEmitter implements IWalletApi { throw new Error(saveSignatureResult.message); } - return credentialId; + return credentialHolderIdDID; } } diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 5f37e832..4204a9e7 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -18,7 +18,7 @@ export enum MessageType { IdProof = 'M_IdProof', ConnectAccounts = 'M_ConnectAccounts', AddWeb3IdCredential = 'M_AddWeb3IdCredential', - AddWeb3IdCredentialGiveSignature = 'M_AddWeb3IdCredentialGiveSignature', + AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish', } /** diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index cbeae286..5867c6ef 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0.0", + "version": "1.1.0", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index ef00f1a1..9507700f 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -1,45 +1,35 @@ import { createMessageTypeFilter, - InternalMessageType, - MessageType, ExtensionMessageHandler, + InternalMessageType, MessageStatusWrapper, + MessageType, } from '@concordium/browser-wallet-message-hub'; +import { deserializeTypeValue, HttpProvider } from '@concordium/web-sdk'; import { - createConcordiumClient, - deserializeTypeValue, - HttpProvider, - verifyWeb3IdCredentialSignature, -} from '@concordium/web-sdk'; -import { - storedSelectedAccount, - storedCurrentNetwork, - sessionPasscode, + getGenesisHash, sessionOpenPrompt, + sessionPasscode, storedAcceptedTerms, - getGenesisHash, storedAllowlist, - storedVerifiableCredentials, - sessionVerifiableCredentials, - useIndexedStorage, + storedCurrentNetwork, + storedSelectedAccount, } from '@shared/storage/access'; -import JSONBig from 'json-bigint'; -import { ChromeStorageKey, NetworkConfiguration, VerifiableCredential } from '@shared/storage/types'; -import { buildURLwithSearchParameters } from '@shared/utils/url-helpers'; +import { mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration'; +import { ChromeStorageKey, NetworkConfiguration } from '@shared/storage/types'; import { getTermsAndConditionsConfig } from '@shared/utils/network-helpers'; -import { Buffer } from 'buffer/'; -import { BackgroundSendTransactionPayload } from '@shared/utils/types'; import { parsePayload } from '@shared/utils/payload-helpers'; -import { GRPCTIMEOUT, mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration'; -import { addToList, web3IdCredentialLock } from '@shared/storage/update'; -import { CredentialProof } from '@concordium/browser-wallet-api-helpers'; -import { - getCredentialRegistryContractAddress, - getCredentialRegistryIssuerKey, - getPublicKeyfromPublicKeyIdentifierDID, -} from '@shared/utils/verifiable-credential-helpers'; +import { BackgroundSendTransactionPayload } from '@shared/utils/types'; +import { buildURLwithSearchParameters } from '@shared/utils/url-helpers'; +import { Buffer } from 'buffer/'; +import JSONBig from 'json-bigint'; +import { startMonitoringPendingStatus } from './confirmation'; +import { sendCredentialHandler } from './credential-deployment'; +import { createIdProofHandler, runIfValidProof } from './id-proof'; +import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; import bgMessageHandler from './message-handler'; +import { setupRecoveryHandler, startRecovery } from './recovery'; import { forwardToPopup, HandleMessage, @@ -49,11 +39,7 @@ import { setPopupSize, testPopupOpen, } from './window-management'; -import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; -import { startMonitoringPendingStatus } from './confirmation'; -import { sendCredentialHandler } from './credential-deployment'; -import { startRecovery, setupRecoveryHandler } from './recovery'; -import { createIdProofHandler, runIfValidProof } from './id-proof'; +import { web3IdAddCredentialFinishHandler } from './web3Id'; const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; @@ -515,75 +501,14 @@ const getSelectedChainHandler: ExtensionMessageHandler = (_msg, sender, respond) bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GetSelectedChain), getSelectedChainHandler); -const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; -const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; - -async function web3IdAddSignatureHandler(input: { - credentialId: string; - proof: CredentialProof; - randomness: Record; -}): Promise { - const { credentialId, proof, randomness } = input; - - const network = await storedCurrentNetwork.get(); - - if (!network) { - throw new Error('No network chosen'); - } - - const { genesisHash } = network; - const tempCredentials = await sessionVerifiableCredentials.get(genesisHash); - - if (!tempCredentials) { - throw new Error(NO_CREDENTIALS_FIT); - } - - const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId); - - if (!saved) { - throw new Error(NO_CREDENTIALS_FIT); - } - - const client = createConcordiumClient(network.grpcUrl, network.grpcPort, { timeout: GRPCTIMEOUT }); - const issuerContract = getCredentialRegistryContractAddress(saved.issuer); - - if ( - !proof?.proofValue || - !verifyWeb3IdCredentialSignature({ - globalContext: await client.getCryptographicParameters(), - signature: proof.proofValue, - randomness, - values: saved.credentialSubject.attributes, - issuerContract, - issuerPublicKey: await getCredentialRegistryIssuerKey(client, issuerContract), - holder: getPublicKeyfromPublicKeyIdentifierDID(saved.credentialSubject.id), - }) - ) { - throw new Error(INVALID_CREDENTIAL_PROOF); - } - - const credential: VerifiableCredential = { - ...saved, - signature: proof.proofValue, - randomness, - }; - - addToList( - web3IdCredentialLock, - credential, - useIndexedStorage(storedVerifiableCredentials, () => Promise.resolve(genesisHash)) - ); - // TODO remove temp in session -} - bgMessageHandler.handleMessage( - createMessageTypeFilter(MessageType.AddWeb3IdCredentialGiveSignature), + createMessageTypeFilter(MessageType.AddWeb3IdCredentialFinish), (input, sender, respond) => { if (!sender.url || !isAllowlisted(sender.url)) { respond({ success: false, message: 'not allowlisted' }); } - web3IdAddSignatureHandler(input.payload) + web3IdAddCredentialFinishHandler(input.payload) .then(() => respond({ success: true })) .catch((error) => respond({ success: false, message: error.message })); diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts new file mode 100644 index 00000000..40a10424 --- /dev/null +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -0,0 +1,78 @@ +import { createConcordiumClient, verifyWeb3IdCredentialSignature } from '@concordium/web-sdk'; +import { + sessionVerifiableCredentials, + storedCurrentNetwork, + storedVerifiableCredentials, + useIndexedStorage, +} from '@shared/storage/access'; + +import { CredentialProof } from '@concordium/browser-wallet-api-helpers'; +import { GRPCTIMEOUT } from '@shared/constants/networkConfiguration'; +import { VerifiableCredential } from '@shared/storage/types'; +import { addToList, web3IdCredentialLock } from '@shared/storage/update'; +import { + getCredentialRegistryContractAddress, + getCredentialRegistryIssuerKey, + getPublicKeyfromPublicKeyIdentifierDID, +} from '@shared/utils/verifiable-credential-helpers'; + +const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; +const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; + +export async function web3IdAddCredentialFinishHandler(input: { + credentialId: string; + proof: CredentialProof; + randomness: Record; +}): Promise { + const { credentialId, proof, randomness } = input; + + const network = await storedCurrentNetwork.get(); + + if (!network) { + throw new Error('No network chosen'); + } + + const { genesisHash } = network; + const tempCredentials = await sessionVerifiableCredentials.get(genesisHash); + + if (!tempCredentials) { + throw new Error(NO_CREDENTIALS_FIT); + } + + const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId); + + if (!saved) { + throw new Error(NO_CREDENTIALS_FIT); + } + + const client = createConcordiumClient(network.grpcUrl, network.grpcPort, { timeout: GRPCTIMEOUT }); + const issuerContract = getCredentialRegistryContractAddress(saved.issuer); + + if ( + !proof?.proofValue || + !verifyWeb3IdCredentialSignature({ + globalContext: await client.getCryptographicParameters(), + signature: proof.proofValue, + randomness, + values: saved.credentialSubject.attributes, + issuerContract, + issuerPublicKey: await getCredentialRegistryIssuerKey(client, issuerContract), + holder: getPublicKeyfromPublicKeyIdentifierDID(saved.credentialSubject.id), + }) + ) { + throw new Error(INVALID_CREDENTIAL_PROOF); + } + + const credential: VerifiableCredential = { + ...saved, + signature: proof.proofValue, + randomness, + }; + + addToList( + web3IdCredentialLock, + credential, + useIndexedStorage(storedVerifiableCredentials, () => Promise.resolve(genesisHash)) + ); + // TODO remove temp in session +} diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index d2fb87a0..e6320258 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -12,18 +12,19 @@ import { storedVerifiableCredentialsAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; -import { NetworkConfiguration, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; +import { VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; import { useAsyncMemo } from 'wallet-common-helpers'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; -import { ContractAddress } from '@concordium/web-sdk'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import { + createCredentialId, + createPublicKeyIdentifier, fetchCredentialMetadata, + fetchCredentialSchema, getCredentialRegistryContractAddress, } from '@shared/utils/verifiable-credential-helpers'; import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; import { networkConfigurationAtom } from '@popup/store/settings'; -import { getNet } from '@shared/utils/network-helpers'; import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; @@ -42,16 +43,6 @@ interface Location { }; } -function createCredentialSubjectId(credentialHolderId: string, network: NetworkConfiguration) { - return `did:ccd:${getNet(network).toLowerCase()}:pkc:${credentialHolderId}`; -} - -function createCredentialId(credentialHolderId: string, issuer: ContractAddress, network: NetworkConfiguration) { - return `did:ccd:${getNet(network).toLowerCase()}:sci:${issuer.index}:${ - issuer.subindex - }/credentialEntry/${credentialHolderId}`; -} - export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const { state } = useLocation() as Location; const { t } = useTranslation('addWeb3IdCredential'); @@ -74,15 +65,19 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const controller = new AbortController(); const metadata = useAsyncMemo( - () => { + async () => { + if (verifiableCredentialMetadata.loading) { + return undefined; + } + if (metadataUrl.url in verifiableCredentialMetadata.value) { + // TODO check hash? + return verifiableCredentialMetadata.value[metadataUrl.url]; + } return fetchCredentialMetadata(metadataUrl, controller); }, undefined, - [metadataUrl] + [verifiableCredentialMetadata.loading] ); - useEffect(() => () => controller.abort(), [metadataUrl]); - - // TODO use Jakobs? const schema = useAsyncMemo( async () => { if (schemas.loading) { @@ -90,19 +85,19 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } const schemaUrl = credential.credentialSchema.id; if (schemaUrl in schemas.value) { + // TODO check hash? return schemas.value[schemaUrl]; } - // TODO check checksum - const response = await fetch(schemaUrl); - return JSON.parse(await response.text()); + return fetchCredentialSchema(metadataUrl, controller); }, undefined, [schemas.loading] ); + useEffect(() => () => controller.abort(), [metadataUrl]); async function addCredential(credentialSchema: VerifiableCredentialSchema) { if (!wallet) { - throw new Error('unreachable'); + throw new Error('Wallet is unexpectedly missing'); } const schemaUrl = credential.credentialSchema.id; @@ -111,7 +106,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { updatedSchemas[schemaUrl] = credentialSchema; setSchemas(updatedSchemas); } - // Find the next unused index (// TODO verify on chain) + // Find the next unused index + // TODO verify index is unused on chain? const index = [...(web3IdCredentials || []), ...(storedWeb3IdCredentials || [])].reduce( (best, cred) => (cred.issuer === credential.issuer ? Math.max(cred.index + 1, best) : best), 0 @@ -120,7 +116,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const issuer = getCredentialRegistryContractAddress(credential.issuer); const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); - const credentialSubjectId = createCredentialSubjectId(credentialHolderId, network); + const credentialSubjectId = createPublicKeyIdentifier(credentialHolderId, network); const credentialSubject = { ...credential.credentialSubject, id: credentialSubjectId }; const fullCredential = { diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 2ab063df..54b22ee7 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -267,22 +267,19 @@ export enum VerifiableCredentialStatus { NotActivated, } -interface CredentialSchema { - id: string; - type: string; -} - export type CredentialSubject = { id: string; attributes: Record; }; export interface VerifiableCredential extends APIVerifiableCredential { + // With ID + credentialSubject: CredentialSubject; id: string; + // Secrets signature: string; randomness: Record; - credentialSchema: CredentialSchema; - credentialSubject: CredentialSubject; + /** index used to derive keys for credential */ index: number; } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index ddd2fd40..6e482691 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,6 +1,7 @@ import { ConcordiumGRPCClient, ContractAddress, sha256 } from '@concordium/web-sdk'; import { MetadataUrl, + NetworkConfiguration, VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus, @@ -8,6 +9,7 @@ import { import { Buffer } from 'buffer/'; import jsonschema from 'jsonschema'; import { getContractName } from './contract-helpers'; +import { getNet } from './network-helpers'; /** * Extracts the credential holder id from a verifiable credential id (did). @@ -679,3 +681,23 @@ export async function getCredentialRegistryIssuerKey( return returnValue; } + +/** + * Create a publicKey DID identitifer for the given key. + */ +export function createPublicKeyIdentifier(publicKey: string, network: NetworkConfiguration): string { + return `did:ccd:${getNet(network).toLowerCase()}:pkc:${publicKey}`; +} + +/** + * Create a DID identitifer for the given web3Id credential. + */ +export function createCredentialId( + credentialHolderId: string, + issuer: ContractAddress, + network: NetworkConfiguration +): string { + return `did:ccd:${getNet(network).toLowerCase()}:sci:${issuer.index}:${ + issuer.subindex + }/credentialEntry/${credentialHolderId}`; +} From 08fb48859b3c2bdbd9f5c6aba073bf109d7cd9e5 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 10 Aug 2023 12:53:09 +0200 Subject: [PATCH 058/231] Fix blank screen when schema is not already saved --- .../pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 8 +++----- .../src/shared/utils/verifiable-credential-helpers.ts | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index e6320258..7eb1ea84 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -1,5 +1,4 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { selectedAccountAtom } from '@popup/store/account'; import { useAtom, useAtomValue } from 'jotai'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -47,7 +46,6 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const { state } = useLocation() as Location; const { t } = useTranslation('addWeb3IdCredential'); const { onClose, withClose } = useContext(fullscreenPromptContext); - const selectedAccount = useAtomValue(selectedAccountAtom); const [acceptButtonDisabled, setAcceptButtonDisabled] = useState(false); const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom); const storedWeb3IdCredentials = useAtomValue(storedVerifiableCredentialsAtom); @@ -88,12 +86,12 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { // TODO check hash? return schemas.value[schemaUrl]; } - return fetchCredentialSchema(metadataUrl, controller); + return fetchCredentialSchema({ url: schemaUrl }, controller); }, undefined, [schemas.loading] ); - useEffect(() => () => controller.abort(), [metadataUrl]); + useEffect(() => () => controller.abort(), []); async function addCredential(credentialSchema: VerifiableCredentialSchema) { if (!wallet) { @@ -134,7 +132,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return credentialSubjectId; } - if (!selectedAccount || !schema || !wallet || !metadata) { + if (!schema || !wallet || !metadata) { // TODO: loading screen? return null; } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 6e482691..3779693a 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -483,10 +483,10 @@ async function fetchDataFromUrl( * Retrieves a credential schema from the specified URL. */ export async function fetchCredentialSchema( - metadata: MetadataUrl, + url: MetadataUrl, abortController: AbortController ): Promise { - return fetchDataFromUrl(metadata, abortController, verifiableCredentialSchemaSchema); + return fetchDataFromUrl(url, abortController, verifiableCredentialSchemaSchema); } /** From 08b580aebc730e5b14d3be4352722a90cd5e4819 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 10 Aug 2023 13:17:08 +0200 Subject: [PATCH 059/231] Fix blocking error when adding web3Id credentials --- packages/browser-wallet/src/background/web3Id.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 40a10424..ae2166bb 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -20,11 +20,11 @@ const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; export async function web3IdAddCredentialFinishHandler(input: { - credentialId: string; + credentialHolderIdDID: string; proof: CredentialProof; randomness: Record; }): Promise { - const { credentialId, proof, randomness } = input; + const { credentialHolderIdDID, proof, randomness } = input; const network = await storedCurrentNetwork.get(); @@ -39,7 +39,7 @@ export async function web3IdAddCredentialFinishHandler(input: { throw new Error(NO_CREDENTIALS_FIT); } - const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId); + const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialHolderIdDID); if (!saved) { throw new Error(NO_CREDENTIALS_FIT); From b16f0fcee5baab8bddc3dd3cfb513ef05b15810d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Hjort?= <87635671+shjortConcordium@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:44:41 +0200 Subject: [PATCH 060/231] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakob Ørhøj <57264157+orhoj@users.noreply.github.com> --- examples/add-example-Web3Id/README.md | 4 ++-- packages/browser-wallet-api-helpers/README.md | 2 +- .../pages/VerifiableCredential/VerifiableCredentialList.tsx | 2 +- packages/browser-wallet/src/shared/storage/update.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/add-example-Web3Id/README.md b/examples/add-example-Web3Id/README.md index 4df27829..8aec899c 100644 --- a/examples/add-example-Web3Id/README.md +++ b/examples/add-example-Web3Id/README.md @@ -5,12 +5,12 @@ The example project included in this repository, serves as a working example of ## Prerequisites - Browser wallet extension must be installed in google chrome. -- A web3Id-issuer running ( https://github.com/Concordium/concordium-web3id/tree/web3id-revision/services/web3id-issuer ) +- A web3Id-issuer running (https://github.com/Concordium/concordium-web3id/tree/main/services/web3id-issuer) ## Installing - Run `yarn` in package root. - q- Build concordium helpers by running `yarn build:api-helpers`. +- Build concordium helpers by running `yarn build:api-helpers`. ## Running the example diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index b3270f18..c69b1d61 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -238,7 +238,7 @@ await provider.requestIdProof('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfE ### Add Web3Id Credentials To add a Web3IdCredential, use the `addWeb3IdCredential` endpoint. -The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet. +The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does not return a valid signature, the credential is not added to the wallet. Note that the id fields of the credential are omitted, and added by the wallet itself, as they require the credentialHolderId. diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index d5d73d7c..8020994f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -107,7 +107,7 @@ export default function VerifiableCredentialList() { diff --git a/packages/browser-wallet/src/shared/storage/update.ts b/packages/browser-wallet/src/shared/storage/update.ts index 48ada0f4..76215359 100644 --- a/packages/browser-wallet/src/shared/storage/update.ts +++ b/packages/browser-wallet/src/shared/storage/update.ts @@ -1,7 +1,7 @@ import { StorageAccessor } from './access'; export const accountInfoCacheLock = 'concordium_account_info_cache_lock'; -export const web3IdCredentialLock = 'concordium_web3IdCredential_lock'; +export const web3IdCredentialLock = 'concordium_web3_id_credential_lock'; /** * Safely updates a record in storage by first acquiring a lock, reading the current value From 52040ed69c1051c256bb4aea37d5571fdf851a8e Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 11 Aug 2023 14:09:27 +0200 Subject: [PATCH 061/231] Address comments --- packages/browser-wallet-api-helpers/README.md | 26 ++-------- .../AddWeb3IdCredential.tsx | 46 ++++++++++------- .../pages/AddWeb3IdCredential/i18n/da.ts | 11 +++- .../pages/AddWeb3IdCredential/i18n/en.ts | 6 +++ .../verifiable-credential-helpers.test.ts | 50 +++++++++++++++++++ 5 files changed, 98 insertions(+), 41 deletions(-) diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index c69b1d61..c74af76f 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -215,37 +215,19 @@ const provider = await detectConcordiumProvider(); await provider.addCIS2Tokens('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0'); ``` -// TODO Remove this (because the function is deprecated) - -### Prove ID statement - -It is possible to request a proof for a given ID statement on a specific account. The function takes 3 arguments. The statement to be proved, a challenge to ensure that the proof was not generated for a different context, and the account that should prove that statement. -This method returns a `Promise` resolving with an object containing the proof and the credential id (field name: credential) of the credential used to prove the statement. - -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects proving the statement, the `Promise` will reject. - -The following exemplifies requesting a proof for a statement name myIdStatement (To see how to create a statement check out [our documentation](https://developer.concordium.software/en/mainnet/net/guides/create-proofs.html)) with a challenge of "12346789ABCD" id, for the account `2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk`. - -// TODO Fix this example. - -```typescript -const statement = myIdStatement; -const challenge = '12346789ABCD'; -const provider = await detectConcordiumProvider(); -await provider.requestIdProof('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0'); -``` - ### Add Web3Id Credentials To add a Web3IdCredential, use the `addWeb3IdCredential` endpoint. -The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does not return a valid signature, the credential is not added to the wallet. +The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and a proof (which can be a signature on the commitments and credentialHolderId) of the credential's validity. If the callback does not return a valid proof, the credential is not added to the wallet. Note that the id fields of the credential are omitted, and added by the wallet itself, as they require the credentialHolderId. +// TODO link to help for how to create proof and randomness + ```typescript provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { const randomness = createRandomness(attributes); // Choose some randomness for the attribute commitments. - const proof = createSignature(credential, id, randomness); // Create a signature to prove that the commitments are created by the issuer. + const proof = createCredentialProof(credential, id, randomness); // Create a proof to prove that the commitments are created by the issuer. return { proof, randomness }; }); ``` diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 7eb1ea84..d3052f25 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -56,6 +56,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const wallet = useHdWallet(); const network = useAtomValue(networkConfigurationAtom); + const [error, setError] = useState(); + const { credential, url, metadataUrl } = state.payload; useEffect(() => onClose(onReject), [onClose, onReject]); @@ -73,9 +75,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } return fetchCredentialMetadata(metadataUrl, controller); }, - undefined, + () => setError(t('error.metadata')), [verifiableCredentialMetadata.loading] ); + const schema = useAsyncMemo( async () => { if (schemas.loading) { @@ -88,7 +91,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } return fetchCredentialSchema({ url: schemaUrl }, controller); }, - undefined, + () => setError(t('error.schema')), [schemas.loading] ); useEffect(() => () => controller.abort(), []); @@ -132,33 +135,40 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return credentialSubjectId; } - if (!schema || !wallet || !metadata) { - // TODO: loading screen? - return null; - } - const urlDisplay = displayUrl(url); return (
-
{t('description', { dapp: urlDisplay })}
- + {error && ( +
+

{t('error.initial')}

+

{error}

+
+ )} + {!error && schema && metadata && ( + <> +
{t('description', { dapp: urlDisplay })}
+ + + )}
+
+ diff --git a/examples/two-step-transfer/index.html b/examples/two-step-transfer/index.html index 1363693b..51cf100e 100644 --- a/examples/two-step-transfer/index.html +++ b/examples/two-step-transfer/index.html @@ -214,6 +214,27 @@ }) .catch(alert); }); + document.getElementById('web3Proof').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute(0).addRange(3, '08000101', '20000101') + ) + .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => + b.revealAttribute(0).addMembership(2, ['2010-06-01T00:00:00Z']) + ) + .getStatements(); + const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; + provider + .requestVerifiablePresentation(challenge, statement) + .then((proof) => { + console.log(proof); + alert('Proof received! (check the console)'); + }) + .catch((error) => { + console.log(error); + alert(error); + }); + }); document.getElementById('addWCCD').addEventListener('click', () => { provider .addCIS2Tokens(currentAccountAddress, [''], 2059n, 0n) @@ -221,7 +242,6 @@ .catch(alert); }); } - setupPage(); @@ -244,6 +264,7 @@

Account address:

+
Message: diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index eeb811ab..acc51e52 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -9,6 +9,8 @@ import type { IdStatement, IdProofOutput, ConcordiumGRPCClient, + CredentialStatements, + VerifiablePresentation, CredentialSubject, HexString, } from '@concordium/web-sdk'; @@ -218,6 +220,11 @@ interface MainWalletApi { credentialHolderIdDID: string ) => Promise<{ randomness: Record; proof: CredentialProof }> ): Promise; + + /** + * @TODO write this + fix return type + */ + requestVerifiablePresentation(challenge: string, statements: CredentialStatements): Promise; } export type WalletApi = MainWalletApi & EventListeners; diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index a676e902..8dfe369d 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -9,6 +9,7 @@ import { AccountTransactionSignature, AccountTransactionType, DeployModulePayload, + HexString, InitContractPayload, SchemaVersion, UpdateContractPayload, @@ -28,6 +29,7 @@ import { import EventEmitter from 'events'; import type { JsonRpcRequest } from '@concordium/common-sdk/lib/providers/provider'; import { IdProofOutput, IdStatement } from '@concordium/common-sdk/lib/idProofTypes'; +import { CredentialStatements, VerifiablePresentation } from '@concordium/common-sdk/lib/web3ProofTypes'; import { ConcordiumGRPCClient } from '@concordium/common-sdk/lib/GRPCClient'; import JSONBig from 'json-bigint'; import { stringify } from './util'; @@ -285,6 +287,19 @@ class WalletApi extends EventEmitter implements IWalletApi { return credentialHolderIdDID; } + + public async requestVerifiablePresentation(challenge: HexString, statements: CredentialStatements) { + const res = await this.messageHandler.sendMessage>(MessageType.Web3Proof, { + statements, + challenge, + }); + + if (!res.success) { + throw new Error(res.message); + } + + return VerifiablePresentation.fromString(res.result); + } } export const walletApi = new WalletApi(); diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 4204a9e7..5e1c65a2 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -16,6 +16,7 @@ export enum MessageType { GrpcRequest = 'M_GrpcRequest', AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', + Web3Proof = 'M_Web3Proof', ConnectAccounts = 'M_ConnectAccounts', AddWeb3IdCredential = 'M_AddWeb3IdCredential', AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish', @@ -40,6 +41,8 @@ export enum InternalMessageType { AddTokens = 'I_AddTokens', IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', + Web3Proof = 'I_Web3Proof', + CreateWeb3Proof = 'I_CreateWeb3Proof', ConnectAccounts = 'I_ConnectAccounts', AddWeb3IdCredential = 'I_AddWeb3IdCredential', } diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index 1e925c6a..1fbb1071 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,10 +1,17 @@ -import { getIdProof, IdProofInput, verifyIdstatement } from '@concordium/web-sdk'; -import { BackgroundResponseStatus, IdProofBackgroundResponse } from '@shared/utils/types'; +import { + getIdProof, + getVerifiablePresentation, + IdProofInput, + IdProofOutput, + verifyIdstatement, + Web3IdProofInput, +} from '@concordium/web-sdk'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { isHex } from 'wallet-common-helpers'; import { RunCondition } from './window-management'; -async function createIdProof(input: IdProofInput): Promise { +async function createIdProof(input: IdProofInput): Promise> { const proof = getIdProof(input); return { status: BackgroundResponseStatus.Success, @@ -39,3 +46,39 @@ export const runIfValidProof: RunCondition> = as }; } }; + +async function createWeb3Proof(input: Web3IdProofInput): Promise> { + const proof = getVerifiablePresentation(input); + return { + status: BackgroundResponseStatus.Success, + proof: proof.toString(), + }; +} + +export const createWeb3ProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { + createWeb3Proof(msg.payload) + .then(respond) + .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); + return true; +}; + +/** + * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. + */ +export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { + if (!isHex(msg.payload.challenge)) { + return { + run: false, + response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, + }; + } + try { + // TODO web3: Check that the request is well-formed + return { run: true }; + } catch (e) { + return { + run: false, + response: { success: false, message: `Id statement is not well-formed: ${(e as Error).message}` }, + }; + } +}; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index fe07de8e..b38289ea 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -26,7 +26,7 @@ import { Buffer } from 'buffer/'; import JSONBig from 'json-bigint'; import { startMonitoringPendingStatus } from './confirmation'; import { sendCredentialHandler } from './credential-deployment'; -import { createIdProofHandler, runIfValidProof } from './id-proof'; +import { createIdProofHandler, createWeb3ProofHandler, runIfValidProof, runIfValidWeb3IdProof } from './id-proof'; import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; import bgMessageHandler from './message-handler'; import { setupRecoveryHandler, startRecovery } from './recovery'; @@ -40,7 +40,6 @@ import { testPopupOpen, } from './window-management'; import { runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler } from './web3Id'; - const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -253,6 +252,8 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); +bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3Proof), createWeb3ProofHandler); + const NOT_WHITELISTED = 'Site is not whitelisted'; /** @@ -588,3 +589,12 @@ forwardToPopup( undefined, withPromptEnd ); + +forwardToPopup( + MessageType.Web3Proof, + InternalMessageType.Web3Proof, + runConditionComposer(runIfAllowlisted, runIfValidWeb3IdProof, withPromptStart()), + appendUrlToPayload, + undefined, + withPromptEnd +); diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index 1ade89e4..e88ee4e3 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -69,6 +69,9 @@ export const relativeRoutes = { idProof: { path: 'id-proof', }, + web3IdProof: { + path: 'web3Id-proof', + }, }, setup: { path: '/setup', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index ea3bb7c4..53c164a8 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -28,7 +28,7 @@ import { isoToCountryName, } from './utils'; -type StatementLine = { +export type StatementLine = { attribute: string; value: string; isRequirementMet: boolean; @@ -36,7 +36,7 @@ type StatementLine = { type StatementLineProps = StatementLine; -function DisplayStatementLine({ attribute, value, isRequirementMet }: StatementLineProps) { +export function DisplayStatementLine({ attribute, value, isRequirementMet }: StatementLineProps) { return (
  • {attribute}:
    @@ -181,10 +181,6 @@ type BaseProps = ClassName & { onInvalid(): void; }; -type DisplayRevealStatementProps = BaseProps & { - statements: RevealStatement[]; -}; - export function DisplayRevealStatement({ dappName, statements, @@ -225,6 +221,10 @@ export function DisplayRevealStatement({ return ; } +type DisplayRevealStatementProps = BaseProps & { + statements: RevealStatement[]; +}; + type DisplaySecretStatementProps = BaseProps & { statement: SecretStatement; }; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts index 6c2dfa13..00855d8a 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts @@ -11,7 +11,7 @@ import { getPastDate, MAX_DATE, } from '@concordium/web-sdk'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import countryTranslations from 'i18n-iso-countries'; import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; @@ -206,14 +206,12 @@ export function useStatementValue(statement: SecretStatement): string { export const isoToCountryName = (locale: string) => (isoCode: string) => countryTranslations.getName(isoCode, locale); -export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { - const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); +export function getStatementDescription( + statement: SecretStatement, + t: TFunction<'idProofRequest', 'displayStatement.descriptions'>, + resolvedLanguage: string +) { const displayAttribute = useDisplayAttributeValue(); - const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; - - if (!hasAttribute) { - return t('missingAttribute', { identityName: identity.name }); - } if (statement.type === StatementTypes.AttributeInRange) { switch (statement.attributeTag) { @@ -228,7 +226,7 @@ export function useStatementDescription(statement: SecretStatement, identity: Co } } else { const text = getTextForSet(t, statement); - const getCountryName = isoToCountryName(i18n.resolvedLanguage); + const getCountryName = isoToCountryName(resolvedLanguage); switch (statement.attributeTag) { case 'countryOfResidence': @@ -255,6 +253,17 @@ export function useStatementDescription(statement: SecretStatement, identity: Co return undefined; } +export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; + + if (!hasAttribute) { + return t('missingAttribute', { identityName: identity.name }); + } + + return getStatementDescription(statement, t, i18n.resolvedLanguage); +} + export function canProveStatement(statement: SecretStatement, identity: ConfirmedIdentity) { const attribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag]; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx index aac0b7ce..d2b1228f 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/IdProofRequest.tsx @@ -12,7 +12,7 @@ import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings' import { addToastAtom } from '@popup/state'; import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; import { getGlobal, getNet } from '@shared/utils/network-helpers'; -import { BackgroundResponseStatus, IdProofBackgroundResponse } from '@shared/utils/types'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import PendingArrows from '@assets/svg/pending-arrows.svg'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; @@ -71,7 +71,7 @@ export default function IdProofRequest({ onReject, onSubmit }: Props) { const global = await getGlobal(client); - const idProofResult: IdProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( + const idProofResult: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( InternalMessageType.CreateIdProof, { identityIndex: credential.identityIndex, diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx new file mode 100644 index 00000000..a0689816 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -0,0 +1,186 @@ +import { + AccountCredentialStatement, + createAccountDID, + RevealStatementV2, + StatementTypes, + AttributeKey, + AttributeList, +} from '@concordium/web-sdk'; +import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; +import { + useConfirmedIdentities, + useDisplayAttributeValue, + useGetAttributeName, +} from '@popup/shared/utils/identity-helpers'; +import { credentialsAtom } from '@popup/store/account'; +import { WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; +import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import CredentialSelector from './CredentialSelector'; +import { DisplayCredentialStatementProps, getViableAccountCredentialsForStatement, SecretStatementV2 } from './utils'; +import { + getStatementDescription, + isoToCountryName, + SecretStatement, + useStatementHeader, + useStatementName, + useStatementValue, +} from '../IdProofRequest/DisplayStatement/utils'; + +type DisplaySecretStatementV2Props = ClassName & { + identity?: ConfirmedIdentity; + dappName: string; + statement: SecretStatementV2; +}; + +export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + + if (!identity) { + // TODO Should we write something here? + return ''; + } + const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; + if (!hasAttribute) { + return t('missingAttribute', { identityName: identity.name }); + } + + return getStatementDescription(statement, t, i18n.resolvedLanguage); +} + +export function DisplaySecretStatementV2({ dappName, statement, identity, className }: DisplaySecretStatementV2Props) { + const v1Statement: SecretStatement = statement as SecretStatement; + const header = useStatementHeader(v1Statement); + const value = useStatementValue(v1Statement); + const description = useStatementDescription(v1Statement, identity); + const attribute = useStatementName(v1Statement); + + const lines: StatementLine[] = [ + { + attribute, + value, + isRequirementMet: identity !== undefined, + }, + ]; + + return ( + + ); +} + +type DisplayRevealStatementV2Props = ClassName & { + identity?: ConfirmedIdentity; + dappName: string; + statements: RevealStatementV2[]; +}; + +export function DisplayRevealStatementV2({ dappName, statements, identity, className }: DisplayRevealStatementV2Props) { + const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const getAttributeName = useGetAttributeName(); + const displayAttribute = useDisplayAttributeValue(); + const header = t('headers.reveal'); + const attributes = identity + ? identity.idObject.value.attributeList.chosenAttributes + : ({} as AttributeList['chosenAttributes']); + + const lines: StatementLine[] = statements.map((s) => { + const stringTag = s.attributeTag as AttributeKey; + const raw = attributes[stringTag]; + let value = displayAttribute(stringTag, raw ?? ''); + + if (value && ['countryOfResidence', 'nationality', 'idDocIssuer'].includes(stringTag)) { + value = isoToCountryName(i18n.resolvedLanguage)(value); + } + + return { + attribute: getAttributeName(stringTag), + value: value ?? 'Unavailable', + isRequirementMet: raw !== undefined, + }; + }); + + return ; +} + +export default function AccountStatement({ + credentialStatement, + dappName, + setChosenId, + net, +}: DisplayCredentialStatementProps) { + const reveals = credentialStatement.statement.filter( + (s) => s.type === StatementTypes.RevealAttribute + ) as RevealStatementV2[]; + const secrets = credentialStatement.statement.filter( + (s) => s.type !== StatementTypes.RevealAttribute + ) as SecretStatementV2[]; + + const identities = useConfirmedIdentities(); + const credentials = useAtomValue(credentialsAtom); + + const validCredentials = useMemo(() => { + if (identities.loading) { + return []; + } + return getViableAccountCredentialsForStatement(credentialStatement, identities.value, credentials); + }, [credentialStatement.idQualifier.issuers]); + + const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + + const onChange = useCallback((credential: WalletCredential) => { + setChosenCredential(credential); + setChosenId(createAccountDID(net, credential.credId)); + }, []); + + // Initially set chosenId + useEffect(() => { + if (chosenCredential) { + setChosenId(createAccountDID(net, chosenCredential.credId)); + } + }, []); + + const identity = useMemo(() => { + if (!chosenCredential) { + return undefined; + } + return identities.value.find((id) => isIdentityOfCredential(id)(chosenCredential)); + }, [chosenCredential?.credId]); + + return ( +
    + displaySplitAddress(option.address)} + onChange={onChange} + /> + {reveals.length !== 0 && ( + + )} + {secrets.map((s, i) => ( + + ))} +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx new file mode 100644 index 00000000..c053dca5 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; + +interface Props { + options: T[]; + onChange: (x: T) => void; + displayOption: (x: T) => string; +} + +/** + * Component to select a credential, either account credential or web3Id credential. + */ +export default function CredentialSelector({ options, onChange, displayOption }: Props) { + const [chosenIndex, setChosenIndex] = useState(0); + + if (options.length === 0) { + // TODO Translate + return
    No candidate available
    ; + } + + return ( + + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx new file mode 100644 index 00000000..3a658180 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx @@ -0,0 +1,22 @@ +import { + CredentialStatement, + isAccountCredentialStatement, + isVerifiableCredentialStatement, +} from '@concordium/web-sdk'; +import React from 'react'; +import { DisplayCredentialStatementProps } from './utils'; +import DisplayWeb3Statement from './VerifiableCredentialStatement'; +import DisplayAccountStatement from './AccountStatement'; + +export function DisplayCredentialStatement({ + credentialStatement, + ...params +}: DisplayCredentialStatementProps) { + if (isAccountCredentialStatement(credentialStatement)) { + return ; + } + if (isVerifiableCredentialStatement(credentialStatement)) { + return ; + } + return null; +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx new file mode 100644 index 00000000..4d693998 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -0,0 +1,176 @@ +import { + AtomicStatementV2, + RevealStatementV2, + StatementTypes, + VerifiableCredentialStatement, +} from '@concordium/web-sdk'; +import { + storedVerifiableCredentialsAtom, + storedVerifiableCredentialSchemasAtom, +} from '@popup/store/verifiable-credential'; +import { VerifiableCredential, CredentialSubject } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import CredentialSelector from './CredentialSelector'; +import { + createWeb3IdDIDFromCredential, + DisplayCredentialStatementProps, + getVerifiableCredentialPublicKeyfromSubjectDID, + getViableWeb3IdCredentialsForStatement, + SecretStatementV2, +} from './utils'; + +type DisplayWeb3StatementProps = ClassName & { + statements: Statement; + dappName: string; + credential: CredentialSubject; +}; + +type AttributeInfo = { + name: string; + value: string | bigint; +}; + +function extractAttributesFromCredentialSubjectForSingleStatement( + { attributeTag }: AtomicStatementV2, + credentialSubject: CredentialSubject +): AttributeInfo { + return { name: attributeTag, value: credentialSubject[attributeTag] }; +} + +function extractAttributesFromCredentialSubject( + statements: AtomicStatementV2[], + credentialSubject: CredentialSubject +): Record { + return statements.reduce>((acc, statement) => { + acc[statement.attributeTag] = extractAttributesFromCredentialSubjectForSingleStatement( + statement, + credentialSubject + ); + return acc; + }, {}); +} + +export function DisplayWeb3RevealStatement({ + statements, + dappName, + credential, + className, +}: DisplayWeb3StatementProps) { + const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const attributes = extractAttributesFromCredentialSubject(statements, credential); + const header = t('headers.reveal'); + + const lines: StatementLine[] = statements.map((s) => { + const { name, value } = attributes[s.attributeTag]; + + return { + attribute: name, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + }; + }); + + return ; +} + +export function DisplayWeb3SecretStatement({ + statements, + dappName, + credential, + className, +}: DisplayWeb3StatementProps) { + const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + const { name, value } = extractAttributesFromCredentialSubjectForSingleStatement(statements, credential); + const header = t('headers.reveal'); + + const lines: StatementLine[] = [ + { + attribute: name, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + }, + ]; + + return ; +} + +export default function DisplayWeb3Statement({ + credentialStatement, + dappName, + setChosenId, + net, +}: DisplayCredentialStatementProps) { + const reveals = credentialStatement.statement.filter( + (s) => s.type === StatementTypes.RevealAttribute + ) as RevealStatementV2[]; + const secrets = credentialStatement.statement.filter( + (s) => s.type !== StatementTypes.RevealAttribute + ) as SecretStatementV2[]; + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + + const validCredentials = useMemo(() => { + if (!verifiableCredentials) { + return []; + } + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials); + }, [credentialStatement.idQualifier.issuers]); + + const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + + const onChange = useCallback((credential: VerifiableCredential) => { + setChosenCredential(credential); + setChosenId(createWeb3IdDIDFromCredential(credential, net)); + }, []); + + // Initially set chosenId + useEffect(() => { + if (chosenCredential) { + setChosenId(createWeb3IdDIDFromCredential(chosenCredential, net)); + } + }, []); + + const schema = useMemo(() => { + if (!verifiableCredentialSchemas.loading && chosenCredential) { + const schemaId = chosenCredential.credentialSchema.id; + return verifiableCredentialSchemas.value[schemaId]; + } + return null; + }, [chosenCredential?.id, verifiableCredentials?.length]); + + if (!chosenCredential || !schema) { + return null; + } + + return ( +
    + getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} + onChange={onChange} + /> + {reveals.length !== 0 && ( + + )} + {secrets.map((s, i) => ( + + ))} +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss new file mode 100644 index 00000000..a9c62941 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -0,0 +1,19 @@ +.web3-id-proof-request { + &__statement-container { + display: flex; + flex-direction: column; + height: 100%; + } + + &__actions { + margin: auto rem(20px) 0; + } + + &__loading-icon { + max-height: rem(20px); + } + + &__credential-statement-container { + margin-bottom: rem(20px); + } +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx new file mode 100644 index 00000000..953137b2 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { useTranslation } from 'react-i18next'; +import { + CredentialStatements, + RequestStatement, + ConcordiumHdWallet, + isAccountCredentialStatement, + Web3IdProofInput, +} from '@concordium/web-sdk'; +import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; + +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; +import { credentialsAtom } from '@popup/store/account'; +import { addToastAtom } from '@popup/state'; +import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers'; +import { getGlobal, getNet } from '@shared/utils/network-helpers'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; +import PendingArrows from '@assets/svg/pending-arrows.svg'; +import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; +import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; +import Button from '@popup/shared/Button'; +import ButtonGroup from '@popup/shared/ButtonGroup'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { + storedVerifiableCredentialsAtom, + storedVerifiableCredentialSchemasAtom, +} from '@popup/store/verifiable-credential'; +import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; +import { DisplayCredentialStatement } from './DisplayStatement'; +import { getCommitmentInput } from './utils'; + +type Props = { + onSubmit(presentationString: string): void; + onReject(): void; +}; + +interface Location { + state: { + payload: { + challenge: string; + statements: CredentialStatements; + url: string; + }; + }; +} + +export default function Web3ProofRequest({ onReject, onSubmit }: Props) { + const { state } = useLocation() as Location; + const { statements, challenge, url } = state.payload; + const { onClose, withClose } = useContext(fullscreenPromptContext); + const { t } = useTranslation('idProofRequest'); + const network = useAtomValue(networkConfigurationAtom); + const client = useAtomValue(grpcClientAtom); + const addToast = useSetAtom(addToastAtom); + const recoveryPhrase = useDecryptedSeedPhrase((e) => addToast(e.message)); + const dappName = displayUrl(url); + const [creatingProof, setCreatingProof] = useState(false); + const net = getNet(network); + + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const identities = useConfirmedIdentities(); + const credentials = useAtomValue(credentialsAtom); + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + + const [ids, setIds] = useState(statements.map(() => '')); + + const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); + + const handleSubmit = useCallback(async () => { + if (!recoveryPhrase) { + throw new Error('Missing recovery phrase'); + } + if (!network) { + throw new Error('Network is not specified'); + } + if (!ids.every((x) => Boolean(x))) { + throw new Error('Network is not specified'); + } + + const global = await getGlobal(client); + const wallet = ConcordiumHdWallet.fromHex(recoveryPhrase, net); + + const type = ['ConcordiumVerifiableCredential', 'TestCredential', 'VerifiableCredential']; + + const parsedStatements: RequestStatement[] = statements.map((statement, index) => { + if (isAccountCredentialStatement(statement)) { + return { statement: statement.statement, id: ids[index] }; + } + return { statement: statement.statement, id: ids[index], type }; + }); + + const commitmentInputs = parsedStatements.map((statement) => + getCommitmentInput( + statement, + wallet, + identities.value, + credentials, + verifiableCredentials || [], + verifiableCredentialSchemas.value + ) + ); + + const request = { + challenge, + credentialStatements: parsedStatements, + }; + + const input: Web3IdProofInput = { + request, + commitmentInputs, + globalContext: global, + }; + + const result: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( + InternalMessageType.CreateWeb3Proof, + input + ); + + if (result.status !== BackgroundResponseStatus.Success) { + throw new Error(result.reason); + } + return result.proof; + }, [recoveryPhrase, network, ids]); + + useEffect(() => onClose(onReject), [onClose, onReject]); + + if (verifiableCredentialSchemas.loading || identities.loading) { + return null; + } + + return ( + +
    + {statements.map((s, index) => ( + + setIds((currentIds) => { + const newIds = [...currentIds]; + newIds[index] = newId; + return newIds; + }) + } + /> + ))} + + + + +
    +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts new file mode 100644 index 00000000..3898bfa2 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/index.ts @@ -0,0 +1 @@ +export { default } from './Web3ProofRequest'; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts new file mode 100644 index 00000000..e2d31037 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -0,0 +1,164 @@ +import { + RequestStatement, + canProveCredentialStatement, + ConcordiumHdWallet, + createWeb3CommitmentInputWithHdWallet, + createAccountCommitmentInputWithHdWallet, + VerifiableCredentialStatement, + AccountCredentialStatement, + Network, + AtomicStatementV2, + RevealStatementV2, + ContractAddress, + CommitmentInput, + VerifiableCredentialSchema, + createWeb3IdDID, +} from '@concordium/web-sdk'; +import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; +import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; +import { ClassName } from 'wallet-common-helpers'; + +export type SecretStatementV2 = Exclude; + +export interface DisplayCredentialStatementProps extends ClassName { + credentialStatement: Statement; + dappName: string; + setChosenId: (id: string) => void; + net: Network; +} + +/** Takes a Web3IdCredential issuer DID string and returns the contract address + * @param did a issuer DID string on the form: "did:ccd:testnet:sci:INDEX:SUBINDEX/issuer" + * @returns the contract address INDEX;SUBINDEX + */ +export function getContractAddressFromIssuerDID(did: string): ContractAddress { + const split = did.split(':'); + if (split.length !== 6 || split[3] !== 'sci') { + throw new Error('Given DID did not follow expected format'); + } + const index = BigInt(split[4]); + const subindex = BigInt(split[5].substring(0, split[5].indexOf('/'))); + return { index, subindex }; +} + +/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential + * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" + * @returns the public key KEY + */ +export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { + const split = did.split('/'); + if (split.length !== 3 || split[0].split(':')[3] !== 'sci') { + throw new Error(`Given DID did not follow expected format:${did}`); + } + return split[2]; +} + +/** Takes a AccountCredential subject DID string and returns the credential id of the account credential + * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" + * @returns the credId CREDID + */ +export function getCredentialIdFromSubjectDID(did: string) { + const split = did.split(':'); + if (split.length !== 5 || split[3] !== 'cred') { + throw new Error(`Given DID did not follow expected format: ${did}`); + } + return split[4]; +} + +/** + * Build the commitmentInputs required to create a presentation for the given statement. + */ +export function getCommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + identities: ConfirmedIdentity[], + credentials: WalletCredential[], + verifiableCredentials: VerifiableCredential[], + verifiableCredentialSchemas: Record +): CommitmentInput { + if (statement.type) { + const cred = verifiableCredentials?.find((c) => c.id === statement.id); + + if (!cred) { + throw new Error('IdQualifier not fulfilled'); + } + + const schemaIndex = cred.credentialSchema.id; + + return createWeb3CommitmentInputWithHdWallet( + wallet, + getContractAddressFromIssuerDID(cred.issuer), + cred.index, + cred.credentialSubject, + verifiableCredentialSchemas[schemaIndex], + cred.randomness, + cred.signature + ); + } + const credId = getCredentialIdFromSubjectDID(statement.id); + const credential = credentials.find((cred) => cred.credId === credId); + + if (!credential) { + throw new Error('IdQualifier not fulfilled'); + } + + const identity = (identities || []).find(isIdentityOfCredential); + + if (!identity || identity.status !== CreationStatus.Confirmed) { + throw new Error('No identity found for credential'); + } + + return createAccountCommitmentInputWithHdWallet( + statement.statement, + identity.providerIndex, + identity.idObject.value.attributeList, + wallet, + identity.providerIndex, + credential.credNumber + ); +} + +/** + * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. + * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. + */ +export function getViableAccountCredentialsForStatement( + credentialStatement: AccountCredentialStatement, + identities: ConfirmedIdentity[], + credentials: WalletCredential[] +): WalletCredential[] { + const allowedIssuers = credentialStatement.idQualifier.issuers; + return credentials?.filter((c) => { + if (allowedIssuers.includes(c.providerIndex)) { + const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); + if (identity && identity.status === CreationStatus.Confirmed) { + return canProveCredentialStatement(credentialStatement, identity.idObject.value.attributeList); + } + } + return false; + }); +} + +/** + * Given a credential statement for a verifiable credential, and a list of verifiable credentials, return the filtered list of verifiable credentials that satisfy the statement. + */ +export function getViableWeb3IdCredentialsForStatement( + credentialStatement: VerifiableCredentialStatement, + verifiableCredentials: VerifiableCredential[] +): VerifiableCredential[] { + const allowedContracts = credentialStatement.idQualifier.issuers; + return verifiableCredentials?.filter((vc) => + allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) + ); +} + +// TODO move to SDK? +export function createWeb3IdDIDFromCredential(credential: VerifiableCredential, net: Network) { + const contractAddress = getContractAddressFromIssuerDID(credential.issuer); + return createWeb3IdDID( + net, + getVerifiableCredentialPublicKeyfromSubjectDID(credential.id), + BigInt(contractAddress.index), + BigInt(contractAddress.subindex) + ); +} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index c7ddd0b2..5edf8bcc 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -33,6 +33,7 @@ import ChangePasscode from '@popup/pages/ChangePasscode/ChangePasscode'; import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; +import Web3ProofRequest from '@popup/pages/Web3ProofRequest'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; @@ -110,6 +111,11 @@ export default function Routes() { InternalMessageType.IdProof, 'idProof' ); + // We manually stringify the presentation + const handleWeb3IdProofResponse = useMessagePrompt>( + InternalMessageType.Web3Proof, + 'web3IdProof' + ); usePrompt(InternalMessageType.EndIdentityIssuance, 'endIdentityIssuance'); usePrompt(InternalMessageType.RecoveryFinished, 'recovery'); @@ -201,6 +207,19 @@ export default function Routes() { /> } /> + + handleWeb3IdProofResponse({ success: true, result: presentationString }) + } + onReject={() => + handleWeb3IdProofResponse({ success: false, message: 'Proof generation was rejected' }) + } + /> + } + /> } /> } /> diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index f073dead..517b0c7b 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -42,6 +42,7 @@ @import '../pages/TermsAndConditions/TermsAndConditions'; @import '../pages/IdProofRequest'; @import '../pages/AddWeb3IdCredential/AddWeb3IdCredential'; +@import '../pages/Web3ProofRequest/Web3ProofRequest'; // Layouts @import '../page-layouts/MainLayout'; diff --git a/packages/browser-wallet/src/shared/utils/proof-helpers.ts b/packages/browser-wallet/src/shared/utils/proof-helpers.ts new file mode 100644 index 00000000..349afb6c --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/proof-helpers.ts @@ -0,0 +1,19 @@ +import { AtomicStatement, StatementTypes } from '@concordium/web-sdk'; +import { ConfirmedIdentity } from '@shared/storage/types'; + +export function canProveStatement(statement: AtomicStatement, identity: ConfirmedIdentity) { + const attribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag]; + + switch (statement.type) { + case StatementTypes.AttributeInSet: + return statement.set.includes(attribute); + case StatementTypes.AttributeNotInSet: + return !statement.set.includes(attribute); + case StatementTypes.AttributeInRange: + return statement.upper > attribute && attribute >= statement.lower; + case StatementTypes.RevealAttribute: + return attribute !== undefined; + default: + throw new Error(`Statement type of ${statement.type} is not supported`); + } +} diff --git a/packages/browser-wallet/src/shared/utils/types.ts b/packages/browser-wallet/src/shared/utils/types.ts index 6d724be2..66627f05 100644 --- a/packages/browser-wallet/src/shared/utils/types.ts +++ b/packages/browser-wallet/src/shared/utils/types.ts @@ -3,7 +3,7 @@ import { SchemaWithContext, SmartContractParameters, } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; -import type { IdProofOutput, SchemaVersion, AccountTransactionType } from '@concordium/web-sdk'; +import type { SchemaVersion, AccountTransactionType } from '@concordium/web-sdk'; import { RefAttributes } from 'react'; /** * @description @@ -42,10 +42,10 @@ export type CredentialDeploymentBackgroundResponse = address: string; }; -export type IdProofBackgroundResponse = +export type ProofBackgroundResponse = | { status: BackgroundResponseStatus.Success; - proof: IdProofOutput; + proof: ProofOutput; } | { status: BackgroundResponseStatus.Error; diff --git a/packages/browser-wallet/test/Web3ProofRequest.test.ts b/packages/browser-wallet/test/Web3ProofRequest.test.ts new file mode 100644 index 00000000..670e367e --- /dev/null +++ b/packages/browser-wallet/test/Web3ProofRequest.test.ts @@ -0,0 +1,27 @@ +import { + getCredentialIdFromSubjectDID, + getContractAddressFromIssuerDID, + getVerifiableCredentialPublicKeyfromSubjectDID, +} from '../src/popup/pages/Web3ProofRequest/utils'; + +test('getContractAddressFromIssuerDID', () => { + const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); + expect(address.index).toBe(1337n); + expect(address.subindex).toBe(42n); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); From 5eec88017532c032a373dece665468bc00f1684c Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 11 Aug 2023 14:28:51 +0200 Subject: [PATCH 067/231] Incomplete fixes --- examples/add-example-Web3Id/index.html | 2 +- packages/browser-wallet-api/src/wallet-api.ts | 5 +- .../browser-wallet-message-hub/src/message.ts | 6 +- packages/browser-wallet/package.json | 2 +- .../browser-wallet/src/background/id-proof.ts | 38 ------------ .../browser-wallet/src/background/index.ts | 10 ++-- .../browser-wallet/src/background/web3Id.ts | 58 ++++++++++++++++++- .../src/popup/pages/IdProofRequest/i18n/da.ts | 1 + .../src/popup/pages/IdProofRequest/i18n/en.ts | 1 + .../VerifiableCredentialStatement.tsx | 52 +++++++++++++---- .../Web3ProofRequest/Web3ProofRequest.tsx | 15 +++-- .../popup/pages/Web3ProofRequest/i18n/en.ts | 34 +++++++++++ .../src/popup/pages/Web3ProofRequest/utils.ts | 10 ++-- .../browser-wallet/src/popup/shell/Routes.tsx | 2 +- .../src/popup/shell/i18n/locales/en.ts | 2 + 15 files changed, 161 insertions(+), 77 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index 654af9e3..f8e07d3c 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -22,7 +22,7 @@ provider.on('chainChanged', (chain) => alert(chain)); document.getElementById('web3Proof').addEventListener('click', () => { const statement = new concordiumSDK.Web3StatementBuilder() - .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => b .revealAttribute('graduationDate') .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 8dfe369d..ca04ecd5 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -289,8 +289,9 @@ class WalletApi extends EventEmitter implements IWalletApi { } public async requestVerifiablePresentation(challenge: HexString, statements: CredentialStatements) { - const res = await this.messageHandler.sendMessage>(MessageType.Web3Proof, { - statements, + const res = await this.messageHandler.sendMessage>(MessageType.Web3IdProof, { + // We have to stringify the statements because they can contain bigints + statements: stringify(statements), challenge, }); diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 5e1c65a2..6e2dcfea 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -16,7 +16,7 @@ export enum MessageType { GrpcRequest = 'M_GrpcRequest', AddTokens = 'M_AddTokens', IdProof = 'M_IdProof', - Web3Proof = 'M_Web3Proof', + Web3IdProof = 'M_Web3Proof', ConnectAccounts = 'M_ConnectAccounts', AddWeb3IdCredential = 'M_AddWeb3IdCredential', AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish', @@ -41,8 +41,8 @@ export enum InternalMessageType { AddTokens = 'I_AddTokens', IdProof = 'I_IdProof', CreateIdProof = 'I_CreateIdProof', - Web3Proof = 'I_Web3Proof', - CreateWeb3Proof = 'I_CreateWeb3Proof', + Web3IdProof = 'I_Web3IdProof', + CreateWeb3IdProof = 'I_CreateWeb3IdProof', ConnectAccounts = 'I_ConnectAccounts', AddWeb3IdCredential = 'I_AddWeb3IdCredential', } diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 5867c6ef..e8977fbc 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0", + "version": "1.1.0.2", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index 1fbb1071..d8b9a446 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,10 +1,8 @@ import { getIdProof, - getVerifiablePresentation, IdProofInput, IdProofOutput, verifyIdstatement, - Web3IdProofInput, } from '@concordium/web-sdk'; import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; @@ -46,39 +44,3 @@ export const runIfValidProof: RunCondition> = as }; } }; - -async function createWeb3Proof(input: Web3IdProofInput): Promise> { - const proof = getVerifiablePresentation(input); - return { - status: BackgroundResponseStatus.Success, - proof: proof.toString(), - }; -} - -export const createWeb3ProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { - createWeb3Proof(msg.payload) - .then(respond) - .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); - return true; -}; - -/** - * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. - */ -export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { - if (!isHex(msg.payload.challenge)) { - return { - run: false, - response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, - }; - } - try { - // TODO web3: Check that the request is well-formed - return { run: true }; - } catch (e) { - return { - run: false, - response: { success: false, message: `Id statement is not well-formed: ${(e as Error).message}` }, - }; - } -}; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index b38289ea..60421e4f 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -26,7 +26,7 @@ import { Buffer } from 'buffer/'; import JSONBig from 'json-bigint'; import { startMonitoringPendingStatus } from './confirmation'; import { sendCredentialHandler } from './credential-deployment'; -import { createIdProofHandler, createWeb3ProofHandler, runIfValidProof, runIfValidWeb3IdProof } from './id-proof'; +import { createIdProofHandler, runIfValidProof } from './id-proof'; import { addIdpListeners, identityIssuanceHandler } from './identity-issuance'; import bgMessageHandler from './message-handler'; import { setupRecoveryHandler, startRecovery } from './recovery'; @@ -39,7 +39,7 @@ import { setPopupSize, testPopupOpen, } from './window-management'; -import { runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler } from './web3Id'; +import {runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof } from './web3Id'; const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -252,7 +252,7 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); -bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3Proof), createWeb3ProofHandler); +bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), createWeb3IdProofHandler); const NOT_WHITELISTED = 'Site is not whitelisted'; @@ -591,8 +591,8 @@ forwardToPopup( ); forwardToPopup( - MessageType.Web3Proof, - InternalMessageType.Web3Proof, + MessageType.Web3IdProof, + InternalMessageType.Web3IdProof, runConditionComposer(runIfAllowlisted, runIfValidWeb3IdProof, withPromptStart()), appendUrlToPayload, undefined, diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 4e7e43ee..ce3f6af9 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -1,4 +1,9 @@ -import { createConcordiumClient, verifyWeb3IdCredentialSignature } from '@concordium/web-sdk'; +import { + CredentialStatements, + getVerifiablePresentation, + Web3IdProofInput, + createConcordiumClient, verifyWeb3IdCredentialSignature, isHex, verifyAtomicStatements +} from '@concordium/web-sdk'; import { sessionVerifiableCredentials, storedCurrentNetwork, @@ -16,8 +21,10 @@ import { getDIDNetwork, getPublicKeyfromPublicKeyIdentifierDID, } from '@shared/utils/verifiable-credential-helpers'; -import { MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; +import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { getNet } from '@shared/utils/network-helpers'; +import { parse } from '@shared/utils/payload-helpers'; +import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { RunCondition } from './window-management'; const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; @@ -132,3 +139,50 @@ export const runIfValidWeb3IdCredentialRequest: RunCondition> { + const proof = getVerifiablePresentation(input); + return { + status: BackgroundResponseStatus.Success, + proof: proof.toString(), + }; +} + +export const createWeb3IdProofHandler: ExtensionMessageHandler = (msg, _sender, respond) => { + createWeb3Proof(msg.payload) + .then(respond) + .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); + return true; +}; + +/** + * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. + */ +export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { + if (!isHex(msg.payload.challenge)) { + return { + run: false, + response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, + }; + } + try { + const statements: CredentialStatements = parse(msg.payload.statements); + // TODO Fix second parameter when SDK is updated + // If a statement does not verify, an error is thrown. + statements.every((credStatement) => verifyAtomicStatements(credStatement.statement, undefined as any)); + + const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); + if (!noEmptyQualifier) { + return { + run: false, + response: { success: false, message: `Statements must have at least 1 possible identity provider / issuer` }, + }; + } + return { run: true }; + } catch (e) { + return { + run: false, + response: { success: false, message: `Statement is not well-formed: ${(e as Error).message}` }, + }; + } +}; diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts index 2da97ad1..33752ddd 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts @@ -26,6 +26,7 @@ const da: typeof en = { residence: 'Zero Knowledge bevis for bopælsland', idDocType: 'Zero Knowledge bevis for identitetsdokumenttype', idDocIssuer: 'Zero Knowledge bevis for identitetsdokumentudsteder', + secret: 'Zero Knowledge bevis' }, names: { age: 'Alder', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts index 1d0ec3d0..7cdaf90a 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts @@ -24,6 +24,7 @@ export default { residence: 'Zero Knowledge proof of country of residence', idDocType: 'Zero Knowledge proof of identity document type', idDocIssuer: 'Zero Knowledge proof of identity document issuer', + secret: 'Zero Knowledge proof' }, names: { age: 'Age', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 4d693998..67469469 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -8,7 +8,7 @@ import { storedVerifiableCredentialsAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, CredentialSubject } from '@shared/storage/types'; +import { VerifiableCredential, CredentialSubject, VerifiableCredentialSchema } from '@shared/storage/types'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,10 +23,31 @@ import { SecretStatementV2, } from './utils'; +function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { + const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; + return property.title; +} + +function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { + const { t } = useTranslation('Web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); + + const name = getPropertyTitle(statement.attributeTag, schema); + if (statement.type === StatementTypes.AttributeInRange) { + return t('range', { name, upper: statement.upper, lower: statement.lower }); + } else if (statement.type === StatementTypes.AttributeInSet) { + return t('membership', { name }); + } else if (statement.type === StatementTypes.AttributeNotInSet) { + return t('nonMembership', { name }); + } + // TODO What to do here? + return ''; +} + type DisplayWeb3StatementProps = ClassName & { statements: Statement; dappName: string; credential: CredentialSubject; + schema: VerifiableCredentialSchema; }; type AttributeInfo = { @@ -38,7 +59,7 @@ function extractAttributesFromCredentialSubjectForSingleStatement( { attributeTag }: AtomicStatementV2, credentialSubject: CredentialSubject ): AttributeInfo { - return { name: attributeTag, value: credentialSubject[attributeTag] }; + return { name: attributeTag, value: credentialSubject.attributes[attributeTag] }; } function extractAttributesFromCredentialSubject( @@ -59,16 +80,17 @@ export function DisplayWeb3RevealStatement({ dappName, credential, className, + schema }: DisplayWeb3StatementProps) { const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); const attributes = extractAttributesFromCredentialSubject(statements, credential); const header = t('headers.reveal'); const lines: StatementLine[] = statements.map((s) => { - const { name, value } = attributes[s.attributeTag]; - + const { value } = attributes[s.attributeTag]; + const property = schema.properties.credentialSubject.properties.attributes.properties[s.attributeTag]; return { - attribute: name, + attribute: property.title, value: value.toString() ?? 'Unavailable', isRequirementMet: value !== undefined, }; @@ -82,19 +104,23 @@ export function DisplayWeb3SecretStatement({ dappName, credential, className, + schema }: DisplayWeb3StatementProps) { const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); - const { name, value } = extractAttributesFromCredentialSubjectForSingleStatement(statements, credential); - const header = t('headers.reveal'); + // Fix double name for membership + const value = useStatementValue(statements, schema); + const header = t('headers.secret'); + const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; const lines: StatementLine[] = [ { - attribute: name, - value: value.toString() ?? 'Unavailable', + attribute: property.title, + value, isRequirementMet: value !== undefined, }, ]; + // TODO Add description / list of options for membership + check range return ; } @@ -147,7 +173,7 @@ export default function DisplayWeb3Statement({ } return ( -
    +
    getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} @@ -158,7 +184,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={reveals} + statements={reveals} + schema={schema} /> )} {secrets.map((s, i) => ( @@ -168,7 +195,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={s} + statements={s} + schema={schema} /> ))}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 953137b2..f8f07d68 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -31,6 +31,7 @@ import { import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; import { DisplayCredentialStatement } from './DisplayStatement'; import { getCommitmentInput } from './utils'; +import { parse } from '@shared/utils/payload-helpers'; type Props = { onSubmit(presentationString: string): void; @@ -41,7 +42,7 @@ interface Location { state: { payload: { challenge: string; - statements: CredentialStatements; + statements: string; url: string; }; }; @@ -49,7 +50,7 @@ interface Location { export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; - const { statements, challenge, url } = state.payload; + const { statements: rawStatements, challenge, url } = state.payload; const { onClose, withClose } = useContext(fullscreenPromptContext); const { t } = useTranslation('idProofRequest'); const network = useAtomValue(networkConfigurationAtom); @@ -60,13 +61,15 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const [creatingProof, setCreatingProof] = useState(false); const net = getNet(network); + const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]) + + const [ids, setIds] = useState(Array(statements.length).fill('')); + const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const identities = useConfirmedIdentities(); const credentials = useAtomValue(credentialsAtom); const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); - const [ids, setIds] = useState(statements.map(() => '')); - const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); const handleSubmit = useCallback(async () => { @@ -92,6 +95,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return { statement: statement.statement, id: ids[index], type }; }); + const commitmentInputs = parsedStatements.map((statement) => getCommitmentInput( statement, @@ -99,7 +103,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { identities.value, credentials, verifiableCredentials || [], - verifiableCredentialSchemas.value ) ); @@ -115,7 +118,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }; const result: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( - InternalMessageType.CreateWeb3Proof, + InternalMessageType.CreateWeb3IdProof, input ); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts new file mode 100644 index 00000000..cacfd156 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -0,0 +1,34 @@ +export default { + header: '{{dappName}} requests the following information about you:', + accept: 'Accept', + reject: 'Reject', + displayStatement: { + requirementsMet: 'You meet this requirement', + requirementsNotMet: "You don't meet this requirement", + revealDescription: + '<1>Important: {{dappName}} will be given all the information above. You should only accept, if you trust the service, and you are familiar with their privacy policy.', + revealTooltip: { + header: 'Revealing information <1 />', + body: 'When you reveal information to a third party, you effectively hand over the information to them. This means you should only do this if you agree to their data usage and protection policies.\n\nYou can read more in\n<1>the developer documentation.', + }, + secretTooltip: { + header: 'Zero Knowledge proofs', + body: 'Zero Knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact age. Another example could be proving your residency is within a given set of countries without revealing which of those countries you reside within.\n\nYou can read more in\n<1>the developer documentation.', + }, + headers: { + reveal: 'Information to reveal', + secret: 'Zero Knowledge proof' + }, + proofs: { + range: '{{ name }} is between {{ lower }} and {{ upper }}', + membership: '{{ name }} is 1 of the following', + nonMembership: '{{ name }} is none of the following', + }, + descriptions: { + + missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', + }, + }, + failedProof: 'Unable to create proof', + failedProofReason: 'Unable to create proof due to: {{ reason }}', +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index e2d31037..0e3d7505 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -11,7 +11,6 @@ import { RevealStatementV2, ContractAddress, CommitmentInput, - VerifiableCredentialSchema, createWeb3IdDID, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; @@ -74,7 +73,6 @@ export function getCommitmentInput( identities: ConfirmedIdentity[], credentials: WalletCredential[], verifiableCredentials: VerifiableCredential[], - verifiableCredentialSchemas: Record ): CommitmentInput { if (statement.type) { const cred = verifiableCredentials?.find((c) => c.id === statement.id); @@ -83,14 +81,11 @@ export function getCommitmentInput( throw new Error('IdQualifier not fulfilled'); } - const schemaIndex = cred.credentialSchema.id; - return createWeb3CommitmentInputWithHdWallet( wallet, getContractAddressFromIssuerDID(cred.issuer), cred.index, cred.credentialSubject, - verifiableCredentialSchemas[schemaIndex], cred.randomness, cred.signature ); @@ -146,13 +141,16 @@ export function getViableWeb3IdCredentialsForStatement( credentialStatement: VerifiableCredentialStatement, verifiableCredentials: VerifiableCredential[] ): VerifiableCredential[] { + // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; return verifiableCredentials?.filter((vc) => allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) ); } -// TODO move to SDK? +/** + * Helper function to create a web3Id DID string from a verifiable credential + */ export function createWeb3IdDIDFromCredential(credential: VerifiableCredential, net: Network) { const contractAddress = getContractAddressFromIssuerDID(credential.issuer); return createWeb3IdDID( diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 5edf8bcc..f6c3eda0 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -113,7 +113,7 @@ export default function Routes() { ); // We manually stringify the presentation const handleWeb3IdProofResponse = useMessagePrompt>( - InternalMessageType.Web3Proof, + InternalMessageType.Web3IdProof, 'web3IdProof' ); diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 93aa0f1c..978674cd 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -21,6 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; +import Web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; const t = { shared, @@ -46,6 +47,7 @@ const t = { allowlist, connectAccountsRequest, addWeb3IdCredential, + Web3IdProofRequest }; export default t; From 5376d1e4f6ed9c4cd4c37267a2c040ed170cef6b Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 10:23:38 +0200 Subject: [PATCH 068/231] Fix display of web3Id proof statement view --- examples/add-example-Web3Id/index.html | 3 +- .../VerifiableCredentialStatement.tsx | 63 +++++++++++++------ .../popup/pages/Web3ProofRequest/i18n/en.ts | 14 +++-- .../src/popup/shell/i18n/locales/da.ts | 3 + .../src/popup/shell/i18n/locales/en.ts | 4 +- 5 files changed, 60 insertions(+), 27 deletions(-) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index f8e07d3c..e07a3749 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -24,7 +24,8 @@ const statement = new concordiumSDK.Web3StatementBuilder() .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => b - .revealAttribute('graduationDate') + .revealAttribute('degreeType') + .addRange('graduationDate', '2000-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) ) .getStatements(); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 67469469..cfdb1890 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -29,24 +29,42 @@ function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSche } function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { - const { t } = useTranslation('Web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); const name = getPropertyTitle(statement.attributeTag, schema); if (statement.type === StatementTypes.AttributeInRange) { return t('range', { name, upper: statement.upper, lower: statement.lower }); - } else if (statement.type === StatementTypes.AttributeInSet) { + } + if (statement.type === StatementTypes.AttributeInSet) { return t('membership', { name }); - } else if (statement.type === StatementTypes.AttributeNotInSet) { + } + if (statement.type === StatementTypes.AttributeNotInSet) { return t('nonMembership', { name }); } // TODO What to do here? return ''; } +export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + const name = getPropertyTitle(statement.attributeTag, schema); + const listToString = (list: (string | bigint)[]) => list.map((member) => member.toString()).join(', '); + + switch (statement.type) { + case StatementTypes.AttributeInRange: + return t('range', { name, lower: statement.lower, upper: statement.upper }); + case StatementTypes.AttributeInSet: + return t('membership', { name, setNames: listToString(statement.set) }); + case StatementTypes.AttributeNotInSet: + return t('nonMembership', { name, setNames: listToString(statement.set) }); + default: + return undefined; + } +} + type DisplayWeb3StatementProps = ClassName & { statements: Statement; dappName: string; - credential: CredentialSubject; schema: VerifiableCredentialSchema; }; @@ -75,14 +93,18 @@ function extractAttributesFromCredentialSubject( }, {}); } +type DisplayWeb3RevealStatementProps = DisplayWeb3StatementProps & { + credential: CredentialSubject; +}; + export function DisplayWeb3RevealStatement({ statements, dappName, credential, className, - schema -}: DisplayWeb3StatementProps) { - const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); + schema, +}: DisplayWeb3RevealStatementProps) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const attributes = extractAttributesFromCredentialSubject(statements, credential); const header = t('headers.reveal'); @@ -102,15 +124,14 @@ export function DisplayWeb3RevealStatement({ export function DisplayWeb3SecretStatement({ statements, dappName, - credential, className, - schema + schema, }: DisplayWeb3StatementProps) { - const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); - // Fix double name for membership + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const value = useStatementValue(statements, schema); const header = t('headers.secret'); const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; + const description = useStatementDescription(statements, schema); const lines: StatementLine[] = [ { @@ -120,8 +141,15 @@ export function DisplayWeb3SecretStatement({ }, ]; - // TODO Add description / list of options for membership + check range - return ; + return ( + + ); } export default function DisplayWeb3Statement({ @@ -184,8 +212,8 @@ export default function DisplayWeb3Statement({ className="m-t-10:not-first" dappName={dappName} credential={chosenCredential.credentialSubject} - statements={reveals} - schema={schema} + statements={reveals} + schema={schema} /> )} {secrets.map((s, i) => ( @@ -194,9 +222,8 @@ export default function DisplayWeb3Statement({ key={i} // Allow this, as we don't expect these to ever change. className="m-t-10:not-first" dappName={dappName} - credential={chosenCredential.credentialSubject} - statements={s} - schema={schema} + statements={s} + schema={schema} /> ))}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index cacfd156..42a31f55 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -17,18 +17,20 @@ export default { }, headers: { reveal: 'Information to reveal', - secret: 'Zero Knowledge proof' + secret: 'Zero Knowledge proof', }, proofs: { - range: '{{ name }} is between {{ lower }} and {{ upper }}', - membership: '{{ name }} is 1 of the following', - nonMembership: '{{ name }} is none of the following', + range: 'Between {{ lower }} and {{ upper }}', + membership: '1 of the following', + nonMembership: 'None of the following', }, descriptions: { - + range: 'This will prove that your {{ name }} is between {{ lower }} and {{ upper }}', + membership: 'This will prove that your {{ name }} is 1 of the following:\n{{ setNames }}', + nonMembership: 'This will prove that your {{ name }} is none of the following:\n{{ setNames }}', missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', }, }, failedProof: 'Unable to create proof', failedProofReason: 'Unable to create proof due to: {{ reason }}', -} +}; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 82bcc0bd..bc6f5dd0 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -21,6 +21,8 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; import allowlist from '@popup/pages/Allowlist/i18n/da'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/da'; +// TODO add "da" version +import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; import type en from './en'; @@ -48,6 +50,7 @@ const t: typeof en = { allowlist, connectAccountsRequest, addWeb3IdCredential, + web3IdProofRequest, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 978674cd..136b4de8 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -21,7 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; -import Web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; +import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; const t = { shared, @@ -47,7 +47,7 @@ const t = { allowlist, connectAccountsRequest, addWeb3IdCredential, - Web3IdProofRequest + web3IdProofRequest, }; export default t; From 872130bf557ff562325e3745e6701a8f6ab2228b Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 13:33:13 +0200 Subject: [PATCH 069/231] First example --- examples/two-step-transfer/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/two-step-transfer/index.html b/examples/two-step-transfer/index.html index 51cf100e..b1841f30 100644 --- a/examples/two-step-transfer/index.html +++ b/examples/two-step-transfer/index.html @@ -217,10 +217,10 @@ document.getElementById('web3Proof').addEventListener('click', () => { const statement = new concordiumSDK.Web3StatementBuilder() .addForIdentityCredentials([0, 1, 2], (b) => - b.revealAttribute(0).addRange(3, '08000101', '20000101') + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') ) .addForVerifiableCredentials([{ index: 5463, subindex: 0 }], (b) => - b.revealAttribute(0).addMembership(2, ['2010-06-01T00:00:00Z']) + b.revealAttribute('degreeName').addMembership('graduationDate', ['2010-06-01T00:00:00Z']) ) .getStatements(); const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; From e28c76989e27789d6b54b0a76dff315b45af895d Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 14 Aug 2023 15:39:11 +0200 Subject: [PATCH 070/231] Display VC card in proof + paging --- .../VerifiableCredentialHooks.tsx | 20 ++-- .../VerifiableCredentialStatement.tsx | 20 +++- .../Web3ProofRequest/Web3ProofRequest.tsx | 97 +++++++++---------- .../popup/pages/Web3ProofRequest/i18n/en.ts | 1 + 4 files changed, 78 insertions(+), 60 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 792f9629..92fc85db 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -64,17 +64,19 @@ export function useCredentialSchema(credential: VerifiableCredential) { * @param credential the verifiable credential to retrieve the credential entry for * @returns the credential entry for the given credential, undefined if one is not found yet */ -export function useCredentialEntry(credential: VerifiableCredential) { +export function useCredentialEntry(credential?: VerifiableCredential) { const [credentialEntry, setCredentialEntry] = useState(); const client = useAtomValue(grpcClientAtom); useEffect(() => { - const credentialHolderId = getCredentialHolderId(credential.id); - const registryContractAddress = getCredentialRegistryContractAddress(credential.id); - getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { - setCredentialEntry(entry); - }); - }, [credential.id, client]); + if (credential) { + const credentialHolderId = getCredentialHolderId(credential.id); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { + setCredentialEntry(entry); + }); + } + }, [credential?.id, client]); return credentialEntry; } @@ -86,7 +88,7 @@ export function useCredentialEntry(credential: VerifiableCredential) { * @throws if no credential metadata is found in storage for the provided credential * @returns the credential's metadata used for rendering the credential */ -export function useCredentialMetadata(credential: VerifiableCredential) { +export function useCredentialMetadata(credential?: VerifiableCredential) { const [metadata, setMetadata] = useState(); const credentialEntry = useCredentialEntry(credential); const storedMetadata = useAtomValue(storedVerifiableCredentialMetadataAtom); @@ -96,7 +98,7 @@ export function useCredentialMetadata(credential: VerifiableCredential) { const storedCredentialMetadata = storedMetadata.value[credentialEntry.credentialInfo.metadataUrl.url]; if (!storedCredentialMetadata) { throw new Error( - `Attempted to find credential metadata for credentialId: ${credential.id} but none was found!` + `Attempted to find credential metadata for credentialId: ${credential?.id} but none was found!` ); } setMetadata(storedCredentialMetadata); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index cfdb1890..ab40317b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -8,12 +8,19 @@ import { storedVerifiableCredentialsAtom, storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, CredentialSubject, VerifiableCredentialSchema } from '@shared/storage/types'; +import { + VerifiableCredential, + CredentialSubject, + VerifiableCredentialSchema, + VerifiableCredentialStatus, +} from '@shared/storage/types'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; +import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; +import { useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, @@ -196,12 +203,21 @@ export default function DisplayWeb3Statement({ return null; }, [chosenCredential?.id, verifiableCredentials?.length]); - if (!chosenCredential || !schema) { + const metadata = useCredentialMetadata(chosenCredential); + + if (!chosenCredential || !schema || !metadata) { return null; } return (
    + + getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index f8f07d68..1eadd1d8 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -29,9 +29,9 @@ import { storedVerifiableCredentialSchemasAtom, } from '@popup/store/verifiable-credential'; import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; +import { parse } from '@shared/utils/payload-helpers'; import { DisplayCredentialStatement } from './DisplayStatement'; import { getCommitmentInput } from './utils'; -import { parse } from '@shared/utils/payload-helpers'; type Props = { onSubmit(presentationString: string): void; @@ -52,16 +52,17 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; const { onClose, withClose } = useContext(fullscreenPromptContext); - const { t } = useTranslation('idProofRequest'); + const { t } = useTranslation('web3IdProofRequest'); const network = useAtomValue(networkConfigurationAtom); const client = useAtomValue(grpcClientAtom); const addToast = useSetAtom(addToastAtom); const recoveryPhrase = useDecryptedSeedPhrase((e) => addToast(e.message)); const dappName = displayUrl(url); const [creatingProof, setCreatingProof] = useState(false); + const [currentStatementIndex, setCurrentStatementIndex] = useState(0); const net = getNet(network); - const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]) + const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]); const [ids, setIds] = useState(Array(statements.length).fill('')); @@ -95,15 +96,8 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return { statement: statement.statement, id: ids[index], type }; }); - const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput( - statement, - wallet, - identities.value, - credentials, - verifiableCredentials || [], - ) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials || []) ); const request = { @@ -137,48 +131,53 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return (
    - {statements.map((s, index) => ( - - setIds((currentIds) => { - const newIds = [...currentIds]; - newIds[index] = newId; - return newIds; - }) - } - /> - ))} + + setIds((currentIds) => { + const newIds = [...currentIds]; + newIds[currentStatementIndex] = newId; + return newIds; + }) + } + /> - + {currentStatementIndex === statements.length - 1 ? ( + + ) : ( + + )}
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 42a31f55..f66d695d 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -1,6 +1,7 @@ export default { header: '{{dappName}} requests the following information about you:', accept: 'Accept', + continue: 'Continue', reject: 'Reject', displayStatement: { requirementsMet: 'You meet this requirement', From 41caf9d1dc0bc6074ae5c9cd72b17304c5cac877 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 09:32:27 +0200 Subject: [PATCH 071/231] Add check for web3IdProof that credential can satisfy statement --- .../VerifiableCredentialStatement.tsx | 4 ++-- .../src/popup/pages/Web3ProofRequest/utils.ts | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index ab40317b..8ffe8637 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -48,8 +48,8 @@ function useStatementValue(statement: SecretStatementV2, schema: VerifiableCrede if (statement.type === StatementTypes.AttributeNotInSet) { return t('nonMembership', { name }); } - // TODO What to do here? - return ''; + + throw new Error('Unknown statement type'); } export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 0e3d7505..f4192451 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -12,6 +12,7 @@ import { ContractAddress, CommitmentInput, createWeb3IdDID, + StatementTypes, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; @@ -72,7 +73,7 @@ export function getCommitmentInput( wallet: ConcordiumHdWallet, identities: ConfirmedIdentity[], credentials: WalletCredential[], - verifiableCredentials: VerifiableCredential[], + verifiableCredentials: VerifiableCredential[] ): CommitmentInput { if (statement.type) { const cred = verifiableCredentials?.find((c) => c.id === statement.id); @@ -134,6 +135,22 @@ export function getViableAccountCredentialsForStatement( }); } +function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { + const value = cred.credentialSubject.attributes[statement.attributeTag]; + switch (statement.type) { + case StatementTypes.AttributeInRange: + return statement.lower <= value && statement.upper > value; + case StatementTypes.AttributeInSet: + return statement.set.includes(value); + case StatementTypes.AttributeNotInSet: + return !statement.set.includes(value); + case StatementTypes.RevealAttribute: + return value !== undefined; + default: + throw new Error('Unknown statementType encountered'); + } +} + /** * Given a credential statement for a verifiable credential, and a list of verifiable credentials, return the filtered list of verifiable credentials that satisfy the statement. */ @@ -143,9 +160,12 @@ export function getViableWeb3IdCredentialsForStatement( ): VerifiableCredential[] { // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; - return verifiableCredentials?.filter((vc) => + const allowedCredentials = verifiableCredentials?.filter((vc) => allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) ); + return allowedCredentials.filter((cred) => + credentialStatement.statement.every((stm) => doesCredentialSatisfyStatement(stm, cred)) + ); } /** From a92eb0a8d506d86f327a4b81d1ccbf54c605066d Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:27:01 +0200 Subject: [PATCH 072/231] Small fixes --- packages/browser-wallet/package.json | 2 +- .../src/popup/pages/IdProofRequest/i18n/da.ts | 1 - .../src/popup/pages/IdProofRequest/i18n/en.ts | 1 - .../VerifiableCredentialStatement.tsx | 6 +-- .../Web3ProofRequest/Web3ProofRequest.tsx | 6 +-- .../src/popup/pages/Web3ProofRequest/utils.ts | 44 ++--------------- .../utils/verifiable-credential-helpers.ts | 42 +++++++++++++++++ .../test/Web3ProofRequest.test.ts | 27 ----------- .../verifiable-credential-helpers.test.ts | 47 +++++++++++++++++++ 9 files changed, 101 insertions(+), 75 deletions(-) delete mode 100644 packages/browser-wallet/test/Web3ProofRequest.test.ts diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e8977fbc..5867c6ef 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0.2", + "version": "1.1.0", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts index 33752ddd..2da97ad1 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts @@ -26,7 +26,6 @@ const da: typeof en = { residence: 'Zero Knowledge bevis for bopælsland', idDocType: 'Zero Knowledge bevis for identitetsdokumenttype', idDocIssuer: 'Zero Knowledge bevis for identitetsdokumentudsteder', - secret: 'Zero Knowledge bevis' }, names: { age: 'Alder', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts index 7cdaf90a..1d0ec3d0 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts @@ -24,7 +24,6 @@ export default { residence: 'Zero Knowledge proof of country of residence', idDocType: 'Zero Knowledge proof of identity document type', idDocIssuer: 'Zero Knowledge proof of identity document issuer', - secret: 'Zero Knowledge proof' }, names: { age: 'Age', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 8ffe8637..6ee45694 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -14,6 +14,7 @@ import { VerifiableCredentialSchema, VerifiableCredentialStatus, } from '@shared/storage/types'; +import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,7 +26,6 @@ import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, - getVerifiableCredentialPublicKeyfromSubjectDID, getViableWeb3IdCredentialsForStatement, SecretStatementV2, } from './utils'; @@ -178,7 +178,7 @@ export default function DisplayWeb3Statement({ if (!verifiableCredentials) { return []; } - return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials); + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); }, [credentialStatement.idQualifier.issuers]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); @@ -201,7 +201,7 @@ export default function DisplayWeb3Statement({ return verifiableCredentialSchemas.value[schemaId]; } return null; - }, [chosenCredential?.id, verifiableCredentials?.length]); + }, [chosenCredential?.id, verifiableCredentialSchemas.loading]); const metadata = useCredentialMetadata(chosenCredential); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 1eadd1d8..648681e6 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -97,7 +97,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }); const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials || []) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value || []) ); const request = { @@ -120,11 +120,11 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentialSchemas.loading || identities.loading) { + if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading) { return null; } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index f4192451..566d4b3e 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -9,7 +9,6 @@ import { Network, AtomicStatementV2, RevealStatementV2, - ContractAddress, CommitmentInput, createWeb3IdDID, StatementTypes, @@ -17,6 +16,11 @@ import { import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { ConfirmedIdentity, CreationStatus, VerifiableCredential, WalletCredential } from '@shared/storage/types'; import { ClassName } from 'wallet-common-helpers'; +import { + getContractAddressFromIssuerDID, + getCredentialIdFromSubjectDID, + getVerifiableCredentialPublicKeyfromSubjectDID, +} from '@shared/utils/verifiable-credential-helpers'; export type SecretStatementV2 = Exclude; @@ -27,44 +31,6 @@ export interface DisplayCredentialStatementProps extends ClassName { net: Network; } -/** Takes a Web3IdCredential issuer DID string and returns the contract address - * @param did a issuer DID string on the form: "did:ccd:testnet:sci:INDEX:SUBINDEX/issuer" - * @returns the contract address INDEX;SUBINDEX - */ -export function getContractAddressFromIssuerDID(did: string): ContractAddress { - const split = did.split(':'); - if (split.length !== 6 || split[3] !== 'sci') { - throw new Error('Given DID did not follow expected format'); - } - const index = BigInt(split[4]); - const subindex = BigInt(split[5].substring(0, split[5].indexOf('/'))); - return { index, subindex }; -} - -/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential - * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" - * @returns the public key KEY - */ -export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { - const split = did.split('/'); - if (split.length !== 3 || split[0].split(':')[3] !== 'sci') { - throw new Error(`Given DID did not follow expected format:${did}`); - } - return split[2]; -} - -/** Takes a AccountCredential subject DID string and returns the credential id of the account credential - * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" - * @returns the credId CREDID - */ -export function getCredentialIdFromSubjectDID(did: string) { - const split = did.split(':'); - if (split.length !== 5 || split[3] !== 'cred') { - throw new Error(`Given DID did not follow expected format: ${did}`); - } - return split[4]; -} - /** * Build the commitmentInputs required to create a presentation for the given statement. */ diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 2482cb00..26776ae2 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -717,3 +717,45 @@ export function getDIDNetwork(did: string): 'mainnet' | 'testnet' { } return network; } + +/** Takes a Web3IdCredential issuer DID string and returns the contract address + * @param did a issuer DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/issuer" + * @returns the contract address INDEX;SUBINDEX + */ +export function getContractAddressFromIssuerDID(did: string): ContractAddress { + const preSplit = did.split('/'); + if (preSplit[1] !== 'issuer') { + throw new Error('Given DID did not follow expected format'); + } + const split = preSplit[0].split(':'); + if ((split.length !== 6 && split.length !== 5) || split[split.length - 3] !== 'sci') { + throw new Error('Given DID did not follow expected format'); + } + const index = BigInt(split[split.length - 2]); + const subindex = BigInt(split[split.length - 1]); + return { index, subindex }; +} + +/** Takes a Web3IdCredential subject DID string and returns the publicKey of the verifiable credential + * @param did a DID string on the form: "did:ccd:NETWORK:sci:INDEX:SUBINDEX/credentialEntry/KEY" + * @returns the public key KEY + */ +export function getVerifiableCredentialPublicKeyfromSubjectDID(did: string) { + const split = did.split('/'); + if (split.length !== 3 || split[0].split(':')[split[0].split(':').length - 3] !== 'sci') { + throw new Error(`Given DID did not follow expected format:${did}`); + } + return split[2]; +} + +/** Takes a AccountCredential subject DID string and returns the credential id of the account credential + * @param did a DID string on the form: "did:ccd:NETWORK:cred:CREDID" + * @returns the credId CREDID + */ +export function getCredentialIdFromSubjectDID(did: string) { + const split = did.split(':'); + if ((split.length !== 5 && split.length !== 4) || split[split.length - 2] !== 'cred') { + throw new Error(`Given DID did not follow expected format: ${did}`); + } + return split[split.length - 1]; +} diff --git a/packages/browser-wallet/test/Web3ProofRequest.test.ts b/packages/browser-wallet/test/Web3ProofRequest.test.ts deleted file mode 100644 index 670e367e..00000000 --- a/packages/browser-wallet/test/Web3ProofRequest.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - getCredentialIdFromSubjectDID, - getContractAddressFromIssuerDID, - getVerifiableCredentialPublicKeyfromSubjectDID, -} from '../src/popup/pages/Web3ProofRequest/utils'; - -test('getContractAddressFromIssuerDID', () => { - const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); - expect(address.index).toBe(1337n); - expect(address.subindex).toBe(42n); -}); - -test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { - const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( - 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' - ); - expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); -}); - -test('getVerifiableCredentialPublicKeyfromSubjectDID', () => { - const credId = getCredentialIdFromSubjectDID( - 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' - ); - expect(credId).toBe( - 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' - ); -}); diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index 6dce34ce..cf927efa 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -4,6 +4,9 @@ import { getCredentialHolderId, getCredentialRegistryContractAddress, getPublicKeyfromPublicKeyIdentifierDID, + getCredentialIdFromSubjectDID, + getContractAddressFromIssuerDID, + getVerifiableCredentialPublicKeyfromSubjectDID, } from '../src/shared/utils/verifiable-credential-helpers'; import { mainnet, testnet } from '../src/shared/constants/networkConfiguration'; @@ -82,3 +85,47 @@ test('credential Id is created with correct network', () => { 'did:ccd:mainnet:sci:2:7/credentialEntry/4799ec95500850d9368ca012faf60e9d632d3b1768d608c7e5e3d53fe96d669a' ); }); + +test('getContractAddressFromIssuerDID extracts contract address', () => { + const address = getContractAddressFromIssuerDID('did:ccd:testnet:sci:1337:42/issuer'); + expect(address.index).toBe(1337n); + expect(address.subindex).toBe(42n); +}); + +test('getContractAddressFromIssuerDID extracts contract address without network', () => { + const address = getContractAddressFromIssuerDID('did:ccd:sci:1338:43/issuer'); + expect(address.index).toBe(1338n); + expect(address.subindex).toBe(43n); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID extracts public key', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:testnet:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getVerifiableCredentialPublicKeyfromSubjectDID extracts public key without network', () => { + const publicKey = getVerifiableCredentialPublicKeyfromSubjectDID( + 'did:ccd:sci:1337:42/credentialEntry/76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d' + ); + expect(publicKey).toBe('76ada0ebd1e8aa5a651a0c4ac1ad3b62d3040f693722f94d61efa4fdd6ee797d'); +}); + +test('getCredentialIdFromSubjectDID extracts credId', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:testnet:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); + +test('getCredentialIdFromSubjectDID extracts credId without network', () => { + const credId = getCredentialIdFromSubjectDID( + 'did:ccd:cred:aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); + expect(credId).toBe( + 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' + ); +}); From b75fdfad040b71a1ccad9641cfd2f61c26836323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Tue, 15 Aug 2023 10:32:11 +0200 Subject: [PATCH 073/231] Change background_color to backgroundColor --- .../VerifiableCredentialCard.stories.tsx | 2 +- .../VerifiableCredential/VerifiableCredentialCard.tsx | 7 ++----- .../src/shared/utils/verifiable-credential-helpers.ts | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx index a069831b..8920d6d8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.stories.tsx @@ -62,7 +62,7 @@ const metadata: VerifiableCredentialMetadata = { url: 'https://img.logoipsum.com/298.svg', hash: '1c74f7eb1b3343a5834e60e9a8fce277f2c7553112accd42e63fae7a09e0caf8', }, - background_color: '#003d73', + backgroundColor: '#003d73', image: { url: 'https://picsum.photos/327/120', }, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 9adce115..6e76b5d5 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -50,7 +50,7 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className return (
    { if (e.key === 'Enter') { @@ -65,10 +65,7 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className ); } return ( -
    +
    {children}
    ); diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 2482cb00..0e370551 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -302,7 +302,7 @@ const verifiableCredentialMetadataSchema = { VerifiableCredentialMetadata: { additionalProperties: false, properties: { - background_color: { + backgroundColor: { type: 'string', }, image: { @@ -321,7 +321,7 @@ const verifiableCredentialMetadataSchema = { type: 'string', }, }, - required: ['title', 'logo', 'background_color'], + required: ['title', 'logo', 'backgroundColor'], type: 'object', }, }, @@ -330,7 +330,7 @@ const verifiableCredentialMetadataSchema = { export interface VerifiableCredentialMetadata { title: string; logo: MetadataUrl; - background_color: string; + backgroundColor: string; image?: MetadataUrl; localization?: Record; } From d34e6b16088c531191120e78e3f658c9314af1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Tue, 15 Aug 2023 10:41:48 +0200 Subject: [PATCH 074/231] Fix issue that would overwrite existing metadata --- .../src/shared/utils/verifiable-credential-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 0e370551..d3ac36be 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -608,7 +608,7 @@ export async function getChangesToCredentialMetadata( let updateReceived = false; for (const updatedMetadata of upToDateCredentialMetadata) { - if (storedMetadata.value === undefined) { + if (Object.keys(updatedStoredMetadata).length === 0) { updatedStoredMetadata = { [updatedMetadata.url]: updatedMetadata.metadata, }; @@ -635,7 +635,7 @@ export async function getChangesToCredentialSchemas( let updateReceived = false; for (const updatedSchema of upToDateSchemas) { - if (storedSchemas === undefined) { + if (Object.keys(updatedSchemasInStorage).length === 0) { updatedSchemasInStorage = { [updatedSchema.$id]: updatedSchema, }; From c574e6ef21ea5b726af18b5407bed235cbb591b9 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:50:20 +0200 Subject: [PATCH 075/231] Add missing documentation --- packages/browser-wallet-api-helpers/README.md | 15 +++++++++++++++ .../src/wallet-api-types.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index c74af76f..7bf05869 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -232,6 +232,21 @@ provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { }); ``` +### Request Verifiable Presentation for web3Id statements + +It is possible to request a verifiable presentation for a given set of web3Id statements. The function takes 2 arguments. A challenge to ensure that the proof was not generated for a different context, and the statements to be proven. This method returns a Promise resolving with the verifiable presentation for the statements. + +If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects proving the statement, the Promise will reject. + +The following exemplifies requesting a verifiable presentation for a statement named myIdStatement (To see how to create a statement check out our documentation) with a challenge of "12346789ABCD". + +```typescript +const statement = myIdStatement; +const challenge = '12346789ABCD'; +const provider = await detectConcordiumProvider(); +const verifiablePresentation = await provider.requestVerifiablePresentation(challenge, statement); +``` + ## Events ### Account changed diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index acc51e52..aa65240d 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -222,7 +222,10 @@ interface MainWalletApi { ): Promise; /** - * @TODO write this + fix return type + * Request that the user provides a proof for the given statements. + * @param challenge bytes chosen by the verifier. Should be HEX encoded. + * @param statement the web3Id statements that should be proven. + * @returns The presentation for the statements. */ requestVerifiablePresentation(challenge: string, statements: CredentialStatements): Promise; } From 2847dffaa17e71295b57be2ce9944f0340da50ba Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 10:53:40 +0200 Subject: [PATCH 076/231] Disable broken validation --- .../browser-wallet/src/background/web3Id.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index ce3f6af9..e1be7b93 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -2,7 +2,9 @@ import { CredentialStatements, getVerifiablePresentation, Web3IdProofInput, - createConcordiumClient, verifyWeb3IdCredentialSignature, isHex, verifyAtomicStatements + createConcordiumClient, + verifyWeb3IdCredentialSignature, + isHex, } from '@concordium/web-sdk'; import { sessionVerifiableCredentials, @@ -166,16 +168,19 @@ export const runIfValidWeb3IdProof: RunCondition }; } try { - const statements: CredentialStatements = parse(msg.payload.statements); - // TODO Fix second parameter when SDK is updated - // If a statement does not verify, an error is thrown. - statements.every((credStatement) => verifyAtomicStatements(credStatement.statement, undefined as any)); + const statements: CredentialStatements = parse(msg.payload.statements); + // TODO Enable when SDK is updated + // // If a statement does not verify, an error is thrown. + // statements.every((credStatement) => verifyAtomicStatements(credStatement.statement)); const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); if (!noEmptyQualifier) { return { run: false, - response: { success: false, message: `Statements must have at least 1 possible identity provider / issuer` }, + response: { + success: false, + message: `Statements must have at least 1 possible identity provider / issuer`, + }, }; } return { run: true }; From 2e95fb454df3a7e54a7763b22c0c2acb253f62cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Tue, 15 Aug 2023 11:21:08 +0200 Subject: [PATCH 077/231] Use Pending status when failing to get status Also added translations for the status values. --- .../VerifiableCredentialHooks.tsx | 6 +++--- .../VerifiableCredentialStatus.tsx | 14 +++++++++++++- .../popup/pages/VerifiableCredential/i18n/da.ts | 13 +++++++++++++ .../popup/pages/VerifiableCredential/i18n/en.ts | 11 +++++++++++ .../src/popup/shell/i18n/locales/da.ts | 2 ++ .../src/popup/shell/i18n/locales/en.ts | 2 ++ .../browser-wallet/src/shared/storage/types.ts | 4 ++++ .../shared/utils/verifiable-credential-helpers.ts | 2 +- 8 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 792f9629..1a7d620c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -27,9 +27,9 @@ export function useCredentialStatus(credential: VerifiableCredential) { const client = useAtomValue(grpcClientAtom); useEffect(() => { - getVerifiableCredentialStatus(client, credential.id).then((credentialStatus) => { - setStatus(credentialStatus); - }); + getVerifiableCredentialStatus(client, credential.id) + .then(setStatus) + .catch(() => setStatus(VerifiableCredentialStatus.Pending)); }, [credential.id, client]); return status; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx index 7155220f..ce9ad400 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx @@ -4,24 +4,36 @@ import RevokedIcon from '@assets/svg/revoked.svg'; import ActiveIcon from '@assets/svg/verified.svg'; import ExpiredIcon from '@assets/svg/block.svg'; import PendingIcon from '@assets/svg/pending.svg'; +import { useTranslation } from 'react-i18next'; /** * Component for displaying the status of a verifiable credential. */ export default function StatusIcon({ status }: { status: VerifiableCredentialStatus }) { + const { t } = useTranslation('verifiableCredential', { keyPrefix: 'status' }); + let icon = null; + let text = ''; switch (status) { case VerifiableCredentialStatus.Active: icon = ; + text = t('Active'); break; case VerifiableCredentialStatus.Revoked: icon = ; + text = t('Revoked'); break; case VerifiableCredentialStatus.Expired: icon = ; + text = t('Expired'); break; case VerifiableCredentialStatus.NotActivated: icon = ; + text = t('Pending'); + break; + case VerifiableCredentialStatus.Pending: + icon = ; + text = t('Pending'); break; default: icon = null; @@ -30,7 +42,7 @@ export default function StatusIcon({ status }: { status: VerifiableCredentialSta return (
    - {VerifiableCredentialStatus[status]} + {text} {icon}
    ); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts new file mode 100644 index 00000000..a581242f --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -0,0 +1,13 @@ +import en from './en'; + +const t: typeof en = { + status: { + Active: 'Aktiv', + Revoked: 'Ophævet', + Expired: 'Udløbet', + NotActivated: 'Ikke aktiveret', + Pending: 'Afventer', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts new file mode 100644 index 00000000..c6e00d4a --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -0,0 +1,11 @@ +const t = { + status: { + Active: 'Active', + Revoked: 'Revoked', + Expired: 'Expired', + NotActivated: 'Not activated', + Pending: 'Pending', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 82bcc0bd..90d0ffa0 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -21,6 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; import allowlist from '@popup/pages/Allowlist/i18n/da'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/da'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/da'; import type en from './en'; @@ -48,6 +49,7 @@ const t: typeof en = { allowlist, connectAccountsRequest, addWeb3IdCredential, + verifiableCredential, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 93aa0f1c..0e2d1280 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -21,6 +21,7 @@ import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/en'; const t = { shared, @@ -46,6 +47,7 @@ const t = { allowlist, connectAccountsRequest, addWeb3IdCredential, + verifiableCredential, }; export default t; diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 54b22ee7..9e00302e 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -265,6 +265,10 @@ export enum VerifiableCredentialStatus { Revoked, Expired, NotActivated, + + // Pending is a local wallet state not reflected on chain. This is used + // when a credential is added to the wallet, but it is still not on chain. + Pending, } export type CredentialSubject = { diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index d3ac36be..6a93fc0f 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -78,7 +78,7 @@ export async function getVerifiableCredentialStatus(client: ConcordiumGRPCClient const contractAddress = getCredentialRegistryContractAddress(credentialId); const instanceInfo = await client.getInstanceInfo(contractAddress); if (instanceInfo === undefined) { - return undefined; + throw new Error('Given contract address was not a created instance'); } const result = await client.invokeContract({ From f09701fd69277efe3b2622ff599321d033164d2f Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 11:21:09 +0200 Subject: [PATCH 078/231] Refresh Statement component when paging --- .../src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 648681e6..0830c46c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -136,6 +136,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { dappName={dappName} credentialStatement={statements[currentStatementIndex]} net={net} + key={currentStatementIndex} setChosenId={(newId) => setIds((currentIds) => { const newIds = [...currentIds]; From 700167052cc486546229922d999c1c7cda432ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Tue, 15 Aug 2023 11:29:12 +0200 Subject: [PATCH 079/231] Run workflow on PR to feature branch --- .github/workflows/build-lint-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 5cd51186..dfae4e08 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -3,9 +3,9 @@ name: Build, lint and test on: # Triggers the workflow on push or pull request events but only for the main branch and release branches push: - branches: [main, release**] + branches: [main, release**, feature/**] pull_request: - branches: [main, release**] + branches: [main, release**, feature/**] # Allows us to run the workflow manually from the Actions tab workflow_dispatch: From fce454a154e3b74acda66c2a5d480419ad46d192 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 17:28:38 +0200 Subject: [PATCH 080/231] Address review comments --- examples/add-example-Web3Id/index.html | 56 +++++++++++---- .../browser-wallet/src/background/web3Id.ts | 3 - .../IdProofRequest/DisplayStatement/utils.ts | 7 +- .../VerifiableCredentialHooks.tsx | 2 +- .../Web3ProofRequest/AccountStatement.tsx | 17 +---- .../Web3ProofRequest/DisplayStatement.tsx | 3 +- .../VerifiableCredentialStatement.tsx | 15 ++-- .../Web3ProofRequest/Web3ProofRequest.tsx | 8 +-- .../popup/pages/Web3ProofRequest/i18n/da.ts | 41 +++++++++++ .../src/popup/pages/Web3ProofRequest/utils.ts | 71 ++++++++++++------- .../src/shared/utils/contract-helpers.ts | 9 ++- 11 files changed, 159 insertions(+), 73 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index e07a3749..57f22252 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -10,25 +10,14 @@ let currentAccountAddress = ''; async function setupPage() { const provider = await concordiumHelpers.detectConcordiumProvider(); - document.getElementById('requestAccounts').addEventListener('click', () => { provider.requestAccounts().then((accountAddresses) => { currentAccountAddress = accountAddresses[0]; document.getElementById('accountAddress').innerHTML = currentAccountAddress; }); }); - provider.on('accountDisconnected', (accountAddress) => (currentAccountAddress = undefined)); - provider.on('accountChanged', (accountAddress) => (currentAccountAddress = accountAddress)); - provider.on('chainChanged', (chain) => alert(chain)); - document.getElementById('web3Proof').addEventListener('click', () => { - const statement = new concordiumSDK.Web3StatementBuilder() - .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => - b - .revealAttribute('degreeType') - .addRange('graduationDate', '2000-01-01T00:00:00.000Z', '2030-01-01T00:00:00.000Z') - .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) - ) - .getStatements(); + + function sendStatement(statement) { // Should be not be hardcoded const challenge = '94d3e85bbc8ff0091e562ad8ef6c30d57f29b19f17c98ce155df2a30100dAAAA'; provider @@ -41,7 +30,41 @@ console.log(error); alert(error); }); + } + + provider.on('accountDisconnected', (accountAddress) => (currentAccountAddress = undefined)); + provider.on('accountChanged', (accountAddress) => (currentAccountAddress = accountAddress)); + provider.on('chainChanged', (chain) => alert(chain)); + // Request proofs + document.getElementById('web3ProofWeb3IdOnly').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => + b + .revealAttribute('degreeType') + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + ).getStatements(); + sendStatement(statement); + }); + document.getElementById('web3ProofIdOnly').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') + ).getStatements(); + sendStatement(statement); }); + document.getElementById('web3ProofMixed').addEventListener('click', () => { + const statement = new concordiumSDK.Web3StatementBuilder() + .addForIdentityCredentials([0, 1, 2], (b) => + b.revealAttribute('firstName').addRange('dob', '08000101', '20000101') + ) + .addForVerifiableCredentials([{ index: 5463n, subindex: 0n }], (b) => + b + .revealAttribute('degreeType') + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + ).getStatements(); + sendStatement(statement); + }); + // Add credential document.getElementById('addWeb3Id').addEventListener('click', () => { const values = { degreeType: degreeType.value, @@ -131,6 +154,11 @@

    Attribute values:



    - +

    Request Proofs:

    + +
    + +
    + diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index e1be7b93..e6c54081 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -157,9 +157,6 @@ export const createWeb3IdProofHandler: ExtensionMessageHandler = (msg, _sender, return true; }; -/** - * Run condition which looks up URL in connected sites for the provided account. Runs handler if URL is included in connected sites. - */ export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { if (!isHex(msg.payload.challenge)) { return { diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts index 00855d8a..284f4e29 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/utils.ts @@ -253,8 +253,13 @@ export function getStatementDescription( return undefined; } -export function useStatementDescription(statement: SecretStatement, identity: ConfirmedIdentity): string | undefined { +export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); + + if (!identity) { + // TODO Should we write something here? + return ''; + } const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; if (!hasAttribute) { diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 92fc85db..8d3058bf 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -98,7 +98,7 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { const storedCredentialMetadata = storedMetadata.value[credentialEntry.credentialInfo.metadataUrl.url]; if (!storedCredentialMetadata) { throw new Error( - `Attempted to find credential metadata for credentialId: ${credential?.id} but none was found!` + `Attempted to find credential metadata for credentialId: ${credentialEntry.credentialInfo.credentialHolderId} but none was found!` ); } setMetadata(storedCredentialMetadata); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index a0689816..a8e20536 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -23,9 +23,9 @@ import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplaySt import CredentialSelector from './CredentialSelector'; import { DisplayCredentialStatementProps, getViableAccountCredentialsForStatement, SecretStatementV2 } from './utils'; import { - getStatementDescription, isoToCountryName, SecretStatement, + useStatementDescription, useStatementHeader, useStatementName, useStatementValue, @@ -37,21 +37,6 @@ type DisplaySecretStatementV2Props = ClassName & { statement: SecretStatementV2; }; -export function useStatementDescription(statement: SecretStatement, identity?: ConfirmedIdentity): string | undefined { - const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement.descriptions' }); - - if (!identity) { - // TODO Should we write something here? - return ''; - } - const hasAttribute = identity.idObject.value.attributeList.chosenAttributes[statement.attributeTag] !== undefined; - if (!hasAttribute) { - return t('missingAttribute', { identityName: identity.name }); - } - - return getStatementDescription(statement, t, i18n.resolvedLanguage); -} - export function DisplaySecretStatementV2({ dappName, statement, identity, className }: DisplaySecretStatementV2Props) { const v1Statement: SecretStatement = statement as SecretStatement; const header = useStatementHeader(v1Statement); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx index 3a658180..015f42a0 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayStatement.tsx @@ -18,5 +18,6 @@ export function DisplayCredentialStatement({ if (isVerifiableCredentialStatement(credentialStatement)) { return ; } - return null; + + throw new Error('Invalid Statement encountered'); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 6ee45694..ae03a7b1 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -31,6 +31,7 @@ import { } from './utils'; function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { + // TODO use localization here const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; return property.title; } @@ -65,7 +66,7 @@ export function useStatementDescription(statement: SecretStatementV2, schema: Ve case StatementTypes.AttributeNotInSet: return t('nonMembership', { name, setNames: listToString(statement.set) }); default: - return undefined; + throw new Error('Unknown statement type encountered: ' + statement.type); } } @@ -117,9 +118,9 @@ export function DisplayWeb3RevealStatement({ const lines: StatementLine[] = statements.map((s) => { const { value } = attributes[s.attributeTag]; - const property = schema.properties.credentialSubject.properties.attributes.properties[s.attributeTag]; + const title = getPropertyTitle(s.attributeTag, schema); return { - attribute: property.title, + attribute: title, value: value.toString() ?? 'Unavailable', isRequirementMet: value !== undefined, }; @@ -137,12 +138,12 @@ export function DisplayWeb3SecretStatement({ const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const value = useStatementValue(statements, schema); const header = t('headers.secret'); - const property = schema.properties.credentialSubject.properties.attributes.properties[statements.attributeTag]; + const title = getPropertyTitle(statements.attributeTag, schema); const description = useStatementDescription(statements, schema); const lines: StatementLine[] = [ { - attribute: property.title, + attribute: title, value, isRequirementMet: value !== undefined, }, @@ -175,11 +176,11 @@ export default function DisplayWeb3Statement({ const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const validCredentials = useMemo(() => { - if (!verifiableCredentials) { + if (verifiableCredentials.loading) { return []; } return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); - }, [credentialStatement.idQualifier.issuers]); + }, [verifiableCredentials.loading]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 0830c46c..08d9a825 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -80,8 +80,8 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { if (!network) { throw new Error('Network is not specified'); } - if (!ids.every((x) => Boolean(x))) { - throw new Error('Network is not specified'); + if (!canProve) { + throw new Error('The statements are not satisfied and cannot be proven'); } const global = await getGlobal(client); @@ -97,7 +97,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }); const commitmentInputs = parsedStatements.map((statement) => - getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value || []) + getCommitmentInput(statement, wallet, identities.value, credentials, verifiableCredentials.value) ); const request = { @@ -120,7 +120,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading, canProve]); useEffect(() => onClose(onReject), [onClose, onReject]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts new file mode 100644 index 00000000..5a46362d --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -0,0 +1,41 @@ +import type en from './en'; + +const t: typeof en = { + header: '{{dappName}} anmoder om følgende information om dig:', + accept: 'Godkend', + reject: 'Afvis', + continue: 'Fortsæt', + displayStatement: { + requirementsMet: 'Du opfylder kravet', + requirementsNotMet: 'Du opfylder ikke kravet', + revealDescription: + '<1>Vigtigt: {{dappName}} får adgang til alt information der står ovenfor. Du skal kun acceptere at afsløre informationen, hvis du har tillid til servicen og hvis du er bekendt med deres privathedspolitik.', + revealTooltip: { + header: 'Afslører information <1 />', + body: 'Når du afslører information til en tredjepart, kan de beholde denne information. Dette betyder, at du kun bør afsløre information til dem hvis du er bekendt med deres databrugs- samt databeskyttelses-politik.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + }, + secretTooltip: { + header: 'Zero Knowledge beviser', + body: 'Zero Knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + }, + headers: { + reveal: 'Information der afsløres', + secret: 'Zero Knowledge bevis', + }, + proofs: { + range: 'Mellem {{ lower }} og {{ upper }}', + membership: '1 af de følgende', + nonMembership: 'Ingen af de følgende', + }, + descriptions: { + range: 'Dette vil bevise at deres {{ name }} er mellem {{ lower }} og {{ upper }}', + membership: 'Dette vil bevise at deres {{ name }} er en af følgende:\n{{ setNames }}', + nonMembership: 'This will prove that your {{ name }} er IKKE en af følgende:\n{{ setNames }}', + missingAttribute: 'Denne Attribut kan ikke findes på identiteten "{{identityName}}"', + } + }, + failedProof: 'Bevis kunne ikke oprettes', + failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 566d4b3e..38b0a03c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -21,6 +21,7 @@ import { getCredentialIdFromSubjectDID, getVerifiableCredentialPublicKeyfromSubjectDID, } from '@shared/utils/verifiable-credential-helpers'; +import { areContractAddressesEqual } from '@shared/utils/contract-helpers'; export type SecretStatementV2 = Exclude; @@ -31,32 +32,12 @@ export interface DisplayCredentialStatementProps extends ClassName { net: Network; } -/** - * Build the commitmentInputs required to create a presentation for the given statement. - */ -export function getCommitmentInput( +function getAccountCredentialCommitmentInput( statement: RequestStatement, wallet: ConcordiumHdWallet, identities: ConfirmedIdentity[], credentials: WalletCredential[], - verifiableCredentials: VerifiableCredential[] -): CommitmentInput { - if (statement.type) { - const cred = verifiableCredentials?.find((c) => c.id === statement.id); - - if (!cred) { - throw new Error('IdQualifier not fulfilled'); - } - - return createWeb3CommitmentInputWithHdWallet( - wallet, - getContractAddressFromIssuerDID(cred.issuer), - cred.index, - cred.credentialSubject, - cred.randomness, - cred.signature - ); - } +) { const credId = getCredentialIdFromSubjectDID(statement.id); const credential = credentials.find((cred) => cred.credId === credId); @@ -75,11 +56,49 @@ export function getCommitmentInput( identity.providerIndex, identity.idObject.value.attributeList, wallet, - identity.providerIndex, + identity.index, credential.credNumber ); } +function createWeb3CommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + verifiableCredentials: VerifiableCredential[] +) { + const cred = verifiableCredentials?.find((c) => c.id === statement.id); + + if (!cred) { + throw new Error('IdQualifier not fulfilled'); + } + + return createWeb3CommitmentInputWithHdWallet( + wallet, + getContractAddressFromIssuerDID(cred.issuer), + cred.index, + cred.credentialSubject, + cred.randomness, + cred.signature + ); +} + +/** + * Build the commitmentInputs required to create a presentation for the given statement. + */ +export function getCommitmentInput( + statement: RequestStatement, + wallet: ConcordiumHdWallet, + identities: ConfirmedIdentity[], + credentials: WalletCredential[], + verifiableCredentials: VerifiableCredential[] +): CommitmentInput { + // TODO replace with isVerifiableCredentialRequestStatement when SDK is updated + if (statement.type) { + return createWeb3CommitmentInput(statement, wallet, verifiableCredentials); + } + return getAccountCredentialCommitmentInput(statement, wallet, identities, credentials); +} + /** * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. @@ -93,7 +112,7 @@ export function getViableAccountCredentialsForStatement( return credentials?.filter((c) => { if (allowedIssuers.includes(c.providerIndex)) { const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); - if (identity && identity.status === CreationStatus.Confirmed) { + if (identity) { return canProveCredentialStatement(credentialStatement, identity.idObject.value.attributeList); } } @@ -101,6 +120,7 @@ export function getViableAccountCredentialsForStatement( }); } +// TODO Replace with canProveAtomicStatement when SDK is updated function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { const value = cred.credentialSubject.attributes[statement.attributeTag]; switch (statement.type) { @@ -127,8 +147,9 @@ export function getViableWeb3IdCredentialsForStatement( // TODO check that credentials are active (maybe before this instead for each statement) const allowedContracts = credentialStatement.idQualifier.issuers; const allowedCredentials = verifiableCredentials?.filter((vc) => - allowedContracts.some((address) => BigInt(address.index) === getContractAddressFromIssuerDID(vc.issuer).index) + allowedContracts.some((address) => areContractAddressesEqual(address, getContractAddressFromIssuerDID(vc.issuer))) ); + return allowedCredentials.filter((cred) => credentialStatement.statement.every((stm) => doesCredentialSatisfyStatement(stm, cred)) ); diff --git a/packages/browser-wallet/src/shared/utils/contract-helpers.ts b/packages/browser-wallet/src/shared/utils/contract-helpers.ts index 031ef543..456c6b17 100644 --- a/packages/browser-wallet/src/shared/utils/contract-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/contract-helpers.ts @@ -1,4 +1,4 @@ -import { InstanceInfo } from '@concordium/web-sdk'; +import { ContractAddress, InstanceInfo } from '@concordium/web-sdk'; /** * Get the name of a contract. @@ -9,3 +9,10 @@ import { InstanceInfo } from '@concordium/web-sdk'; export function getContractName(instanceInfo: InstanceInfo): string | undefined { return instanceInfo.name.substring(5); } + +/** + * Determine whether two contract addresses are the same + */ +export function areContractAddressesEqual(a: ContractAddress, b: ContractAddress) { + return a.index === b.index && a.subindex === b.subindex; +} From 51b1c1ed3fd21f08a1e6c8ff4b46861636e29925 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 15 Aug 2023 17:31:14 +0200 Subject: [PATCH 081/231] Replace whitelist with allowlist in api-helpers README --- packages/browser-wallet-api-helpers/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md index 7bf05869..0d664705 100644 --- a/packages/browser-wallet-api-helpers/README.md +++ b/packages/browser-wallet-api-helpers/README.md @@ -51,14 +51,14 @@ declare global { ### connect -To request a connection to the wallet from the user, the `connect` method has to be invoked. The method returns a `Promise` resolving with information related to the most recently selected account, which has whitelisted the dApp, or rejecting if the request is rejected in the wallet. If the wallet is locked, then this call prompts the user to first unlock the wallet before accepting or rejecting the connection request. +To request a connection to the wallet from the user, the `connect` method has to be invoked. The method returns a `Promise` resolving with information related to the most recently selected account, which has allowlisted the dApp, or rejecting if the request is rejected in the wallet. If the wallet is locked, then this call prompts the user to first unlock the wallet before accepting or rejecting the connection request. ```typescript const provider = await detectConcordiumProvider(); const accountAddress = await provider.connect(); ``` -N.B. In the current version, if the dApp is already whitelisted, but not by the currently selected account, the returned account will not actually be the most recently selected account, but instead the oldest account that has whitelisted the dApp. +N.B. In the current version, if the dApp is already allowlisted, but not by the currently selected account, the returned account will not actually be the most recently selected account, but instead the oldest account that has allowlisted the dApp. ### getMostRecentlySelectedAccount @@ -94,13 +94,13 @@ const provider = await detectConcordiumProvider(); const genesisHash = await provider.getSelectedChain(); ``` -N.B. In the current version, if the currently selected account has not whitelisted the dApp, the returned account will not actually be the most recently selected account, but instead the oldest account that has whitelisted the dApp. +N.B. In the current version, if the currently selected account has not allowlisted the dApp, the returned account will not actually be the most recently selected account, but instead the oldest account that has allowlisted the dApp. ### sendTransaction To send a transaction, three arguments need to be provided: The account address for the account in the wallet that should sign the transaction, a transaction type and a corresponding payload. Invoking `sendTransaction` returns a `Promise`, which resolves with the transaction hash for the submitted transaction. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the transaction, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the transaction, the `Promise` will reject. The following exemplifies how to create a simple transfer of funds from one account to another. Please note that [@concordium/web-sdk](https://github.com/Concordium/concordium-node-sdk-js/tree/main/packages/web) is used to provide the correct formats and types for the transaction payload. @@ -160,7 +160,7 @@ const parameterSchema = { It is possible to sign arbitrary messages using the keys for an account stored in the wallet, by invoking the `signMessage` method. The first parameter is the account to be used for signing the message. This method returns a `Promise` resolving with a signature of the message. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the meesage, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the meesage, the `Promise` will reject. The message should be either a utf8 string or an object with the following fields: @@ -206,7 +206,7 @@ In this example the user will be shown: It is possible to suggest CIS-2 tokens to be added to an account's display. This method returns a `Promise` resolving with a list containing the ids of the tokens that were added. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects signing the meesage, the `Promise` will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects signing the meesage, the `Promise` will reject. The following exemplifies requesting tokens with id AA and BB from the contract on index 1399, and subindex 0 to the account `2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk`. @@ -236,7 +236,7 @@ provider.addWeb3IdCredential(credential, metadataUrl, async (id) => { It is possible to request a verifiable presentation for a given set of web3Id statements. The function takes 2 arguments. A challenge to ensure that the proof was not generated for a different context, and the statements to be proven. This method returns a Promise resolving with the verifiable presentation for the statements. -If the wallet is locked, or you have not connected with the wallet (or previously been whitelisted) or if the user rejects proving the statement, the Promise will reject. +If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects proving the statement, the Promise will reject. The following exemplifies requesting a verifiable presentation for a statement named myIdStatement (To see how to create a statement check out our documentation) with a challenge of "12346789ABCD". @@ -286,7 +286,7 @@ provider.connect().then((accountAddress) => (selectedAccountAddress = accountAdd The wallet API exposes access to a JSON-RPC client. This allows a dApp to communicate with the same node as the wallet is connected to, and enables dApps to access the JSON-RPC interface without being connected to a separate server itself. The client is accessed as shown in the example below. The dApp does not need to recreate the client again when the wallet changes node or network, the client will always use the wallet's current connected JSON-RPC server. -If you have not connected with the wallet (or previously been whitelisted), the commands will not be executed and the method will throw an error. +If you have not connected with the wallet (or previously been allowlisted), the commands will not be executed and the method will throw an error. ```typescript const provider = await detectConcordiumProvider(); From 206413cbffd0d62cb3d8f41ab64cfe552e46c918 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 16 Aug 2023 09:33:37 +0200 Subject: [PATCH 082/231] Fix no credentials UI issue --- .../VerifiableCredentialList.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 8b780044..32c4c96b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -33,12 +33,16 @@ function LoadingVerifiableCredentials() { * Component to display when there are no verifiable credentials in the wallet. */ function NoVerifiableCredentials() { + const { t } = useTranslation('verifiableCredential'); return ( -
    -
    -

    You do not have any verifiable credentials in your wallet.

    + <> + +
    +
    +

    You do not have any verifiable credentials in your wallet.

    +
    -
    + ); } From 46d9eb1c42fa60ef61e45db664a1261c8a4afe3e Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 16 Aug 2023 09:58:57 +0200 Subject: [PATCH 083/231] Do not allow revoking already revoked credentials --- .../VerifiableCredential/VerifiableCredentialDetails.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index f75c88b8..68424eb4 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -91,7 +91,11 @@ export default function VerifiableCredentialDetails({ }, [client, credential, hdWallet, credentialEntry, nav, pathname]); const menuButton: MenuButton | undefined = useMemo(() => { - if (credentialEntry === undefined || !credentialEntry.credentialInfo.holderRevocable) { + if ( + credentialEntry === undefined || + !credentialEntry.credentialInfo.holderRevocable || + status === VerifiableCredentialStatus.Revoked + ) { return undefined; } From 17c5f805bbc0590b9b68e0f639dd7d06da285d9d Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 16 Aug 2023 11:15:01 +0200 Subject: [PATCH 084/231] Fix test and revert background color --- .../VerifiableCredential.scss | 1 - .../verifiable-credential-helpers.test.ts | 51 ++++++++++--------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 86775559..f90ce37f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -15,7 +15,6 @@ } .verifiable-credential { - background-color: black; color: $color-white; border-radius: rem(16px); box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index 6cf3f722..ac3c4da3 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -8,6 +8,8 @@ import { createCredentialId, createPublicKeyIdentifier, getPublicKeyfromPublicKeyIdentifierDID, + CredentialQueryResponse, + deserializeCredentialEntry, } from '../src/shared/utils/verifiable-credential-helpers'; import { mainnet, testnet } from '../src/shared/constants/networkConfiguration'; @@ -35,31 +37,30 @@ test('serializing a revoke credential holder parameter', () => { ); }); -// TODO Fix this test later. -// test('deserializing a credential entry', () => { -// const serializedCredentialEntry = -// '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d10601300099cc213d3bda6677df0663ec8ec54a2c05ccae0e8b99ba6130f6931ee85c3de04f4d42f0e4bb3f541e5592ade117b0fe26f5c17688010000000c4d7943726564656e7469616c0300666f6f001100687474703a2f2f736368656d612e6f7267000000000000000000'; - -// const expected: CredentialQueryResponse = { -// credentialInfo: { -// credentialHolderId: '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106', -// holderRevocable: true, -// metadataUrl: { -// url: 'foo', -// }, -// validFrom: 1685619602726n, -// validUntil: undefined, -// }, -// schemaRef: { -// schema: { -// url: 'http://schema.org', -// } -// }, -// revocationNonce: 0n, -// }; - -// expect(deserializeCredentialEntry(serializedCredentialEntry)).toEqual(expected); -// }); +test('deserializing a credential entry', () => { + const serializedCredentialEntry = + '22ea01dfab98d77c686358528faade31b88d2866633a31a2d6dd4119cf7a58c401f9ad46f889010000004e0068747470733a2f2f676973742e67697468756275736572636f6e74656e742e636f6d2f6f72686f6a2f32326162376364316161373464633834663766663734643534303136346463342f7261772f001500687474703a2f2f636f6e636f726469756d2e636f6d000100000000000000'; + + const expected: CredentialQueryResponse = { + credentialInfo: { + credentialHolderId: '22ea01dfab98d77c686358528faade31b88d2866633a31a2d6dd4119cf7a58c4', + holderRevocable: true, + metadataUrl: { + url: 'https://gist.githubusercontent.com/orhoj/22ab7cd1aa74dc84f7ff74d540164dc4/raw/', + }, + validFrom: 1692087528953n, + validUntil: undefined, + }, + schemaRef: { + schema: { + url: 'http://concordium.com', + }, + }, + revocationNonce: 1n, + }; + + expect(deserializeCredentialEntry(serializedCredentialEntry)).toEqual(expected); +}); test('credential holder id is extracted from verifiable credential id field', () => { const id = From eafc6db58cc5b965d368adee7dc7a4718fbd1898 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 16 Aug 2023 11:33:35 +0200 Subject: [PATCH 085/231] Only use active web3Id credentials for proofs --- .../VerifiableCredentialStatement.tsx | 5 +-- .../Web3ProofRequest/Web3ProofRequest.tsx | 31 +++++++++++++++++-- .../src/popup/pages/Web3ProofRequest/utils.ts | 22 +++++++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index ae03a7b1..66007140 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -66,7 +66,7 @@ export function useStatementDescription(statement: SecretStatementV2, schema: Ve case StatementTypes.AttributeNotInSet: return t('nonMembership', { name, setNames: listToString(statement.set) }); default: - throw new Error('Unknown statement type encountered: ' + statement.type); + throw new Error(`Unknown statement type encountered: ${statement.type}`); } } @@ -165,6 +165,7 @@ export default function DisplayWeb3Statement({ dappName, setChosenId, net, + statuses, }: DisplayCredentialStatementProps) { const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute @@ -179,7 +180,7 @@ export default function DisplayWeb3Statement({ if (verifiableCredentials.loading) { return []; } - return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value); + return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value, statuses); }, [verifiableCredentials.loading]); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 08d9a825..24a6e5bd 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -8,6 +8,7 @@ import { ConcordiumHdWallet, isAccountCredentialStatement, Web3IdProofInput, + ConcordiumGRPCClient, } from '@concordium/web-sdk'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; @@ -30,8 +31,11 @@ import { } from '@popup/store/verifiable-credential'; import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; import { parse } from '@shared/utils/payload-helpers'; -import { DisplayCredentialStatement } from './DisplayStatement'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; +import { getVerifiableCredentialStatus } from '@shared/utils/verifiable-credential-helpers'; +import { useAsyncMemo } from 'wallet-common-helpers'; import { getCommitmentInput } from './utils'; +import { DisplayCredentialStatement } from './DisplayStatement'; type Props = { onSubmit(presentationString: string): void; @@ -48,6 +52,18 @@ interface Location { }; } +async function getAllCredentialStatuses( + client: ConcordiumGRPCClient, + credentials: VerifiableCredential[] +): Promise> { + const statuses = await Promise.all( + credentials.map((credential) => + getVerifiableCredentialStatus(client, credential.id).then((status) => [credential.id, status]) + ) + ); + return Object.fromEntries(statuses); +} + export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; @@ -73,6 +89,16 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); + // TODO filter so that we only look up VC that are viable for some statement + const statuses = useAsyncMemo( + () => + verifiableCredentials.loading + ? Promise.resolve(undefined) + : getAllCredentialStatuses(client, verifiableCredentials.value), + undefined, + [verifiableCredentials.loading] + ); + const handleSubmit = useCallback(async () => { if (!recoveryPhrase) { throw new Error('Missing recovery phrase'); @@ -124,7 +150,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading) { + if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading || !statuses) { return null; } @@ -144,6 +170,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return newIds; }) } + statuses={statuses} /> )} diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index f8f4fd8a..d961ce97 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -141,11 +141,13 @@ export async function buildRevokeTransactionParameters( nonce: bigint, signingKey: string ) { + const fiveMinutesInMilliseconds = 5 * 60000; + const signatureExpirationTimestamp = BigInt(Date.now() + fiveMinutesInMilliseconds); const signingData: SigningData = { contractAddress: address, entryPoint: 'revokeCredentialHolder', nonce, - timestamp: BigInt(Date.now() + 5 * 60000), + timestamp: signatureExpirationTimestamp, }; const data: RevocationDataHolder = { From a3ec363ad7d88a8535966d78051c16cfc4e57308 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 17 Aug 2023 13:52:08 +0200 Subject: [PATCH 091/231] 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 5280c91e..75341d4e 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 963896e2..025b14c9 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 9de2a1cf..416c3c2e 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 092/231] 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 a50992b9..eda5c20f 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 792b59cd..f13449d8 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 093/231] 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 f2fa3038..493e3492 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. From 682704f88fb4bca9254bec59f0512b4773c1c755 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 17 Aug 2023 14:23:52 +0200 Subject: [PATCH 094/231] Improve handling when unable to prove web3IdProof --- .../Web3ProofRequest/AccountStatement.tsx | 37 +++-------- .../Web3ProofRequest/DisplayStatement.tsx | 20 +++++- .../VerifiableCredentialStatement.tsx | 24 ++----- .../Web3ProofRequest/Web3ProofRequest.tsx | 66 ++++++++++++++++--- .../popup/pages/Web3ProofRequest/i18n/en.ts | 2 + .../src/popup/pages/Web3ProofRequest/utils.ts | 4 +- 6 files changed, 90 insertions(+), 63 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index a8e20536..f636598c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -6,22 +6,15 @@ import { AttributeKey, AttributeList, } from '@concordium/web-sdk'; -import { displaySplitAddress } from '@popup/shared/utils/account-helpers'; -import { - useConfirmedIdentities, - useDisplayAttributeValue, - useGetAttributeName, -} from '@popup/shared/utils/identity-helpers'; -import { credentialsAtom } from '@popup/store/account'; +import { displaySplitAddress, useIdentityOf } from '@popup/shared/utils/account-helpers'; +import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; import { WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; -import { useAtomValue } from 'jotai'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; -import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; import CredentialSelector from './CredentialSelector'; -import { DisplayCredentialStatementProps, getViableAccountCredentialsForStatement, SecretStatementV2 } from './utils'; +import { DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; import { isoToCountryName, SecretStatement, @@ -99,10 +92,11 @@ export function DisplayRevealStatementV2({ dappName, statements, identity, class export default function AccountStatement({ credentialStatement, + validCredentials, dappName, setChosenId, net, -}: DisplayCredentialStatementProps) { +}: DisplayCredentialStatementProps) { const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute ) as RevealStatementV2[]; @@ -110,17 +104,9 @@ export default function AccountStatement({ (s) => s.type !== StatementTypes.RevealAttribute ) as SecretStatementV2[]; - const identities = useConfirmedIdentities(); - const credentials = useAtomValue(credentialsAtom); - - const validCredentials = useMemo(() => { - if (identities.loading) { - return []; - } - return getViableAccountCredentialsForStatement(credentialStatement, identities.value, credentials); - }, [credentialStatement.idQualifier.issuers]); - const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + // We do the type cast, because the check should have been done to filter validCredentials. + const identity = useIdentityOf(chosenCredential) as ConfirmedIdentity | undefined; const onChange = useCallback((credential: WalletCredential) => { setChosenCredential(credential); @@ -134,13 +120,6 @@ export default function AccountStatement({ } }, []); - const identity = useMemo(() => { - if (!chosenCredential) { - return undefined; - } - return identities.value.find((id) => isIdentityOfCredential(id)(chosenCredential)); - }, [chosenCredential?.credId]); - return (
    ) { +}: DisplayCredentialStatementProps) { if (isAccountCredentialStatement(credentialStatement)) { - return ; + return ( + + ); } if (isVerifiableCredentialStatement(credentialStatement)) { - return ; + return ( + + ); } throw new Error('Invalid Statement encountered'); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 66007140..f2d7c6c2 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -4,10 +4,7 @@ import { StatementTypes, VerifiableCredentialStatement, } from '@concordium/web-sdk'; -import { - storedVerifiableCredentialsAtom, - storedVerifiableCredentialSchemasAtom, -} from '@popup/store/verifiable-credential'; +import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { VerifiableCredential, CredentialSubject, @@ -23,12 +20,7 @@ import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplaySt import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; import { useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; -import { - createWeb3IdDIDFromCredential, - DisplayCredentialStatementProps, - getViableWeb3IdCredentialsForStatement, - SecretStatementV2, -} from './utils'; +import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { // TODO use localization here @@ -162,27 +154,19 @@ export function DisplayWeb3SecretStatement({ export default function DisplayWeb3Statement({ credentialStatement, + validCredentials, dappName, setChosenId, net, - statuses, -}: DisplayCredentialStatementProps) { +}: DisplayCredentialStatementProps) { const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute ) as RevealStatementV2[]; const secrets = credentialStatement.statement.filter( (s) => s.type !== StatementTypes.RevealAttribute ) as SecretStatementV2[]; - const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); - const validCredentials = useMemo(() => { - if (verifiableCredentials.loading) { - return []; - } - return getViableWeb3IdCredentialsForStatement(credentialStatement, verifiableCredentials.value, statuses); - }, [verifiableCredentials.loading]); - const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); const onChange = useCallback((credential: VerifiableCredential) => { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 8c10d982..2b155cb5 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -10,6 +10,7 @@ import { Web3IdProofInput, ConcordiumGRPCClient, CommitmentInput, + isVerifiableCredentialStatement, } from '@concordium/web-sdk'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; @@ -35,7 +36,12 @@ import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialStatus } from '@shared/utils/verifiable-credential-helpers'; import { useAsyncMemo } from 'wallet-common-helpers'; -import { getAccountCredentialCommitmentInput, getWeb3CommitmentInput } from './utils'; +import { + getAccountCredentialCommitmentInput, + getViableAccountCredentialsForStatement, + getViableWeb3IdCredentialsForStatement, + getWeb3CommitmentInput, +} from './utils'; import { DisplayCredentialStatement } from './DisplayStatement'; type Props = { @@ -65,6 +71,24 @@ async function getAllCredentialStatuses( return Object.fromEntries(statuses); } +function DisplayNotProvable({ onClick, dappName }: { onClick: () => void; dappName: string }) { + const { t } = useTranslation('web3IdProofRequest'); + + return ( + +
    +

    {t('unableToProve', { dappName })}

    + + + + +
    +
    + ); +} + export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const { state } = useLocation() as Location; const { statements: rawStatements, challenge, url } = state.payload; @@ -88,8 +112,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const credentials = useAtomValue(credentialsAtom); const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); - const canProve = useMemo(() => ids.every((x) => Boolean(x)), [ids]); - // TODO filter so that we only look up VC that are viable for some statement const statuses = useAsyncMemo( () => @@ -100,6 +122,26 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { [verifiableCredentials.loading] ); + const validCredentials = useMemo(() => { + if (identities.loading || verifiableCredentials.loading || !statuses) { + return undefined; + } + return statements.map((statement) => { + if (isAccountCredentialStatement(statement)) { + return getViableAccountCredentialsForStatement(statement, identities.value, credentials); + } + if (isVerifiableCredentialStatement(statement)) { + return getViableWeb3IdCredentialsForStatement(statement, verifiableCredentials.value, statuses); + } + throw new Error('Unknown statement type'); + }); + }, [identities.loading, verifiableCredentials.loading, Boolean(statuses)]); + + const canProve = useMemo( + () => validCredentials && validCredentials.every((x) => x.length > 0), + [Boolean(validCredentials)] + ); + const handleSubmit = useCallback(async () => { if (!recoveryPhrase) { throw new Error('Missing recovery phrase'); @@ -107,9 +149,6 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { if (!network) { throw new Error('Network is not specified'); } - if (!canProve) { - throw new Error('The statements are not satisfied and cannot be proven'); - } const global = await getGlobal(client); const wallet = ConcordiumHdWallet.fromHex(recoveryPhrase, net); @@ -157,20 +196,30 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { throw new Error(result.reason); } return result.proof; - }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading, canProve]); + }, [recoveryPhrase, network, ids, verifiableCredentials.loading, identities.loading]); useEffect(() => onClose(onReject), [onClose, onReject]); - if (verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading || !statuses) { + if ( + verifiableCredentials.loading || + verifiableCredentialSchemas.loading || + identities.loading || + !validCredentials + ) { return null; } + if (!canProve) { + return ; + } + return (
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 5a46362d..9b030d36 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -32,10 +32,12 @@ const t: typeof en = { membership: 'Dette vil bevise at deres {{ name }} er en af følgende:\n{{ setNames }}', nonMembership: 'This will prove that your {{ name }} er IKKE en af følgende:\n{{ setNames }}', missingAttribute: 'Denne Attribut kan ikke findes på identiteten "{{identityName}}"', - } + }, }, failedProof: 'Bevis kunne ikke oprettes', failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', + unableToProve: + ' {{ dappName }} har anmodet et bevis for identitet fra dig, men du opfølger ikke kravene for beviset, så du kan ikke lave et bevis', }; export default t; From 4750a2701b1551b3cd243074333d7279826ff2b5 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 17 Aug 2023 14:58:07 +0200 Subject: [PATCH 097/231] Manually appease linter --- packages/browser-wallet/src/background/id-proof.ts | 7 +------ packages/browser-wallet/src/background/index.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/browser-wallet/src/background/id-proof.ts b/packages/browser-wallet/src/background/id-proof.ts index d8b9a446..efeaa569 100644 --- a/packages/browser-wallet/src/background/id-proof.ts +++ b/packages/browser-wallet/src/background/id-proof.ts @@ -1,9 +1,4 @@ -import { - getIdProof, - IdProofInput, - IdProofOutput, - verifyIdstatement, -} from '@concordium/web-sdk'; +import { getIdProof, IdProofInput, IdProofOutput, verifyIdstatement } from '@concordium/web-sdk'; import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; import { isHex } from 'wallet-common-helpers'; diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 60421e4f..6bf6592b 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -39,7 +39,13 @@ import { setPopupSize, testPopupOpen, } from './window-management'; -import {runIfValidWeb3IdCredentialRequest, web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof } from './web3Id'; +import { + runIfValidWeb3IdCredentialRequest, + web3IdAddCredentialFinishHandler, + createWeb3IdProofHandler, + runIfValidWeb3IdProof, +} from './web3Id'; + const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; const walletLockedMessage = 'The wallet is locked'; async function isWalletLocked(): Promise { @@ -252,7 +258,10 @@ bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GrpcRequest), bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateIdProof), createIdProofHandler); -bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), createWeb3IdProofHandler); +bgMessageHandler.handleMessage( + createMessageTypeFilter(InternalMessageType.CreateWeb3IdProof), + createWeb3IdProofHandler +); const NOT_WHITELISTED = 'Site is not whitelisted'; From 1b2d61cc004354282779ae7d32ed5ad5a4394343 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 17 Aug 2023 16:30:33 +0200 Subject: [PATCH 098/231] Adjust scrollbar to improve list view --- .../VerifiableCredential/VerifiableCredential.scss | 14 +++++--------- .../src/popup/styles/elements/_base.scss | 7 +++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index eda5c20f..66ebf379 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -2,15 +2,12 @@ height: calc(100% - 56px); background-color: $color-bg; overflow-y: auto; + scrollbar-gutter: stable; &__card { - margin-left: rem(16px); - margin-right: rem(16px); - margin-bottom: rem(16px); - - &:not(:first-child) { - margin-top: rem(16px); - } + margin-left: 24px; + margin-right: calc(24px - $scrollbar-width); + margin-bottom: 24px; } } @@ -68,8 +65,7 @@ } &__image { - margin-left: rem(8px); - margin-right: rem(8px); + margin: auto; height: 120px; width: 308px; diff --git a/packages/browser-wallet/src/popup/styles/elements/_base.scss b/packages/browser-wallet/src/popup/styles/elements/_base.scss index e4d41cb7..5f9eb660 100644 --- a/packages/browser-wallet/src/popup/styles/elements/_base.scss +++ b/packages/browser-wallet/src/popup/styles/elements/_base.scss @@ -95,10 +95,13 @@ body { } // Scrollbar section inspired by https://stackoverflow.com/questions/21684101/css-vertical-scrollbar-padding-left-right-in-ul-possible + +$scrollbar-width: 16px; + ::-webkit-scrollbar { // TODO cross-browser compatibility - width: rem(12px); - height: rem(12px); + width: $scrollbar-width; + height: 16px; } ::-webkit-scrollbar-thumb { From 157a17be94a39b990e051502dc0dc148a630ad5e Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 17 Aug 2023 17:58:42 +0200 Subject: [PATCH 099/231] improve support for number attributes --- packages/browser-wallet-api/src/wallet-api.ts | 2 +- packages/browser-wallet/src/background/web3Id.ts | 4 ++-- .../AddWeb3IdCredential/AddWeb3IdCredential.tsx | 6 ++++-- .../VerifiableCredentialStatement.tsx | 2 +- .../pages/Web3ProofRequest/Web3ProofRequest.tsx | 3 ++- .../src/popup/pages/Web3ProofRequest/utils.ts | 12 +++++++++++- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index ca04ecd5..aceef2cf 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -259,7 +259,7 @@ class WalletApi extends EventEmitter implements IWalletApi { const res = await this.messageHandler.sendMessage>( MessageType.AddWeb3IdCredential, { - credential, + credential: stringify(credential), metadataUrl, } ); diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index e6c54081..aafc88ad 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -103,7 +103,7 @@ export async function web3IdAddCredentialFinishHandler(input: { * Run condition which ensures that the web3IdCredential request is valid. */ export const runIfValidWeb3IdCredentialRequest: RunCondition> = async (msg) => { - const { credential }: { credential: APIVerifiableCredential } = msg.payload; + const credential: APIVerifiableCredential = parse(msg.payload.credential); const network = await storedCurrentNetwork.get(); if (!network) { @@ -151,7 +151,7 @@ async function createWeb3Proof(input: Web3IdProofInput): Promise { - createWeb3Proof(msg.payload) + createWeb3Proof(parse(msg.payload)) .then(respond) .catch((e: Error) => respond({ status: BackgroundResponseStatus.Error, error: e.message })); return true; diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index b8ea98d7..b1e91024 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -25,6 +25,7 @@ import { import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; import { networkConfigurationAtom } from '@popup/store/settings'; import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; +import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; type Props = { @@ -36,7 +37,7 @@ interface Location { state: { payload: { url: string; - credential: APIVerifiableCredential; + credential: string; metadataUrl: MetadataUrl; }; }; @@ -58,7 +59,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const [error, setError] = useState(); - const { credential, url, metadataUrl } = state.payload; + const { credential: rawCredential, url, metadataUrl } = state.payload; + const credential: APIVerifiableCredential = parse(rawCredential); useEffect(() => onClose(onReject), [onClose, onReject]); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index f2d7c6c2..b8a4849c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -25,7 +25,7 @@ import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretS function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { // TODO use localization here const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; - return property.title; + return property ? property.title : attributeTag; } function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 8712e8ba..c04788cd 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -36,6 +36,7 @@ import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialStatus } from '@shared/utils/verifiable-credential-helpers'; import { useAsyncMemo } from 'wallet-common-helpers'; +import { stringify } from '@concordium/browser-wallet-api/src/util'; import { getAccountCredentialCommitmentInput, getViableAccountCredentialsForStatement, @@ -189,7 +190,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const result: ProofBackgroundResponse = await popupMessageHandler.sendInternalMessage( InternalMessageType.CreateWeb3IdProof, - input + stringify(input) ); if (result.status !== BackgroundResponseStatus.Success) { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 86040d5b..ea2e8d9c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -101,7 +101,17 @@ export function getViableAccountCredentialsForStatement( // TODO Replace with canProveAtomicStatement when SDK is updated function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { - const value = cred.credentialSubject.attributes[statement.attributeTag]; + let value = cred.credentialSubject.attributes[statement.attributeTag]; + + // temporary handling of numbers saved as numbers; + if (typeof value === 'number') { + value = BigInt(value); + } + + if (value === undefined) { + return false; + } + switch (statement.type) { case StatementTypes.AttributeInRange: return statement.lower <= value && statement.upper > value; From b258d37e54aee538efca866dd0b67f22df874b23 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 18 Aug 2023 11:24:44 +0200 Subject: [PATCH 100/231] Display issuer metadata in credential details page --- .../VerifiableCredential.scss | 5 ++ .../VerifiableCredentialDetails.tsx | 56 +++++++++++++++- .../VerifiableCredentialHooks.tsx | 25 +++++++ .../pages/VerifiableCredential/i18n/da.ts | 6 ++ .../pages/VerifiableCredential/i18n/en.ts | 6 ++ .../utils/verifiable-credential-helpers.ts | 65 ++++++++++++++++++- 6 files changed, 160 insertions(+), 3 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 66ebf379..ad7a30b7 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -11,6 +11,11 @@ } } +.issuer-logo { + width: rem(28px); + height: rem(28px); +} + .verifiable-credential { color: $color-white; border-radius: rem(16px); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 45905a33..f83785fe 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -20,12 +20,62 @@ import { import { fetchContractName } from '@shared/utils/token-helpers'; import { TimeStampUnit, dateFromTimestamp, ClassName } from 'wallet-common-helpers'; import { withDateAndTime } from '@shared/utils/time-helpers'; +import Img from '@popup/shared/Img'; import { accountRoutes } from '../Account/routes'; import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer'; import RevokeIcon from '../../../assets/svg/revoke.svg'; -import { useCredentialEntry } from './VerifiableCredentialHooks'; +import { useCredentialEntry, useIssuerMetadata } from './VerifiableCredentialHooks'; import { DisplayAttribute, VerifiableCredentialCard, VerifiableCredentialCardHeader } from './VerifiableCredentialCard'; +/** + * Component for displaying issuer metadata, if any is available. + * All the fields in the issuer metadata are optional, and if none of + * them are provided, then the component returns null. + * @param issuer the did for the issuer + */ +function DisplayIssuerMetadata({ issuer }: { issuer: string }) { + const { t } = useTranslation('verifiableCredential'); + const issuerMetadata = useIssuerMetadata(issuer); + + if ( + issuerMetadata === undefined || + (issuerMetadata.description === undefined && + issuerMetadata.icon === undefined && + issuerMetadata.name === undefined && + issuerMetadata.url === undefined) + ) { + return null; + } + + return ( +
    +

    {t('details.issuer.title')}

    + {issuerMetadata.icon && } + {issuerMetadata.name && ( + + )} + {issuerMetadata.description && ( + + )} + {issuerMetadata.url && ( + + )} +
    + ); +} + /** * 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. @@ -35,10 +85,12 @@ function VerifiableCredentialExtraDetails({ status, metadata, className, + issuer, }: { credentialEntry: CredentialQueryResponse; status: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; + issuer: string; } & ClassName) { const { t } = useTranslation('verifiableCredential'); @@ -72,6 +124,7 @@ function VerifiableCredentialExtraDetails({ /> )}
    +
    ); @@ -194,6 +247,7 @@ export default function VerifiableCredentialDetails({ credentialEntry={credentialEntry} status={status} metadata={metadata} + issuer={credential.issuer} /> )} {!showExtraDetails && ( diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 01626333..107b7def 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -2,9 +2,12 @@ import { grpcClientAtom } from '@popup/store/settings'; import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; import { CredentialQueryResponse, + IssuerMetadata, VerifiableCredentialMetadata, + fetchIssuerMetadata, getCredentialHolderId, getCredentialRegistryContractAddress, + getCredentialRegistryMetadata, getVerifiableCredentialEntry, getVerifiableCredentialStatus, } from '@shared/utils/verifiable-credential-helpers'; @@ -108,6 +111,28 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { return metadata; } +/** + * Retrieves the issuer metadata JSON file. This is done by getting the credential + * registry metadata from the credential registry contract, and then fetching the + * issuer metadata JSON file at the extracted URL. + * @param issuer the issuer did + * @returns the issuer metadata for the provided issuer did + */ +export function useIssuerMetadata(issuer: string): IssuerMetadata | undefined { + const [issuerMetadata, setIssuerMetadata] = useState(); + const client = useAtomValue(grpcClientAtom); + + useEffect(() => { + const registryContractAddress = getCredentialRegistryContractAddress(issuer); + getCredentialRegistryMetadata(client, registryContractAddress).then((res) => { + const abortController = new AbortController(); + fetchIssuerMetadata(res.issuerMetadata, abortController).then(setIssuerMetadata); + }); + }, [client, issuer]); + + return issuerMetadata; +} + /** * Retrieves data and uses the provided data setter to update chrome.storage with the changes found. * The dataFetcher is responsible for delivering the exact updated picture that should be set. 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 59d8f243..bc6534ef 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -13,6 +13,12 @@ const t: typeof en = { id: 'Legitimationholders ID', validFrom: 'Gyldig fra', validUntil: 'Gyldig indtil', + issuer: { + title: 'Udstedt af', + name: 'Navn', + description: 'Beskrivelse', + url: 'Hjemmeside', + }, }, status: { Active: 'Aktiv', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts index f88d5d75..d561da2f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -11,6 +11,12 @@ const t = { id: 'Credential holder ID', validFrom: 'Valid from', validUntil: 'Valid until', + issuer: { + title: 'Issued by', + name: 'Name', + description: 'Description', + url: 'Website', + }, }, status: { Active: 'Active', diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 93cc5c08..d9c37e46 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -335,7 +335,7 @@ function deserializeRegistryMetadata(serializedRegistryMetadata: string): Metada export async function getCredentialRegistryMetadata(client: ConcordiumGRPCClient, contractAddress: ContractAddress) { const instanceInfo = await client.getInstanceInfo(contractAddress); if (instanceInfo === undefined) { - return undefined; + throw new Error('Given contract address was not a created instance'); } const result = await client.invokeContract({ @@ -491,6 +491,47 @@ const verifiableCredentialMetadataSchema = { }, }; +const issuerMetadataSchema = { + $ref: '#/definitions/IssuerMetadata', + $schema: 'http://json-schema.org/draft-07/schema#', + definitions: { + HexString: { + type: 'string', + }, + IssuerMetadata: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + icon: { + $ref: '#/definitions/MetadataUrl', + }, + name: { + type: 'string', + }, + url: { + type: 'string', + }, + }, + type: 'object', + }, + MetadataUrl: { + additionalProperties: false, + properties: { + hash: { + $ref: '#/definitions/HexString', + }, + url: { + type: 'string', + }, + }, + required: ['url'], + type: 'object', + }, + }, +}; + export interface VerifiableCredentialMetadata { title: string; logo: MetadataUrl; @@ -606,7 +647,10 @@ export async function getVerifiableCredentialEntry( async function fetchDataFromUrl( { url, hash }: MetadataUrl, abortController: AbortController, - jsonSchema: typeof verifiableCredentialMetadataSchema | typeof verifiableCredentialSchemaSchema + jsonSchema: + | typeof verifiableCredentialMetadataSchema + | typeof verifiableCredentialSchemaSchema + | typeof issuerMetadataSchema ): Promise { const response = await fetch(url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), @@ -663,6 +707,23 @@ export async function fetchCredentialMetadata( return fetchDataFromUrl(metadata, abortController, verifiableCredentialMetadataSchema); } +export interface IssuerMetadata { + name?: string; + icon?: MetadataUrl; + description?: string; + url?: string; +} + +/** + * Retrieves registry issuer metadata from the specified URL. + */ +export async function fetchIssuerMetadata( + metadata: MetadataUrl, + abortController: AbortController +): Promise { + return fetchDataFromUrl(metadata, abortController, issuerMetadataSchema); +} + /** * Retrieves credential schemas for each of the provided credentials. The method ensures * that duplicate schemas are not fetched multiple times, by only fetching once per From d4af90b7e25b0d1f6c27b6e1d02c415b6458d649 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 18 Jul 2023 11:21:25 +0200 Subject: [PATCH 101/231] Backup in progress --- .../src/popup/constants/routes.ts | 1 + .../VerifiableCredentialList.tsx | 10 ++- .../VerifiableCredentialBackup.tsx | 82 +++++++++++++++++++ .../browser-wallet/src/popup/shell/Routes.tsx | 5 ++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index e88ee4e3..1269a9f5 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -20,6 +20,7 @@ export const relativeRoutes = { }, verifiableCredentials: { path: 'verifiable-credentials', + backup: { path: 'backup' }, }, settings: { path: 'settings', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index d2e0e9bc..8840352e 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -13,6 +13,8 @@ import { getChangesToCredentialMetadata, getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; +import { Link } from 'react-router-dom'; +import { absoluteRoutes } from '@popup/constants/routes'; import { useCredentialMetadata, useCredentialSchema, @@ -118,8 +120,14 @@ export default function VerifiableCredentialList() { if (verifiableCredentials.loading) { return ; } + if (verifiableCredentials.value.length === 0) { - return ; + return ( + <> + Backup + + + ); } if (selected) { diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx new file mode 100644 index 00000000..c3cf4aba --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { + storedVerifiableCredentialSchemasAtom, + storedVerifiableCredentialsAtom, +} from '@popup/store/verifiable-credential'; +import { useAtom, useAtomValue } from 'jotai'; +import { NetworkConfiguration, VerifiableCredential } from '@shared/storage/types'; +import Button from '@popup/shared/Button'; +import { getNet } from '@shared/utils/network-helpers'; +import { networkConfigurationAtom } from '@popup/store/settings'; +import { saveData } from '@popup/shared/utils/file-helpers'; + +type VerifiableCredentialExport = { + credentials: VerifiableCredential[]; +}; + +type ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials'; + v: number; + environment: string; // 'testnet' or 'mainnet' + value: VerifiableCredentialExport; +}; + +function createPlainExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { + const docContent: ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials', + v: 0, + environment: getNet(network).toLowerCase(), + value: { + credentials: verifiableCredentials, + }, + }; + + return docContent; +} + +function createExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { + const plain = createPlainExport(verifiableCredentials, network); + // TODO Encrypt + return plain; +} + +/** + * Renders all verifiable credentials that are in the wallet. The credentials + * are selectable by clicking them, which will move the user to a view containing + * a single credential. + */ +export default function VerifiableCredentialList() { + const [verifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const network = useAtomValue(networkConfigurationAtom); + + if (schemas.loading || !verifiableCredentials) { + return null; + } + + const handleExport = () => { + const data = createExport(verifiableCredentials, network); + saveData(data, `web3IdCredentials.export`); + }; + + const handleImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const backup: ExportFormat = JSON.parse(await file.text()); + // TODO Decrypt + const credentials = backup.value.credentials.filter( + (cred) => !(verifiableCredentials || []).some((existing) => existing.id === cred.id) + ); + setVerifiableCredentials([...verifiableCredentials, ...credentials]); + } + }; + + return ( +
    + + handleImport(x)} /> +
    + ); +} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index f6c3eda0..6c676815 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -34,6 +34,7 @@ import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; import Web3ProofRequest from '@popup/pages/Web3ProofRequest'; +import VerifiableCredentialBackup from '@popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; @@ -233,6 +234,10 @@ export default function Routes() { path={`${relativePath(relativeRoutes.home.path, absoluteRoutes.home.identities.add.path)}/*`} /> } path={relativeRoutes.home.identities.path} /> + } + path={absoluteRoutes.home.verifiableCredentials.backup.path} + /> } path={relativeRoutes.home.verifiableCredentials.path} /> } /> From 4b413978660436fa0a197189203fa9fd2a93e119 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 17 Aug 2023 13:43:34 +0200 Subject: [PATCH 102/231] Simple import/export added --- .../browser-wallet-message-hub/src/message.ts | 2 + .../browser-wallet/src/background/index.ts | 3 + .../browser-wallet/src/background/web3Id.ts | 21 ++++- .../src/popup/constants/routes.ts | 3 + .../VerifiableCredentialList.tsx | 2 +- .../VerifiableCredentialBackup.tsx | 38 +++------ .../VerifiableCredentialImport.tsx | 83 +++++++++++++++++++ .../VerifiableCredentialBackup/i18n/en.ts | 7 ++ .../pages/VerifiableCredentialBackup/utils.ts | 12 +++ .../browser-wallet/src/popup/shell/Routes.tsx | 3 + .../src/popup/shell/i18n/locales/da.ts | 3 + .../src/popup/shell/i18n/locales/en.ts | 2 + 12 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts index 6e2dcfea..d92570a9 100644 --- a/packages/browser-wallet-message-hub/src/message.ts +++ b/packages/browser-wallet-message-hub/src/message.ts @@ -45,6 +45,8 @@ export enum InternalMessageType { CreateWeb3IdProof = 'I_CreateWeb3IdProof', ConnectAccounts = 'I_ConnectAccounts', AddWeb3IdCredential = 'I_AddWeb3IdCredential', + LoadWeb3IdBackup = 'I_LoadWeb3IdBackup', + ImportWeb3IdBackup = 'I_ImportWeb3IdBackup', } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index 6bf6592b..f0668e72 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -44,6 +44,7 @@ import { web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof, + loadWeb3IdBackupHandler } from './web3Id'; const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; @@ -526,6 +527,8 @@ bgMessageHandler.handleMessage( } ); +bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.LoadWeb3IdBackup), loadWeb3IdBackupHandler); + function withPromptStart(): RunCondition> { return async () => { const isPromptOpen = await sessionOpenPrompt.get(); diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index e6c54081..32a1b69a 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -23,11 +23,16 @@ import { getDIDNetwork, getPublicKeyfromPublicKeyIdentifierDID, } from '@shared/utils/verifiable-credential-helpers'; -import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub'; -import { getNet } from '@shared/utils/network-helpers'; import { parse } from '@shared/utils/payload-helpers'; import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types'; -import { RunCondition } from './window-management'; +import { + ExtensionMessageHandler, + InternalMessageType, + MessageStatusWrapper, +} from '@concordium/browser-wallet-message-hub'; +import { getNet } from '@shared/utils/network-helpers'; +import { openWindow, RunCondition } from './window-management'; +import bgMessageHandler from './message-handler'; const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; @@ -187,4 +192,14 @@ export const runIfValidWeb3IdProof: RunCondition response: { success: false, message: `Statement is not well-formed: ${(e as Error).message}` }, }; } +} + +async function loadWeb3IdBackup(): Promise { + await openWindow(); + bgMessageHandler.sendInternalMessage(InternalMessageType.ImportWeb3IdBackup); +} + +export const loadWeb3IdBackupHandler: ExtensionMessageHandler = (_msg, _sender, respond) => { + loadWeb3IdBackup(); + respond(undefined); }; diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index 1269a9f5..e3d4f9e7 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -64,6 +64,9 @@ export const relativeRoutes = { recovery: { path: 'recovery', }, + importWeb3IdBackup: { + path: 'import-web3Id-Backup', + }, addTokens: { path: 'add-tokens', }, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 8840352e..b415f618 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -48,7 +48,7 @@ function NoVerifiableCredentials() { ); } -function VerifiableCredentialCardWithStatusFromChain({ +export function VerifiableCredentialCardWithStatusFromChain({ credential, onClick, className, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx index c3cf4aba..0c92204a 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -9,17 +9,9 @@ import Button from '@popup/shared/Button'; import { getNet } from '@shared/utils/network-helpers'; import { networkConfigurationAtom } from '@popup/store/settings'; import { saveData } from '@popup/shared/utils/file-helpers'; - -type VerifiableCredentialExport = { - credentials: VerifiableCredential[]; -}; - -type ExportFormat = { - type: 'concordium-browser-wallet-verifiable-credentials'; - v: number; - environment: string; // 'testnet' or 'mainnet' - value: VerifiableCredentialExport; -}; +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; +import { ExportFormat } from './utils'; function createPlainExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { const docContent: ExportFormat = { @@ -46,37 +38,31 @@ function createExport(verifiableCredentials: VerifiableCredential[], network: Ne * a single credential. */ export default function VerifiableCredentialList() { - const [verifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); + const [verifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const network = useAtomValue(networkConfigurationAtom); - if (schemas.loading || !verifiableCredentials) { + if (schemas.loading || verifiableCredentials.loading) { return null; } const handleExport = () => { - const data = createExport(verifiableCredentials, network); + const data = createExport(verifiableCredentials.value, network); saveData(data, `web3IdCredentials.export`); }; - const handleImport = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - const backup: ExportFormat = JSON.parse(await file.text()); - // TODO Decrypt - const credentials = backup.value.credentials.filter( - (cred) => !(verifiableCredentials || []).some((existing) => existing.id === cred.id) - ); - setVerifiableCredentials([...verifiableCredentials, ...credentials]); - } + const handleImport = () => { + popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup).then(() => window.close()); }; return (
    + - handleImport(x)} />
    ); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx new file mode 100644 index 00000000..0b89729f --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -0,0 +1,83 @@ +import React, { useContext, useState } from 'react'; +import { + storedVerifiableCredentialSchemasAtom, + storedVerifiableCredentialsAtom, +} from '@popup/store/verifiable-credential'; +import { useAtom, useAtomValue } from 'jotai'; +import PageHeader from '@popup/shared/PageHeader'; +import { VerifiableCredential } from '@shared/storage/types'; +import { useTranslation } from 'react-i18next'; +import Button from '@popup/shared/Button'; +import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; +import { noOp } from 'wallet-common-helpers'; +import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; +import { ExportFormat } from './utils'; + +function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { + const { t } = useTranslation('verifiableCredentialBackup'); + const { withClose } = useContext(fullscreenPromptContext); + + return ( + <> + {t('import.title')} +
    + {imported.length === 0 && <>{t('import.noImported')}} + {imported.map((credential) => { + return ( + + ); + })} +
    + + + ); +} + +/** + * Renders all verifiable credentials that are in the wallet. The credentials + * are selectable by clicking them, which will move the user to a view containing + * a single credential. + */ +export default function VerifiableCredentialImport() { + const [verifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); + const [imported, setImported] = useState(); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const { t } = useTranslation('verifiableCredentialBackup'); + const { withClose } = useContext(fullscreenPromptContext); + + if (schemas.loading || verifiableCredentials.loading) { + return null; + } + + const handleImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const backup: ExportFormat = JSON.parse(await file.text()); + // TODO error handling + // TODO validation + // TODO Decrypt + const credentials = backup.value.credentials.filter( + (cred) => !(verifiableCredentials.value || []).some((existing) => existing.id === cred.id) + ); + setVerifiableCredentials([...verifiableCredentials.value, ...credentials]); + setImported(credentials); + } + }; + + if (imported) { + return ; + } + + return ( + <> + {t('import.title')} +
    + handleImport(x)} /> +
    + + + ); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts new file mode 100644 index 00000000..8da146b2 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -0,0 +1,7 @@ +export default { + import: { + title: 'import', + noImported: 'No verifiable credentials were imported', + }, + close: 'Close', +}; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts new file mode 100644 index 00000000..94d3eb7a --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts @@ -0,0 +1,12 @@ +import { VerifiableCredential } from '@shared/storage/types'; + +export type VerifiableCredentialExport = { + credentials: VerifiableCredential[]; +}; + +export type ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials'; + v: number; + environment: string; // 'testnet' or 'mainnet' + value: VerifiableCredentialExport; +}; diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 6c676815..6c31cb0c 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -38,6 +38,7 @@ import VerifiableCredentialBackup from '@popup/pages/VerifiableCredentialBackup/ import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; +import VerifiableCredentialImport from '@popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport'; type PromptKey = keyof Omit; @@ -120,6 +121,7 @@ export default function Routes() { usePrompt(InternalMessageType.EndIdentityIssuance, 'endIdentityIssuance'); usePrompt(InternalMessageType.RecoveryFinished, 'recovery'); + usePrompt(InternalMessageType.ImportWeb3IdBackup, 'importWeb3IdBackup'); useEffect(() => { popupMessageHandler.sendInternalMessage(InternalMessageType.PopupReady).catch(noOp); @@ -223,6 +225,7 @@ export default function Routes() { /> } /> } /> + } /> } /> } path={relativeRoutes.recovery.path} /> diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index b20015ec..afe8db84 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -23,6 +23,8 @@ import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da' import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/da'; import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/da'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/da'; +// TODO dansk +import verifiableCredentialBackup from '@popup/pages/VerifiableCredentialBackup/i18n/en'; import type en from './en'; @@ -52,6 +54,7 @@ const t: typeof en = { addWeb3IdCredential, web3IdProofRequest, verifiableCredential, + verifiableCredentialBackup }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index a6161009..5d491f7e 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -23,6 +23,7 @@ import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en' import web3IdProofRequest from '@popup/pages/Web3ProofRequest/i18n/en'; import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/en'; import addWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/i18n/en'; +import verifiableCredentialBackup from '@popup/pages/VerifiableCredentialBackup/i18n/en'; const t = { shared, @@ -50,6 +51,7 @@ const t = { addWeb3IdCredential, web3IdProofRequest, verifiableCredential, + verifiableCredentialBackup, }; export default t; From c896144e365e0b23435347c5312ceb93d19cf075 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 11:18:13 +0200 Subject: [PATCH 103/231] Web3Id Backup now encrypts with dummy key + some styling --- .../VerifiableCredentialBackup.scss | 32 ++++++++++ .../VerifiableCredentialBackup.tsx | 32 ++++++---- .../VerifiableCredentialImport.tsx | 62 ++++++++++++------- .../VerifiableCredentialBackup/i18n/en.ts | 2 +- .../src/popup/styles/_components.scss | 1 + 5 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss new file mode 100644 index 00000000..ad37b58a --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss @@ -0,0 +1,32 @@ +$header-height: rem(56px); + +.verifiable-credential-backup { + overflow-y: auto; + + &__card { + margin: rem(16px); + } +} + +.verifiable-credential-import { + display: flex; + flex-direction: column; + height: calc(100% - $header-height); + + &__empty { + text-align: center; + } + + &__list { + overflow-y: auto; + margin-bottom: rem(10px); + } + + &__card { + margin: rem(16px); + } + + &__button { + margin: auto auto 20px; + } +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx index 0c92204a..c87bfb58 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -11,6 +11,8 @@ import { networkConfigurationAtom } from '@popup/store/settings'; import { saveData } from '@popup/shared/utils/file-helpers'; import { popupMessageHandler } from '@popup/shared/message-handler'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; +import { encrypt } from '@shared/utils/crypto'; +import ButtonGroup from '@popup/shared/ButtonGroup'; import { ExportFormat } from './utils'; function createPlainExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { @@ -26,10 +28,15 @@ function createPlainExport(verifiableCredentials: VerifiableCredential[], networ return docContent; } -function createExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { +function createExport( + verifiableCredentials: VerifiableCredential[], + network: NetworkConfiguration, + encryptionKey: string +) { const plain = createPlainExport(verifiableCredentials, network); - // TODO Encrypt - return plain; + // TODO don't use key as password; + // TODO handle bigints + return encrypt(JSON.stringify(plain), encryptionKey); } /** @@ -46,8 +53,9 @@ export default function VerifiableCredentialList() { return null; } - const handleExport = () => { - const data = createExport(verifiableCredentials.value, network); + const handleExport = async () => { + // TODO get key + const data = await createExport(verifiableCredentials.value, network, 'myKey'); saveData(data, `web3IdCredentials.export`); }; @@ -57,12 +65,14 @@ export default function VerifiableCredentialList() { return (
    - - + + + +
    ); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 0b89729f..f0658c0d 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -5,11 +5,12 @@ import { } from '@popup/store/verifiable-credential'; import { useAtom, useAtomValue } from 'jotai'; import PageHeader from '@popup/shared/PageHeader'; -import { VerifiableCredential } from '@shared/storage/types'; +import { EncryptedData, VerifiableCredential } from '@shared/storage/types'; import { useTranslation } from 'react-i18next'; import Button from '@popup/shared/Button'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; import { noOp } from 'wallet-common-helpers'; +import { decrypt } from '@shared/utils/crypto'; import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; import { ExportFormat } from './utils'; @@ -20,22 +21,38 @@ function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { return ( <> {t('import.title')} -
    - {imported.length === 0 && <>{t('import.noImported')}} - {imported.map((credential) => { - return ( - - ); - })} +
    + {imported.length === 0 && ( +

    {t('import.noImported')}

    + )} + {imported.length > 0 && ( +
    + {imported.map((credential) => { + return ( + + ); + })} +
    + )} +
    - ); } +async function parseExport(data: EncryptedData, encryptionKey: string): Promise { + // TODO handle bigints + // TODO don't use key as password; + const backup: ExportFormat = JSON.parse(await decrypt(data, encryptionKey)); + // TODO validation + return backup.value.credentials; +} + /** * Renders all verifiable credentials that are in the wallet. The credentials * are selectable by clicking them, which will move the user to a view containing @@ -55,15 +72,15 @@ export default function VerifiableCredentialImport() { const handleImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { - const backup: ExportFormat = JSON.parse(await file.text()); // TODO error handling - // TODO validation - // TODO Decrypt - const credentials = backup.value.credentials.filter( + const encryptedBackup: EncryptedData = JSON.parse(await file.text()); + // TODO get key + const credentials = await parseExport(encryptedBackup, 'myKey'); + const filteredCredentials = credentials.filter( (cred) => !(verifiableCredentials.value || []).some((existing) => existing.id === cred.id) ); - setVerifiableCredentials([...verifiableCredentials.value, ...credentials]); - setImported(credentials); + setVerifiableCredentials([...verifiableCredentials.value, ...filteredCredentials]); + setImported(filteredCredentials); } }; @@ -71,13 +88,16 @@ export default function VerifiableCredentialImport() { return ; } + // TODO drag and drop return ( <> {t('import.title')} -
    - handleImport(x)} /> +
    + +
    - ); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index 8da146b2..e1ced962 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -1,6 +1,6 @@ export default { import: { - title: 'import', + title: 'Import Web3Id credentials', noImported: 'No verifiable credentials were imported', }, close: 'Close', diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index d29f1c27..b63d7b3a 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -45,6 +45,7 @@ @import '../pages/IdProofRequest'; @import '../pages/AddWeb3IdCredential/AddWeb3IdCredential'; @import '../pages/Web3ProofRequest/Web3ProofRequest'; +@import '../pages/VerifiableCredentialBackup/VerifiableCredentialBackup'; // Layouts @import '../page-layouts/MainLayout'; From 62dcd54c8e78ef108ac879013d5eb65f14046975 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 12:01:31 +0200 Subject: [PATCH 104/231] Add backup to menu of VC list + some styling --- .../VerifiableCredentialList.tsx | 21 +++++++++++++++---- .../pages/VerifiableCredential/i18n/da.ts | 1 + .../pages/VerifiableCredential/i18n/en.ts | 1 + .../VerifiableCredentialBackup.scss | 11 +++++----- .../VerifiableCredentialBackup.tsx | 19 ++++++++++------- .../VerifiableCredentialBackup/i18n/en.ts | 7 +++++++ 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index b415f618..e2d1105c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialSchemasAtom, storedVerifiableCredentialsAtom, } from '@popup/store/verifiable-credential'; import { useAtomValue, useAtom } from 'jotai'; -import Topbar from '@popup/shared/Topbar/Topbar'; +import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; import { @@ -13,7 +13,7 @@ import { getChangesToCredentialMetadata, getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { absoluteRoutes } from '@popup/constants/routes'; import { useCredentialMetadata, @@ -102,6 +102,7 @@ export default function VerifiableCredentialList() { }>(); const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); + const nav = useNavigate(); // Hooks that update the stored credential schemas and stored credential metadata. useFetchingEffect( @@ -143,9 +144,21 @@ export default function VerifiableCredentialList() { ); } + const menuButton = useMemo(() => { + const backupButton = { + title: t('menu.backup'), + onClick: () => nav(absoluteRoutes.home.verifiableCredentials.backup.path), + }; + + return { + type: ButtonTypes.More, + items: [backupButton], + }; + }, [nav]); + return ( <> - +
    {verifiableCredentials.value.map((credential) => { return ( 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 59d8f243..a930bba9 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -8,6 +8,7 @@ const t: typeof en = { menu: { revoke: 'Ophæv', details: 'Detaljer', + backup: 'Backup,', }, details: { id: 'Legitimationholders ID', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts index f88d5d75..9c24db85 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -6,6 +6,7 @@ const t = { menu: { revoke: 'Revoke', details: 'Details', + backup: 'Backup', }, details: { id: 'Credential holder ID', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss index ad37b58a..cf7ee980 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss @@ -1,17 +1,18 @@ $header-height: rem(56px); .verifiable-credential-backup { - overflow-y: auto; - - &__card { - margin: rem(16px); - } + display: flex; + justify-content: space-around; + flex-direction: column; + height: calc(100% - 56px); + background-color: $color-bg; } .verifiable-credential-import { display: flex; flex-direction: column; height: calc(100% - $header-height); + background-color: $color-bg; &__empty { text-align: center; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx index c87bfb58..edde0941 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -12,7 +12,9 @@ import { saveData } from '@popup/shared/utils/file-helpers'; import { popupMessageHandler } from '@popup/shared/message-handler'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; import { encrypt } from '@shared/utils/crypto'; -import ButtonGroup from '@popup/shared/ButtonGroup'; +import Topbar from '@popup/shared/Topbar'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { ExportFormat } from './utils'; function createPlainExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { @@ -48,6 +50,8 @@ export default function VerifiableCredentialList() { const [verifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const network = useAtomValue(networkConfigurationAtom); + const nav = useNavigate(); + const { t } = useTranslation('verifiableCredentialBackup'); if (schemas.loading || verifiableCredentials.loading) { return null; @@ -64,15 +68,16 @@ export default function VerifiableCredentialList() { }; return ( -
    - + <> + nav(-1)} /> +
    - -
    +
    + ); } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index e1ced962..bcd9c4ad 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -3,5 +3,12 @@ export default { title: 'Import Web3Id credentials', noImported: 'No verifiable credentials were imported', }, + backup: { + header: 'Web3Id credentials backup', + button: { + import: 'Go to Import page', + export: 'Download export file', + }, + }, close: 'Close', }; From c3b6535c978993f93a3a92af9423230e0bbf1cd7 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 12:53:30 +0200 Subject: [PATCH 105/231] Add danish for import + minor fixes --- .../VerifiableCredentialBackup.tsx | 5 ----- .../VerifiableCredentialImport.tsx | 5 ----- .../VerifiableCredentialBackup/i18n/da.ts | 18 ++++++++++++++++++ .../VerifiableCredentialBackup/i18n/en.ts | 8 ++++---- 4 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx index edde0941..0f289b12 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -41,11 +41,6 @@ function createExport( return encrypt(JSON.stringify(plain), encryptionKey); } -/** - * Renders all verifiable credentials that are in the wallet. The credentials - * are selectable by clicking them, which will move the user to a view containing - * a single credential. - */ export default function VerifiableCredentialList() { const [verifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index f0658c0d..012baa4b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -53,11 +53,6 @@ async function parseExport(data: EncryptedData, encryptionKey: string): Promise< return backup.value.credentials; } -/** - * Renders all verifiable credentials that are in the wallet. The credentials - * are selectable by clicking them, which will move the user to a view containing - * a single credential. - */ export default function VerifiableCredentialImport() { const [verifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); const [imported, setImported] = useState(); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts new file mode 100644 index 00000000..2c79a224 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts @@ -0,0 +1,18 @@ +import en from './en'; + +const t: typeof en = { + import: { + title: 'Importer Web3 ID Credentials', + noImported: 'Ingen Web3 ID Credentials blev importeret', + }, + backup: { + header: 'Web3 ID Credentials backup', + button: { + import: 'Gå til Import siden', + export: 'Download eksport filen', + }, + }, + close: 'Luk', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index bcd9c4ad..b3e89f11 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -1,12 +1,12 @@ export default { import: { - title: 'Import Web3Id credentials', - noImported: 'No verifiable credentials were imported', + title: 'Import Web3 ID Credentials', + noImported: 'No Web3 ID Credentials were imported', }, backup: { - header: 'Web3Id credentials backup', + header: 'Web3 ID Credentials backup', button: { - import: 'Go to Import page', + import: 'Go to the import page', export: 'Download export file', }, }, From 26202a2a3ecf57541fe8377e4ed1fc97f16b656c Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 14:53:47 +0200 Subject: [PATCH 106/231] Add schemas and metadata to export + styling + get fake key --- .../VerifiableCredentialList.tsx | 25 ++-- .../VerifiableCredentialBackup.scss | 4 + .../VerifiableCredentialBackup.tsx | 42 +++--- .../VerifiableCredentialImport.tsx | 124 +++++++++++------- .../VerifiableCredentialBackup/i18n/da.ts | 1 + .../VerifiableCredentialBackup/i18n/en.ts | 3 +- .../pages/VerifiableCredentialBackup/utils.ts | 7 +- .../src/popup/shared/utils/account-helpers.ts | 16 ++- 8 files changed, 139 insertions(+), 83 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index e2d1105c..2d841a73 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -104,6 +104,18 @@ export default function VerifiableCredentialList() { const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); const nav = useNavigate(); + const menuButton = useMemo(() => { + const backupButton = { + title: t('menu.backup'), + onClick: () => nav(absoluteRoutes.home.verifiableCredentials.backup.path), + }; + + return { + type: ButtonTypes.More, + items: [backupButton], + }; + }, [nav]); + // Hooks that update the stored credential schemas and stored credential metadata. useFetchingEffect( verifiableCredentials, @@ -143,19 +155,6 @@ export default function VerifiableCredentialList() { /> ); } - - const menuButton = useMemo(() => { - const backupButton = { - title: t('menu.backup'), - onClick: () => nav(absoluteRoutes.home.verifiableCredentials.backup.path), - }; - - return { - type: ButtonTypes.More, - items: [backupButton], - }; - }, [nav]); - return ( <> diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss index cf7ee980..1fc792c1 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss @@ -14,6 +14,10 @@ $header-height: rem(56px); height: calc(100% - $header-height); background-color: $color-bg; + &__header { + background-color: $color-bg; + } + &__empty { text-align: center; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx index 0f289b12..552986c1 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { storedVerifiableCredentialSchemasAtom, storedVerifiableCredentialsAtom, + storedVerifiableCredentialMetadataAtom, } from '@popup/store/verifiable-credential'; -import { useAtom, useAtomValue } from 'jotai'; -import { NetworkConfiguration, VerifiableCredential } from '@shared/storage/types'; +import { useAtomValue } from 'jotai'; +import { NetworkConfiguration, VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; import Button from '@popup/shared/Button'; import { getNet } from '@shared/utils/network-helpers'; import { networkConfigurationAtom } from '@popup/store/settings'; @@ -15,46 +16,49 @@ import { encrypt } from '@shared/utils/crypto'; import Topbar from '@popup/shared/Topbar'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { ExportFormat } from './utils'; -function createPlainExport(verifiableCredentials: VerifiableCredential[], network: NetworkConfiguration) { - const docContent: ExportFormat = { +function createExport( + verifiableCredentials: VerifiableCredential[], + schemas: Record, + metadata: Record, + network: NetworkConfiguration, + encryptionKey: string +) { + const exportContent: ExportFormat = { type: 'concordium-browser-wallet-verifiable-credentials', v: 0, environment: getNet(network).toLowerCase(), value: { - credentials: verifiableCredentials, + verifiableCredentials, + schemas, + metadata, }, }; - return docContent; -} - -function createExport( - verifiableCredentials: VerifiableCredential[], - network: NetworkConfiguration, - encryptionKey: string -) { - const plain = createPlainExport(verifiableCredentials, network); // TODO don't use key as password; // TODO handle bigints - return encrypt(JSON.stringify(plain), encryptionKey); + return encrypt(JSON.stringify(exportContent), encryptionKey); } export default function VerifiableCredentialList() { - const [verifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const metadata = useAtomValue(storedVerifiableCredentialMetadataAtom); const network = useAtomValue(networkConfigurationAtom); const nav = useNavigate(); const { t } = useTranslation('verifiableCredentialBackup'); + const wallet = useHdWallet(); - if (schemas.loading || verifiableCredentials.loading) { + if (schemas.loading || verifiableCredentials.loading || metadata.loading || !wallet) { return null; } const handleExport = async () => { - // TODO get key - const data = await createExport(verifiableCredentials.value, network, 'myKey'); + const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex'); + const data = await createExport(verifiableCredentials.value, schemas.value, metadata.value, network, key); saveData(data, `web3IdCredentials.export`); }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 012baa4b..6a1ba79c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -2,8 +2,9 @@ import React, { useContext, useState } from 'react'; import { storedVerifiableCredentialSchemasAtom, storedVerifiableCredentialsAtom, + storedVerifiableCredentialMetadataAtom, } from '@popup/store/verifiable-credential'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom } from 'jotai'; import PageHeader from '@popup/shared/PageHeader'; import { EncryptedData, VerifiableCredential } from '@shared/storage/types'; import { useTranslation } from 'react-i18next'; @@ -11,84 +12,115 @@ import Button from '@popup/shared/Button'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; import { noOp } from 'wallet-common-helpers'; import { decrypt } from '@shared/utils/crypto'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; -import { ExportFormat } from './utils'; +import { ExportFormat, VerifiableCredentialExport } from './utils'; function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { const { t } = useTranslation('verifiableCredentialBackup'); - const { withClose } = useContext(fullscreenPromptContext); return ( <> - {t('import.title')} -
    - {imported.length === 0 && ( -

    {t('import.noImported')}

    - )} - {imported.length > 0 && ( -
    - {imported.map((credential) => { - return ( - - ); - })} -
    - )} - -
    + {imported.length === 0 &&

    {t('import.noImported')}

    } + {imported.length > 0 && ( +
    + {imported.map((credential) => { + return ( + + ); + })} +
    + )} ); } -async function parseExport(data: EncryptedData, encryptionKey: string): Promise { +async function parseExport(data: EncryptedData, encryptionKey: string): Promise { // TODO handle bigints // TODO don't use key as password; const backup: ExportFormat = JSON.parse(await decrypt(data, encryptionKey)); // TODO validation - return backup.value.credentials; + return backup.value; +} + +/** + * Adds items from toAdd that does not exist in stored, using the given update. Returns the items from toAdd that was actually added. + */ +function updateList(stored: T[], toAdd: T[], isEqual: (a: T, b: T) => boolean, update: (updated: T[]) => void): T[] { + const filtered = toAdd.filter((item) => stored.every((existing) => !isEqual(item, existing))); + update([...stored, ...filtered]); + return filtered; +} + +/** + * Adds items from toAdd that does not exist in stored, using the given update. + */ +function updateRecord( + stored: Record, + toAdd: Record, + update: (updated: Record) => void +) { + const updated = { ...stored }; + Object.entries(toAdd).forEach(([key, value]) => { + if (!stored[key]) { + updated[key] = value; + } + }); + + update(updated); } export default function VerifiableCredentialImport() { - const [verifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); + const [storedVerifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom); const [imported, setImported] = useState(); - const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const [storedSchemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); + const [storedMetadata, setMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); const { t } = useTranslation('verifiableCredentialBackup'); const { withClose } = useContext(fullscreenPromptContext); + const wallet = useHdWallet(); + const [error, setError] = useState(); - if (schemas.loading || verifiableCredentials.loading) { + if (storedSchemas.loading || storedMetadata.loading || storedVerifiableCredentials.loading || !wallet) { return null; } const handleImport = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - // TODO error handling - const encryptedBackup: EncryptedData = JSON.parse(await file.text()); - // TODO get key - const credentials = await parseExport(encryptedBackup, 'myKey'); - const filteredCredentials = credentials.filter( - (cred) => !(verifiableCredentials.value || []).some((existing) => existing.id === cred.id) - ); - setVerifiableCredentials([...verifiableCredentials.value, ...filteredCredentials]); - setImported(filteredCredentials); + try { + const file = event.target.files?.[0]; + if (file) { + const encryptedBackup: EncryptedData = JSON.parse(await file.text()); + const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex'); + const { verifiableCredentials, schemas, metadata } = await parseExport(encryptedBackup, key); + const filteredCredentials = updateList( + storedVerifiableCredentials.value, + verifiableCredentials, + (a, b) => a.id === b.id, + setVerifiableCredentials + ); + updateRecord(storedSchemas.value, schemas, setSchemas); + updateRecord(storedMetadata.value, metadata, setMetadata); + setImported(filteredCredentials); + } + } catch (e) { + setError(t('import.error')); } }; - if (imported) { - return ; - } - // TODO drag and drop return ( <> - {t('import.title')} + {t('import.title')}
    - + {imported && } + {!imported && ( + <> + + {error &&

    {error}

    } + + )} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts index 2c79a224..344d6093 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts @@ -4,6 +4,7 @@ const t: typeof en = { import: { title: 'Importer Web3 ID Credentials', noImported: 'Ingen Web3 ID Credentials blev importeret', + error: 'Det var ikke muligt at importere den valgte fil. Filen skal være en backup lavet med en samme seed phrase.', }, backup: { header: 'Web3 ID Credentials backup', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index b3e89f11..6a9f4f68 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -2,11 +2,12 @@ export default { import: { title: 'Import Web3 ID Credentials', noImported: 'No Web3 ID Credentials were imported', + error: 'Unable to import the chosen file. The file must be a backup created with the same seed phrase.', }, backup: { header: 'Web3 ID Credentials backup', button: { - import: 'Go to the import page', + import: 'Go to import page', export: 'Download export file', }, }, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts index 94d3eb7a..75a01c76 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts @@ -1,7 +1,10 @@ -import { VerifiableCredential } from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; +import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; export type VerifiableCredentialExport = { - credentials: VerifiableCredential[]; + verifiableCredentials: VerifiableCredential[]; + schemas: Record; + metadata: Record; }; export type ExportFormat = { diff --git a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts index cb7e4938..6058810d 100644 --- a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts @@ -67,7 +67,10 @@ export function useSelectedCredential() { return useCredential(selectedAccount); } -export function useHdWallet(): ConcordiumHdWallet | undefined { +// TODO fix type +export function useHdWallet(): + | (ConcordiumHdWallet & { getVerifiableCredentialBackupEncryptionKey: () => Buffer }) + | undefined { const network = useAtomValue(networkConfigurationAtom); const seedPhrase = useDecryptedSeedPhrase(); @@ -76,7 +79,16 @@ export function useHdWallet(): ConcordiumHdWallet | undefined { return undefined; } - return ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); + // return ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); + // TODO remove this hack when SDK is updated + // START OF HACK + const w = ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (w as any).getVerifiableCredentialBackupEncryptionKey = () => + w.getVerifiableCredentialSigningKey({ index: 0n, subindex: 0n }, 0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return w as any; + // END OF HACK }, [seedPhrase]); return wallet; From 31d8bfae5684e7ee6dd78c63d87cb3665ab1ae4c Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 15:31:14 +0200 Subject: [PATCH 107/231] Remove backup page --- .../src/popup/constants/routes.ts | 1 - .../VerifiableCredentialList.tsx | 32 ++++---- .../pages/VerifiableCredential/i18n/da.ts | 3 +- .../pages/VerifiableCredential/i18n/en.ts | 3 +- .../VerifiableCredentialBackup.scss | 8 -- .../VerifiableCredentialBackup.tsx | 82 ------------------- .../VerifiableCredentialImport.tsx | 7 +- .../VerifiableCredentialBackup/i18n/da.ts | 15 +--- .../VerifiableCredentialBackup/i18n/en.ts | 15 +--- .../pages/VerifiableCredentialBackup/utils.ts | 55 ++++++++++++- .../browser-wallet/src/popup/shell/Routes.tsx | 5 -- 11 files changed, 85 insertions(+), 141 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index e3d4f9e7..94b84fa6 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -20,7 +20,6 @@ export const relativeRoutes = { }, verifiableCredentials: { path: 'verifiable-credentials', - backup: { path: 'backup' }, }, settings: { path: 'settings', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 2d841a73..6ae9c4db 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -13,8 +13,8 @@ import { getChangesToCredentialMetadata, getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; -import { Link, useNavigate } from 'react-router-dom'; -import { absoluteRoutes } from '@popup/constants/routes'; +import { popupMessageHandler } from '@popup/shared/message-handler'; +import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; import { useCredentialMetadata, useCredentialSchema, @@ -23,6 +23,7 @@ import { } from './VerifiableCredentialHooks'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import VerifiableCredentialDetails from './VerifiableCredentialDetails'; +import { useVerifiableCredentialExport } from '../VerifiableCredentialBackup/utils'; /** * Component to display while loading verifiable credentials from storage. @@ -102,19 +103,27 @@ export default function VerifiableCredentialList() { }>(); const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); - const nav = useNavigate(); + + const exportCredentials = useVerifiableCredentialExport(); const menuButton = useMemo(() => { + const goToImportPage = () => + popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup).then(() => window.close()); + const backupButton = { - title: t('menu.backup'), - onClick: () => nav(absoluteRoutes.home.verifiableCredentials.backup.path), + title: t('menu.export'), + onClick: exportCredentials, + }; + const importButton = { + title: t('menu.import'), + onClick: goToImportPage, }; return { type: ButtonTypes.More, - items: [backupButton], + items: [backupButton, importButton], }; - }, [nav]); + }, [exportCredentials]); // Hooks that update the stored credential schemas and stored credential metadata. useFetchingEffect( @@ -130,17 +139,12 @@ export default function VerifiableCredentialList() { getChangesToCredentialSchemas ); - if (verifiableCredentials.loading) { + if (verifiableCredentials.loading || !exportCredentials) { return ; } if (verifiableCredentials.value.length === 0) { - return ( - <> - Backup - - - ); + return ; } if (selected) { 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 a930bba9..eb1b4cdc 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -8,7 +8,8 @@ const t: typeof en = { menu: { revoke: 'Ophæv', details: 'Detaljer', - backup: 'Backup,', + import: 'Åben import vinduet', + export: 'Download export fil', }, details: { id: 'Legitimationholders ID', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts index 9c24db85..dd8e0e08 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -6,7 +6,8 @@ const t = { menu: { revoke: 'Revoke', details: 'Details', - backup: 'Backup', + import: 'Open import window', + export: 'Download export file', }, details: { id: 'Credential holder ID', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss index 1fc792c1..e1f15895 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss @@ -1,13 +1,5 @@ $header-height: rem(56px); -.verifiable-credential-backup { - display: flex; - justify-content: space-around; - flex-direction: column; - height: calc(100% - 56px); - background-color: $color-bg; -} - .verifiable-credential-import { display: flex; flex-direction: column; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx deleted file mode 100644 index 552986c1..00000000 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import { - storedVerifiableCredentialSchemasAtom, - storedVerifiableCredentialsAtom, - storedVerifiableCredentialMetadataAtom, -} from '@popup/store/verifiable-credential'; -import { useAtomValue } from 'jotai'; -import { NetworkConfiguration, VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; -import Button from '@popup/shared/Button'; -import { getNet } from '@shared/utils/network-helpers'; -import { networkConfigurationAtom } from '@popup/store/settings'; -import { saveData } from '@popup/shared/utils/file-helpers'; -import { popupMessageHandler } from '@popup/shared/message-handler'; -import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; -import { encrypt } from '@shared/utils/crypto'; -import Topbar from '@popup/shared/Topbar'; -import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; -import { useHdWallet } from '@popup/shared/utils/account-helpers'; -import { ExportFormat } from './utils'; - -function createExport( - verifiableCredentials: VerifiableCredential[], - schemas: Record, - metadata: Record, - network: NetworkConfiguration, - encryptionKey: string -) { - const exportContent: ExportFormat = { - type: 'concordium-browser-wallet-verifiable-credentials', - v: 0, - environment: getNet(network).toLowerCase(), - value: { - verifiableCredentials, - schemas, - metadata, - }, - }; - - // TODO don't use key as password; - // TODO handle bigints - return encrypt(JSON.stringify(exportContent), encryptionKey); -} - -export default function VerifiableCredentialList() { - const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); - const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); - const metadata = useAtomValue(storedVerifiableCredentialMetadataAtom); - const network = useAtomValue(networkConfigurationAtom); - const nav = useNavigate(); - const { t } = useTranslation('verifiableCredentialBackup'); - const wallet = useHdWallet(); - - if (schemas.loading || verifiableCredentials.loading || metadata.loading || !wallet) { - return null; - } - - const handleExport = async () => { - const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex'); - const data = await createExport(verifiableCredentials.value, schemas.value, metadata.value, network, key); - saveData(data, `web3IdCredentials.export`); - }; - - const handleImport = () => { - popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup).then(() => window.close()); - }; - - return ( - <> - nav(-1)} /> -
    - - -
    - - ); -} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 6a1ba79c..8f9bb9c8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -21,7 +21,7 @@ function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { return ( <> - {imported.length === 0 &&

    {t('import.noImported')}

    } + {imported.length === 0 &&

    {t('noImported')}

    } {imported.length > 0 && (
    {imported.map((credential) => { @@ -40,7 +40,6 @@ function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { async function parseExport(data: EncryptedData, encryptionKey: string): Promise { // TODO handle bigints - // TODO don't use key as password; const backup: ExportFormat = JSON.parse(await decrypt(data, encryptionKey)); // TODO validation return backup.value; @@ -105,14 +104,14 @@ export default function VerifiableCredentialImport() { setImported(filteredCredentials); } } catch (e) { - setError(t('import.error')); + setError(t('error')); } }; // TODO drag and drop return ( <> - {t('import.title')} + {t('title')}
    {imported && } {!imported && ( diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts index 344d6093..e810872c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts @@ -1,18 +1,9 @@ import en from './en'; const t: typeof en = { - import: { - title: 'Importer Web3 ID Credentials', - noImported: 'Ingen Web3 ID Credentials blev importeret', - error: 'Det var ikke muligt at importere den valgte fil. Filen skal være en backup lavet med en samme seed phrase.', - }, - backup: { - header: 'Web3 ID Credentials backup', - button: { - import: 'Gå til Import siden', - export: 'Download eksport filen', - }, - }, + title: 'Importer Web3 ID Credentials', + noImported: 'Ingen Web3 ID Credentials blev importeret', + error: 'Det var ikke muligt at importere den valgte fil. Filen skal være en backup lavet med en samme seed phrase.', close: 'Luk', }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index 6a9f4f68..5bc9130c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -1,15 +1,6 @@ export default { - import: { - title: 'Import Web3 ID Credentials', - noImported: 'No Web3 ID Credentials were imported', - error: 'Unable to import the chosen file. The file must be a backup created with the same seed phrase.', - }, - backup: { - header: 'Web3 ID Credentials backup', - button: { - import: 'Go to import page', - export: 'Download export file', - }, - }, + title: 'Import Web3 ID Credentials', + noImported: 'No Web3 ID Credentials were imported', + error: 'Unable to import the chosen file. The file must be a backup created with the same seed phrase.', close: 'Close', }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts index 75a01c76..64ef65e9 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts @@ -1,5 +1,16 @@ -import { VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; +import { NetworkConfiguration, VerifiableCredential, VerifiableCredentialSchema } from '@shared/storage/types'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; +import { encrypt } from '@shared/utils/crypto'; +import { getNet } from '@shared/utils/network-helpers'; +import { useAtomValue } from 'jotai'; +import { + storedVerifiableCredentialSchemasAtom, + storedVerifiableCredentialsAtom, + storedVerifiableCredentialMetadataAtom, +} from '@popup/store/verifiable-credential'; +import { networkConfigurationAtom } from '@popup/store/settings'; +import { saveData } from '@popup/shared/utils/file-helpers'; export type VerifiableCredentialExport = { verifiableCredentials: VerifiableCredential[]; @@ -13,3 +24,45 @@ export type ExportFormat = { environment: string; // 'testnet' or 'mainnet' value: VerifiableCredentialExport; }; + +function createExport( + verifiableCredentials: VerifiableCredential[], + schemas: Record, + metadata: Record, + network: NetworkConfiguration, + encryptionKey: string +) { + const exportContent: ExportFormat = { + type: 'concordium-browser-wallet-verifiable-credentials', + v: 0, + environment: getNet(network).toLowerCase(), + value: { + verifiableCredentials, + schemas, + metadata, + }, + }; + + // TODO handle bigints + return encrypt(JSON.stringify(exportContent), encryptionKey); +} + +export function useVerifiableCredentialExport() { + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const metadata = useAtomValue(storedVerifiableCredentialMetadataAtom); + const network = useAtomValue(networkConfigurationAtom); + const wallet = useHdWallet(); + + if (schemas.loading || verifiableCredentials.loading || metadata.loading || !wallet) { + return undefined; + } + + const handleExport = async () => { + const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex'); + const data = await createExport(verifiableCredentials.value, schemas.value, metadata.value, network, key); + saveData(data, `web3IdCredentials.export`); + }; + + return handleExport; +} diff --git a/packages/browser-wallet/src/popup/shell/Routes.tsx b/packages/browser-wallet/src/popup/shell/Routes.tsx index 6c31cb0c..106880c2 100644 --- a/packages/browser-wallet/src/popup/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/shell/Routes.tsx @@ -34,7 +34,6 @@ import AddTokensPrompt from '@popup/pages/ExternalAddTokens/ExternalAddTokens'; import IdProofRequest from '@popup/pages/IdProofRequest'; import VerifiableCredentialList from '@popup/pages/VerifiableCredential'; import Web3ProofRequest from '@popup/pages/Web3ProofRequest'; -import VerifiableCredentialBackup from '@popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup'; import ConnectAccountsRequest from '@popup/pages/ConnectAccountsRequest'; import AllowListRoutes from '@popup/pages/Allowlist'; import AddWeb3IdCredential from '@popup/pages/AddWeb3IdCredential/AddWeb3IdCredential'; @@ -237,10 +236,6 @@ export default function Routes() { path={`${relativePath(relativeRoutes.home.path, absoluteRoutes.home.identities.add.path)}/*`} /> } path={relativeRoutes.home.identities.path} /> - } - path={absoluteRoutes.home.verifiableCredentials.backup.path} - /> } path={relativeRoutes.home.verifiableCredentials.path} /> } /> From 9ff0f5a68801ea583c031ec06107a289480989c1 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 18 Aug 2023 15:54:06 +0200 Subject: [PATCH 108/231] Add initial support for localization --- .../AddWeb3IdCredential.tsx | 24 +++++++ .../VerifiableCredentialCard.tsx | 30 +++++++-- .../VerifiableCredentialDetails.tsx | 3 + .../VerifiableCredentialHooks.tsx | 64 ++++++++++++++++++- .../VerifiableCredentialList.tsx | 17 +++-- .../VerifiableCredentialStatement.tsx | 6 +- .../utils/verifiable-credential-helpers.ts | 24 ++++++- 7 files changed, 152 insertions(+), 16 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index b8ea98d7..72d68dca 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -20,6 +20,7 @@ import { createPublicKeyIdentifier, fetchCredentialMetadata, fetchCredentialSchema, + fetchLocalization, getCredentialRegistryContractAddress, } from '@shared/utils/verifiable-credential-helpers'; import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; @@ -45,6 +46,7 @@ interface Location { export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const { state } = useLocation() as Location; const { t } = useTranslation('addWeb3IdCredential'); + const { i18n } = useTranslation(); const { onClose, withClose } = useContext(fullscreenPromptContext); const [acceptButtonDisabled, setAcceptButtonDisabled] = useState(false); const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom); @@ -96,6 +98,27 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { ); useEffect(() => () => controller.abort(), []); + const localization = useAsyncMemo( + async () => { + if (metadata === undefined) { + return undefined; + } + + if (metadata.localization === undefined) { + return undefined; + } + + const currentLanguageLocalization = metadata.localization[i18n.language]; + if (currentLanguageLocalization === undefined) { + return undefined; + } + + return fetchLocalization(currentLanguageLocalization, controller); + }, + () => setError('Failed to get localization'), + [metadata, i18n] + ); + async function addCredential(credentialSchema: VerifiableCredentialSchema) { if (!wallet) { throw new Error('Wallet is unexpectedly missing'); @@ -154,6 +177,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { schema={schema} credentialStatus={VerifiableCredentialStatus.NotActivated} metadata={metadata} + localization={localization} /> )} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 30765062..d740ce44 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -72,22 +72,38 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className } /** - * Apply the schema to an attribute, adding the title from the schema, which + * Apply the schema and localization to an attribute, adding the title from the schema or localization, which * should be displayed to the user. * @param schema the schema to apply + * @param localization the localization to apply * @returns the attribute together with its title. * @throws if there is a mismatch in fields between the credential and the schema, i.e. the schema is invalid. */ -function applySchema( - schema: VerifiableCredentialSchema +function applySchemaAndLocalization( + schema: VerifiableCredentialSchema, + localization?: Record ): (value: [string, string | bigint]) => { title: string; key: string; value: string | bigint } { return (value: [string, string | bigint]) => { + let title; const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); } + title = attributeSchema.title; + + if (localization) { + const localizedTitle = localization[value[0]]; + if (localizedTitle !== undefined) { + title = localizedTitle; + } else { + // TODO Throw an error if we are missing a localization attribute key when we have added + // validation at the time of retrieving localization data. + // throw new Error(`Missing localization for key: ${value[0]}`); + } + } + return { - title: attributeSchema.title, + title, key: value[0], value: value[1], }; @@ -116,6 +132,7 @@ interface CardProps extends ClassName { credentialStatus: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; onClick?: () => void; + localization?: Record; } export function VerifiableCredentialCard({ @@ -125,8 +142,11 @@ export function VerifiableCredentialCard({ credentialStatus, metadata, className, + localization, }: CardProps) { - const attributes = Object.entries(credentialSubject.attributes).map(applySchema(schema)); + const attributes = Object.entries(credentialSubject.attributes).map( + applySchemaAndLocalization(schema, localization) + ); return ( diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 45905a33..c5c91de8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -82,6 +82,7 @@ interface CredentialDetailsProps extends ClassName { status: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; schema: VerifiableCredentialSchema; + localization?: Record; backButtonOnClick: () => void; } @@ -90,6 +91,7 @@ export default function VerifiableCredentialDetails({ status, metadata, schema, + localization, backButtonOnClick, className, }: CredentialDetailsProps) { @@ -204,6 +206,7 @@ export default function VerifiableCredentialDetails({ schema={schema} credentialStatus={status} metadata={metadata} + localization={localization} />
    )} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 01626333..6d986398 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -3,6 +3,7 @@ import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialS import { CredentialQueryResponse, VerifiableCredentialMetadata, + fetchLocalization, getCredentialHolderId, getCredentialRegistryContractAddress, getVerifiableCredentialEntry, @@ -16,6 +17,7 @@ import { } from '@popup/store/verifiable-credential'; import { AsyncWrapper } from '@popup/store/utils'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; +import { useTranslation } from 'react-i18next'; /** * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. @@ -42,19 +44,19 @@ export function useCredentialStatus(credential: VerifiableCredential) { * @throws if no schema is found in storage for the provided credential * @returns the credential's schema used for rendering the credential */ -export function useCredentialSchema(credential: VerifiableCredential) { +export function useCredentialSchema(credential?: VerifiableCredential) { const [schema, setSchema] = useState(); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); useEffect(() => { - if (!schemas.loading) { + if (!schemas.loading && credential) { const schemaValue = schemas.value[credential.credentialSchema.id]; if (!schemaValue) { throw new Error(`Attempted to find schema for credentialId: ${credential.id} but none was found!`); } setSchema(schemaValue); } - }, [schemas.loading]); + }, [credential?.id, schemas.loading]); return schema; } @@ -108,6 +110,62 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { return metadata; } +interface SuccessfulLocalizationResult { + loading: false; + result: Record; +} + +interface FailedLocalizationResult { + loading: false; + result?: never; +} + +interface LoadingLocalizationResult { + loading: true; +} + +type LocalizationResult = SuccessfulLocalizationResult | FailedLocalizationResult | LoadingLocalizationResult; + +export function useCredentialLocalization(credential?: VerifiableCredential): LocalizationResult { + const [localization, setLocalization] = useState({ loading: true }); + const { i18n } = useTranslation(); + const metadata = useCredentialMetadata(credential); + const schema = useCredentialSchema(credential); + + useEffect(() => { + if (metadata === undefined || schema === undefined) { + return () => {}; + } + + // No localization is available for the provided metadata. + if (metadata.localization === undefined) { + setLocalization({ loading: false }); + return () => {}; + } + + const currentLanguageLocalization = metadata.localization[i18n.language]; + // No localization is available for the selected language. + if (currentLanguageLocalization === undefined) { + setLocalization({ loading: false }); + return () => {}; + } + + const abortController = new AbortController(); + fetchLocalization(currentLanguageLocalization, abortController) + .then((res) => { + // TODO Validate that localization is present for all keys. + setLocalization({ loading: false, result: res }); + }) + .catch(() => setLocalization({ loading: false })); + + return () => { + abortController.abort(); + }; + }, [JSON.stringify(metadata), i18n.language]); + + return localization; +} + /** * Retrieves data and uses the provided data setter to update chrome.storage with the changes found. * The dataFetcher is responsible for delivering the exact updated picture that should be set. diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index d2e0e9bc..b1590e59 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -14,6 +14,7 @@ import { getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; import { + useCredentialLocalization, useCredentialMetadata, useCredentialSchema, useCredentialStatus, @@ -56,15 +57,17 @@ function VerifiableCredentialCardWithStatusFromChain({ onClick?: ( status: VerifiableCredentialStatus, schema: VerifiableCredentialSchema, - metadata: VerifiableCredentialMetadata + metadata: VerifiableCredentialMetadata, + localization?: Record ) => void; }) { const status = useCredentialStatus(credential); const schema = useCredentialSchema(credential); const metadata = useCredentialMetadata(credential); + const localization = useCredentialLocalization(credential); // Render nothing until all the required data is available. - if (!schema || !metadata || status === undefined) { + if (!schema || !metadata || localization.loading || status === undefined) { return null; } @@ -75,11 +78,12 @@ function VerifiableCredentialCardWithStatusFromChain({ className={className} onClick={() => { if (onClick) { - onClick(status, schema, metadata); + onClick(status, schema, metadata, localization.result); } }} credentialStatus={status} metadata={metadata} + localization={localization.result} /> ); } @@ -97,6 +101,7 @@ export default function VerifiableCredentialList() { status: VerifiableCredentialStatus; schema: VerifiableCredentialSchema; metadata: VerifiableCredentialMetadata; + localization?: Record; }>(); const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom); @@ -130,6 +135,7 @@ export default function VerifiableCredentialList() { schema={selected.schema} status={selected.status} metadata={selected.metadata} + localization={selected.localization} backButtonOnClick={() => setSelected(undefined)} /> ); @@ -148,8 +154,9 @@ export default function VerifiableCredentialList() { onClick={( status: VerifiableCredentialStatus, schema: VerifiableCredentialSchema, - metadata: VerifiableCredentialMetadata - ) => setSelected({ credential, status, schema, metadata })} + metadata: VerifiableCredentialMetadata, + localization?: Record + ) => setSelected({ credential, status, schema, metadata, localization })} /> ); })} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index f2d7c6c2..41a44adf 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; -import { useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; +import { useCredentialLocalization, useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; @@ -190,8 +190,9 @@ export default function DisplayWeb3Statement({ }, [chosenCredential?.id, verifiableCredentialSchemas.loading]); const metadata = useCredentialMetadata(chosenCredential); + const localization = useCredentialLocalization(chosenCredential); - if (!chosenCredential || !schema || !metadata) { + if (!chosenCredential || !schema || !metadata || localization.loading) { return null; } @@ -202,6 +203,7 @@ export default function DisplayWeb3Statement({ schema={schema} credentialStatus={VerifiableCredentialStatus.Active} metadata={metadata} + localization={localization.result} /> ( { url, hash }: MetadataUrl, abortController: AbortController, - jsonSchema: typeof verifiableCredentialMetadataSchema | typeof verifiableCredentialSchemaSchema + jsonSchema: + | typeof verifiableCredentialMetadataSchema + | typeof verifiableCredentialSchemaSchema + | typeof localizationRecordSchema ): Promise { const response = await fetch(url, { headers: new Headers({ 'Access-Control-Allow-Origin': '*' }), @@ -663,6 +678,13 @@ export async function fetchCredentialMetadata( return fetchDataFromUrl(metadata, abortController, verifiableCredentialMetadataSchema); } +export async function fetchLocalization( + url: MetadataUrl, + abortController: AbortController +): Promise> { + return fetchDataFromUrl(url, abortController, localizationRecordSchema); +} + /** * Retrieves credential schemas for each of the provided credentials. The method ensures * that duplicate schemas are not fetched multiple times, by only fetching once per From 4a12cc15abb2559651e05764592640a8eee84695 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 15:56:45 +0200 Subject: [PATCH 109/231] Manually appease linter --- packages/browser-wallet/src/background/index.ts | 2 +- packages/browser-wallet/src/background/web3Id.ts | 2 +- packages/browser-wallet/src/popup/shell/i18n/locales/da.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index f0668e72..987b2124 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -44,7 +44,7 @@ import { web3IdAddCredentialFinishHandler, createWeb3IdProofHandler, runIfValidWeb3IdProof, - loadWeb3IdBackupHandler + loadWeb3IdBackupHandler, } from './web3Id'; const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites'; diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 32a1b69a..0bfd0383 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -192,7 +192,7 @@ export const runIfValidWeb3IdProof: RunCondition response: { success: false, message: `Statement is not well-formed: ${(e as Error).message}` }, }; } -} +}; async function loadWeb3IdBackup(): Promise { await openWindow(); diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index afe8db84..8f3459e1 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -54,7 +54,7 @@ const t: typeof en = { addWeb3IdCredential, web3IdProofRequest, verifiableCredential, - verifiableCredentialBackup + verifiableCredentialBackup, }; export default t; From fd45ea713dd59a0ac5611d2493c4b78ef8d136fe Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 18 Aug 2023 15:57:05 +0200 Subject: [PATCH 110/231] Add missing item to dependency array --- .../pages/VerifiableCredential/VerifiableCredentialHooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 6d986398..6f22bdbe 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -161,7 +161,7 @@ export function useCredentialLocalization(credential?: VerifiableCredential): Lo return () => { abortController.abort(); }; - }, [JSON.stringify(metadata), i18n.language]); + }, [JSON.stringify(metadata), JSON.stringify(schema), i18n.language]); return localization; } From e2f4bb2ced05d90ae6f73b25f90b3a100c734171 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 18 Aug 2023 16:12:01 +0200 Subject: [PATCH 111/231] Add import when there is no web3Id credentials --- .../VerifiableCredentialList.tsx | 23 +++++++++++++++---- .../VerifiableCredentialImport.tsx | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 6ae9c4db..f00c5058 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -25,6 +25,11 @@ import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import VerifiableCredentialDetails from './VerifiableCredentialDetails'; import { useVerifiableCredentialExport } from '../VerifiableCredentialBackup/utils'; +async function goToImportPage() { + await popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup); + window.close(); +} + /** * Component to display while loading verifiable credentials from storage. */ @@ -37,9 +42,22 @@ function LoadingVerifiableCredentials() { */ function NoVerifiableCredentials() { const { t } = useTranslation('verifiableCredential'); + + const menuButton = useMemo(() => { + const importButton = { + title: t('menu.import'), + onClick: goToImportPage, + }; + + return { + type: ButtonTypes.More, + items: [importButton], + }; + }, []); + return ( <> - +

    You do not have any verifiable credentials in your wallet.

    @@ -107,9 +125,6 @@ export default function VerifiableCredentialList() { const exportCredentials = useVerifiableCredentialExport(); const menuButton = useMemo(() => { - const goToImportPage = () => - popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup).then(() => window.close()); - const backupButton = { title: t('menu.export'), onClick: exportCredentials, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 8f9bb9c8..2c53f88d 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -27,6 +27,7 @@ function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { {imported.map((credential) => { return ( From 407a301eb2c3af9188bb1a70dcef61a85a2e2e57 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 18 Aug 2023 16:35:56 +0200 Subject: [PATCH 112/231] Fix incorrect error message --- .../src/shared/utils/verifiable-credential-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index d9c37e46..c4898026 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -659,7 +659,7 @@ async function fetchDataFromUrl( }); if (!response.ok) { - throw new Error(`Failed to fetch the schema at: ${url}`); + throw new Error(`Failed to fetch the data at: ${url}`); } const body = Buffer.from(await response.arrayBuffer()); From 73277621d9d99ea9d029a47d50abd0353475b8cf Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 21 Aug 2023 17:03:50 +0200 Subject: [PATCH 113/231] Serialize web3IdCredentials before storage, to support bigints --- .../AddWeb3IdCredential.tsx | 8 +++- .../src/popup/store/verifiable-credential.ts | 4 +- .../src/shared/storage/access.ts | 44 ++++++++++++++++++- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index b1e91024..ddfe0a43 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -111,7 +111,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } // Find the next unused index // TODO verify index is unused on chain? - const index = [...(web3IdCredentials || []), ...(storedWeb3IdCredentials.value || [])].reduce( + const index = [...web3IdCredentials.value, ...storedWeb3IdCredentials.value].reduce( (best, cred) => (cred.issuer === credential.issuer ? Math.max(cred.index + 1, best) : best), 0 ); @@ -128,7 +128,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { id: createCredentialId(credentialHolderId, issuer, network), index, }; - await setWeb3IdCredentials([...(web3IdCredentials || []), fullCredential]); + await setWeb3IdCredentials([...web3IdCredentials.value, fullCredential]); if (metadata) { const newMetadata = { ...verifiableCredentialMetadata.value }; newMetadata[metadataUrl.url] = metadata; @@ -137,6 +137,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return credentialSubjectId; } + if (web3IdCredentials.loading || storedWeb3IdCredentials.loading) { + return null; + } + const urlDisplay = displayUrl(url); return ( diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts index 3b9b1267..134b30dc 100644 --- a/packages/browser-wallet/src/popup/store/verifiable-credential.ts +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -19,5 +19,5 @@ export const storedVerifiableCredentialMetadataAtom = atomWithChromeStorage< >(ChromeStorageKey.VerifiableCredentialMetadata, {}, true); export const sessionTemporaryVerifiableCredentialsAtom = atomWithChromeStorage< - Omit[] | undefined ->(ChromeStorageKey.TemporaryVerifiableCredentials, []); + Omit[] +>(ChromeStorageKey.TemporaryVerifiableCredentials, [], true); diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 3b543f6b..53f0e5b4 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -1,3 +1,5 @@ +import { stringify } from '@concordium/browser-wallet-api/src/util'; +import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import { ChromeStorageKey, @@ -111,6 +113,44 @@ export function useIndexedStorage( }; } +/** + * Factory function for creating a StorageAccessor, which serializes before storing the value and deserializes before loading the value, from a key. + * + * @param area storeage area to store value in + * @param key key used to store value + */ +export function makeSerializedStorageAccessor( + area: chrome.storage.AreaName, + key: ChromeStorageKey +): StorageAccessor { + const inner = makeStorageAccessor(area, key); + return { + get: (): Promise => inner.get().then((v) => (v !== undefined ? parse(v) : undefined)), + set: (value: V) => inner.set(stringify(value)), + remove: () => inner.remove(), + area, + }; +} + +/** + * Factory function for creating an IndexedStorageAccessor, which serializes before storing the value and deserializes before loading the value, from a key. + * + * @param area storage area to store value in + * @param key key used to store value + */ +export function makeSerializedAndIndexedStorageAccessor( + area: chrome.storage.AreaName, + key: ChromeStorageKey +): IndexedStorageAccessor { + const inner = makeIndexedStorageAccessor(area, key); + return { + get: (index: string) => inner.get(index).then((s) => (s !== undefined ? parse(s) : undefined)), + set: (index: string, value: Value) => inner.set(index, stringify(value)), + remove: (index: string) => inner.remove(index), + area, + }; +} + export const storedCurrentNetwork = makeStorageAccessor( 'local', ChromeStorageKey.NetworkConfiguration @@ -148,7 +188,7 @@ export const storedTokenMetadata = makeStorageAccessor('local', ChromeStorageKey.AcceptedTerms); -export const storedVerifiableCredentials = makeIndexedStorageAccessor( +export const storedVerifiableCredentials = makeSerializedAndIndexedStorageAccessor( 'local', ChromeStorageKey.VerifiableCredentials ); @@ -187,6 +227,6 @@ export const sessionPendingTransactions = makeIndexedStorageAccessor( 'session', ChromeStorageKey.PendingTransactions ); -export const sessionVerifiableCredentials = makeIndexedStorageAccessor< +export const sessionVerifiableCredentials = makeSerializedAndIndexedStorageAccessor< Omit[] >('session', ChromeStorageKey.TemporaryVerifiableCredentials); From 573379e153bae769b24c008235a385b4746c1040 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 22 Aug 2023 10:23:33 +0200 Subject: [PATCH 114/231] Wait for schemas and metadata to be saved before displaying imported credentials --- .../VerifiableCredentialImport.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 2c53f88d..5314bece 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -61,7 +61,7 @@ function updateList(stored: T[], toAdd: T[], isEqual: (a: T, b: T) => boolean function updateRecord( stored: Record, toAdd: Record, - update: (updated: Record) => void + update: (updated: Record) => Promise ) { const updated = { ...stored }; Object.entries(toAdd).forEach(([key, value]) => { @@ -70,7 +70,7 @@ function updateRecord( } }); - update(updated); + return update(updated); } export default function VerifiableCredentialImport() { @@ -100,8 +100,8 @@ export default function VerifiableCredentialImport() { (a, b) => a.id === b.id, setVerifiableCredentials ); - updateRecord(storedSchemas.value, schemas, setSchemas); - updateRecord(storedMetadata.value, metadata, setMetadata); + await updateRecord(storedSchemas.value, schemas, setSchemas); + await updateRecord(storedMetadata.value, metadata, setMetadata); setImported(filteredCredentials); } } catch (e) { From 8a9e932cdcc1bff7961186e7bc6947a9174b8c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Wed, 23 Aug 2023 10:04:19 +0200 Subject: [PATCH 115/231] Refactor setting variable --- .../pages/VerifiableCredential/VerifiableCredentialCard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index d740ce44..ddc59202 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -84,12 +84,11 @@ function applySchemaAndLocalization( localization?: Record ): (value: [string, string | bigint]) => { title: string; key: string; value: string | bigint } { return (value: [string, string | bigint]) => { - let title; const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); } - title = attributeSchema.title; + let { title } = attributeSchema; if (localization) { const localizedTitle = localization[value[0]]; From 345000ef725e6557e4bf9f8b63116dde9b28c402 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 23 Aug 2023 11:18:40 +0200 Subject: [PATCH 116/231] Handle bigints in backup --- .../VerifiableCredentialImport.tsx | 13 +++++++++++-- .../popup/pages/VerifiableCredentialBackup/utils.ts | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 2c53f88d..7e32c485 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -13,6 +13,7 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLay import { noOp } from 'wallet-common-helpers'; import { decrypt } from '@shared/utils/crypto'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; +import JSONBigInt from 'json-bigint'; import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; import { ExportFormat, VerifiableCredentialExport } from './utils'; @@ -40,8 +41,16 @@ function DisplayResult({ imported }: { imported: VerifiableCredential[] }) { } async function parseExport(data: EncryptedData, encryptionKey: string): Promise { - // TODO handle bigints - const backup: ExportFormat = JSON.parse(await decrypt(data, encryptionKey)); + const decrypted = await decrypt(data, encryptionKey); + const backup: ExportFormat = JSONBigInt({ + alwaysParseAsBig: true, + useNativeBigInt: true, + }).parse(decrypted); + // Change index to number, due to parse changing all numbers to bigints. + backup.value.verifiableCredentials = backup.value.verifiableCredentials.map((v) => ({ + ...v, + index: Number(v.index), + })); // TODO validation return backup.value; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts index 64ef65e9..e6607e44 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts @@ -11,6 +11,7 @@ import { } from '@popup/store/verifiable-credential'; import { networkConfigurationAtom } from '@popup/store/settings'; import { saveData } from '@popup/shared/utils/file-helpers'; +import { stringify } from 'json-bigint'; export type VerifiableCredentialExport = { verifiableCredentials: VerifiableCredential[]; @@ -43,8 +44,8 @@ function createExport( }, }; - // TODO handle bigints - return encrypt(JSON.stringify(exportContent), encryptionKey); + // Use json-bigint to serialize bigints as json numbers. + return encrypt(stringify(exportContent), encryptionKey); } export function useVerifiableCredentialExport() { From 6c3f69e5a795815ee9109e0d27297052865bd14d Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 23 Aug 2023 11:28:53 +0200 Subject: [PATCH 117/231] Fix spelling --- packages/browser-wallet/src/shared/storage/access.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 53f0e5b4..56cdd4d6 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -59,7 +59,7 @@ export type IndexedStorageAccessor = { /** * Factory function for creating a StorageAccessor from a key. * - * @param area storeage area to store value in + * @param area storage area to store value in * @param key key used to store value */ const makeStorageAccessor = (area: chrome.storage.AreaName, key: ChromeStorageKey): StorageAccessor => { @@ -116,7 +116,7 @@ export function useIndexedStorage( /** * Factory function for creating a StorageAccessor, which serializes before storing the value and deserializes before loading the value, from a key. * - * @param area storeage area to store value in + * @param area storage area to store value in * @param key key used to store value */ export function makeSerializedStorageAccessor( From 7b5823fef29bee7d49abe39b0e1f83d0f53ae9f2 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 23 Aug 2023 11:40:40 +0200 Subject: [PATCH 118/231] Bump version + update changelog --- packages/browser-wallet/CHANGELOG.md | 14 ++++++++++++++ packages/browser-wallet/package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b0f0742b..b0f5000f 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 1.1.1 + +### Fixed + +- Support number properties as bigints for verifiable credentials. +- Use localization for verifiable credentials. +- Fix wallet crashing when importing verifiable credentials with new schemas. + +## 1.1.0 + +### Added + +- Web3IdCredentials support. + ## 1.0.6 ### Changed diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index c8df16b9..15292581 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.0", + "version": "1.1.1", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 8061655896618317e4a4e12667e0c4f9770d508c Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 23 Aug 2023 15:36:41 +0200 Subject: [PATCH 119/231] Save temporary VCmetadata to display pending VC + handle credentialEntry not existing --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ .../AddWeb3IdCredential.tsx | 6 +++++- .../VerifiableCredentialHooks.tsx | 19 +++++++++++++++---- .../browser-wallet/src/popup/store/utils.ts | 5 +++++ .../src/popup/store/verifiable-credential.ts | 4 ++++ .../src/shared/storage/access.ts | 4 ++++ .../src/shared/storage/types.ts | 1 + .../utils/verifiable-credential-helpers.ts | 18 +++++++++++------- 8 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b0f5000f..69f68cad 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Show verifiable credentials in overview before they are put on chain. + ## 1.1.1 ### Fixed diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 59caea2d..a33fe8ba 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -6,6 +6,7 @@ import { useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { + sessionTemporaryVerifiableCredentialMetadataAtom, sessionTemporaryVerifiableCredentialsAtom, storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialsAtom, @@ -51,6 +52,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const { onClose, withClose } = useContext(fullscreenPromptContext); const [acceptButtonDisabled, setAcceptButtonDisabled] = useState(false); const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom); + const [tempMetdata, setTempMetadata] = useAtom(sessionTemporaryVerifiableCredentialMetadataAtom); const storedWeb3IdCredentials = useAtomValue(storedVerifiableCredentialsAtom); const [verifiableCredentialMetadata, setVerifiableCredentialMetadata] = useAtom( storedVerifiableCredentialMetadataAtom @@ -144,11 +146,12 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); const credentialSubjectId = createPublicKeyIdentifier(credentialHolderId, network); const credentialSubject = { ...credential.credentialSubject, id: credentialSubjectId }; + const credentialId = createCredentialId(credentialHolderId, issuer, network); const fullCredential = { ...credential, credentialSubject, - id: createCredentialId(credentialHolderId, issuer, network), + id: credentialId, index, }; await setWeb3IdCredentials([...web3IdCredentials.value, fullCredential]); @@ -156,6 +159,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const newMetadata = { ...verifiableCredentialMetadata.value }; newMetadata[metadataUrl.url] = metadata; await setVerifiableCredentialMetadata(newMetadata); + await setTempMetadata({ ...tempMetdata.value, [credentialId]: metadata }); } return credentialSubjectId; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index d1822777..2bcf6d9e 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -17,10 +17,12 @@ import { useEffect, useState } from 'react'; import { storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialSchemasAtom, + sessionTemporaryVerifiableCredentialMetadataAtom, } from '@popup/store/verifiable-credential'; import { AsyncWrapper } from '@popup/store/utils'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; import { useTranslation } from 'react-i18next'; +import { noOp } from 'wallet-common-helpers'; /** * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. @@ -77,9 +79,11 @@ export function useCredentialEntry(credential?: VerifiableCredential) { if (credential) { const credentialHolderId = getCredentialHolderId(credential.id); const registryContractAddress = getCredentialRegistryContractAddress(credential.id); - getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { - setCredentialEntry(entry); - }); + getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId) + .then((entry) => { + setCredentialEntry(entry); + }) + .catch(noOp); // TODO add logging on catch? } }, [credential?.id, client]); @@ -97,6 +101,7 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { const [metadata, setMetadata] = useState(); const credentialEntry = useCredentialEntry(credential); const storedMetadata = useAtomValue(storedVerifiableCredentialMetadataAtom); + const tempMetadata = useAtomValue(sessionTemporaryVerifiableCredentialMetadataAtom); useEffect(() => { if (!storedMetadata.loading && credentialEntry) { @@ -107,8 +112,14 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { ); } setMetadata(storedCredentialMetadata); + } else if (!storedMetadata.loading && !credentialEntry && !tempMetadata.loading && credential) { + // Use temporary metadata + const tMetadata = tempMetadata.value[credential.id]; + if (tMetadata) { + setMetadata(tMetadata); + } } - }, [storedMetadata, credentialEntry]); + }, [storedMetadata.loading, tempMetadata.loading, credentialEntry, credential?.id]); return metadata; } diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index 897239e1..d7ccb758 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -31,6 +31,7 @@ import { storedAllowlist, storedVerifiableCredentialMetadata, sessionVerifiableCredentials, + sessionVerifiableCredentialMetadata, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -66,6 +67,10 @@ const accessorMap: Record> = { [ChromeStorageKey.VerifiableCredentialMetadata]: storedVerifiableCredentialMetadata, [ChromeStorageKey.Allowlist]: storedAllowlist, [ChromeStorageKey.TemporaryVerifiableCredentials]: useIndexedStorage(sessionVerifiableCredentials, getGenesisHash), + [ChromeStorageKey.TemporaryVerifiableCredentialMetadata]: useIndexedStorage( + sessionVerifiableCredentialMetadata, + getGenesisHash + ), }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts index 134b30dc..06de4d5f 100644 --- a/packages/browser-wallet/src/popup/store/verifiable-credential.ts +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -21,3 +21,7 @@ export const storedVerifiableCredentialMetadataAtom = atomWithChromeStorage< export const sessionTemporaryVerifiableCredentialsAtom = atomWithChromeStorage< Omit[] >(ChromeStorageKey.TemporaryVerifiableCredentials, [], true); + +export const sessionTemporaryVerifiableCredentialMetadataAtom = atomWithChromeStorage< + Record +>(ChromeStorageKey.TemporaryVerifiableCredentialMetadata, {}, true); diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 56cdd4d6..6244d145 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -230,3 +230,7 @@ export const sessionPendingTransactions = makeIndexedStorageAccessor( export const sessionVerifiableCredentials = makeSerializedAndIndexedStorageAccessor< Omit[] >('session', ChromeStorageKey.TemporaryVerifiableCredentials); + +export const sessionVerifiableCredentialMetadata = makeIndexedStorageAccessor< + Record +>('session', ChromeStorageKey.TemporaryVerifiableCredentialMetadata); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 9e00302e..2587e82b 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -30,6 +30,7 @@ export enum ChromeStorageKey { VerifiableCredentialSchemas = 'verifiableCredentialSchemas', VerifiableCredentialMetadata = 'verifiableCredentialMetadata', TemporaryVerifiableCredentials = 'tempVerifiableCredentials', + TemporaryVerifiableCredentialMetadata = 'tempVerifiableCredentialMetadata', Allowlist = 'allowlist', } diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 74f3b02b..b778d905 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -809,13 +809,17 @@ export async function getCredentialMetadata( ) { const metadataUrls: MetadataUrl[] = []; for (const vc of credentials) { - const entry = await getVerifiableCredentialEntry( - client, - getCredentialRegistryContractAddress(vc.id), - getCredentialHolderId(vc.id) - ); - if (entry) { - metadataUrls.push(entry.credentialInfo.metadataUrl); + try { + const entry = await getVerifiableCredentialEntry( + client, + getCredentialRegistryContractAddress(vc.id), + getCredentialHolderId(vc.id) + ); + if (entry) { + metadataUrls.push(entry.credentialInfo.metadataUrl); + } + } catch (e) { + // If we fail, the credential most likely doesn't exist and we skip it } } From e4688c89afe700a8addf5173b80af953fa89f421 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 09:38:17 +0200 Subject: [PATCH 120/231] Fix proof blank screen, when wallet contains vc that is not on chain --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ .../src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b0f5000f..0f72e4fb 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Wallet crashing when showing a proof request, while having a verifiable credential that is not yet on chain (or we otherwise fail to retrieve the status) + ## 1.1.1 ### Fixed diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index c04788cd..fcf3a6f8 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -66,7 +66,9 @@ async function getAllCredentialStatuses( ): Promise> { const statuses = await Promise.all( credentials.map((credential) => - getVerifiableCredentialStatus(client, credential.id).then((status) => [credential.id, status]) + getVerifiableCredentialStatus(client, credential.id) + .then((status) => [credential.id, status]) + .catch(() => [credential.id, VerifiableCredentialStatus.Pending]) ) ); return Object.fromEntries(statuses); From 822ba023edd1b6512e77669e34518d0d672a33f5 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 10:45:35 +0200 Subject: [PATCH 121/231] Fix faulty identity selector --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ .../src/popup/pages/Web3ProofRequest/utils.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b0f5000f..b2ce4dde 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Incorrect verifiable presentations created, due to incorrect identity/identityProviderIndex used. + ## 1.1.1 ### Fixed diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index ea2e8d9c..511717d2 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -51,7 +51,7 @@ export function getAccountCredentialCommitmentInput( throw new Error('IdQualifier not fulfilled'); } - const identity = (identities || []).find(isIdentityOfCredential); + const identity = (identities || []).find((id) => isIdentityOfCredential(id)(credential)); if (!identity || identity.status !== CreationStatus.Confirmed) { throw new Error('No identity found for credential'); From ade5e0eecf1be3bd3806a78201e83338c2b8c227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Thu, 24 Aug 2023 11:03:45 +0200 Subject: [PATCH 122/231] Wait for extension to be closed This is a hacky fix that awaits the popup to close before continuing with import. --- .../browser-wallet/src/background/web3Id.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 7a321726..ecc0d8a3 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -31,7 +31,7 @@ import { MessageStatusWrapper, } from '@concordium/browser-wallet-message-hub'; import { getNet } from '@shared/utils/network-helpers'; -import { openWindow, RunCondition } from './window-management'; +import { openWindow, RunCondition, testPopupOpen } from './window-management'; import bgMessageHandler from './message-handler'; const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; @@ -195,6 +195,22 @@ export const runIfValidWeb3IdProof: RunCondition }; async function loadWeb3IdBackup(): Promise { + const waitForClosedPopup = new Promise((resolve, reject) => { + let escapeCounter = 0; + setTimeout(async function waitForClosed() { + const isOpen = await testPopupOpen(); + if (!isOpen) { + resolve(true); + } else { + if (escapeCounter > 10) { + reject(); + } + escapeCounter += 1; + setTimeout(waitForClosed, 100); + } + }, 100); + }); + await waitForClosedPopup; await openWindow(); bgMessageHandler.sendInternalMessage(InternalMessageType.ImportWeb3IdBackup); } From 85f5444e8e42437cd84b60cfefd50c18d236c303 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 13:19:20 +0200 Subject: [PATCH 123/231] Add contract address to verifiableCredentialDetails --- packages/browser-wallet/CHANGELOG.md | 4 +++ .../VerifiableCredentialDetails.tsx | 29 +++++++++---------- .../pages/VerifiableCredential/i18n/da.ts | 1 + .../pages/VerifiableCredential/i18n/en.ts | 1 + 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index e1015b68..a609f8b9 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Display contract address of issuer in verifiable credential details. + ### Fixed - Incorrect verifiable presentations created, due to incorrect identity/identityProviderIndex used. diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 7c74de00..ad1148a0 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -16,6 +16,7 @@ import { getCredentialHolderId, getCredentialRegistryContractAddress, getRevokeTransactionExecutionEnergyEstimate, + getContractAddressFromIssuerDID, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; import { TimeStampUnit, dateFromTimestamp, ClassName } from 'wallet-common-helpers'; @@ -36,38 +37,34 @@ import { DisplayAttribute, VerifiableCredentialCard, VerifiableCredentialCardHea function DisplayIssuerMetadata({ issuer }: { issuer: string }) { const { t } = useTranslation('verifiableCredential'); const issuerMetadata = useIssuerMetadata(issuer); - - if ( - issuerMetadata === undefined || - (issuerMetadata.description === undefined && - issuerMetadata.icon === undefined && - issuerMetadata.name === undefined && - issuerMetadata.url === undefined) - ) { - return null; - } + const issuerContract = getContractAddressFromIssuerDID(issuer); return (

    {t('details.issuer.title')}

    - {issuerMetadata.icon && } - {issuerMetadata.name && ( + + {issuerMetadata?.icon && } + {issuerMetadata?.name && ( )} - {issuerMetadata.description && ( + {issuerMetadata?.description && ( )} - {issuerMetadata.url && ( + {issuerMetadata?.url && ( 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 62c3f003..dd0a11db 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -17,6 +17,7 @@ const t: typeof en = { validUntil: 'Gyldig indtil', issuer: { title: 'Udstedt af', + contract: 'kontraktindeks, subindeks', name: 'Navn', description: 'Beskrivelse', url: 'Hjemmeside', diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts index aec8ad54..45beb1f6 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -15,6 +15,7 @@ const t = { validUntil: 'Valid until', issuer: { title: 'Issued by', + contract: 'contract index, subindex', name: 'Name', description: 'Description', url: 'Website', From 67ed77f534498e73aa8d871c93097f2f994c2c1a Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 13:44:46 +0200 Subject: [PATCH 124/231] Save metadata url in session storage instead of metadata, for new verifiable credentials --- .../AddWeb3IdCredential.tsx | 6 +-- .../VerifiableCredentialHooks.tsx | 38 +++++++++++-------- .../browser-wallet/src/popup/store/utils.ts | 6 +-- .../src/popup/store/verifiable-credential.ts | 8 ++-- .../src/shared/storage/access.ts | 7 ++-- .../src/shared/storage/types.ts | 2 +- 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index a33fe8ba..20b33166 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import Button from '@popup/shared/Button'; import { - sessionTemporaryVerifiableCredentialMetadataAtom, + sessionTemporaryVerifiableCredentialMetadataUrlsAtom, sessionTemporaryVerifiableCredentialsAtom, storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialsAtom, @@ -52,7 +52,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const { onClose, withClose } = useContext(fullscreenPromptContext); const [acceptButtonDisabled, setAcceptButtonDisabled] = useState(false); const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom); - const [tempMetdata, setTempMetadata] = useAtom(sessionTemporaryVerifiableCredentialMetadataAtom); + const [metadataUrls, setMetadataUrls] = useAtom(sessionTemporaryVerifiableCredentialMetadataUrlsAtom); const storedWeb3IdCredentials = useAtomValue(storedVerifiableCredentialsAtom); const [verifiableCredentialMetadata, setVerifiableCredentialMetadata] = useAtom( storedVerifiableCredentialMetadataAtom @@ -159,7 +159,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const newMetadata = { ...verifiableCredentialMetadata.value }; newMetadata[metadataUrl.url] = metadata; await setVerifiableCredentialMetadata(newMetadata); - await setTempMetadata({ ...tempMetdata.value, [credentialId]: metadata }); + await setMetadataUrls({ ...metadataUrls.value, [credentialId]: metadataUrl.url }); } return credentialSubjectId; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 2bcf6d9e..c1143f9c 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -17,7 +17,7 @@ import { useEffect, useState } from 'react'; import { storedVerifiableCredentialMetadataAtom, storedVerifiableCredentialSchemasAtom, - sessionTemporaryVerifiableCredentialMetadataAtom, + sessionTemporaryVerifiableCredentialMetadataUrlsAtom, } from '@popup/store/verifiable-credential'; import { AsyncWrapper } from '@popup/store/utils'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; @@ -101,24 +101,30 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { const [metadata, setMetadata] = useState(); const credentialEntry = useCredentialEntry(credential); const storedMetadata = useAtomValue(storedVerifiableCredentialMetadataAtom); - const tempMetadata = useAtomValue(sessionTemporaryVerifiableCredentialMetadataAtom); + const tempMetadata = useAtomValue(sessionTemporaryVerifiableCredentialMetadataUrlsAtom); useEffect(() => { - if (!storedMetadata.loading && credentialEntry) { - const storedCredentialMetadata = storedMetadata.value[credentialEntry.credentialInfo.metadataUrl.url]; - if (!storedCredentialMetadata) { - throw new Error( - `Attempted to find credential metadata for credentialId: ${credentialEntry.credentialInfo.credentialHolderId} but none was found!` - ); - } - setMetadata(storedCredentialMetadata); - } else if (!storedMetadata.loading && !credentialEntry && !tempMetadata.loading && credential) { - // Use temporary metadata - const tMetadata = tempMetadata.value[credential.id]; - if (tMetadata) { - setMetadata(tMetadata); - } + if (storedMetadata.loading) { + return; + } + let url; + if (credentialEntry) { + url = credentialEntry.credentialInfo.metadataUrl.url; + } else if (!tempMetadata.loading && credential) { + url = tempMetadata.value[credential.id]; + } + if (!url) { + return; + } + const storedCredentialMetadata = storedMetadata.value[url]; + if (!storedCredentialMetadata) { + throw new Error( + `Attempted to find credential metadata for credentialId: ${ + credentialEntry?.credentialInfo.credentialHolderId || credential?.id + } but none was found!` + ); } + setMetadata(storedCredentialMetadata); }, [storedMetadata.loading, tempMetadata.loading, credentialEntry, credential?.id]); return metadata; diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index d7ccb758..f221a632 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -31,7 +31,7 @@ import { storedAllowlist, storedVerifiableCredentialMetadata, sessionVerifiableCredentials, - sessionVerifiableCredentialMetadata, + sessionVerifiableCredentialMetadataUrls, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -67,8 +67,8 @@ const accessorMap: Record> = { [ChromeStorageKey.VerifiableCredentialMetadata]: storedVerifiableCredentialMetadata, [ChromeStorageKey.Allowlist]: storedAllowlist, [ChromeStorageKey.TemporaryVerifiableCredentials]: useIndexedStorage(sessionVerifiableCredentials, getGenesisHash), - [ChromeStorageKey.TemporaryVerifiableCredentialMetadata]: useIndexedStorage( - sessionVerifiableCredentialMetadata, + [ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls]: useIndexedStorage( + sessionVerifiableCredentialMetadataUrls, getGenesisHash ), }; diff --git a/packages/browser-wallet/src/popup/store/verifiable-credential.ts b/packages/browser-wallet/src/popup/store/verifiable-credential.ts index 06de4d5f..93ab2278 100644 --- a/packages/browser-wallet/src/popup/store/verifiable-credential.ts +++ b/packages/browser-wallet/src/popup/store/verifiable-credential.ts @@ -22,6 +22,8 @@ export const sessionTemporaryVerifiableCredentialsAtom = atomWithChromeStorage< Omit[] >(ChromeStorageKey.TemporaryVerifiableCredentials, [], true); -export const sessionTemporaryVerifiableCredentialMetadataAtom = atomWithChromeStorage< - Record ->(ChromeStorageKey.TemporaryVerifiableCredentialMetadata, {}, true); +export const sessionTemporaryVerifiableCredentialMetadataUrlsAtom = atomWithChromeStorage>( + ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls, + {}, + true +); diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index 6244d145..c714f3d0 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -231,6 +231,7 @@ export const sessionVerifiableCredentials = makeSerializedAndIndexedStorageAcces Omit[] >('session', ChromeStorageKey.TemporaryVerifiableCredentials); -export const sessionVerifiableCredentialMetadata = makeIndexedStorageAccessor< - Record ->('session', ChromeStorageKey.TemporaryVerifiableCredentialMetadata); +export const sessionVerifiableCredentialMetadataUrls = makeIndexedStorageAccessor>( + 'session', + ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls +); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 2587e82b..4fb7756d 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -30,7 +30,7 @@ export enum ChromeStorageKey { VerifiableCredentialSchemas = 'verifiableCredentialSchemas', VerifiableCredentialMetadata = 'verifiableCredentialMetadata', TemporaryVerifiableCredentials = 'tempVerifiableCredentials', - TemporaryVerifiableCredentialMetadata = 'tempVerifiableCredentialMetadata', + TemporaryVerifiableCredentialMetadataUrls = 'tempVerifiableCredentialMetadata', Allowlist = 'allowlist', } From 6aac64f2b495eae870f2f3ccecf16cb334ac37cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Thu, 24 Aug 2023 13:48:10 +0200 Subject: [PATCH 125/231] Check required attributes when adding credential --- .../AddWeb3IdCredential.tsx | 19 +++++++++++++++++++ .../pages/AddWeb3IdCredential/i18n/da.ts | 1 + .../pages/AddWeb3IdCredential/i18n/en.ts | 1 + 3 files changed, 21 insertions(+) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 59caea2d..c33e0baa 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -98,6 +98,25 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { () => setError(t('error.schema')), [schemas.loading] ); + + useEffect(() => { + if (schema) { + // Ensure that all attributes required by the schema are in the attributes. If not, then + // the credential should not be allowed to be added. + const missingRequiredAttributeKeys = []; + const requiredAttributes = schema.properties.credentialSubject.properties.attributes.required; + for (const requiredAttribute of requiredAttributes) { + if (!Object.keys(credential.credentialSubject.attributes).includes(requiredAttribute)) { + missingRequiredAttributeKeys.push(requiredAttribute); + } + } + + if (missingRequiredAttributeKeys.length > 0) { + setError(t('error.attribute', { attributeKeys: missingRequiredAttributeKeys })); + } + } + }, [schema?.properties.credentialSubject.properties.attributes.required]); + useEffect(() => () => controller.abort(), []); const localization = useAsyncMemo( diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts index a7498ace..06607d84 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts @@ -10,6 +10,7 @@ const t: typeof en = { // We don't translate these because they are mainly for bug reporting. metadata: en.error.metadata, schema: en.error.schema, + attribute: en.error.attribute, }, }; diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts index d9c6fe68..c4c243d0 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -7,6 +7,7 @@ const t = { 'We are unable to add the web3Id credential to the wallet due to the following issue, please report this to the issuer:', metadata: 'We are unable to load the metadata for the credential.', schema: 'We are unable to load the schema specification for the credential.', + attribute: 'The received credential is missing one or more required attributes ({{ attributeKeys }})', }, }; From 26b877517ee1d725dd0e5a5cdf8a7b07f94825bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Thu, 24 Aug 2023 13:49:45 +0200 Subject: [PATCH 126/231] Update CHANGELOG --- packages/browser-wallet/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index e1015b68..c4f91aa6 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Validation of required attributes when adding a credential. + ### Fixed - Incorrect verifiable presentations created, due to incorrect identity/identityProviderIndex used. From 58fe871edacda6b0a745093a386e2b9e9a0ac466 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 14:22:56 +0200 Subject: [PATCH 127/231] VerifiableCredentialDetails now handles missing credentialEntry --- .../VerifiableCredentialDetails.tsx | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 7c74de00..a54134e1 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -76,6 +76,48 @@ function DisplayIssuerMetadata({ issuer }: { issuer: string }) { ); } +/** + * Component for displaying information from the credentialEntry, if the entry is available. + * @param credentialEntry the credentialEntry to display info from + */ + +function DisplayCredentialEntryInfo({ credentialEntry }: { credentialEntry?: CredentialQueryResponse }) { + const { t } = useTranslation('verifiableCredential'); + + if (!credentialEntry) { + return null; + } + + 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 && ( + + )} +
    + ); +} + /** * 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. @@ -87,43 +129,16 @@ function VerifiableCredentialExtraDetails({ className, issuer, }: { - credentialEntry: CredentialQueryResponse; + credentialEntry?: CredentialQueryResponse; status: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; issuer: string; } & 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 (
    -
    - - - {credentialEntry.credentialInfo.validUntil !== undefined && ( - - )} -
    +
    @@ -198,10 +213,6 @@ export default function VerifiableCredentialDetails({ }, [client, credential, hdWallet, credentialEntry, nav, pathname]); const menuButton: MenuButton | undefined = useMemo(() => { - if (credentialEntry === undefined) { - return undefined; - } - const menuButtons = []; if (credentialEntry?.credentialInfo.holderRevocable && status !== VerifiableCredentialStatus.Revoked) { @@ -230,9 +241,9 @@ export default function VerifiableCredentialDetails({ return undefined; }, [credentialEntry?.credentialInfo.holderRevocable, goToConfirmPage, showExtraDetails]); - // Wait for the credential entry to be loaded from the chain, and for the HdWallet + // Wait for the HdWallet // to be loaded to be ready to derive keys. - if (credentialEntry === undefined || hdWallet === undefined) { + if (hdWallet === undefined) { return null; } From e7f19003cc15618fba68406775fe0f0d7cd75af2 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 24 Aug 2023 15:35:38 +0200 Subject: [PATCH 128/231] Bump version --- packages/browser-wallet/CHANGELOG.md | 2 +- packages/browser-wallet/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b91d25f6..47bdd3c0 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.1.2 ### Added diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 15292581..ec1c0787 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.1", + "version": "1.1.2", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 6392ea9d546ebdaab1d4e3e6cc6d1b64771656f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Hjort?= <87635671+shjortConcordium@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:59:45 +0200 Subject: [PATCH 129/231] Update packages/browser-wallet/src/shared/storage/types.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakob Ørhøj <57264157+orhoj@users.noreply.github.com> --- packages/browser-wallet/src/shared/storage/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 4fb7756d..f514c367 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -30,7 +30,7 @@ export enum ChromeStorageKey { VerifiableCredentialSchemas = 'verifiableCredentialSchemas', VerifiableCredentialMetadata = 'verifiableCredentialMetadata', TemporaryVerifiableCredentials = 'tempVerifiableCredentials', - TemporaryVerifiableCredentialMetadataUrls = 'tempVerifiableCredentialMetadata', + TemporaryVerifiableCredentialMetadataUrls = 'tempVerifiableCredentialMetadataUrls', Allowlist = 'allowlist', } From c65771729b157643675779a797af5bc2835378d2 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 10:34:18 +0200 Subject: [PATCH 130/231] Update verifiable credential schema JSON schema --- packages/browser-wallet/CHANGELOG.md | 6 ++ .../utils/verifiable-credential-helpers.ts | 58 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 41681cdc..41d93113 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. + ## 1.1.2 ### Added diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index b778d905..47baae86 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -368,16 +368,64 @@ const verifiableCredentialSchemaSchema = { additionalProperties: false, properties: { properties: { - additionalProperties: { - type: 'object', - }, + additionalProperties: false, properties: { + attributes: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + format: { + type: 'string', + }, + properties: { + additionalProperties: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + format: { + type: 'string', + }, + title: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['title', 'type', 'description'], + type: 'object', + }, + type: 'object', + }, + required: { + items: { + type: 'string', + }, + type: 'array', + }, + title: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['description', 'properties', 'required', 'title', 'type'], + type: 'object', + }, id: { additionalProperties: false, properties: { description: { type: 'string', }, + format: { + type: 'string', + }, title: { type: 'string', }, @@ -389,7 +437,7 @@ const verifiableCredentialSchemaSchema = { type: 'object', }, }, - required: ['id'], + required: ['id', 'attributes'], type: 'object', }, required: { @@ -402,7 +450,7 @@ const verifiableCredentialSchemaSchema = { type: 'string', }, }, - required: ['type', 'required', 'properties'], + required: ['type', 'properties', 'required'], type: 'object', }, }, From c018fb6913a3ebbd8e22754475bc26d64e4f19a2 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 12:53:52 +0200 Subject: [PATCH 131/231] Add initial log functionality --- .../AddWeb3IdCredential.tsx | 16 ++++- .../pages/AddWeb3IdCredential/i18n/da.ts | 1 + .../pages/AddWeb3IdCredential/i18n/en.ts | 1 + .../VerifiableCredentialHooks.tsx | 36 ++++++---- .../browser-wallet/src/popup/store/utils.ts | 2 + .../src/shared/storage/access.ts | 2 + .../src/shared/storage/types.ts | 1 + .../src/shared/storage/update.ts | 22 ++++++ .../src/shared/utils/log-helpers.ts | 71 +++++++++++++++++++ .../utils/verifiable-credential-helpers.ts | 7 +- 10 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 packages/browser-wallet/src/shared/utils/log-helpers.ts diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index c2b47aa0..e894fb98 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -28,6 +28,7 @@ import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers' import { networkConfigurationAtom } from '@popup/store/settings'; import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { parse } from '@shared/utils/payload-helpers'; +import { logError } from '@shared/utils/log-helpers'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; type Props = { @@ -81,7 +82,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } return fetchCredentialMetadata(metadataUrl, controller); }, - () => setError(t('error.metadata')), + (e) => { + setError(t('error.metadata')); + logError(e); + }, [verifiableCredentialMetadata.loading] ); @@ -97,7 +101,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } return fetchCredentialSchema({ url: schemaUrl }, controller); }, - () => setError(t('error.schema')), + (e) => { + setError(t('error.schema')); + logError(e); + }, [schemas.loading] ); @@ -138,7 +145,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return fetchLocalization(currentLanguageLocalization, controller); }, - () => setError('Failed to get localization'), + (e) => { + setError(t('error.localization')); + logError(e); + }, [metadata, i18n] ); diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts index 06607d84..f7245542 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts @@ -11,6 +11,7 @@ const t: typeof en = { metadata: en.error.metadata, schema: en.error.schema, attribute: en.error.attribute, + localization: en.error.localization, }, }; diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts index c4c243d0..16569fe9 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -8,6 +8,7 @@ const t = { metadata: 'We are unable to load the metadata for the credential.', schema: 'We are unable to load the schema specification for the credential.', attribute: 'The received credential is missing one or more required attributes ({{ attributeKeys }})', + localization: 'Failed to get localization', }, }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index c1143f9c..57949f1b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -22,7 +22,7 @@ import { import { AsyncWrapper } from '@popup/store/utils'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; import { useTranslation } from 'react-i18next'; -import { noOp } from 'wallet-common-helpers'; +import { logError } from '@shared/utils/log-helpers'; /** * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. @@ -36,7 +36,10 @@ export function useCredentialStatus(credential: VerifiableCredential) { useEffect(() => { getVerifiableCredentialStatus(client, credential.id) .then(setStatus) - .catch(() => setStatus(VerifiableCredentialStatus.Pending)); + .catch((e) => { + setStatus(VerifiableCredentialStatus.Pending); + logError(e); + }); }, [credential.id, client]); return status; @@ -83,7 +86,7 @@ export function useCredentialEntry(credential?: VerifiableCredential) { .then((entry) => { setCredentialEntry(entry); }) - .catch(noOp); // TODO add logging on catch? + .catch(logError); } }, [credential?.id, client]); @@ -176,7 +179,10 @@ export function useCredentialLocalization(credential?: VerifiableCredential): Lo // TODO Validate that localization is present for all keys. setLocalization({ loading: false, result: res }); }) - .catch(() => setLocalization({ loading: false })); + .catch((e) => { + setLocalization({ loading: false }); + logError(e); + }); return () => { abortController.abort(); @@ -199,10 +205,12 @@ export function useIssuerMetadata(issuer: string): IssuerMetadata | undefined { useEffect(() => { const registryContractAddress = getCredentialRegistryContractAddress(issuer); - getCredentialRegistryMetadata(client, registryContractAddress).then((res) => { - const abortController = new AbortController(); - fetchIssuerMetadata(res.issuerMetadata, abortController).then(setIssuerMetadata); - }); + getCredentialRegistryMetadata(client, registryContractAddress) + .then((res) => { + const abortController = new AbortController(); + fetchIssuerMetadata(res.issuerMetadata, abortController).then(setIssuerMetadata).catch(logError); + }) + .catch(logError); }, [client, issuer]); return issuerMetadata; @@ -234,11 +242,13 @@ export function useFetchingEffect( const abortControllers: AbortController[] = []; if (!credentials.loading && credentials.value.length !== 0 && !storedData.loading) { - dataFetcher(credentials.value, client, abortControllers, storedData.value).then((result) => { - if (!isCancelled && result.updateReceived) { - setStoredData(result.data); - } - }); + dataFetcher(credentials.value, client, abortControllers, storedData.value) + .then((result) => { + if (!isCancelled && result.updateReceived) { + setStoredData(result.data); + } + }) + .catch(logError); } return () => { diff --git a/packages/browser-wallet/src/popup/store/utils.ts b/packages/browser-wallet/src/popup/store/utils.ts index f221a632..6f5d26b0 100644 --- a/packages/browser-wallet/src/popup/store/utils.ts +++ b/packages/browser-wallet/src/popup/store/utils.ts @@ -32,6 +32,7 @@ import { storedVerifiableCredentialMetadata, sessionVerifiableCredentials, sessionVerifiableCredentialMetadataUrls, + storedLog, } from '@shared/storage/access'; import { ChromeStorageKey } from '@shared/storage/types'; import { atom, PrimitiveAtom, WritableAtom } from 'jotai'; @@ -71,6 +72,7 @@ const accessorMap: Record> = { sessionVerifiableCredentialMetadataUrls, getGenesisHash ), + [ChromeStorageKey.Log]: storedLog, }; export function resetOnUnmountAtom(initial: V): PrimitiveAtom { diff --git a/packages/browser-wallet/src/shared/storage/access.ts b/packages/browser-wallet/src/shared/storage/access.ts index c714f3d0..3b31f115 100644 --- a/packages/browser-wallet/src/shared/storage/access.ts +++ b/packages/browser-wallet/src/shared/storage/access.ts @@ -1,6 +1,7 @@ import { stringify } from '@concordium/browser-wallet-api/src/util'; import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; +import { Log } from '@shared/utils/log-helpers'; import { ChromeStorageKey, EncryptedData, @@ -205,6 +206,7 @@ const indexedStoredAllowlist = makeIndexedStorageAccessor('local', ChromeStorageKey.Log); export const sessionOpenPrompt = makeStorageAccessor('session', ChromeStorageKey.OpenPrompt); export const sessionPasscode = makeStorageAccessor('session', ChromeStorageKey.Passcode); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index f514c367..25c95147 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -32,6 +32,7 @@ export enum ChromeStorageKey { TemporaryVerifiableCredentials = 'tempVerifiableCredentials', TemporaryVerifiableCredentialMetadataUrls = 'tempVerifiableCredentialMetadataUrls', Allowlist = 'allowlist', + Log = 'log', } export enum Theme { diff --git a/packages/browser-wallet/src/shared/storage/update.ts b/packages/browser-wallet/src/shared/storage/update.ts index 6bc18700..5957379c 100644 --- a/packages/browser-wallet/src/shared/storage/update.ts +++ b/packages/browser-wallet/src/shared/storage/update.ts @@ -60,6 +60,28 @@ export async function removeFromList( }); } +/* + * Generic method to add an element to list in storage while ensuring that + * the list never grows beyond the provided size. If the list is still small + * enough, then the addition is prepended to the list. If the the list would have + * grown greater than the max size, then the addition is prepended to the list and + * the last element of the list is removed. + */ +export async function addToListMaxSize( + lock: string, + addition: Type, + storage: StorageAccessor, + size: number +): Promise { + return navigator.locks.request(lock, async () => { + const list = (await storage.get()) || []; + if (list.length < size) { + return storage.set([addition].concat(list)); + } + return storage.set([addition].concat(list.slice(0, list.length - 1))); + }); +} + /** * Generic method to edit/update elements in a list in storage * Note that this replaces the element found by the findPredicate with the edit. diff --git a/packages/browser-wallet/src/shared/utils/log-helpers.ts b/packages/browser-wallet/src/shared/utils/log-helpers.ts new file mode 100644 index 00000000..80547672 --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/log-helpers.ts @@ -0,0 +1,71 @@ +import { storedLog } from '@shared/storage/access'; +import { addToListMaxSize } from '@shared/storage/update'; +import { isDevelopmentBuild } from './environment-helpers'; + +const loggingLock = 'concordium_log_lock'; + +// Determines the maximum number of log entries that is stored +// at a time. +const LOG_MAX_ENTRIES = 100; + +export enum LoggingLevel { + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR', +} + +export interface Log { + timestamp: number; + level: LoggingLevel; + message: string; +} + +function isError(error: unknown): error is { message: string } { + return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string'; +} + +function logForDevelopmentBuild(logEntry: Log) { + const logMessage = `[${new Date(logEntry.timestamp).toISOString()}] ${logEntry.level} ${logEntry.message}`; + switch (logEntry.level) { + case LoggingLevel.WARN: + // eslint-disable-next-line no-console + console.warn(logMessage); + break; + case LoggingLevel.ERROR: + // eslint-disable-next-line no-console + console.error(logMessage); + break; + case LoggingLevel.INFO: + default: + // eslint-disable-next-line no-console + console.log(logMessage); + break; + } +} + +async function log(message: string, level: LoggingLevel) { + const timestamp = Date.now(); + const logEntry: Log = { + level, + message, + timestamp, + }; + + if (isDevelopmentBuild()) { + logForDevelopmentBuild(logEntry); + } + + await addToListMaxSize(loggingLock, logEntry, storedLog, LOG_MAX_ENTRIES); +} + +export async function logErrorMessage(message: string) { + log(message, LoggingLevel.ERROR); +} + +export async function logError(error: unknown) { + if (isError(error)) { + logErrorMessage(error.message); + } else { + logErrorMessage(String(error)); + } +} diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index b778d905..70e13874 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -11,6 +11,7 @@ import { Buffer } from 'buffer/'; import jsonschema from 'jsonschema'; import { applyExecutionNRGBuffer, getContractName } from './contract-helpers'; import { getNet } from './network-helpers'; +import { logError } from './log-helpers'; /** * Extracts the credential holder id from a verifiable credential id (did). @@ -784,7 +785,7 @@ export async function getCredentialSchemas( } catch (e) { // Ignore errors that occur because we aborted, as that is expected to happen. if (!controller.signal.aborted) { - // TODO This should be logged. + logError(e); } } } @@ -819,7 +820,7 @@ export async function getCredentialMetadata( metadataUrls.push(entry.credentialInfo.metadataUrl); } } catch (e) { - // If we fail, the credential most likely doesn't exist and we skip it + logError(e); } } @@ -839,7 +840,7 @@ export async function getCredentialMetadata( } catch (e) { // Ignore errors that occur because we aborted, as that is expected to happen. if (!controller.signal.aborted) { - // TODO This should be logged. + logError(e); } } } From 8ce3720d77a0dae7a5e3c2288a8d4a2b3d155186 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 14:36:00 +0200 Subject: [PATCH 132/231] Improve the documentation --- .../browser-wallet/src/background/web3Id.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index ecc0d8a3..42e4f43b 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -194,15 +194,24 @@ export const runIfValidWeb3IdProof: RunCondition } }; -async function loadWeb3IdBackup(): Promise { - const waitForClosedPopup = new Promise((resolve, reject) => { +/** + * Wait until there are no popups open, or until the number of iterations supplied + * have been waited out. One check is attempted every 100ms. + * + * NOTE: We do this because we have found no better way to wait for the extension + * window to be closed from the verifiable credential list. It signals the background + * script and then closes, but it can take some time for it to close. + * @param waitIterations number of iterations before escaping the waiting + */ +async function waitForClosedPopup(waitIterations: number): Promise { + return new Promise((resolve, reject) => { let escapeCounter = 0; setTimeout(async function waitForClosed() { const isOpen = await testPopupOpen(); if (!isOpen) { - resolve(true); + resolve(); } else { - if (escapeCounter > 10) { + if (escapeCounter > waitIterations) { reject(); } escapeCounter += 1; @@ -210,7 +219,10 @@ async function loadWeb3IdBackup(): Promise { } }, 100); }); - await waitForClosedPopup; +} + +async function loadWeb3IdBackup(): Promise { + await waitForClosedPopup(30); await openWindow(); bgMessageHandler.sendInternalMessage(InternalMessageType.ImportWeb3IdBackup); } From 63638f6a47d57a7e938522314858d48168de172f Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 25 Aug 2023 14:40:54 +0200 Subject: [PATCH 133/231] Fix danish delegate update text --- packages/browser-wallet/src/popup/pages/Account/i18n/da.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts index 7a20ce1c..ded694b3 100644 --- a/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Account/i18n/da.ts @@ -147,7 +147,7 @@ const t: typeof en = { }, '3': { title: 'Opdateringer med længere cool-downs', - body: 'Hvis du vælger at formindske din delegation saldo, så vil ændringen træde i kraft efter en cool-down periode.\n\nI løbet af denne cool-down periode, vil delegation saldoen være låst og kan ikke ændres, og du vil ikke kunne stoppe delegation.\n\nDu vil stadig få fortjenester i cool-down perioden, og du vil stadig kunne ændre andre delegationindstillinger.n\n.Hvis du laver andre ændringer samtidigt med at du formindsker delegation saldoen, vil disse ændringer træde i kraft ved den næste pay day, som beskrevet på sidste side.', + body: 'Hvis du vælger at formindske din delegation saldo, så vil ændringen træde i kraft efter en cool-down periode.\n\nI løbet af denne cool-down periode, vil delegation saldoen være låst og kan ikke ændres, og du vil ikke kunne stoppe delegation.\n\nDu vil stadig få fortjenester i cool-down perioden, og du vil stadig kunne ændre andre delegationindstillinger.\n\nHvis du laver andre ændringer samtidigt med at du formindsker delegation saldoen, vil disse ændringer træde i kraft ved den næste pay day, som beskrevet på sidste side.', }, }, removeIntro: { From b3a38fc06b7ca60b3b40eca398ab89494083655b Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 14:42:21 +0200 Subject: [PATCH 134/231] Update CHANGELOG --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 41681cdc..e3fe6ce8 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- An issue where the import window would fail to open. + ## 1.1.2 ### Added From b3a6dc85bba0de3b679ca4836b317d5d767ed935 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 14:45:15 +0200 Subject: [PATCH 135/231] Fix type error --- packages/browser-wallet/src/shared/utils/log-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/utils/log-helpers.ts b/packages/browser-wallet/src/shared/utils/log-helpers.ts index 80547672..bfc46c79 100644 --- a/packages/browser-wallet/src/shared/utils/log-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/log-helpers.ts @@ -21,7 +21,7 @@ export interface Log { } function isError(error: unknown): error is { message: string } { - return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string'; + return typeof error === 'object' && error !== null && 'message' in error; } function logForDevelopmentBuild(logEntry: Log) { From 821b45a2af9e6fc0ac472704c6e0efc08dd61165 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 15:02:15 +0200 Subject: [PATCH 136/231] Use correct text for not activated status --- packages/browser-wallet/CHANGELOG.md | 1 + .../pages/VerifiableCredential/VerifiableCredentialStatus.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 41d93113..c47a91cd 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. +- An issue where a verifiable with the `NotActive` status would show as `Pending`. ## 1.1.2 diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx index ce9ad400..0f35596d 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialStatus.tsx @@ -29,7 +29,7 @@ export default function StatusIcon({ status }: { status: VerifiableCredentialSta break; case VerifiableCredentialStatus.NotActivated: icon = ; - text = t('Pending'); + text = t('NotActivated'); break; case VerifiableCredentialStatus.Pending: icon = ; From 1154313b4694aa6116e04fe4abb6351bda581b67 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 15:03:37 +0200 Subject: [PATCH 137/231] Fix typo in CHANGELOG --- packages/browser-wallet/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index c47a91cd..bfd84514 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,7 +5,7 @@ ### Fixed - Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. -- An issue where a verifiable with the `NotActive` status would show as `Pending`. +- An issue where a verifiable with the `NotActivated` status would show as `Pending`. ## 1.1.2 From 2ad43977d03b538447e8a4cd6864bdcc6028383c Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 16:17:34 +0200 Subject: [PATCH 138/231] Refactoring according to review --- .../VerifiableCredential/VerifiableCredentialHooks.tsx | 6 +++++- packages/browser-wallet/src/shared/constants/logging.ts | 3 +++ packages/browser-wallet/src/shared/storage/update.ts | 7 ++----- packages/browser-wallet/src/shared/utils/log-helpers.ts | 5 +---- 4 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 packages/browser-wallet/src/shared/constants/logging.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 57949f1b..882ea356 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -205,12 +205,16 @@ export function useIssuerMetadata(issuer: string): IssuerMetadata | undefined { useEffect(() => { const registryContractAddress = getCredentialRegistryContractAddress(issuer); + const abortController = new AbortController(); getCredentialRegistryMetadata(client, registryContractAddress) .then((res) => { - const abortController = new AbortController(); fetchIssuerMetadata(res.issuerMetadata, abortController).then(setIssuerMetadata).catch(logError); }) .catch(logError); + + return () => { + abortController.abort(); + }; }, [client, issuer]); return issuerMetadata; diff --git a/packages/browser-wallet/src/shared/constants/logging.ts b/packages/browser-wallet/src/shared/constants/logging.ts new file mode 100644 index 00000000..1a76cb8a --- /dev/null +++ b/packages/browser-wallet/src/shared/constants/logging.ts @@ -0,0 +1,3 @@ +// Determines the maximum number of log entries that is stored +// at a time. +export const LOG_MAX_ENTRIES = 100; diff --git a/packages/browser-wallet/src/shared/storage/update.ts b/packages/browser-wallet/src/shared/storage/update.ts index 5957379c..336786a2 100644 --- a/packages/browser-wallet/src/shared/storage/update.ts +++ b/packages/browser-wallet/src/shared/storage/update.ts @@ -61,11 +61,8 @@ export async function removeFromList( } /* - * Generic method to add an element to list in storage while ensuring that - * the list never grows beyond the provided size. If the list is still small - * enough, then the addition is prepended to the list. If the the list would have - * grown greater than the max size, then the addition is prepended to the list and - * the last element of the list is removed. + * Generic method to add an element to list in storage, while treating the list as a FIFO queue, + * to ensure the list never grows beyond the provided size. */ export async function addToListMaxSize( lock: string, diff --git a/packages/browser-wallet/src/shared/utils/log-helpers.ts b/packages/browser-wallet/src/shared/utils/log-helpers.ts index bfc46c79..312889d0 100644 --- a/packages/browser-wallet/src/shared/utils/log-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/log-helpers.ts @@ -1,13 +1,10 @@ import { storedLog } from '@shared/storage/access'; import { addToListMaxSize } from '@shared/storage/update'; +import { LOG_MAX_ENTRIES } from '@shared/constants/logging'; import { isDevelopmentBuild } from './environment-helpers'; const loggingLock = 'concordium_log_lock'; -// Determines the maximum number of log entries that is stored -// at a time. -const LOG_MAX_ENTRIES = 100; - export enum LoggingLevel { INFO = 'INFO', WARN = 'WARN', From 4c045671ee17948f5a73b0c68b99d70a8c5c0503 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 16:23:33 +0200 Subject: [PATCH 139/231] Reduce usage of magic variables --- packages/browser-wallet/src/background/web3Id.ts | 7 ++++--- packages/browser-wallet/src/shared/constants/web3id.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 packages/browser-wallet/src/shared/constants/web3id.ts diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 42e4f43b..aeef7b44 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -31,6 +31,7 @@ import { MessageStatusWrapper, } from '@concordium/browser-wallet-message-hub'; import { getNet } from '@shared/utils/network-helpers'; +import { WAIT_FOR_CLOSED_POPUP_ITERATIONS, WAIT_FOR_CLOSED_POPUP_TIMEOUT_MS } from '@shared/constants/web3id'; import { openWindow, RunCondition, testPopupOpen } from './window-management'; import bgMessageHandler from './message-handler'; @@ -215,14 +216,14 @@ async function waitForClosedPopup(waitIterations: number): Promise { reject(); } escapeCounter += 1; - setTimeout(waitForClosed, 100); + setTimeout(waitForClosed, WAIT_FOR_CLOSED_POPUP_TIMEOUT_MS); } - }, 100); + }, WAIT_FOR_CLOSED_POPUP_TIMEOUT_MS); }); } async function loadWeb3IdBackup(): Promise { - await waitForClosedPopup(30); + await waitForClosedPopup(WAIT_FOR_CLOSED_POPUP_ITERATIONS); await openWindow(); bgMessageHandler.sendInternalMessage(InternalMessageType.ImportWeb3IdBackup); } diff --git a/packages/browser-wallet/src/shared/constants/web3id.ts b/packages/browser-wallet/src/shared/constants/web3id.ts new file mode 100644 index 00000000..c23802ea --- /dev/null +++ b/packages/browser-wallet/src/shared/constants/web3id.ts @@ -0,0 +1,2 @@ +export const WAIT_FOR_CLOSED_POPUP_ITERATIONS = 30; +export const WAIT_FOR_CLOSED_POPUP_TIMEOUT_MS = 100; From 472915fe4471bd6290997f0a0957a1a6a89ffa12 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 25 Aug 2023 16:28:30 +0200 Subject: [PATCH 140/231] Change status of credential to-be-added to pending --- packages/browser-wallet/CHANGELOG.md | 4 ++++ .../popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index bfd84514..0c1df038 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- The status of a credential being added is now `Pending` instead of `NotActivated`. + ### Fixed - Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index c2b47aa0..392bafcc 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -204,7 +204,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { credentialSubject={credential.credentialSubject} className="add-web3Id-credential__card" schema={schema} - credentialStatus={VerifiableCredentialStatus.NotActivated} + credentialStatus={VerifiableCredentialStatus.Pending} metadata={metadata} localization={localization} /> From 09ebeee7d2b2b844252d32990170e2e2e08e02b7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 11:30:08 +0200 Subject: [PATCH 141/231] Remove bad margin from buttons --- .../src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 0247e382..e2fa4305 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -220,7 +220,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { /> )} -
    +
    From a15fcc0b47b8c148ef7b07b93c316d550652c4e6 Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 28 Aug 2023 11:48:17 +0200 Subject: [PATCH 142/231] Update SDK and fix TODOS that waited for the update --- examples/add-example-Web3Id/package.json | 2 +- examples/eSealing/package.json | 2 +- examples/nft-minting/package.json | 2 +- examples/piggybank/package.json | 2 +- examples/two-step-transfer/package.json | 2 +- examples/voting/package.json | 2 +- examples/wCCD/package.json | 2 +- .../browser-wallet-api-helpers/package.json | 2 +- packages/browser-wallet-api/package.json | 2 +- packages/browser-wallet/package.json | 2 +- .../browser-wallet/src/background/web3Id.ts | 12 +++- .../DisplayStatement/DisplayStatement.tsx | 2 +- .../src/popup/pages/Web3ProofRequest/utils.ts | 36 ++---------- .../src/popup/shared/utils/account-helpers.ts | 16 +---- yarn.lock | 58 +++++++++++-------- 15 files changed, 61 insertions(+), 83 deletions(-) diff --git a/examples/add-example-Web3Id/package.json b/examples/add-example-Web3Id/package.json index cdeeaaec..e1f5b3a4 100644 --- a/examples/add-example-Web3Id/package.json +++ b/examples/add-example-Web3Id/package.json @@ -8,6 +8,6 @@ "start": "live-server ./index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha.1" + "@concordium/web-sdk": "^6.1.0" } } diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index dfa8d916..67f8ba15 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/examples/nft-minting/package.json b/examples/nft-minting/package.json index b8c6afb8..52af0675 100644 --- a/examples/nft-minting/package.json +++ b/examples/nft-minting/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "cors": "^2.8.5", "express": "^4.18.1", "express-fileupload": "^1.4.0", diff --git a/examples/piggybank/package.json b/examples/piggybank/package.json index 8b6a10f7..14315a7c 100644 --- a/examples/piggybank/package.json +++ b/examples/piggybank/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "react": "^18.1.0", "react-dom": "^18.1.0" }, diff --git a/examples/two-step-transfer/package.json b/examples/two-step-transfer/package.json index ed4b56dd..0ed1ef66 100644 --- a/examples/two-step-transfer/package.json +++ b/examples/two-step-transfer/package.json @@ -8,6 +8,6 @@ "start": "live-server ../two-step-transfer/index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha.1" + "@concordium/web-sdk": "^6.1.0" } } diff --git a/examples/voting/package.json b/examples/voting/package.json index 7d33b767..35af23c1 100644 --- a/examples/voting/package.json +++ b/examples/voting/package.json @@ -4,7 +4,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "^2.0.0", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "bootstrap": "^5.2.1", "cross-env": "^7.0.3", "moment": "^2.29.4", diff --git a/examples/wCCD/package.json b/examples/wCCD/package.json index e81b826d..3174a5f2 100644 --- a/examples/wCCD/package.json +++ b/examples/wCCD/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "@thi.ng/leb128": "^2.1.18", "@walletconnect/types": "^2.1.4", "mathjs": "^11.4.0", diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index 5a1db886..0e24990d 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -19,7 +19,7 @@ "url": "https://concordium.com" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0-alpha.1" + "@concordium/web-sdk": "^6.1.0" }, "devDependencies": { "@babel/core": "^7.17.10", diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index e8a163eb..e6d7fe57 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/common-sdk": "^9.1.0-alpha.1", + "@concordium/common-sdk": "^9.1.0", "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3", diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index ec1c0787..b18fc61d 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -19,7 +19,7 @@ "dependencies": { "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0-alpha.1", + "@concordium/web-sdk": "^6.1.0", "@noble/ed25519": "^1.7.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index aeef7b44..a3dcbd3a 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -5,6 +5,9 @@ import { createConcordiumClient, verifyWeb3IdCredentialSignature, isHex, + verifyAtomicStatements, + isAccountCredentialStatement, + IDENTITY_SUBJECT_SCHEMA, } from '@concordium/web-sdk'; import { sessionVerifiableCredentials, @@ -172,9 +175,12 @@ export const runIfValidWeb3IdProof: RunCondition } try { const statements: CredentialStatements = parse(msg.payload.statements); - // TODO Enable when SDK is updated - // // If a statement does not verify, an error is thrown. - // statements.every((credStatement) => verifyAtomicStatements(credStatement.statement)); + // If a statement does not verify, an error is thrown. + statements.every((credStatement) => + isAccountCredentialStatement(credStatement) + ? verifyAtomicStatements(credStatement.statement, IDENTITY_SUBJECT_SCHEMA) + : verifyAtomicStatements(credStatement.statement) + ); const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); if (!noEmptyQualifier) { diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index 53c164a8..41fe9f3b 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -56,7 +56,7 @@ type StatementTooltipProps = { reveal?: boolean; }; -function StatementTooltip({ reveal }: StatementTooltipProps) { +export function StatementTooltip({ reveal }: StatementTooltipProps) { const [open, setOpen] = useState(false); const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 511717d2..64ab799d 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -10,7 +10,7 @@ import { AtomicStatementV2, RevealStatementV2, createWeb3IdDID, - StatementTypes, + canProveAtomicStatement, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { @@ -92,40 +92,16 @@ export function getViableAccountCredentialsForStatement( if (allowedIssuers.includes(c.providerIndex)) { const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); if (identity) { - return canProveCredentialStatement(credentialStatement, identity.idObject.value.attributeList); + return canProveCredentialStatement( + credentialStatement, + identity.idObject.value.attributeList.chosenAttributes + ); } } return false; }); } -// TODO Replace with canProveAtomicStatement when SDK is updated -function doesCredentialSatisfyStatement(statement: AtomicStatementV2, cred: VerifiableCredential): boolean { - let value = cred.credentialSubject.attributes[statement.attributeTag]; - - // temporary handling of numbers saved as numbers; - if (typeof value === 'number') { - value = BigInt(value); - } - - if (value === undefined) { - return false; - } - - switch (statement.type) { - case StatementTypes.AttributeInRange: - return statement.lower <= value && statement.upper > value; - case StatementTypes.AttributeInSet: - return statement.set.includes(value); - case StatementTypes.AttributeNotInSet: - return !statement.set.includes(value); - case StatementTypes.RevealAttribute: - return value !== undefined; - default: - throw new Error('Unknown statementType encountered'); - } -} - /** * Given a credential statement for a verifiable credential, and a list of verifiable credentials, return the filtered list of verifiable credentials that satisfy the statement. */ @@ -143,7 +119,7 @@ export function getViableWeb3IdCredentialsForStatement( ); return allowedCredentials.filter((cred) => - credentialStatement.statement.every((stm) => doesCredentialSatisfyStatement(stm, cred)) + credentialStatement.statement.every((stm) => canProveAtomicStatement(stm, cred.credentialSubject.attributes)) ); } diff --git a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts index 6058810d..cb7e4938 100644 --- a/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/account-helpers.ts @@ -67,10 +67,7 @@ export function useSelectedCredential() { return useCredential(selectedAccount); } -// TODO fix type -export function useHdWallet(): - | (ConcordiumHdWallet & { getVerifiableCredentialBackupEncryptionKey: () => Buffer }) - | undefined { +export function useHdWallet(): ConcordiumHdWallet | undefined { const network = useAtomValue(networkConfigurationAtom); const seedPhrase = useDecryptedSeedPhrase(); @@ -79,16 +76,7 @@ export function useHdWallet(): return undefined; } - // return ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); - // TODO remove this hack when SDK is updated - // START OF HACK - const w = ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (w as any).getVerifiableCredentialBackupEncryptionKey = () => - w.getVerifiableCredentialSigningKey({ index: 0n, subindex: 0n }, 0); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return w as any; - // END OF HACK + return ConcordiumHdWallet.fromHex(seedPhrase, getNet(network)); }, [seedPhrase]); return wallet; diff --git a/yarn.lock b/yarn.lock index 273a9331..75527a13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1930,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^9.1.0-alpha.1 + "@concordium/common-sdk": ^9.1.0 "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -1958,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@noble/ed25519": ^1.7.0 @@ -2027,15 +2027,16 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:9.1.0-alpha.1, @concordium/common-sdk@npm:^9.1.0-alpha.1": - version: 9.1.0-alpha.1 - resolution: "@concordium/common-sdk@npm:9.1.0-alpha.1" +"@concordium/common-sdk@npm:9.1.0, @concordium/common-sdk@npm:^9.1.0": + version: 9.1.0 + resolution: "@concordium/common-sdk@npm:9.1.0" dependencies: - "@concordium/rust-bindings": 1.2.0-alpha.1 + "@concordium/rust-bindings": 1.2.0 "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 + big.js: ^6.2.0 bs58check: ^2.1.2 buffer: ^6.0.3 cross-fetch: 3.1.5 @@ -2043,7 +2044,7 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: dd87f2e6fae7f9590ed167ad5f739d82b25bebb29cca14f871db2a8c9c5957f9e7d19cdf9d062a7191417968a7544d0a8678674f3cb06949055088ee4d44e443 + checksum: 1adc45dee4abcd1b06d9066515745346593f7bd6abbcdd7d85e62862e58102b80f973cc8d6cf481f573301ba13188c5bd3cddeb7105cab41d2344041cd001291 languageName: node linkType: hard @@ -2085,10 +2086,10 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:1.2.0-alpha.1": - version: 1.2.0-alpha.1 - resolution: "@concordium/rust-bindings@npm:1.2.0-alpha.1" - checksum: c5f17b05a19eef4edcdcd0ef8372615662f5a05f8de062dff87c3c95b35583c556a5633d0394c91ac8680cd9bb7b06134b745cb7e850e9dcc94715d8cf72ef5f +"@concordium/rust-bindings@npm:1.2.0": + version: 1.2.0 + resolution: "@concordium/rust-bindings@npm:1.2.0" + checksum: a0deb7d2a8bea7b32487a85fd9981e40a1e0ad5ca72c625887e7d735933abae9adac34ab3047a0ac2f3e690ac8c5ac5d7a763a5fef9efe2c6d209860aa5cf6ec languageName: node linkType: hard @@ -2103,17 +2104,17 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^6.1.0-alpha.1": - version: 6.1.0-alpha.1 - resolution: "@concordium/web-sdk@npm:6.1.0-alpha.1" +"@concordium/web-sdk@npm:^6.1.0": + version: 6.1.0 + resolution: "@concordium/web-sdk@npm:6.1.0" dependencies: - "@concordium/common-sdk": 9.1.0-alpha.1 - "@concordium/rust-bindings": 1.2.0-alpha.1 + "@concordium/common-sdk": 9.1.0 + "@concordium/rust-bindings": 1.2.0 "@grpc/grpc-js": ^1.3.4 "@protobuf-ts/grpcweb-transport": ^2.8.2 buffer: ^6.0.3 process: ^0.11.10 - checksum: 97590bae0e27fcb1157407aac12f4f3fd05c89580ea2e75007b3803e1fa32c142279558610597b263de84e8e28a3f3ca478c69a173c35578b3d215a3cc16cc1e + checksum: 17f99eb2c17ada999ed699dde03668e3b9b7a95c59da6391cb09a0bb61f5dd7aa2bf4c6e935b728f46126f16ba521ef1695e37e2b040db7ba72da2cf5de8ba63 languageName: node linkType: hard @@ -6735,7 +6736,7 @@ __metadata: resolution: "NFT-Minting@workspace:examples/nft-minting" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -6878,7 +6879,7 @@ __metadata: version: 0.0.0-use.local resolution: "add-example-web3-id@workspace:examples/add-example-Web3Id" dependencies: - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -7855,6 +7856,13 @@ __metadata: languageName: node linkType: hard +"big.js@npm:^6.2.0": + version: 6.2.1 + resolution: "big.js@npm:6.2.1" + checksum: 0b234a2fd56c52bed2798ed2020bcab6fef5e9523b99a05406ad071d1aed6ee97ada9fb8de9576092da74c68825c276e19015743b8d1baea269b60a5c666b0cd + languageName: node + linkType: hard + "bignumber.js@npm:^9.0.0": version: 9.0.2 resolution: "bignumber.js@npm:9.0.2" @@ -10187,7 +10195,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -17567,7 +17575,7 @@ __metadata: resolution: "piggybank@workspace:examples/piggybank" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -21484,7 +21492,7 @@ __metadata: version: 0.0.0-use.local resolution: "two-step-transfer@workspace:examples/two-step-transfer" dependencies: - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -22182,7 +22190,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -22307,7 +22315,7 @@ __metadata: resolution: "wccd@workspace:examples/wCCD" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0-alpha.1 + "@concordium/web-sdk": ^6.1.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 From bd3e1b01abc9f9ccb1d78e0a84a469d615d80144 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 11:58:33 +0200 Subject: [PATCH 143/231] Fix illegible text in allowlist --- .../browser-wallet/src/popup/pages/Allowlist/Allowlist.scss | 2 +- packages/browser-wallet/src/popup/styles/config/_colors.scss | 3 +++ packages/browser-wallet/src/popup/styles/elements/_base.scss | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss index ed0458f4..3b0d820b 100644 --- a/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss +++ b/packages/browser-wallet/src/popup/pages/Allowlist/Allowlist.scss @@ -47,7 +47,7 @@ width: 100%; padding: rem(10px); padding-top: rem(0); - background: #eaf0f5; + background-color: $color-allowlist-background; border: rem(1px) solid #dae4ee; border-radius: rem(10px) rem(10px) 0 0; } diff --git a/packages/browser-wallet/src/popup/styles/config/_colors.scss b/packages/browser-wallet/src/popup/styles/config/_colors.scss index b2323d2f..7e2b0f38 100644 --- a/packages/browser-wallet/src/popup/styles/config/_colors.scss +++ b/packages/browser-wallet/src/popup/styles/config/_colors.scss @@ -39,6 +39,7 @@ $color-main-header-light: #f4f5f5; $color-highlight-mainnet-light: #e0e9ed; $color-highlight-testnet-light: #dde9e5; $color-status-box-light: $color-off-black; +$color-allowlist-background-light: #eaf0f5; // Dark mode $color-bg-dark: $color-off-black; @@ -51,6 +52,7 @@ $color-main-header-dark: #111; $color-highlight-mainnet-dark: #203e4f; $color-highlight-testnet-dark: #1a3d3e; $color-status-box-dark: #313131; +$color-allowlist-background-dark: #171818; // Theme variables $color-bg: var(--color-bg, $color-bg-light); // off-white/off-black @@ -74,3 +76,4 @@ $color-highlight-mainnet: var(--color-highlight-mainnet, $color-highlight-mainne $color-highlight-testnet: var(--color-highlight-testnet, $color-highlight-testnet-light); $color-highlight: var(--color-highlight, $color-highlight-testnet); $color-status-box: var(--color-status-box, $color-status-box-light); +$color-allowlist-background: var(--color-allowlist-background, $color-allowlist-background-light); diff --git a/packages/browser-wallet/src/popup/styles/elements/_base.scss b/packages/browser-wallet/src/popup/styles/elements/_base.scss index 5f9eb660..611f645c 100644 --- a/packages/browser-wallet/src/popup/styles/elements/_base.scss +++ b/packages/browser-wallet/src/popup/styles/elements/_base.scss @@ -66,6 +66,7 @@ body { --color-main-header: #{$color-main-header-dark}; --color-disabled-input-bg: #{$color-main-header-dark}; --color-status-box: #{$color-status-box-dark}; + --color-allowlist-background: #{$color-allowlist-background-dark}; .bg { background-image: url('../assets/images/bg-dark.jpg'); From d78eefa4ce67b7fda52ef52bbb6f0f1b8cda611d Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 28 Aug 2023 12:51:40 +0200 Subject: [PATCH 144/231] re-add doesCredentialSatisfyStatement and improve range check for string/dates --- packages/browser-wallet/CHANGELOG.md | 1 + .../src/popup/pages/Web3ProofRequest/utils.ts | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 3e55c07d..1081a51c 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -11,6 +11,7 @@ - An issue where the import window would fail to open. - Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. - An issue where a verifiable with the `NotActivated` status would show as `Pending`. +- Enable validation of veriable presentation requests before opening the popup window. ## 1.1.2 diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 64ab799d..37278e91 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -1,6 +1,5 @@ import { RequestStatement, - canProveCredentialStatement, ConcordiumHdWallet, createWeb3CommitmentInputWithHdWallet, createAccountCommitmentInputWithHdWallet, @@ -10,7 +9,9 @@ import { AtomicStatementV2, RevealStatementV2, createWeb3IdDID, - canProveAtomicStatement, + StatementTypes, + isStringAttributeInRange, + AttributeType, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { @@ -78,6 +79,45 @@ export function getWeb3CommitmentInput(verifiableCredential: VerifiableCredentia ); } +function isInRange(value: AttributeType, lower: AttributeType, upper: AttributeType) { + if (typeof value === 'string' && typeof lower === 'string' && typeof upper === 'string') { + return isStringAttributeInRange(value, lower, upper); + } + if (typeof value === 'bigint' && typeof lower === 'bigint' && typeof upper === 'bigint') { + return lower <= value && upper > value; + } + if (value instanceof Date && lower instanceof Date && upper instanceof Date) { + return lower.getTime() <= value.getTime() && upper.getTime() > value.getTime(); + } + // Mismatch in types. + return false; +} + +// TODO Replace with canProveAtomicStatement when SDK is updated (AttributeNotInSet vs undefined) +function doesCredentialSatisfyStatement( + statement: AtomicStatementV2, + attributes: Record +): boolean { + const value = attributes[statement.attributeTag]; + + if (value === undefined) { + return false; + } + + switch (statement.type) { + case StatementTypes.AttributeInRange: + return isInRange(value, statement.lower, statement.upper); + case StatementTypes.AttributeInSet: + return statement.set.includes(value); + case StatementTypes.AttributeNotInSet: + return !statement.set.includes(value); + case StatementTypes.RevealAttribute: + return value !== undefined; + default: + throw new Error('Unknown statementType encountered'); + } +} + /** * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. @@ -92,9 +132,8 @@ export function getViableAccountCredentialsForStatement( if (allowedIssuers.includes(c.providerIndex)) { const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); if (identity) { - return canProveCredentialStatement( - credentialStatement, - identity.idObject.value.attributeList.chosenAttributes + credentialStatement.statement.every((stm) => + doesCredentialSatisfyStatement(stm, identity.idObject.value.attributeList.chosenAttributes) ); } } @@ -119,7 +158,9 @@ export function getViableWeb3IdCredentialsForStatement( ); return allowedCredentials.filter((cred) => - credentialStatement.statement.every((stm) => canProveAtomicStatement(stm, cred.credentialSubject.attributes)) + credentialStatement.statement.every((stm) => + doesCredentialSatisfyStatement(stm, cred.credentialSubject.attributes) + ) ); } From 2d4898f6b82e2127a5bd2d855effedd258a52277 Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 28 Aug 2023 13:10:16 +0200 Subject: [PATCH 145/231] Fix errors + use AttributeType --- packages/browser-wallet-api/src/wallet-api.ts | 3 ++- .../VerifiableCredentialCard.tsx | 8 ++++---- .../VerifiableCredentialStatement.tsx | 13 +++++-------- .../browser-wallet/src/shared/storage/types.ts | 14 ++++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index aceef2cf..c7d22bcf 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -29,7 +29,8 @@ import { import EventEmitter from 'events'; import type { JsonRpcRequest } from '@concordium/common-sdk/lib/providers/provider'; import { IdProofOutput, IdStatement } from '@concordium/common-sdk/lib/idProofTypes'; -import { CredentialStatements, VerifiablePresentation } from '@concordium/common-sdk/lib/web3ProofTypes'; +import { CredentialStatements } from '@concordium/common-sdk/lib/web3ProofTypes'; +import { VerifiablePresentation } from '@concordium/common-sdk/lib/types/VerifiablePresentation'; import { ConcordiumGRPCClient } from '@concordium/common-sdk/lib/GRPCClient'; import JSONBig from 'json-bigint'; import { stringify } from './util'; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index ddc59202..e18a5b62 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -3,7 +3,7 @@ import { ClassName } from 'wallet-common-helpers'; import clsx from 'clsx'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import Img from '@popup/shared/Img'; -import { CredentialSubject } from '@concordium/web-sdk'; +import { AttributeType, CredentialSubject } from '@concordium/web-sdk'; import { VerifiableCredentialStatus, MetadataUrl, VerifiableCredentialSchema } from '@shared/storage/types'; import StatusIcon from './VerifiableCredentialStatus'; @@ -28,7 +28,7 @@ export function DisplayAttribute({ attributeTitle, }: { attributeKey: string; - attributeValue: string | bigint; + attributeValue: AttributeType; attributeTitle: string; }) { return ( @@ -82,8 +82,8 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className function applySchemaAndLocalization( schema: VerifiableCredentialSchema, localization?: Record -): (value: [string, string | bigint]) => { title: string; key: string; value: string | bigint } { - return (value: [string, string | bigint]) => { +): (value: [string, AttributeType]) => { title: string; key: string; value: AttributeType } { + return (value: [string, AttributeType]) => { const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; if (!attributeSchema) { throw new Error(`Missing attribute schema for key: ${value[0]}`); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 0d432539..54adbf67 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -3,14 +3,11 @@ import { RevealStatementV2, StatementTypes, VerifiableCredentialStatement, + CredentialSubject, + AttributeType, } from '@concordium/web-sdk'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; -import { - VerifiableCredential, - CredentialSubject, - VerifiableCredentialSchema, - VerifiableCredentialStatus, -} from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -48,7 +45,7 @@ function useStatementValue(statement: SecretStatementV2, schema: VerifiableCrede export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.descriptions' }); const name = getPropertyTitle(statement.attributeTag, schema); - const listToString = (list: (string | bigint)[]) => list.map((member) => member.toString()).join(', '); + const listToString = (list: AttributeType[]) => list.map((member) => member.toString()).join(', '); switch (statement.type) { case StatementTypes.AttributeInRange: @@ -70,7 +67,7 @@ type DisplayWeb3StatementProps = ClassName & { type AttributeInfo = { name: string; - value: string | bigint; + value: AttributeType; }; function extractAttributesFromCredentialSubjectForSingleStatement( diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 25c95147..000ca5f0 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -1,5 +1,12 @@ import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; -import type { CryptographicParameters, HexString, IdentityObjectV1, Network, Versioned } from '@concordium/web-sdk'; +import type { + CredentialSubject, + CryptographicParameters, + HexString, + IdentityObjectV1, + Network, + Versioned, +} from '@concordium/web-sdk'; export enum ChromeStorageKey { ConnectedSites = 'connectedSites', @@ -273,11 +280,6 @@ export enum VerifiableCredentialStatus { Pending, } -export type CredentialSubject = { - id: string; - attributes: Record; -}; - export interface VerifiableCredential extends APIVerifiableCredential { // With ID credentialSubject: CredentialSubject; From c1489b3aaf8bfca9f2a3f2d1f89c523d044366f4 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 14:16:53 +0200 Subject: [PATCH 146/231] Improve UI of popup menu --- .../src/popup/shared/PopupMenu/PopupMenu.scss | 13 +++++++++---- .../src/popup/shared/Topbar/Topbar.scss | 2 ++ .../src/popup/styles/config/_colors.scss | 3 +++ .../src/popup/styles/elements/_base.scss | 1 + 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss index 097c20b7..d079ebc1 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -1,14 +1,15 @@ .popup-menu { width: 200px; - box-shadow: 0 0 20px rgb(0 0 0 / 15%); + box-shadow: 0 0 20px rgb(0 0 0 / $color-shadow-alpha); border-radius: 16px; display: flex; flex-direction: column; - background-color: $color-white; + opacity: 0.95; + background-color: $color-bg; &__item { &:hover { - background-color: lightgray; + background-color: rgb(0 0 0 / $color-shadow-alpha); } &:first-child { @@ -28,7 +29,7 @@ height: 44px; &:not(:last-child) { - border-bottom: 1px solid lightgray; + border-bottom: 1px solid rgb(0 0 0 / $color-shadow-alpha); } &--disabled { @@ -41,6 +42,10 @@ display: flex; align-items: center; justify-content: center; + + path { + fill: $color-text; + } } } } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss index 8ad3eefc..1ef024d0 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss @@ -40,6 +40,8 @@ &__show { z-index: 1; display: block; + border-radius: 16px; + backdrop-filter: blur(2px); } } } diff --git a/packages/browser-wallet/src/popup/styles/config/_colors.scss b/packages/browser-wallet/src/popup/styles/config/_colors.scss index 7e2b0f38..7a3b30b8 100644 --- a/packages/browser-wallet/src/popup/styles/config/_colors.scss +++ b/packages/browser-wallet/src/popup/styles/config/_colors.scss @@ -40,6 +40,7 @@ $color-highlight-mainnet-light: #e0e9ed; $color-highlight-testnet-light: #dde9e5; $color-status-box-light: $color-off-black; $color-allowlist-background-light: #eaf0f5; +$color-shadow-alpha-light: 15%; // Dark mode $color-bg-dark: $color-off-black; @@ -53,6 +54,7 @@ $color-highlight-mainnet-dark: #203e4f; $color-highlight-testnet-dark: #1a3d3e; $color-status-box-dark: #313131; $color-allowlist-background-dark: #171818; +$color-shadow-alpha-dark: 85%; // Theme variables $color-bg: var(--color-bg, $color-bg-light); // off-white/off-black @@ -77,3 +79,4 @@ $color-highlight-testnet: var(--color-highlight-testnet, $color-highlight-testne $color-highlight: var(--color-highlight, $color-highlight-testnet); $color-status-box: var(--color-status-box, $color-status-box-light); $color-allowlist-background: var(--color-allowlist-background, $color-allowlist-background-light); +$color-shadow-alpha: var(--color-shadow-alpha, $color-shadow-alpha-light); diff --git a/packages/browser-wallet/src/popup/styles/elements/_base.scss b/packages/browser-wallet/src/popup/styles/elements/_base.scss index 611f645c..818016b3 100644 --- a/packages/browser-wallet/src/popup/styles/elements/_base.scss +++ b/packages/browser-wallet/src/popup/styles/elements/_base.scss @@ -67,6 +67,7 @@ body { --color-disabled-input-bg: #{$color-main-header-dark}; --color-status-box: #{$color-status-box-dark}; --color-allowlist-background: #{$color-allowlist-background-dark}; + --color-shadow-alpha: #{$color-shadow-alpha-dark}; .bg { background-image: url('../assets/images/bg-dark.jpg'); From 6842621a06487915e54d9cc67e407ec1e7f70f2f Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 14:34:11 +0200 Subject: [PATCH 147/231] Validate that credential statements are not empty --- packages/browser-wallet-api-helpers/src/wallet-api-types.ts | 2 +- packages/browser-wallet-api/src/wallet-api.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 67847f28..1bb1186e 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -225,7 +225,7 @@ interface MainWalletApi { /** * Request that the user provides a proof for the given statements. * @param challenge bytes chosen by the verifier. Should be HEX encoded. - * @param statement the web3Id statements that should be proven. + * @param statements the web3Id statements that should be proven. The promise rejects if the array of statements is empty. * @returns The presentation for the statements. */ requestVerifiablePresentation(challenge: string, statements: CredentialStatements): Promise; diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index aceef2cf..682f008e 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -289,6 +289,10 @@ class WalletApi extends EventEmitter implements IWalletApi { } public async requestVerifiablePresentation(challenge: HexString, statements: CredentialStatements) { + if (statements === undefined || statements.length === 0) { + throw new Error('A request for a verifiable presentation must contain statements'); + } + const res = await this.messageHandler.sendMessage>(MessageType.Web3IdProof, { // We have to stringify the statements because they can contain bigints statements: stringify(statements), From fa9dbcd2dba6180392bb3bfe84cea298bc7cedad Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 15:06:39 +0200 Subject: [PATCH 148/231] Prevent adding a credential with attributes not listed in schema --- packages/browser-wallet/CHANGELOG.md | 2 ++ .../pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 10 +++++++++- .../src/popup/pages/AddWeb3IdCredential/i18n/en.ts | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 3e55c07d..855710c7 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -11,6 +11,8 @@ - An issue where the import window would fail to open. - Updated the JSON schema for the verifiable credential schema validation, so that invalid schemas are rejected. - An issue where a verifiable with the `NotActivated` status would show as `Pending`. +- An issue that allowed empty credential statements to be accepted by the wallet-api. +- An issue where the wallet allowed for requests adding credentials with more attributes than listed in the schema. ## 1.1.2 diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 0247e382..e9882f55 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -121,7 +121,15 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { } if (missingRequiredAttributeKeys.length > 0) { - setError(t('error.attribute', { attributeKeys: missingRequiredAttributeKeys })); + setError(t('error.attribute.required', { attributeKeys: missingRequiredAttributeKeys })); + } + + // Ensure that a credential with more attributes than listed by the schema cannot be added. + const schemaAttributes = Object.keys(schema.properties.credentialSubject.properties.attributes.properties); + for (const credentialAttribute of Object.keys(credential.credentialSubject.attributes)) { + if (!schemaAttributes.includes(credentialAttribute)) { + setError(t('error.attribute.additional', { credentialAttribute, schemaAttributes })); + } } } }, [schema?.properties.credentialSubject.properties.attributes.required]); diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts index 16569fe9..b7d74fed 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -7,7 +7,11 @@ const t = { 'We are unable to add the web3Id credential to the wallet due to the following issue, please report this to the issuer:', metadata: 'We are unable to load the metadata for the credential.', schema: 'We are unable to load the schema specification for the credential.', - attribute: 'The received credential is missing one or more required attributes ({{ attributeKeys }})', + attribute: { + required: 'The received credential is missing one or more required attributes ({{ attributeKeys }})', + additional: + 'The attribute with key [{{ credentialAttribute }}] is not available in the list of schema attributes: [{{ schemaAttributes }} ]', + }, localization: 'Failed to get localization', }, }; From da271f1fe1167610bd43cb152e727b445d48a9f0 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 15:31:25 +0200 Subject: [PATCH 149/231] Remove space --- .../src/popup/pages/AddWeb3IdCredential/i18n/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts index b7d74fed..c1dbe60f 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -10,7 +10,7 @@ const t = { attribute: { required: 'The received credential is missing one or more required attributes ({{ attributeKeys }})', additional: - 'The attribute with key [{{ credentialAttribute }}] is not available in the list of schema attributes: [{{ schemaAttributes }} ]', + 'The attribute with key [{{ credentialAttribute }}] is not available in the list of schema attributes: [{{ schemaAttributes }}]', }, localization: 'Failed to get localization', }, From 6f19d7d3b09c269730d19670e473bf7e1208dde3 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 16:22:46 +0200 Subject: [PATCH 150/231] Stop rendering before validation --- .../pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index e9882f55..7029b0df 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -63,6 +63,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const network = useAtomValue(networkConfigurationAtom); const [error, setError] = useState(); + const [validationComplete, setValidationComplete] = useState(); const { credential: rawCredential, url, metadataUrl } = state.payload; const credential: APIVerifiableCredential = parse(rawCredential); @@ -122,6 +123,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { if (missingRequiredAttributeKeys.length > 0) { setError(t('error.attribute.required', { attributeKeys: missingRequiredAttributeKeys })); + setValidationComplete(true); + return; } // Ensure that a credential with more attributes than listed by the schema cannot be added. @@ -129,10 +132,12 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { for (const credentialAttribute of Object.keys(credential.credentialSubject.attributes)) { if (!schemaAttributes.includes(credentialAttribute)) { setError(t('error.attribute.additional', { credentialAttribute, schemaAttributes })); + setValidationComplete(true); + return; } } } - }, [schema?.properties.credentialSubject.properties.attributes.required]); + }, [Boolean(schema)]); useEffect(() => () => controller.abort(), []); @@ -201,7 +206,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return credentialSubjectId; } - if (web3IdCredentials.loading || storedWeb3IdCredentials.loading) { + if (web3IdCredentials.loading || storedWeb3IdCredentials.loading || !validationComplete) { return null; } From 63b5209be649106bf55b1b828dd9fd8e42ac96a1 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 28 Aug 2023 16:26:34 +0200 Subject: [PATCH 151/231] Fix validation --- .../popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 7029b0df..cfa6a46a 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -63,7 +63,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const network = useAtomValue(networkConfigurationAtom); const [error, setError] = useState(); - const [validationComplete, setValidationComplete] = useState(); + const [validationComplete, setValidationComplete] = useState(false); const { credential: rawCredential, url, metadataUrl } = state.payload; const credential: APIVerifiableCredential = parse(rawCredential); @@ -136,6 +136,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return; } } + + setValidationComplete(true); } }, [Boolean(schema)]); From f122fb89b95a76f7f7b2c3fd78ff4ecabc4c40f1 Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 28 Aug 2023 15:14:47 +0200 Subject: [PATCH 152/231] re-remove doesCredentialSatisfyStatement + bump versions --- examples/add-example-Web3Id/package.json | 2 +- examples/eSealing/package.json | 2 +- examples/nft-minting/package.json | 2 +- examples/piggybank/package.json | 2 +- examples/two-step-transfer/package.json | 2 +- examples/voting/package.json | 2 +- examples/wCCD/package.json | 2 +- .../browser-wallet-api-helpers/package.json | 2 +- packages/browser-wallet-api/package.json | 2 +- packages/browser-wallet/package.json | 2 +- .../DisplayStatement/DisplayStatement.tsx | 2 +- .../src/popup/pages/Web3ProofRequest/utils.ts | 52 ++----------------- yarn.lock | 38 +++++++------- 13 files changed, 35 insertions(+), 77 deletions(-) diff --git a/examples/add-example-Web3Id/package.json b/examples/add-example-Web3Id/package.json index e1f5b3a4..8730dfc5 100644 --- a/examples/add-example-Web3Id/package.json +++ b/examples/add-example-Web3Id/package.json @@ -8,6 +8,6 @@ "start": "live-server ./index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0" + "@concordium/web-sdk": "^6.1.1" } } diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index 67f8ba15..165690f0 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/examples/nft-minting/package.json b/examples/nft-minting/package.json index 52af0675..c6113b42 100644 --- a/examples/nft-minting/package.json +++ b/examples/nft-minting/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "cors": "^2.8.5", "express": "^4.18.1", "express-fileupload": "^1.4.0", diff --git a/examples/piggybank/package.json b/examples/piggybank/package.json index 14315a7c..275e75c3 100644 --- a/examples/piggybank/package.json +++ b/examples/piggybank/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "react": "^18.1.0", "react-dom": "^18.1.0" }, diff --git a/examples/two-step-transfer/package.json b/examples/two-step-transfer/package.json index 0ed1ef66..42dca1cc 100644 --- a/examples/two-step-transfer/package.json +++ b/examples/two-step-transfer/package.json @@ -8,6 +8,6 @@ "start": "live-server ../two-step-transfer/index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0" + "@concordium/web-sdk": "^6.1.1" } } diff --git a/examples/voting/package.json b/examples/voting/package.json index 35af23c1..cc316ced 100644 --- a/examples/voting/package.json +++ b/examples/voting/package.json @@ -4,7 +4,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "^2.0.0", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "bootstrap": "^5.2.1", "cross-env": "^7.0.3", "moment": "^2.29.4", diff --git a/examples/wCCD/package.json b/examples/wCCD/package.json index 3174a5f2..21d95191 100644 --- a/examples/wCCD/package.json +++ b/examples/wCCD/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "@thi.ng/leb128": "^2.1.18", "@walletconnect/types": "^2.1.4", "mathjs": "^11.4.0", diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index 0e24990d..ffdb86fb 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -19,7 +19,7 @@ "url": "https://concordium.com" }, "dependencies": { - "@concordium/web-sdk": "^6.1.0" + "@concordium/web-sdk": "^6.1.1" }, "devDependencies": { "@babel/core": "^7.17.10", diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index e6d7fe57..47a19976 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/common-sdk": "^9.1.0", + "@concordium/common-sdk": "^9.1.1", "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3", diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index b18fc61d..49057213 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -19,7 +19,7 @@ "dependencies": { "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.1.0", + "@concordium/web-sdk": "^6.1.1", "@noble/ed25519": "^1.7.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index 41fe9f3b..53c164a8 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -56,7 +56,7 @@ type StatementTooltipProps = { reveal?: boolean; }; -export function StatementTooltip({ reveal }: StatementTooltipProps) { +function StatementTooltip({ reveal }: StatementTooltipProps) { const [open, setOpen] = useState(false); const { t } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 37278e91..6123bc5c 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -9,9 +9,7 @@ import { AtomicStatementV2, RevealStatementV2, createWeb3IdDID, - StatementTypes, - isStringAttributeInRange, - AttributeType, + canProveCredentialStatement, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { @@ -79,45 +77,6 @@ export function getWeb3CommitmentInput(verifiableCredential: VerifiableCredentia ); } -function isInRange(value: AttributeType, lower: AttributeType, upper: AttributeType) { - if (typeof value === 'string' && typeof lower === 'string' && typeof upper === 'string') { - return isStringAttributeInRange(value, lower, upper); - } - if (typeof value === 'bigint' && typeof lower === 'bigint' && typeof upper === 'bigint') { - return lower <= value && upper > value; - } - if (value instanceof Date && lower instanceof Date && upper instanceof Date) { - return lower.getTime() <= value.getTime() && upper.getTime() > value.getTime(); - } - // Mismatch in types. - return false; -} - -// TODO Replace with canProveAtomicStatement when SDK is updated (AttributeNotInSet vs undefined) -function doesCredentialSatisfyStatement( - statement: AtomicStatementV2, - attributes: Record -): boolean { - const value = attributes[statement.attributeTag]; - - if (value === undefined) { - return false; - } - - switch (statement.type) { - case StatementTypes.AttributeInRange: - return isInRange(value, statement.lower, statement.upper); - case StatementTypes.AttributeInSet: - return statement.set.includes(value); - case StatementTypes.AttributeNotInSet: - return !statement.set.includes(value); - case StatementTypes.RevealAttribute: - return value !== undefined; - default: - throw new Error('Unknown statementType encountered'); - } -} - /** * Given a credential statement for an account credential, and a list of account credentials, return the filtered list of credentials that satisfy the statement. * Note this also requires the identities for the account credentials as an additional argument, to actually check the attributes of the credential. @@ -132,8 +91,9 @@ export function getViableAccountCredentialsForStatement( if (allowedIssuers.includes(c.providerIndex)) { const identity = (identities || []).find((id) => isIdentityOfCredential(id)(c)); if (identity) { - credentialStatement.statement.every((stm) => - doesCredentialSatisfyStatement(stm, identity.idObject.value.attributeList.chosenAttributes) + return canProveCredentialStatement( + credentialStatement, + identity.idObject.value.attributeList.chosenAttributes ); } } @@ -158,9 +118,7 @@ export function getViableWeb3IdCredentialsForStatement( ); return allowedCredentials.filter((cred) => - credentialStatement.statement.every((stm) => - doesCredentialSatisfyStatement(stm, cred.credentialSubject.attributes) - ) + canProveCredentialStatement(credentialStatement, cred.credentialSubject.attributes) ); } diff --git a/yarn.lock b/yarn.lock index 75527a13..cceee53b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1930,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^9.1.0 + "@concordium/common-sdk": ^9.1.1 "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -1958,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@noble/ed25519": ^1.7.0 @@ -2027,9 +2027,9 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:9.1.0, @concordium/common-sdk@npm:^9.1.0": - version: 9.1.0 - resolution: "@concordium/common-sdk@npm:9.1.0" +"@concordium/common-sdk@npm:9.1.1, @concordium/common-sdk@npm:^9.1.1": + version: 9.1.1 + resolution: "@concordium/common-sdk@npm:9.1.1" dependencies: "@concordium/rust-bindings": 1.2.0 "@grpc/grpc-js": ^1.3.4 @@ -2044,7 +2044,7 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: 1adc45dee4abcd1b06d9066515745346593f7bd6abbcdd7d85e62862e58102b80f973cc8d6cf481f573301ba13188c5bd3cddeb7105cab41d2344041cd001291 + checksum: e7f43e085b968e9772f2559b2530bca59a9aab426332c3e088f266a76bc1e913bedff097aa61bc5a9c5ddb78db4decf2fd6a027d7231e95fcb368fe41f2820c8 languageName: node linkType: hard @@ -2104,17 +2104,17 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^6.1.0": - version: 6.1.0 - resolution: "@concordium/web-sdk@npm:6.1.0" +"@concordium/web-sdk@npm:^6.1.1": + version: 6.1.1 + resolution: "@concordium/web-sdk@npm:6.1.1" dependencies: - "@concordium/common-sdk": 9.1.0 + "@concordium/common-sdk": 9.1.1 "@concordium/rust-bindings": 1.2.0 "@grpc/grpc-js": ^1.3.4 "@protobuf-ts/grpcweb-transport": ^2.8.2 buffer: ^6.0.3 process: ^0.11.10 - checksum: 17f99eb2c17ada999ed699dde03668e3b9b7a95c59da6391cb09a0bb61f5dd7aa2bf4c6e935b728f46126f16ba521ef1695e37e2b040db7ba72da2cf5de8ba63 + checksum: 85fa565d4ea9e6dfbc7b924b96e511fdb8094d7d3fdbc733204e65495f1969f9a9dd13a3e869c6b5e2501d1b65e1eb7296f282e78b456d3f03d0fac2c101ccad languageName: node linkType: hard @@ -6736,7 +6736,7 @@ __metadata: resolution: "NFT-Minting@workspace:examples/nft-minting" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -6879,7 +6879,7 @@ __metadata: version: 0.0.0-use.local resolution: "add-example-web3-id@workspace:examples/add-example-Web3Id" dependencies: - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -10195,7 +10195,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -17575,7 +17575,7 @@ __metadata: resolution: "piggybank@workspace:examples/piggybank" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -21492,7 +21492,7 @@ __metadata: version: 0.0.0-use.local resolution: "two-step-transfer@workspace:examples/two-step-transfer" dependencies: - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -22190,7 +22190,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -22315,7 +22315,7 @@ __metadata: resolution: "wccd@workspace:examples/wCCD" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.1.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 From 35f11aa75763af1cd744f552fbfeec09edf9616f Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 28 Aug 2023 17:19:44 +0200 Subject: [PATCH 153/231] Add drag and drop for verifiable credential import --- .../VerifiableCredentialBackup.scss | 5 + .../VerifiableCredentialImport.tsx | 8 +- .../popup/shared/Form/FileInput/FileInput.tsx | 103 ++++++++++++++++++ .../src/popup/shared/Form/FileInput/index.ts | 1 + .../src/popup/shared/Form/Form.scss | 72 ++++++++++++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx create mode 100644 packages/browser-wallet/src/popup/shared/Form/FileInput/index.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss index e1f15895..b673a715 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialBackup.scss @@ -26,4 +26,9 @@ $header-height: rem(56px); &__button { margin: auto auto 20px; } + + &__import { + height: calc(100% - 32px); + margin: 20px; + } } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 8c4262fa..9a82aa29 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -14,6 +14,7 @@ import { noOp } from 'wallet-common-helpers'; import { decrypt } from '@shared/utils/crypto'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; import JSONBigInt from 'json-bigint'; +import { FileInput } from '@popup/shared/Form/FileInput'; import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; import { ExportFormat, VerifiableCredentialExport } from './utils'; @@ -126,7 +127,12 @@ export default function VerifiableCredentialImport() { {imported && } {!imported && ( <> - + {error &&

    {error}

    } )} diff --git a/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx b/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx new file mode 100644 index 00000000..7943e356 --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx @@ -0,0 +1,103 @@ +import clsx from 'clsx'; +import React, { forwardRef, InputHTMLAttributes, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import Button from '../../Button'; +import { CommonFieldProps, RequiredUncontrolledFieldProps } from '../common/types'; +import ErrorMessage from '../ErrorMessage'; +import { makeUncontrolled } from '../common/utils'; + +export interface FileInputRef { + reset(): void; +} + +export type FileInputValue = FileList | null; + +export type FileInputProps = Pick< + InputHTMLAttributes, + 'accept' | 'multiple' | 'placeholder' | 'disabled' | 'className' +> & + RequiredUncontrolledFieldProps & + CommonFieldProps & { + buttonTitle: string; + value: FileInputValue; + disableFileNames?: boolean; + }; + +/** + * @description + * Component for handling file input. Parsing of file should be done externally. Supports drag and drop + click to browse. + * + * @example + * + */ +export const FileInput = forwardRef( + ( + { + value, + onChange, + label, + valid, + error, + placeholder, + className, + buttonTitle, + disableFileNames = false, + ...inputProps + }, + ref + ): JSX.Element => { + const inputRef = useRef(null); + const [dragOver, setDragOver] = useState(false); + const files = useMemo(() => new Array(value?.length ?? 0).fill(0).map((_, i) => value?.item(i)), [value]); + + const { disabled } = inputProps; + + useImperativeHandle(ref, () => ({ + reset: () => { + if (inputRef.current) { + inputRef.current.value = ''; + } + }, + })); + + return ( + + ); + } +); + +const FormFileInput = makeUncontrolled(FileInput); + +export default FormFileInput; diff --git a/packages/browser-wallet/src/popup/shared/Form/FileInput/index.ts b/packages/browser-wallet/src/popup/shared/Form/FileInput/index.ts new file mode 100644 index 00000000..8e0b9e8f --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Form/FileInput/index.ts @@ -0,0 +1 @@ +export { default, FileInput } from './FileInput'; diff --git a/packages/browser-wallet/src/popup/shared/Form/Form.scss b/packages/browser-wallet/src/popup/shared/Form/Form.scss index 6388e288..84d361e5 100644 --- a/packages/browser-wallet/src/popup/shared/Form/Form.scss +++ b/packages/browser-wallet/src/popup/shared/Form/Form.scss @@ -329,3 +329,75 @@ $handle-scale: scale(1.02); } } } + +.form-file-input { + &__root, + &__wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 10px; + } + + &__label { + margin-bottom: 5px; + } + + &__root { + font-size: rem(12px); + } + + &__disabled { + cursor: default; + } + + &__wrapper { + flex: 1; + height: 100%; + width: 100%; + padding: 20px 30px; + position: relative; + border: 2px dashed $color-grey; + + &__hovering & { + border-color: $color-cta; + } + + &__invalid & { + border-color: $color-error; + } + } + + &__fileName { + word-break: break-all; + text-align: center; + } + + &__empty { + color: $color-grey; + } + + &__input { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + width: 100%; + cursor: pointer; + + &__disabled & { + cursor: default; + } + } + + &__button { + pointer-events: none; + + * + & { + margin-top: 30px; + } + } +} From 88c9c37e65498c2e470148cb2cffcda590dbd09f Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 10:24:48 +0200 Subject: [PATCH 154/231] Bump version --- packages/browser-wallet/CHANGELOG.md | 2 +- packages/browser-wallet/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index ec6e3fb1..26d19c00 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.1.3 ### Changed diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 49057213..02e9c5bc 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.2", + "version": "1.1.3", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 38fb4c570a4924459294a10c5bd547c3b3520a4c Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 10:43:44 +0200 Subject: [PATCH 155/231] Add timestamp property to credentialSchema --- .../src/shared/storage/types.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 000ca5f0..1ef3c8c5 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -298,8 +298,25 @@ interface CredentialSchemaProperty { format?: string; } +export type TimestampProperty = { + title: string; + type: 'object'; + properties: { + type: { + type: 'string'; + const: 'date-time'; + }; + timestamp: { + type: 'string'; + format?: 'date-time'; + }; + }; + required: ['type', 'timestamp']; + description?: string; +}; + type CredentialSchemaAttributes = { - properties: Record; + properties: Record; required: string[]; } & CredentialSchemaProperty; From c379da2e4f653b3ced756879636b6c0245e497a9 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 29 Aug 2023 10:49:47 +0200 Subject: [PATCH 156/231] Update schema check to support timestamp --- .../utils/verifiable-credential-helpers.ts | 106 +++++++++++++++--- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index c5f66489..61a0146b 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -382,23 +382,30 @@ const verifiableCredentialSchemaSchema = { }, properties: { additionalProperties: { - additionalProperties: false, - properties: { - description: { - type: 'string', + anyOf: [ + { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + format: { + type: 'string', + }, + title: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + required: ['title', 'type', 'description'], + type: 'object', }, - format: { - type: 'string', + { + $ref: '#/definitions/TimestampProperty', }, - title: { - type: 'string', - }, - type: { - type: 'string', - }, - }, - required: ['title', 'type', 'description'], - type: 'object', + ], }, type: 'object', }, @@ -458,6 +465,75 @@ const verifiableCredentialSchemaSchema = { required: ['credentialSubject'], type: 'object', }, + TimestampProperty: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + properties: { + additionalProperties: false, + properties: { + timestamp: { + additionalProperties: false, + properties: { + format: { + const: 'date-time', + type: 'string', + }, + type: { + const: 'string', + type: 'string', + }, + }, + required: ['type'], + type: 'object', + }, + type: { + additionalProperties: false, + properties: { + const: { + const: 'date-time', + type: 'string', + }, + type: { + const: 'string', + type: 'string', + }, + }, + required: ['type', 'const'], + type: 'object', + }, + }, + required: ['type', 'timestamp'], + type: 'object', + }, + required: { + items: [ + { + const: 'type', + type: 'string', + }, + { + const: 'timestamp', + type: 'string', + }, + ], + maxItems: 2, + minItems: 2, + type: 'array', + }, + title: { + type: 'string', + }, + type: { + const: 'object', + type: 'string', + }, + }, + required: ['title', 'type', 'properties', 'required'], + type: 'object', + }, VerifiableCredentialSchema: { additionalProperties: false, properties: { From 4727811a45618538e07b33b79589ad23ec12badd Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 11:04:05 +0200 Subject: [PATCH 157/231] Fix blank screen when fetching schema causes an error --- .../src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index dcddfd64..818c85ae 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -208,7 +208,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { return credentialSubjectId; } - if (web3IdCredentials.loading || storedWeb3IdCredentials.loading || !validationComplete) { + if (web3IdCredentials.loading || storedWeb3IdCredentials.loading || (!validationComplete && !error)) { return null; } From 11caad62e354a7fa21c64769eab972206411e2e2 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 11:21:51 +0200 Subject: [PATCH 158/231] Bump api-helpers version --- packages/browser-wallet-api-helpers/CHANGELOG.md | 4 +++- packages/browser-wallet-api-helpers/package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet-api-helpers/CHANGELOG.md b/packages/browser-wallet-api-helpers/CHANGELOG.md index 069e38e5..fd7c1516 100644 --- a/packages/browser-wallet-api-helpers/CHANGELOG.md +++ b/packages/browser-wallet-api-helpers/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog -## Unreleased +## 2.6.0 ### Added - `requestAccounts` entrypoint to connect to a list of accounts. +- `addWeb3IdCredential` entrypoint to add verifiable credentials. +- `requestVerifiablePresentation`entrypoint to prove statements about identities and verifiable credentials. ## 2.5.1 diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index ffdb86fb..fbfb26c9 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet-api-helpers", - "version": "2.5.1", + "version": "2.6.0", "license": "Apache-2.0", "packageManager": "yarn@3.2.0", "main": "lib/index.js", From ee995d92f2eee8bffd0b883dffad8c0e6b59336d Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 11:37:13 +0200 Subject: [PATCH 159/231] Fix yarn.lock --- examples/eSealing/package.json | 2 +- yarn.lock | 117 ++++++--------------------------- 2 files changed, 20 insertions(+), 99 deletions(-) diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index 26a0a662..7e32c7ba 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -4,8 +4,8 @@ "version": "1.1.2", "license": "Apache-2.0", "dependencies": { - "@concordium/web-sdk": "^6.1.1", "@concordium/react-components": "^0.3.0", + "@concordium/web-sdk": "^6.1.1", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/yarn.lock b/yarn.lock index 688b3416..b949eda0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -1915,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^5.0.0 + "@concordium/web-sdk": ^6.1.1 typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1927,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^8.0.0 + "@concordium/common-sdk": ^9.1.1 "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -1955,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^5.0.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@noble/ed25519": ^1.7.0 @@ -2024,35 +2027,16 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:8.0.0, @concordium/common-sdk@npm:^8.0.0": - version: 8.0.0 - resolution: "@concordium/common-sdk@npm:8.0.0" - dependencies: - "@concordium/rust-bindings": 1.0.0 - "@grpc/grpc-js": ^1.3.4 - "@noble/ed25519": ^1.7.1 - "@protobuf-ts/runtime-rpc": ^2.8.2 - "@scure/bip39": ^1.1.0 - bs58check: ^2.1.2 - buffer: ^6.0.3 - cross-fetch: 3.1.5 - hash.js: ^1.1.7 - iso-3166-1: ^2.1.1 - json-bigint: ^1.0.0 - uuid: ^8.3.2 - checksum: 2ee043af6fe32a6e78193876dc3dface9121f9ad6f88b4a70e3db3b1c94b8d9c636394c10e235b98234acfa31717c8f3f58b82af0300242a170ba8249d93fb94 - languageName: node - linkType: hard - -"@concordium/common-sdk@npm:9.0.0": - version: 9.0.0 - resolution: "@concordium/common-sdk@npm:9.0.0" +"@concordium/common-sdk@npm:9.1.1, @concordium/common-sdk@npm:^9.1.1": + version: 9.1.1 + resolution: "@concordium/common-sdk@npm:9.1.1" dependencies: - "@concordium/rust-bindings": 1.1.0 + "@concordium/rust-bindings": 1.2.0 "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 + big.js: ^6.2.0 bs58check: ^2.1.2 buffer: ^6.0.3 cross-fetch: 3.1.5 @@ -2060,20 +2044,19 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: b640846415dfb18b4bb549519ab7fa7825c6b5e84adf1cfaeeea89bb54341e5176e71f00b732ea9bb375aac938842434ab1d6d5ffe2a5f360efc236ea5b0ef4a + checksum: e7f43e085b968e9772f2559b2530bca59a9aab426332c3e088f266a76bc1e913bedff097aa61bc5a9c5ddb78db4decf2fd6a027d7231e95fcb368fe41f2820c8 languageName: node linkType: hard -"@concordium/common-sdk@npm:9.1.1, @concordium/common-sdk@npm:^9.1.1": - version: 9.1.1 - resolution: "@concordium/common-sdk@npm:9.1.1" +"@concordium/common-sdk@npm:^8.0.0": + version: 8.0.0 + resolution: "@concordium/common-sdk@npm:8.0.0" dependencies: - "@concordium/rust-bindings": 1.2.0 + "@concordium/rust-bindings": 1.0.0 "@grpc/grpc-js": ^1.3.4 "@noble/ed25519": ^1.7.1 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 - big.js: ^6.2.0 bs58check: ^2.1.2 buffer: ^6.0.3 cross-fetch: 3.1.5 @@ -2081,7 +2064,7 @@ __metadata: iso-3166-1: ^2.1.1 json-bigint: ^1.0.0 uuid: ^8.3.2 - checksum: 6ce6e130bce8f3011b00a37e7429a5e19d4b21c21918e3ba8fb0438af841ec9a78d9de5eb9aa277d42154d6784dd182aed9198f92826b3b124fc59dbf5d30078 + checksum: 2ee043af6fe32a6e78193876dc3dface9121f9ad6f88b4a70e3db3b1c94b8d9c636394c10e235b98234acfa31717c8f3f58b82af0300242a170ba8249d93fb94 languageName: node linkType: hard @@ -2107,20 +2090,6 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:0.10.0": - version: 0.10.0 - resolution: "@concordium/rust-bindings@npm:0.10.0" - checksum: f09c754080fe7e026308342e0139e18de72e68ced8228d80c34d873c065a93e2774015da33ee3f970f33f97916526a43f54a410e42451137cffa3ccbe7e660a3 - languageName: node - linkType: hard - -"@concordium/rust-bindings@npm:0.11.0": - version: 0.11.0 - resolution: "@concordium/rust-bindings@npm:0.11.0" - checksum: a6c437cb782b7b2e9f217215dd4dbed9ffb8b3ef46fa307952aca3ae8f166cb96687de64efd18490aec7e86d58712dccb7d1a8bf63c151e3ed2a0170f066cd6d - languageName: node - linkType: hard - "@concordium/rust-bindings@npm:1.0.0": version: 1.0.0 resolution: "@concordium/rust-bindings@npm:1.0.0" @@ -2128,13 +2097,6 @@ __metadata: languageName: node linkType: hard -"@concordium/rust-bindings@npm:1.1.0": - version: 1.1.0 - resolution: "@concordium/rust-bindings@npm:1.1.0" - checksum: 4890306b5df5fb85012cc6c6a6daf15ec1d24c528ac487a4547e8758738a1205b2386a1d4b015dc45312811466d853819edff4b9921d3674afe13ade5cacc061 - languageName: node - linkType: hard - "@concordium/rust-bindings@npm:1.2.0": version: 1.2.0 resolution: "@concordium/rust-bindings@npm:1.2.0" @@ -2164,48 +2126,6 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^3.3.1": - version: 3.3.1 - resolution: "@concordium/web-sdk@npm:3.3.1" - dependencies: - "@concordium/common-sdk": 6.3.0 - "@concordium/rust-bindings": 0.10.0 - "@grpc/grpc-js": ^1.3.4 - "@protobuf-ts/grpcweb-transport": ^2.8.2 - buffer: ^6.0.3 - process: ^0.11.10 - checksum: a7be99af72d605ecbb874a7098b5cea2b5bd42b51899bc9a02ef6c95c4fd184898a19ed6b4745a8ea92deea41b998e529a0e1e83ade7c290fd697e4f7dc618b6 - languageName: node - linkType: hard - -"@concordium/web-sdk@npm:^5.0.0": - version: 5.0.0 - resolution: "@concordium/web-sdk@npm:5.0.0" - dependencies: - "@concordium/common-sdk": 8.0.0 - "@concordium/rust-bindings": 1.0.0 - "@grpc/grpc-js": ^1.3.4 - "@protobuf-ts/grpcweb-transport": ^2.8.2 - buffer: ^6.0.3 - process: ^0.11.10 - checksum: 779bb90cb7e5bfa25ad5bd0fa40447fc1fd92acc1b4bc24ac78f5c0010a0ff282a2db00153a84239328290b21c91d5d21364c54aac7404a81e7ad58a74daa597 - languageName: node - linkType: hard - -"@concordium/web-sdk@npm:^6.0.0": - version: 6.0.0 - resolution: "@concordium/web-sdk@npm:6.0.0" - dependencies: - "@concordium/common-sdk": 9.0.0 - "@concordium/rust-bindings": 1.1.0 - "@grpc/grpc-js": ^1.3.4 - "@protobuf-ts/grpcweb-transport": ^2.8.2 - buffer: ^6.0.3 - process: ^0.11.10 - checksum: 23cfda2813ed86ed165f427edab237e5c20af70405d450541479294a1093b60843ce9d7f8a2dbeb69e174e0aed0ec51bee109e0fb7e22f121bff5c11c4bb24de - languageName: node - linkType: hard - "@concordium/web-sdk@npm:^6.1.1": version: 6.1.1 resolution: "@concordium/web-sdk@npm:6.1.1" @@ -10297,6 +10217,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.3.0 + "@concordium/web-sdk": ^6.1.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -22291,7 +22212,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^6.0.0 + "@concordium/web-sdk": ^6.1.1 "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 From e15c2104f72c02f571ae755dc3f205394e6b1d5b Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 29 Aug 2023 13:16:18 +0200 Subject: [PATCH 160/231] Add credential metadata fallback --- packages/browser-wallet/CHANGELOG.md | 6 ++++ .../AddWeb3IdCredential.tsx | 1 + .../VerifiableCredentialHooks.tsx | 28 +++++++++++++------ .../src/shared/storage/types.ts | 5 +++- .../src/shared/utils/log-helpers.ts | 4 +++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 26d19c00..7219ed77 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. + ## 1.1.3 ### Changed diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 818c85ae..1db85b8c 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -197,6 +197,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { credentialSubject, id: credentialId, index, + metadataUrl: metadataUrl.url, }; await setWeb3IdCredentials([...web3IdCredentials.value, fullCredential]); if (metadata) { diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 882ea356..c104d44a 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -22,7 +22,7 @@ import { import { AsyncWrapper } from '@popup/store/utils'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; import { useTranslation } from 'react-i18next'; -import { logError } from '@shared/utils/log-helpers'; +import { logError, logWarningMessage } from '@shared/utils/log-helpers'; /** * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. @@ -110,24 +110,36 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { if (storedMetadata.loading) { return; } - let url; + + let url: string | undefined; if (credentialEntry) { url = credentialEntry.credentialInfo.metadataUrl.url; } else if (!tempMetadata.loading && credential) { url = tempMetadata.value[credential.id]; } - if (!url) { + if (url === undefined || credential === undefined) { return; } + const storedCredentialMetadata = storedMetadata.value[url]; - if (!storedCredentialMetadata) { + if (storedCredentialMetadata) { + setMetadata(storedCredentialMetadata); + return; + } + + // The URL we got does not have a corresponding entry in our local storage. + // In this case we fallback to using the known "good" value so that we can` + // still get metadata for this credential. + const fallbackCredentialMetadata = storedMetadata.value[credential.metadataUrl]; + if (!fallbackCredentialMetadata) { throw new Error( - `Attempted to find credential metadata for credentialId: ${ - credentialEntry?.credentialInfo.credentialHolderId || credential?.id - } but none was found!` + `Attempted to find credential metadata for credentialId: ${credential.id} at URL ${credential.metadataUrl} but none was found!` ); } - setMetadata(storedCredentialMetadata); + logWarningMessage( + `Using fallback credential metadata for credential ${credential.id}. The credential entry metadata URL is [${credentialEntry?.credentialInfo.metadataUrl.url}]` + ); + setMetadata(fallbackCredentialMetadata); }, [storedMetadata.loading, tempMetadata.loading, credentialEntry, credential?.id]); return metadata; diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 1ef3c8c5..10241c4e 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -287,8 +287,11 @@ export interface VerifiableCredential extends APIVerifiableCredential { // Secrets signature: string; randomness: Record; - /** index used to derive keys for credential */ + // Index used to derive keys for credential index: number; + // The original metadataUrl received when first adding the credential + // TODO: The URL should be updated when there are valid updates to the metadata. + metadataUrl: string; } interface CredentialSchemaProperty { diff --git a/packages/browser-wallet/src/shared/utils/log-helpers.ts b/packages/browser-wallet/src/shared/utils/log-helpers.ts index 312889d0..1e3f0a23 100644 --- a/packages/browser-wallet/src/shared/utils/log-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/log-helpers.ts @@ -66,3 +66,7 @@ export async function logError(error: unknown) { logErrorMessage(String(error)); } } + +export async function logWarningMessage(message: string) { + log(message, LoggingLevel.WARN); +} From 93eab55fb0cde03c943805f646992fddf56e0c47 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 14:54:15 +0200 Subject: [PATCH 161/231] UI changes --- .../DisplayStatement/DisplayStatement.tsx | 6 +- .../Web3ProofRequest/CredentialSelector.tsx | 1 + .../pages/Web3ProofRequest/DisplayBox.tsx | 165 ++++++++++++++++++ .../VerifiableCredentialStatement.tsx | 24 +-- .../Web3ProofRequest/Web3ProofRequest.scss | 29 +++ 5 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayBox.tsx diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index 53c164a8..d14d3c90 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -34,11 +34,11 @@ export type StatementLine = { isRequirementMet: boolean; }; -type StatementLineProps = StatementLine; +type StatementLineProps = StatementLine & ClassName; -export function DisplayStatementLine({ attribute, value, isRequirementMet }: StatementLineProps) { +export function DisplayStatementLine({ attribute, value, isRequirementMet, className }: StatementLineProps) { return ( -
  • +
  • {attribute}:
    {value} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index 2da3fb7f..877b68a5 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -19,6 +19,7 @@ export default function CredentialSelector({ options, onChange, displayOption return ( + issuer Index:

    Attribute values:

    degreeType: From c8f60679c51cdbc7c84cb809724ea782d69092db Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 15:29:41 +0200 Subject: [PATCH 163/231] Fix import button text + adjust size --- .../VerifiableCredentialBackup/VerifiableCredentialImport.tsx | 2 +- .../src/popup/pages/VerifiableCredentialBackup/i18n/da.ts | 1 + .../src/popup/pages/VerifiableCredentialBackup/i18n/en.ts | 1 + .../src/popup/shared/Form/FileInput/FileInput.tsx | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 9a82aa29..a981c7e3 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -130,7 +130,7 @@ export default function VerifiableCredentialImport() { {error &&

    {error}

    } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts index e810872c..14e3812e 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/da.ts @@ -4,6 +4,7 @@ const t: typeof en = { title: 'Importer Web3 ID Credentials', noImported: 'Ingen Web3 ID Credentials blev importeret', error: 'Det var ikke muligt at importere den valgte fil. Filen skal være en backup lavet med en samme seed phrase.', + importButton: 'Vælg fil at importere', close: 'Luk', }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts index 5bc9130c..0494c5ca 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/i18n/en.ts @@ -2,5 +2,6 @@ export default { title: 'Import Web3 ID Credentials', noImported: 'No Web3 ID Credentials were imported', error: 'Unable to import the chosen file. The file must be a backup created with the same seed phrase.', + importButton: 'Select file to import', close: 'Close', }; diff --git a/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx b/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx index 7943e356..c72244d3 100644 --- a/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx +++ b/packages/browser-wallet/src/popup/shared/Form/FileInput/FileInput.tsx @@ -81,7 +81,7 @@ export const FileInput = forwardRef( {f?.name}
    ))} - Date: Tue, 29 Aug 2023 16:26:56 +0200 Subject: [PATCH 164/231] Update packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Hjort <87635671+shjortConcordium@users.noreply.github.com> --- .../pages/VerifiableCredential/VerifiableCredentialHooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index c104d44a..20aad2a8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -128,7 +128,7 @@ export function useCredentialMetadata(credential?: VerifiableCredential) { } // The URL we got does not have a corresponding entry in our local storage. - // In this case we fallback to using the known "good" value so that we can` + // In this case we fallback to using the known "good" value so that we can // still get metadata for this credential. const fallbackCredentialMetadata = storedMetadata.value[credential.metadataUrl]; if (!fallbackCredentialMetadata) { From 42dd99f4a1afa811aad198630d6aa0aaf35c3019 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 16:45:56 +0200 Subject: [PATCH 165/231] Further UI change --- .../Web3ProofRequest/Display/DisplayBox.tsx | 40 +++++ .../Display/DisplayRevealStatements.tsx | 63 +++++++ .../Display/DisplaySecretStatements.tsx | 99 +++++++++++ .../pages/Web3ProofRequest/Display/utils.ts | 7 + .../pages/Web3ProofRequest/DisplayBox.tsx | 165 ------------------ .../VerifiableCredentialStatement.tsx | 162 ++--------------- .../Web3ProofRequest/Web3ProofRequest.scss | 17 +- .../popup/pages/Web3ProofRequest/i18n/en.ts | 8 +- .../src/popup/pages/Web3ProofRequest/utils.ts | 27 +++ 9 files changed, 264 insertions(+), 324 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts delete mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayBox.tsx diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx new file mode 100644 index 00000000..7464ecaa --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx @@ -0,0 +1,40 @@ +import Button from '@popup/shared/Button'; +import Modal from '@popup/shared/Modal'; +import clsx from 'clsx'; +import React, { ReactNode, useState } from 'react'; +import { ClassName } from 'wallet-common-helpers'; +import InfoTooltipIcon from '@assets/svg/info-tooltip.svg'; + +type DisplayBoxProps = ClassName & { + header: string; + children: ReactNode; + infoBox: ReactNode; +}; + +export function DisplayBox({ className, children, header, infoBox }: DisplayBoxProps) { + const [open, setOpen] = useState(false); + + return ( +
    +
    +
    {header}
    + setOpen(true)} + onClose={() => setOpen(false)} + trigger={ + + } + > + {infoBox} + + +
    + {children} +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx new file mode 100644 index 00000000..00d9db7b --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx @@ -0,0 +1,63 @@ +import { AttributeType, RevealStatementV2 } from '@concordium/web-sdk'; +import { VerifiableCredentialSchema } from '@shared/storage/types'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { DisplayStatementLine } from '../../IdProofRequest/DisplayStatement/DisplayStatement'; +import { DisplayBox } from './DisplayBox'; +import { getPropertyTitle } from './utils'; + +type RevealProps = ClassName & { + dappName: string; + statements: RevealStatementV2[]; + attributes: Record; + schema: VerifiableCredentialSchema; + className: string; +}; + +export function DisplayRevealStatements({ className, statements, attributes, dappName, schema }: RevealProps) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); + const header = t('headers.reveal'); + + const lines = statements.map((s) => { + const value = attributes[s.attributeTag]; + const title = getPropertyTitle(s.attributeTag, schema); + return { + attribute: title, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + }; + }); + + return ( + +

    {t('revealTooltip.header')}

    +

    {t('revealTooltip.body')}

    + + } + > +
      + {lines.map((l, i) => ( + + ))} +
    +
    + }} + values={{ dappName }} + /> +
    +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx new file mode 100644 index 00000000..1a447bb9 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -0,0 +1,99 @@ +import { AttributeType, StatementTypes } from '@concordium/web-sdk'; +import { DisplayStatementLine } from '@popup/pages/IdProofRequest/DisplayStatement/DisplayStatement'; +import { VerifiableCredentialSchema } from '@shared/storage/types'; +import React from 'react'; +import { TFunction, useTranslation } from 'react-i18next'; +import { ClassName } from 'wallet-common-helpers'; +import { DisplayBox } from './DisplayBox'; +import { SecretStatementV2 } from '../utils'; +import { getPropertyTitle } from './utils'; + +type SecretProps = ClassName & { + statements: SecretStatementV2[]; + attributes: Record; + schema: VerifiableCredentialSchema; + className: string; +}; + +function getStatementValue( + statement: SecretStatementV2, + schema: VerifiableCredentialSchema, + t: TFunction<'web3IdProofRequest', 'displayStatement'> +): string { + const name = getPropertyTitle(statement.attributeTag, schema); + if (statement.type === StatementTypes.AttributeInRange) { + return t('proofs.range', { name, upper: statement.upper, lower: statement.lower }); + } + if (statement.type === StatementTypes.AttributeInSet) { + return t('proofs.membership', { name }); + } + if (statement.type === StatementTypes.AttributeNotInSet) { + return t('proofs.nonMembership', { name }); + } + + throw new Error('Unknown statement type'); +} + +function getStatementDescription( + statement: SecretStatementV2, + schema: VerifiableCredentialSchema, + t: TFunction<'web3IdProofRequest', 'displayStatement'> +) { + const name = getPropertyTitle(statement.attributeTag, schema); + const listToString = (list: AttributeType[]) => list.map((member) => member.toString()).join(', '); + + switch (statement.type) { + case StatementTypes.AttributeInRange: + return t('descriptions.range', { name, lower: statement.lower, upper: statement.upper }); + case StatementTypes.AttributeInSet: + return t('descriptions.membership', { name, setNames: listToString(statement.set) }); + case StatementTypes.AttributeNotInSet: + return t('descriptions.nonMembership', { name, setNames: listToString(statement.set) }); + default: + throw new Error(`Unknown statement type encountered: ${statement.type}`); + } +} + +export function DisplaySecretStatements({ schema, statements, className }: SecretProps) { + const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); + const header = t('headers.secret'); + + const lines = statements.map((s) => { + const value = getStatementValue(s, schema, t); + const title = getPropertyTitle(s.attributeTag, schema); + const description = getStatementDescription(s, schema, t); + return { + attribute: title, + value: value.toString() ?? 'Unavailable', + isRequirementMet: value !== undefined, + description, + }; + }); + + return ( + +

    {t('secretTooltip.header')}

    +

    {t('secretTooltip.body')}

    + + } + > +
      + {lines.map(({ description, ...l }, i) => ( +
      + +
      {description}
      +
      + ))} +
    +
    + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts new file mode 100644 index 00000000..9a9b669d --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts @@ -0,0 +1,7 @@ +import { VerifiableCredentialSchema } from '@shared/storage/types'; + +export function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { + // TODO use localization here + const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; + return property ? property.title : attributeTag; +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayBox.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayBox.tsx deleted file mode 100644 index 64beb899..00000000 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/DisplayBox.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import Button from '@popup/shared/Button'; -import Modal from '@popup/shared/Modal'; -import clsx from 'clsx'; -import React, { ReactNode, useState } from 'react'; -import { ClassName } from 'wallet-common-helpers'; -import InfoTooltipIcon from '@assets/svg/info-tooltip.svg'; -import { AttributeType, RevealStatementV2, StatementTypes } from '@concordium/web-sdk'; -import { Trans, useTranslation } from 'react-i18next'; -import { getPropertyTitle } from './VerifiableCredentialStatement'; -import { VerifiableCredentialSchema } from '@shared/storage/types'; -import { DisplayStatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; -import { SecretStatementV2 } from './utils'; -import { useStatementValue } from '../IdProofRequest/DisplayStatement/utils'; -import { TFunction } from 'react-i18next'; - -type DisplayBoxProps = ClassName & { - header: string; - children: ReactNode; - infoBox: ReactNode; -} - -export function DisplayBox ({ className, children, header, infoBox }: DisplayBoxProps) { - const [open, setOpen] = useState(false); - - return ( -
    -
    -
    - {header} -
    - setOpen(true)} - onClose={() => setOpen(false)} - trigger={ - - } - > - {infoBox} - -
    - {children} -
    - ); -} - -type RevealProps = ClassName & { - dappName: string; - statements: RevealStatementV2[]; - attributes: Record; - schema: VerifiableCredentialSchema; - className: string; -} - -export function DisplayRevealStatements({ className, statements, attributes, dappName, schema}: RevealProps) { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); - const header = t('headers.reveal'); - - const lines = statements.map((s) => { - const value = attributes[s.attributeTag]; - const title = getPropertyTitle(s.attributeTag, schema); - return { - attribute: title, - value: value.toString() ?? 'Unavailable', - isRequirementMet: value !== undefined, - }; - }); - - return ( - )}> -
      - {lines.map((l, i) => ( - - ))} -
    -
    - }} - values={{ dappName }} - /> -
    -
    - ) -} - -type SecretProps = ClassName & { - dappName: string; - statements: SecretStatementV2[]; - attributes: Record; - schema: VerifiableCredentialSchema; - className: string; -} - -function getStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema, t: TFunction<'web3IdProofRequest', 'displayStatement.proofs'>): string { - const name = getPropertyTitle(statement.attributeTag, schema); - if (statement.type === StatementTypes.AttributeInRange) { - return t('range', { name, upper: statement.upper, lower: statement.lower }); - } - if (statement.type === StatementTypes.AttributeInSet) { - return t('membership', { name }); - } - if (statement.type === StatementTypes.AttributeNotInSet) { - return t('nonMembership', { name }); - } - - throw new Error('Unknown statement type'); -} - -export function getStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema, t: TFunction<'web3IdProofRequest', 'displayStatement.descriptions'>) { - const name = getPropertyTitle(statement.attributeTag, schema); - const listToString = (list: AttributeType[]) => list.map((member) => member.toString()).join(', '); - - switch (statement.type) { - case StatementTypes.AttributeInRange: - return t('range', { name, lower: statement.lower, upper: statement.upper }); - case StatementTypes.AttributeInSet: - return t('membership', { name, setNames: listToString(statement.set) }); - case StatementTypes.AttributeNotInSet: - return t('nonMembership', { name, setNames: listToString(statement.set) }); - default: - throw new Error(`Unknown statement type encountered: ${statement.type}`); - } -} - -export function DisplaySecretStatements({ schema, statements, className, dappName }: SecretProps) { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); - const header = t('headers.secret'); - - const lines = statements.map((s) => { - const value = getStatementValue(s, schema, t); - const title = getPropertyTitle(s.attributeTag, schema); - const description = getStatementDescription(s, schema, t); - return { - attribute: title, - value: value.toString() ?? 'Unavailable', - isRequirementMet: value !== undefined, - description - }; - }); - - return ( - )}> -
      - {lines.map((l, i) => ( - - ))} -
    -
    - ) - -} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index b840a87d..9cf32cc9 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -1,155 +1,16 @@ -import { - AtomicStatementV2, - RevealStatementV2, - StatementTypes, - VerifiableCredentialStatement, - CredentialSubject, - AttributeType, -} from '@concordium/web-sdk'; +import { RevealStatementV2, StatementTypes, VerifiableCredentialStatement } from '@concordium/web-sdk'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ClassName } from 'wallet-common-helpers'; -import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; import { useCredentialLocalization, useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; -import { DisplayRevealStatements, DisplaySecretStatements } from './DisplayBox'; +import { DisplayRevealStatements } from './Display/DisplayRevealStatements'; +import { DisplaySecretStatements } from './Display/DisplaySecretStatements'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; -export function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { - // TODO use localization here - const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; - return property ? property.title : attributeTag; -} - -function useStatementValue(statement: SecretStatementV2, schema: VerifiableCredentialSchema): string { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.proofs' }); - - const name = getPropertyTitle(statement.attributeTag, schema); - if (statement.type === StatementTypes.AttributeInRange) { - return t('range', { name, upper: statement.upper, lower: statement.lower }); - } - if (statement.type === StatementTypes.AttributeInSet) { - return t('membership', { name }); - } - if (statement.type === StatementTypes.AttributeNotInSet) { - return t('nonMembership', { name }); - } - - throw new Error('Unknown statement type'); -} - -export function useStatementDescription(statement: SecretStatementV2, schema: VerifiableCredentialSchema) { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement.descriptions' }); - const name = getPropertyTitle(statement.attributeTag, schema); - const listToString = (list: AttributeType[]) => list.map((member) => member.toString()).join(', '); - - switch (statement.type) { - case StatementTypes.AttributeInRange: - return t('range', { name, lower: statement.lower, upper: statement.upper }); - case StatementTypes.AttributeInSet: - return t('membership', { name, setNames: listToString(statement.set) }); - case StatementTypes.AttributeNotInSet: - return t('nonMembership', { name, setNames: listToString(statement.set) }); - default: - throw new Error(`Unknown statement type encountered: ${statement.type}`); - } -} - -type DisplayWeb3StatementProps = ClassName & { - statements: Statement; - dappName: string; - schema: VerifiableCredentialSchema; -}; - -type AttributeInfo = { - name: string; - value: AttributeType; -}; - -function extractAttributesFromCredentialSubjectForSingleStatement( - { attributeTag }: AtomicStatementV2, - credentialSubject: CredentialSubject -): AttributeInfo { - return { name: attributeTag, value: credentialSubject.attributes[attributeTag] }; -} - -function extractAttributesFromCredentialSubject( - statements: AtomicStatementV2[], - credentialSubject: CredentialSubject -): Record { - return statements.reduce>((acc, statement) => { - acc[statement.attributeTag] = extractAttributesFromCredentialSubjectForSingleStatement( - statement, - credentialSubject - ); - return acc; - }, {}); -} - -type DisplayWeb3RevealStatementProps = DisplayWeb3StatementProps & { - credential: CredentialSubject; -}; - -export function DisplayWeb3RevealStatement({ - statements, - dappName, - credential, - className, - schema, -}: DisplayWeb3RevealStatementProps) { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); - const attributes = extractAttributesFromCredentialSubject(statements, credential); - const header = t('headers.reveal'); - - const lines: StatementLine[] = statements.map((s) => { - const { value } = attributes[s.attributeTag]; - const title = getPropertyTitle(s.attributeTag, schema); - return { - attribute: title, - value: value.toString() ?? 'Unavailable', - isRequirementMet: value !== undefined, - }; - }); - - return ; -} - -export function DisplayWeb3SecretStatement({ - statements, - dappName, - className, - schema, -}: DisplayWeb3StatementProps) { - const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); - const value = useStatementValue(statements, schema); - const header = t('headers.secret'); - const title = getPropertyTitle(statements.attributeTag, schema); - const description = useStatementDescription(statements, schema); - - const lines: StatementLine[] = [ - { - attribute: title, - value, - isRequirementMet: value !== undefined, - }, - ]; - - return ( - - ); -} - export default function DisplayWeb3Statement({ credentialStatement, validCredentials, @@ -218,15 +79,14 @@ export default function DisplayWeb3Statement({ schema={schema} /> )} - {secrets.length !== 0 && ( - - )} + )}
  • ); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 9ae276ea..e1d7d4e7 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -20,17 +20,18 @@ .display-box { border-radius: 16px; - box-shadow: 0px 0px 15px 0px #0000001A; + box-shadow: 0 0 15px 0 #0000001a; &__header { - background: linear-gradient(91.35deg, #B6DADF 13.38%, #DAECEF 93.45%); + background: linear-gradient(91.35deg, #b6dadf 13.38%, #daecef 93.45%); border-top-left-radius: 16px; border-top-right-radius: 16px; height: 48px; padding: 15px; display: flex; justify-content: space-between; - } + color: $color-secondary-ocean-blue; + } &__tooltip-icon { height: 24px; @@ -38,11 +39,19 @@ } .display-reveal-statements { - &__line { &:not(:last-child) { margin-bottom: 8px; } } +} + +.display-secret-statements { + &__line { + margin-top: 5px; + &:not(:last-child) { + border-bottom: 1px solid #e5e5e5; + } + } } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 070f05d4..e1c6a3b4 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -9,12 +9,12 @@ export default { revealDescription: '<1>Important: {{dappName}} will be given all the information above. You should only accept, if you trust the service, and you are familiar with their privacy policy.', revealTooltip: { - header: 'Revealing information <1 />', - body: 'When you reveal information to a third party, you effectively hand over the information to them. This means you should only do this if you agree to their data usage and protection policies.\n\nYou can read more in\n<1>the developer documentation.', + header: 'Information to reveal', + body: 'When you reveal information for a third party, you effectively hand over the information to them. This means that you should only do this if you have absolute trust in them, and if you are familiar with their data usage and protection procedures.\n\nYou can read more on\ndeveloper.concordium.software', }, secretTooltip: { - header: 'Zero Knowledge proofs', - body: 'Zero Knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact age. Another example could be proving your residency is within a given set of countries without revealing which of those countries you reside within.\n\nYou can read more in\n<1>the developer documentation.', + header: 'Zero Knowledge proof', + body: 'Zero-knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact date of birth. Another example could be that you live in one of a range of countries without revealing exactly which country you live in.\n\nYou can read more on\ndeveloper.concordium.software', }, headers: { reveal: 'Information to reveal', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index 6123bc5c..24d4fe70 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -10,6 +10,8 @@ import { RevealStatementV2, createWeb3IdDID, canProveCredentialStatement, + AttributeType, + CredentialSubject, } from '@concordium/web-sdk'; import { isIdentityOfCredential } from '@shared/utils/identity-helpers'; import { @@ -134,3 +136,28 @@ export function createWeb3IdDIDFromCredential(credential: VerifiableCredential, BigInt(contractAddress.subindex) ); } + +type AttributeInfo = { + name: string; + value: AttributeType; +}; + +function extractAttributesFromCredentialSubjectForSingleStatement( + { attributeTag }: AtomicStatementV2, + credentialSubject: CredentialSubject +): AttributeInfo { + return { name: attributeTag, value: credentialSubject.attributes[attributeTag] }; +} + +export function extractAttributesFromCredentialSubject( + statements: AtomicStatementV2[], + credentialSubject: CredentialSubject +): Record { + return statements.reduce>((acc, statement) => { + acc[statement.attributeTag] = extractAttributesFromCredentialSubjectForSingleStatement( + statement, + credentialSubject + ); + return acc; + }, {}); +} From e039caf5574bfbadba561bbc21a1a3dde7814425 Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 29 Aug 2023 17:22:50 +0200 Subject: [PATCH 166/231] Selector WIP --- .../Web3ProofRequest/CredentialSelector.tsx | 50 ++++++++++++------- .../VerifiableCredentialStatement.tsx | 27 ++++++++-- .../Web3ProofRequest/Web3ProofRequest.scss | 10 ++++ 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index 877b68a5..65922f0b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -1,37 +1,51 @@ -import React, { useState } from 'react'; +import Button from '@popup/shared/Button'; +import Modal from '@popup/shared/Modal'; +import React, { ComponentType, useState } from 'react'; interface Props { options: T[]; + initialIndex?: number; onChange: (x: T) => void; - displayOption: (x: T) => string; + DisplayOption: ComponentType<{ option: T }>; } /** * Component to select a credential, either account credential or web3Id credential. */ -export default function CredentialSelector({ options, onChange, displayOption }: Props) { - const [chosenIndex, setChosenIndex] = useState(0); +export default function CredentialSelector({ + options, + initialIndex = 0, + onChange, + DisplayOption, +}: Props) { + const [chosenIndex, setChosenIndex] = useState(initialIndex); + const [open, setOpen] = useState(false); if (options.length === 0) { - // TODO Translate - return
    No candidate available
    ; + return null; + } + + function onClick(index: number) { + setChosenIndex(index); + onChange(options[index]); } return ( - + ); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 9cf32cc9..2ec49f35 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -1,7 +1,8 @@ +import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { RevealStatementV2, StatementTypes, VerifiableCredentialStatement } from '@concordium/web-sdk'; +import Img from '@popup/shared/Img'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; -import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; @@ -11,6 +12,25 @@ import { DisplayRevealStatements } from './Display/DisplayRevealStatements'; import { DisplaySecretStatements } from './Display/DisplaySecretStatements'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; +function Logo({ logo }: { logo: MetadataUrl }) { + return ; +} + +export function DisplayVC({ option }: { option: VerifiableCredential }) { + const metadata = useCredentialMetadata(option); + + if (!metadata) { + return null; + } + + return ( +
    + +
    {metadata.title}
    +
    + ); +} + export default function DisplayWeb3Statement({ credentialStatement, validCredentials, @@ -64,10 +84,9 @@ export default function DisplayWeb3Statement({ metadata={metadata} localization={localization.result} /> - - options={validCredentials} - displayOption={(option) => getVerifiableCredentialPublicKeyfromSubjectDID(option.id)} + DisplayOption={DisplayVC} onChange={onChange} /> {reveals.length !== 0 && ( diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index e1d7d4e7..8a4477ef 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -55,3 +55,13 @@ } } } + +.verifiable-credential { + &__selector { + width: 376px; + height: 60px; + border-radius: 12px; + box-shadow: 0 0 15px 0 #0000001a; + background: #fff; + } +} From c518f138ed636c5634d041ab8eadcffff0c27b2f Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 10:38:39 +0200 Subject: [PATCH 167/231] Improve handling of changes to schema --- packages/browser-wallet/CHANGELOG.md | 1 + .../popup/page-layouts/MainLayout/i18n/da.ts | 2 +- .../popup/page-layouts/MainLayout/i18n/en.ts | 2 +- .../AddWeb3IdCredential.tsx | 2 +- .../pages/AddWeb3IdCredential/i18n/da.ts | 2 +- .../pages/AddWeb3IdCredential/i18n/en.ts | 4 +- .../VerifiableCredentialCard.stories.tsx | 2 +- .../VerifiableCredentialCard.tsx | 42 ++++++++++++++++--- .../VerifiableCredentialDetails.tsx | 8 +++- .../VerifiableCredentialHooks.tsx | 35 ++++++++++++---- .../VerifiableCredentialList.tsx | 13 ++++-- .../pages/VerifiableCredential/i18n/da.ts | 6 +++ .../pages/VerifiableCredential/i18n/en.ts | 6 +++ .../VerifiableCredentialStatement.tsx | 21 ++++------ .../src/shared/storage/types.ts | 2 + .../utils/verifiable-credential-helpers.ts | 2 +- 16 files changed, 107 insertions(+), 43 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..be061d31 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. +- Issues with a contract switching to an invalid schema or switching the schema to a new URL. ## 1.1.3 diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts index dfbf76fd..01c3370a 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/da.ts @@ -18,7 +18,7 @@ const t: typeof en = { passcode: 'Skift adgangskode', about: 'Om', }, - addWeb3IdCredential: 'Tilføj Web3Id Credential', + addWeb3IdCredential: 'Tilføj Web3 ID credential', connectAccountsRequest: 'Forbind konti', addTokens: 'Tilføj tokens', idProof: 'Bevis for identitet', diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index b4717f67..2d90c2ce 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -16,7 +16,7 @@ const t = { passcode: 'Change passcode', about: 'About', }, - addWeb3IdCredential: 'Add Web3Id Credential', + addWeb3IdCredential: 'Add Web3 ID Credential', connectAccountsRequest: 'Connect accounts', addTokens: 'Add tokens', idProof: 'Proof of identity', diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 1db85b8c..5c05c8c6 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -229,7 +229,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { = () => {
    diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index e18a5b62..86af2b23 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -4,7 +4,13 @@ import clsx from 'clsx'; import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; import Img from '@popup/shared/Img'; import { AttributeType, CredentialSubject } from '@concordium/web-sdk'; -import { VerifiableCredentialStatus, MetadataUrl, VerifiableCredentialSchema } from '@shared/storage/types'; +import { + VerifiableCredentialStatus, + MetadataUrl, + VerifiableCredentialSchema, + VerifiableCredentialSchemaWithFallback, +} from '@shared/storage/types'; +import { useTranslation } from 'react-i18next'; import StatusIcon from './VerifiableCredentialStatus'; function Logo({ logo }: { logo: MetadataUrl }) { @@ -71,13 +77,32 @@ function ClickableVerifiableCredential({ children, onClick, metadata, className ); } +/** + * Checks that the schema has an entry for each attribute. + * @param schema the schema to validate against the attributes + * @param attributes the attributes which keys should be in the schema + * @returns true if all attribute keys are present in the schema, otherwise false + */ +function validateSchemaMatchesAttributes( + schema: VerifiableCredentialSchema, + attributes: Record +) { + for (const attributeKey of Object.keys(attributes)) { + const schemaProperty = schema.properties.credentialSubject.properties.attributes.properties[attributeKey]; + if (!schemaProperty) { + return false; + } + } + return true; +} + /** * Apply the schema and localization to an attribute, adding the title from the schema or localization, which * should be displayed to the user. + * If there is a missing key in the schema, then the attribute key is used as the title instead. * @param schema the schema to apply * @param localization the localization to apply * @returns the attribute together with its title. - * @throws if there is a mismatch in fields between the credential and the schema, i.e. the schema is invalid. */ function applySchemaAndLocalization( schema: VerifiableCredentialSchema, @@ -85,10 +110,10 @@ function applySchemaAndLocalization( ): (value: [string, AttributeType]) => { title: string; key: string; value: AttributeType } { return (value: [string, AttributeType]) => { const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; - if (!attributeSchema) { - throw new Error(`Missing attribute schema for key: ${value[0]}`); + let title = value[0]; + if (attributeSchema) { + title = attributeSchema.title; } - let { title } = attributeSchema; if (localization) { const localizedTitle = localization[value[0]]; @@ -127,7 +152,7 @@ export function VerifiableCredentialCardHeader({ interface CardProps extends ClassName { credentialSubject: Omit; - schema: VerifiableCredentialSchema; + schema: VerifiableCredentialSchemaWithFallback; credentialStatus: VerifiableCredentialStatus; metadata: VerifiableCredentialMetadata; onClick?: () => void; @@ -143,6 +168,9 @@ export function VerifiableCredentialCard({ className, localization, }: CardProps) { + const { t } = useTranslation('verifiableCredential'); + + const schemaMatchesCredentialAttributes = validateSchemaMatchesAttributes(schema, credentialSubject.attributes); const attributes = Object.entries(credentialSubject.attributes).map( applySchemaAndLocalization(schema, localization) ); @@ -152,6 +180,8 @@ export function VerifiableCredentialCard({ {metadata.image && }
    + {schema.usingFallback &&
    {t('errors.fallbackSchema')}
    } + {!schemaMatchesCredentialAttributes &&
    {t('errors.badSchema')}
    } {attributes && attributes.map((attribute) => ( ; backButtonOnClick: () => void; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 20aad2a8..3f97bdaf 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,5 +1,10 @@ import { grpcClientAtom } from '@popup/store/settings'; -import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types'; +import { + VerifiableCredential, + VerifiableCredentialStatus, + VerifiableCredentialSchema, + VerifiableCredentialSchemaWithFallback, +} from '@shared/storage/types'; import { CredentialQueryResponse, IssuerMetadata, @@ -52,19 +57,31 @@ export function useCredentialStatus(credential: VerifiableCredential) { * @throws if no schema is found in storage for the provided credential * @returns the credential's schema used for rendering the credential */ -export function useCredentialSchema(credential?: VerifiableCredential) { - const [schema, setSchema] = useState(); +export function useCredentialSchema( + credential?: VerifiableCredential +): VerifiableCredentialSchemaWithFallback | undefined { + const [schema, setSchema] = useState(); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const client = useAtomValue(grpcClientAtom); useEffect(() => { if (!schemas.loading && credential) { - const schemaValue = schemas.value[credential.credentialSchema.id]; - if (!schemaValue) { - throw new Error(`Attempted to find schema for credentialId: ${credential.id} but none was found!`); - } - setSchema(schemaValue); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getCredentialRegistryMetadata(client, registryContractAddress) + .then((registryMetadata) => { + let usingFallback = false; + let schemaValue = schemas.value[registryMetadata.credentialSchema.schema.url]; + if (!schemaValue) { + // Use the schema we got when originally adding the credential as a fallback for the + // credential, if we do not have the new schema saved yet. + usingFallback = true; + schemaValue = schemas.value[credential.credentialSchema.id]; + } + setSchema({ ...schemaValue, usingFallback }); + }) + .catch(logError); } - }, [credential?.id, schemas.loading]); + }, [credential?.id, schemas.loading, JSON.stringify(schemas.value)]); return schema; } diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 21542d2e..74cd26ca 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -7,7 +7,12 @@ import { import { useAtomValue, useAtom } from 'jotai'; import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; -import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; +import { + VerifiableCredential, + VerifiableCredentialSchema, + VerifiableCredentialSchemaWithFallback, + VerifiableCredentialStatus, +} from '@shared/storage/types'; import { VerifiableCredentialMetadata, getChangesToCredentialMetadata, @@ -77,7 +82,7 @@ export function VerifiableCredentialCardWithStatusFromChain({ className: string; onClick?: ( status: VerifiableCredentialStatus, - schema: VerifiableCredentialSchema, + schema: VerifiableCredentialSchemaWithFallback, metadata: VerifiableCredentialMetadata, localization?: Record ) => void; @@ -120,7 +125,7 @@ export default function VerifiableCredentialList() { const [selected, setSelected] = useState<{ credential: VerifiableCredential; status: VerifiableCredentialStatus; - schema: VerifiableCredentialSchema; + schema: VerifiableCredentialSchemaWithFallback; metadata: VerifiableCredentialMetadata; localization?: Record; }>(); @@ -192,7 +197,7 @@ export default function VerifiableCredentialList() { credential={credential} onClick={( status: VerifiableCredentialStatus, - schema: VerifiableCredentialSchema, + schema: VerifiableCredentialSchemaWithFallback, metadata: VerifiableCredentialMetadata, localization?: Record ) => setSelected({ credential, status, schema, metadata, localization })} 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 dd0a11db..58f41bef 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -30,6 +30,12 @@ const t: typeof en = { NotActivated: 'Ikke aktiveret', Pending: 'Afventer', }, + errors: { + badSchema: + 'Skemaet stemmer ikke overens med attributterne. Kontakt udstederen af dit Web3 ID kort for at rapportere fejlen.', + fallbackSchema: + 'Det var ikke muligt at hente skemaet for denne credential. Der benyttes et fallback skema til at vise denne credential. Kontakt udstederen af dit Web3 ID kort for at rapportere fejlen.', + }, }; export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts index 45beb1f6..c25d3319 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -28,6 +28,12 @@ const t = { NotActivated: 'Not activated', Pending: 'Pending', }, + errors: { + badSchema: + 'The credential schema does not match the credential attributes. Please contact the issuer of the Web3 ID credential to report the error.', + fallbackSchema: + 'There was an issue retrieving the schema for the credential. Using fallback schema to display the credential. Please contact the issuer of the Web3 ID credential to report the error.', + }, }; export default t; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 54adbf67..974dd4bc 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -6,16 +6,18 @@ import { CredentialSubject, AttributeType, } from '@concordium/web-sdk'; -import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialPublicKeyfromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; -import { useAtomValue } from 'jotai'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; -import { useCredentialLocalization, useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; +import { + useCredentialLocalization, + useCredentialMetadata, + useCredentialSchema, +} from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; @@ -162,8 +164,6 @@ export default function DisplayWeb3Statement({ const secrets = credentialStatement.statement.filter( (s) => s.type !== StatementTypes.RevealAttribute ) as SecretStatementV2[]; - const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); - const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); const onChange = useCallback((credential: VerifiableCredential) => { @@ -178,14 +178,7 @@ export default function DisplayWeb3Statement({ } }, []); - const schema = useMemo(() => { - if (!verifiableCredentialSchemas.loading && chosenCredential) { - const schemaId = chosenCredential.credentialSchema.id; - return verifiableCredentialSchemas.value[schemaId]; - } - return null; - }, [chosenCredential?.id, verifiableCredentialSchemas.loading]); - + const schema = useCredentialSchema(chosenCredential); const metadata = useCredentialMetadata(chosenCredential); const localization = useCredentialLocalization(chosenCredential); diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 10241c4e..b208e731 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -345,3 +345,5 @@ export interface VerifiableCredentialSchema { properties: SchemaProperties; required: string[]; } + +export type VerifiableCredentialSchemaWithFallback = VerifiableCredentialSchema & { usingFallback: boolean }; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 61a0146b..854e0f1c 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -42,7 +42,7 @@ export function getPublicKeyfromPublicKeyIdentifierDID(did: string) { } /** - * Extracts the credential registry contract addres from a verifiable credential id (did). + * Extracts the credential registry contract address from a verifiable credential id (did). * @param credentialId the did for a credential * @returns the contract address of the issuing contract of the provided credential id */ From 96d4aa3773dbf16d1eb5c3c8662648aa773c7906 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 10:57:05 +0200 Subject: [PATCH 168/231] Update schema validation for credential schema --- packages/browser-wallet/CHANGELOG.md | 4 ++++ packages/browser-wallet/src/shared/storage/types.ts | 5 ++++- .../src/shared/utils/verifiable-credential-helpers.ts | 6 ++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..d1505b5b 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- Adjusted the schema validation for credential schemas to no longer require title and description. The type is now required to be 'object'. + ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 10241c4e..763d29dc 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -319,9 +319,12 @@ export type TimestampProperty = { }; type CredentialSchemaAttributes = { + title?: string; + description?: string; + type: 'object'; properties: Record; required: string[]; -} & CredentialSchemaProperty; +}; interface CredentialSchemaSubject { type: string; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 61a0146b..aea28bb6 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -377,9 +377,6 @@ const verifiableCredentialSchemaSchema = { description: { type: 'string', }, - format: { - type: 'string', - }, properties: { additionalProperties: { anyOf: [ @@ -419,10 +416,11 @@ const verifiableCredentialSchemaSchema = { type: 'string', }, type: { + const: 'object', type: 'string', }, }, - required: ['description', 'properties', 'required', 'title', 'type'], + required: ['type', 'properties', 'required'], type: 'object', }, id: { From 8581aa91bd0e696b2234196b7d85f49ece26a973 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 13:04:38 +0200 Subject: [PATCH 169/231] Find correct next index for verifiable credentials --- packages/browser-wallet/CHANGELOG.md | 1 + .../AddWeb3IdCredential.tsx | 58 +++++++++++++++---- .../pages/AddWeb3IdCredential/i18n/da.ts | 1 + .../pages/AddWeb3IdCredential/i18n/en.ts | 1 + .../utils/verifiable-credential-helpers.ts | 25 ++++++++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..ab1e54cb 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. +- The wallet now ensures that the verifiable credential index used when adding a credential has not already been used in the contract. ## 1.1.3 diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 1db85b8c..de91131a 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -1,5 +1,5 @@ import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -23,12 +23,15 @@ import { fetchCredentialSchema, fetchLocalization, getCredentialRegistryContractAddress, + isVerifiableCredentialInContract, } from '@shared/utils/verifiable-credential-helpers'; import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; -import { networkConfigurationAtom } from '@popup/store/settings'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { parse } from '@shared/utils/payload-helpers'; import { logError } from '@shared/utils/log-helpers'; +import { ConcordiumHdWallet, ContractAddress } from '@concordium/web-sdk'; +import { addToastAtom } from '@popup/state'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; type Props = { @@ -61,8 +64,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom); const wallet = useHdWallet(); const network = useAtomValue(networkConfigurationAtom); + const client = useAtomValue(grpcClientAtom); const [error, setError] = useState(); + const addToast = useSetAtom(addToastAtom); const [validationComplete, setValidationComplete] = useState(false); const { credential: rawCredential, url, metadataUrl } = state.payload; @@ -167,27 +172,56 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { [metadata, i18n] ); + async function findNextUnusedVerifiableCredentialIndex( + localIndex: number, + issuer: ContractAddress, + hdWallet: ConcordiumHdWallet + ) { + let index = localIndex; + let credentialAlreadyExists = true; + + do { + const credentialHolderId = hdWallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); + credentialAlreadyExists = await isVerifiableCredentialInContract(client, credentialHolderId, issuer); + if (credentialAlreadyExists) { + index += 1; + } + } while (credentialAlreadyExists); + + return index; + } + async function addCredential(credentialSchema: VerifiableCredentialSchema) { if (!wallet) { throw new Error('Wallet is unexpectedly missing'); } - const schemaUrl = credential.credentialSchema.id; - if (!Object.keys(schemas.value).includes(schemaUrl)) { - const updatedSchemas = { ...schemas.value }; - updatedSchemas[schemaUrl] = credentialSchema; - setSchemas(updatedSchemas); - } - // Find the next unused index - // TODO verify index is unused on chain? + // Find the next unused verifiable credential index, based on what we have stored locally. const index = [...web3IdCredentials.value, ...storedWeb3IdCredentials.value].reduce( (best, cred) => (cred.issuer === credential.issuer ? Math.max(cred.index + 1, best) : best), 0 ); const issuer = getCredentialRegistryContractAddress(credential.issuer); + // Check if the index has already been used in the contract on-chain, and use that to find + // the next unused index based on that. + let nextUnusedIndex: number; + try { + nextUnusedIndex = await findNextUnusedVerifiableCredentialIndex(index, issuer, wallet); + } catch (e) { + addToast(t('error.findingNextIndex')); + logError(e); + throw e; + } + + const schemaUrl = credential.credentialSchema.id; + if (!Object.keys(schemas.value).includes(schemaUrl)) { + const updatedSchemas = { ...schemas.value }; + updatedSchemas[schemaUrl] = credentialSchema; + setSchemas(updatedSchemas); + } - const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); + const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, nextUnusedIndex).toString('hex'); const credentialSubjectId = createPublicKeyIdentifier(credentialHolderId, network); const credentialSubject = { ...credential.credentialSubject, id: credentialSubjectId }; const credentialId = createCredentialId(credentialHolderId, issuer, network); @@ -196,7 +230,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { ...credential, credentialSubject, id: credentialId, - index, + index: nextUnusedIndex, metadataUrl: metadataUrl.url, }; await setWeb3IdCredentials([...web3IdCredentials.value, fullCredential]); diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts index f7245542..a16fcbc2 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/da.ts @@ -12,6 +12,7 @@ const t: typeof en = { schema: en.error.schema, attribute: en.error.attribute, localization: en.error.localization, + findingNextIndex: en.error.findingNextIndex, }, }; diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts index c1dbe60f..30dc4c5d 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/i18n/en.ts @@ -13,6 +13,7 @@ const t = { 'The attribute with key [{{ credentialAttribute }}] is not available in the list of schema attributes: [{{ schemaAttributes }}]', }, localization: 'Failed to get localization', + findingNextIndex: 'An error ocurred while attempting to add the credential. Please try again.', }, }; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 61a0146b..2792e505 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -232,6 +232,31 @@ export async function getRevokeTransactionExecutionEnergyEstimate( return applyExecutionNRGBuffer(invokeResult.usedEnergy); } +/** + * Determines whether a verifiable credential with the provided key already + * exists in the provided contract. + * + * This is done by invoking the credentialStatus method and checking that the + * call succeeded. + */ +export async function isVerifiableCredentialInContract( + client: ConcordiumGRPCClient, + credentialHolderId: string, + issuer: ContractAddress +) { + const instanceInfo = await client.getInstanceInfo(issuer); + if (instanceInfo === undefined) { + throw new Error('Given contract address was not a created instance'); + } + const result = await client.invokeContract({ + contract: issuer, + method: `${getContractName(instanceInfo)}.credentialStatus`, + parameter: Buffer.from(credentialHolderId, 'hex'), + }); + + return result.tag === 'success'; +} + /** * Get the status of a verifiable credential in a CIS-4 contract. * @param client the GRPC client for accessing a node From 7501435143bccd29f176916e34e8d15464e9c781 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 13:08:52 +0200 Subject: [PATCH 170/231] Re-activate button on error --- .../popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index de91131a..f781b84d 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -280,7 +280,9 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { onClick={() => { if (schema) { setAcceptButtonDisabled(true); - addCredential(schema).then(withClose(onAllow)); + addCredential(schema) + .then(withClose(onAllow)) + .catch(() => setAcceptButtonDisabled(false)); } }} > From d238334ad93e99f33b95230e9894bea5cd8e31cb Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 30 Aug 2023 13:13:03 +0200 Subject: [PATCH 171/231] More proof UI --- .../Web3ProofRequest/CredentialSelector.tsx | 19 +++- .../Web3ProofRequest/Display/DisplayBox.tsx | 2 +- .../VerifiableCredentialStatement.tsx | 26 +++--- .../Web3ProofRequest/Web3ProofRequest.scss | 90 ++++++++++++++++++- .../Web3ProofRequest/Web3ProofRequest.tsx | 27 ++++-- .../popup/pages/Web3ProofRequest/i18n/en.ts | 6 ++ .../src/popup/shared/Modal/Modal.tsx | 7 +- .../src/popup/styles/config/_typography.scss | 14 --- 8 files changed, 149 insertions(+), 42 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index 65922f0b..4ba17e8a 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -1,12 +1,14 @@ import Button from '@popup/shared/Button'; import Modal from '@popup/shared/Modal'; import React, { ComponentType, useState } from 'react'; +import BackIcon from '@assets/svg/back-arrow.svg'; interface Props { options: T[]; initialIndex?: number; onChange: (x: T) => void; DisplayOption: ComponentType<{ option: T }>; + header: string; } /** @@ -17,6 +19,7 @@ export default function CredentialSelector({ initialIndex = 0, onChange, DisplayOption, + header, }: Props) { const [chosenIndex, setChosenIndex] = useState(initialIndex); const [open, setOpen] = useState(false); @@ -28,21 +31,33 @@ export default function CredentialSelector({ function onClick(index: number) { setChosenIndex(index); onChange(options[index]); + setOpen(false); } return ( setOpen(true)} onClose={() => setOpen(false)} trigger={ - } + className="p-0" > +
    +

    {header}

    + +
    {options.map((opt, index) => ( - ))} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx index 7464ecaa..620e82da 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx @@ -29,7 +29,7 @@ export function DisplayBox({ className, children, header, infoBox }: DisplayBoxP } > {infoBox} -
    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 2ec49f35..97358c2d 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -1,21 +1,16 @@ -import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { RevealStatementV2, StatementTypes, VerifiableCredentialStatement } from '@concordium/web-sdk'; import Img from '@popup/shared/Img'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; -import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; +import { VerifiableCredential } from '@shared/storage/types'; import { useAtomValue } from 'jotai'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; +import { useTranslation } from 'react-i18next'; import { useCredentialLocalization, useCredentialMetadata } from '../VerifiableCredential/VerifiableCredentialHooks'; import CredentialSelector from './CredentialSelector'; import { DisplayRevealStatements } from './Display/DisplayRevealStatements'; import { DisplaySecretStatements } from './Display/DisplaySecretStatements'; import { createWeb3IdDIDFromCredential, DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; -function Logo({ logo }: { logo: MetadataUrl }) { - return ; -} - export function DisplayVC({ option }: { option: VerifiableCredential }) { const metadata = useCredentialMetadata(option); @@ -25,8 +20,10 @@ export function DisplayVC({ option }: { option: VerifiableCredential }) { return (
    - -
    {metadata.title}
    +
    + +
    +
    {metadata.title}
    ); } @@ -38,6 +35,7 @@ export default function DisplayWeb3Statement({ setChosenId, net, }: DisplayCredentialStatementProps) { + const { t } = useTranslation('web3IdProofRequest'); const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute ) as RevealStatementV2[]; @@ -75,19 +73,15 @@ export default function DisplayWeb3Statement({ return null; } + // TODO translate selector header return (
    - +

    {t('descriptions.verifiableCredential')}

    options={validCredentials} DisplayOption={DisplayVC} onChange={onChange} + header="Select verifiable credential" /> {reveals.length !== 0 && ( - - + {currentStatementIndex > 0 && ( + + )} {currentStatementIndex === statements.length - 1 ? ( ) : ( )} - +
    ); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index e1c6a3b4..e37b1e6f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -2,6 +2,7 @@ export default { header: '{{dappName}} requests the following information about you:', accept: 'Accept', continue: 'Continue', + back: 'Back', reject: 'Reject', displayStatement: { requirementsMet: 'You meet this requirement', @@ -32,6 +33,11 @@ export default { missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', }, }, + descriptions: { + verifiableCredential: 'Select a verifiable credential to reveal/prove the requested information.', + accountCredential: + 'Select an account associated with the identity whose credentials will be used to reveal/prove the requested information.', + }, failedProof: 'Unable to create proof', failedProofReason: 'Unable to create proof due to: {{ reason }}', unableToProve: diff --git a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx index 5d172680..17323363 100644 --- a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx +++ b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx @@ -51,6 +51,10 @@ export type ModalProps = { onOpen?(): void; onClose?(): void; bottom?: boolean; + /** + * Used to overwrite styling for the modal content box + */ + className?: string; }; /** @@ -64,6 +68,7 @@ export type ModalProps = { */ export default function Modal({ trigger, + className, disableClose = false, open: isOpenOverride, error = false, @@ -143,7 +148,7 @@ export default function Modal({ {!isExiting && ( Date: Wed, 30 Aug 2023 13:38:32 +0200 Subject: [PATCH 172/231] Verify id statements when checking for valid proofs --- packages/browser-wallet/CHANGELOG.md | 1 + packages/browser-wallet/src/background/web3Id.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..48caa062 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. +- Enabled ID statement checks for Web3 ID proof requests containing account credential statements. ## 1.1.3 diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index a3dcbd3a..2713743b 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -8,6 +8,8 @@ import { verifyAtomicStatements, isAccountCredentialStatement, IDENTITY_SUBJECT_SCHEMA, + verifyIdstatement, + IdStatement, } from '@concordium/web-sdk'; import { sessionVerifiableCredentials, @@ -178,7 +180,8 @@ export const runIfValidWeb3IdProof: RunCondition // If a statement does not verify, an error is thrown. statements.every((credStatement) => isAccountCredentialStatement(credStatement) - ? verifyAtomicStatements(credStatement.statement, IDENTITY_SUBJECT_SCHEMA) + ? verifyAtomicStatements(credStatement.statement, IDENTITY_SUBJECT_SCHEMA) && + verifyIdstatement(credStatement.statement as IdStatement) : verifyAtomicStatements(credStatement.statement) ); From 990d641e59b51701a09bd24f384388cce3a92a92 Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 30 Aug 2023 13:14:38 +0200 Subject: [PATCH 173/231] Add new styling --- packages/browser-wallet/src/popup/index.scss | 6 + .../Web3ProofRequest/Web3ProofRequest.scss | 3 +- .../src/popup/styles-new/config/_bundle.scss | 2 + .../src/popup/styles-new/config/_colors.scss | 56 +++++++ .../popup/styles-new/config/_typography.scss | 146 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 packages/browser-wallet/src/popup/styles-new/config/_bundle.scss create mode 100644 packages/browser-wallet/src/popup/styles-new/config/_colors.scss create mode 100644 packages/browser-wallet/src/popup/styles-new/config/_typography.scss diff --git a/packages/browser-wallet/src/popup/index.scss b/packages/browser-wallet/src/popup/index.scss index 694da9a1..27323526 100644 --- a/packages/browser-wallet/src/popup/index.scss +++ b/packages/browser-wallet/src/popup/index.scss @@ -1,3 +1,9 @@ +// New styling + +@import 'styles-new/config/bundle'; + +// Old styling + @import 'styles/config/bundle'; @import 'styles/elements/bundle'; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 0075b3df..8cfdb182 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -18,8 +18,9 @@ &__credential-statement-container { height: calc(100% - 115px); - overflow-y: overlay; margin-bottom: rem(20px); + scrollbar-gutter: stable; + margin-right: 16px; } &__selector-icon { diff --git a/packages/browser-wallet/src/popup/styles-new/config/_bundle.scss b/packages/browser-wallet/src/popup/styles-new/config/_bundle.scss new file mode 100644 index 00000000..69708e5b --- /dev/null +++ b/packages/browser-wallet/src/popup/styles-new/config/_bundle.scss @@ -0,0 +1,2 @@ +@import 'colors'; +@import 'typography'; diff --git a/packages/browser-wallet/src/popup/styles-new/config/_colors.scss b/packages/browser-wallet/src/popup/styles-new/config/_colors.scss new file mode 100644 index 00000000..66f9e78a --- /dev/null +++ b/packages/browser-wallet/src/popup/styles-new/config/_colors.scss @@ -0,0 +1,56 @@ +// Color definitions + +// Primary Colors +$color-primary-mineral-blue: #48a2ae; +$color-primary-mineral-80: #6db5be; +$color-primary-mineral-60: #91c7ce; +$color-primary-mineral-40: #b6dadf; +$color-primary-mineral-20: #daecef; +$color-primary-mineral-10: #ecf6f7; +$color-primary-mineral-05: #f6fafb; + +// Secondary Colors +$color-secondary-dark-blue: #052535; +$color-secondary-ocean-140: #013243; +$color-secondary-ocean-120: #01475f; +$color-secondary-ocean-blue: #005a78; +$color-secondary-ocean-80: #337b93; +$color-secondary-ocean-60: #7facbb; +$color-secondary-egg-shell-white: #fffde4; +$color-secondary-off-white: #ebf0f0; + +// Neutral Colors +$color-neutral-black: #000; +$color-neutral-gray-90: #191919; +$color-neutral-gray-80: #333; +$color-neutral-gray-70: #4c4c4c; +$color-neutral-gray-60: #666; +$color-neutral-gray-50: #7f7f7f; +$color-neutral-gray-40: #999; +$color-neutral-gray-30: #b2b2b2; +$color-neutral-gray-20: #ccc; +$color-neutral-gray-10: #e5e5e5; +$color-neutral-gray-05: #f2f2f2; +$color-neutral-white: #fff; + +// State Colors +$color-feedback-negative-dark: #ab2b2b; +$color-feedback-negative-base: #dc5050; +$color-feedback-negative-light: #e87e90; +$color-feedback-positive-dark: #189e46; +$color-feedback-positive-base: #33c364; +$color-feedback-positive-light: #8be7aa; +$color-feedback-warning-dark: #c89e0a; +$color-feedback-warning-base: #fbcd29; +$color-feedback-warning-light: #f6db9a; +$color-feedback-info-dark: #075cab; +$color-feedback-info-base: #2485df; +$color-feedback-info-light: #65a4dd; +$color-feedback-help-dark: #53198e; +$color-feedback-help-base: #7939ba; +$color-feedback-help-light: #b37cdf; + +// Grdients +$gradient-dark-mineral-blue-bg: linear-gradient(144deg, #005a78 0%, #2e8894 100%); +$gradient-dark-mineral-blue-button: linear-gradient(161deg, #005a78 0%, #48a2ae 100%); +$gradient-disable-button: linear-gradient(170deg, #ccc 0%, #e5e5e5 100%); diff --git a/packages/browser-wallet/src/popup/styles-new/config/_typography.scss b/packages/browser-wallet/src/popup/styles-new/config/_typography.scss new file mode 100644 index 00000000..3406c129 --- /dev/null +++ b/packages/browser-wallet/src/popup/styles-new/config/_typography.scss @@ -0,0 +1,146 @@ +@use 'sass:map'; +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;700&display=swap'); + +$t-font-family-ibm: 'IBM Plex Sans', sans-serif; +$t-font-weight-light: 300; +$t-font-weight-regular: 400; +$t-font-weight-medium: 500; +$t-font-weight-semi-bold: 600; +$t-font-weight-bold: 700; + +$s-type-config: ( + display: ( + font-weight: $t-font-weight-bold, + styles: ( + display1: ( + font-size: 40px, + line-height: 47px, + ), + display2: ( + font-size: 32px, + line-height: 36px, + ), + display3: ( + font-size: 25px, + line-height: 30px, + ), + display4: ( + font-size: 20px, + line-height: 24px, + ), + display5: ( + font-size: 16px, + line-height: 19px, + ), + display6: ( + font-size: 14px, + line-height: 17px, + ), + ), + ), + heading: ( + font-weight: $t-font-weight-medium, + styles: ( + heading1: ( + font-size: 28px, + line-height: 36px, + ), + heading2: ( + font-size: 24px, + line-height: 32px, + ), + heading3: ( + font-size: 20px, + line-height: 28px, + ), + heading4: ( + font-size: 18px, + line-height: 26px, + ), + heading5: ( + font-size: 16px, + line-height: 20px, + ), + heading6: ( + font-size: 14px, + line-height: 18px, + ), + heading7: ( + font-size: 12px, + line-height: 16px, + ), + ), + ), + body: ( + font-weight: $t-font-weight-regular, + styles: ( + bodyXL: ( + font-size: 20px, + line-height: 26px, + ), + bodyL: ( + font-size: 16px, + line-height: 20px, + ), + bodyM: ( + font-size: 14px, + line-height: 18px, + ), + bodyS: ( + font-size: 12px, + line-height: 16px, + ), + bodyXS: ( + font-size: 11px, + line-height: 14px, + ), + ), + ), + bodyLight: ( + font-weight: $t-font-weight-light, + styles: ( + bodyLightXL: ( + font-size: 20px, + line-height: 26px, + ), + bodyLightL: ( + font-size: 16px, + line-height: 20px, + ), + bodyLightM: ( + font-size: 14px, + line-height: 18px, + ), + bodyLightS: ( + font-size: 12px, + line-height: 16px, + ), + bodyLightXS: ( + font-size: 11px, + line-height: 14px, + ), + ), + ), + button: ( + font-weight: $t-font-weight-semi-bold, + styles: ( + button: ( + font-size: 14px, + line-height: 20px, + ), + ), + ), +); + +@each $cat-name, $cat-content in $s-type-config { + $style-weight: map.get($cat-content, 'font-weight'); + @each $style-name, $style-content in map.get($cat-content, 'styles') { + .#{$style-name} { + font-weight: $style-weight; + font-family: $t-font-family-ibm; + @each $key, $value in $style-content { + #{$key}: #{$value}; + } + } + } +} From 4556444767eb8920da8987ea0cda43cabfd5adef Mon Sep 17 00:00:00 2001 From: Hjort Date: Wed, 30 Aug 2023 13:41:40 +0200 Subject: [PATCH 174/231] Improve add-web3Id example --- examples/add-example-Web3Id/index.html | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index e741778d..144bfc74 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -43,10 +43,17 @@ b .revealAttribute('degreeType') .revealAttribute('degreeName') - .addMembership('graduationDate', [ - graduationDate.valueAsDate, - new Date('2023-08-28T00:00:00.000Z'), - ]) + .revealAttribute('graduationDate') + ) + .addForVerifiableCredentials([{ index: 6105n, subindex: 0n }], (b) => + b + .addNonMembership('degreeType', ['test', 'test2']) + .addMembership('degreeName', ['Bachelor of Science and Arts', 'Bachelor of Finance']) + .addRange( + 'graduationDate', + new Date('1900-08-28T00:00:00.000Z'), + new Date('2030-08-28T00:00:00.000Z') + ) ) .getStatements(); @@ -100,7 +107,7 @@ attributes: values, }, credentialSchema: { - id: 'https://gist.githubusercontent.com/shjortConcordium/a2dc69761d2007c308f6511abaa3eb70/raw/11ad6745dcfa57e7049b08be146858a928a7aa82/gistfile1.txt', + id: web3Schema.value, type: 'JsonSchema2023', }, }, @@ -162,6 +169,13 @@

    Account address:

    value="https://raw.githubusercontent.com/Concordium/concordium-web3id/credential-metadata-example/examples/json-schemas/metadata/credential-metadata.json" />
    + Web3Id Schema: + +
    issuer Index:

    Attribute values:

    From 44332a1748c30758de8b15cca0d0003893c0d3ad Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 15:04:23 +0200 Subject: [PATCH 175/231] Validate attributes based on their types --- packages/browser-wallet/CHANGELOG.md | 4 + .../browser-wallet/src/background/web3Id.ts | 74 +++++++++++-------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..94d29159 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- The wallet now validates verifiable credential attributes based on their type. String attributes can at most be 31 bytes (UTF-8), integer attributes must fit in a u64 and Date attributes must be between -262144-01-01T00:00:00> and +262143-12-31T23:59:59.999999999Z'. + ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index a3dcbd3a..94599ea8 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -35,6 +35,7 @@ import { } from '@concordium/browser-wallet-message-hub'; import { getNet } from '@shared/utils/network-helpers'; import { WAIT_FOR_CLOSED_POPUP_ITERATIONS, WAIT_FOR_CLOSED_POPUP_TIMEOUT_MS } from '@shared/constants/web3id'; +import { Buffer } from 'buffer/'; import { openWindow, RunCondition, testPopupOpen } from './window-management'; import bgMessageHandler from './message-handler'; @@ -42,6 +43,12 @@ const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; const MISSING_CREDENTIAL_PROOF = 'No credential proof given'; +const MAX_U64 = 2n ** 64n; +const MIN_DATE_ISO = '-262144-01-01T00:00:00Z'; +const MAX_DATE_ISO = '+262143-12-31T23:59:59.999999999Z'; +const MIN_DATE_TIMESTAMP = Date.parse(MIN_DATE_ISO); +const MAX_DATE_TIMESTAMP = Date.parse(MAX_DATE_ISO); + export async function web3IdAddCredentialFinishHandler(input: { credentialHolderIdDID: string; proof: CredentialProof; @@ -108,6 +115,13 @@ export async function web3IdAddCredentialFinishHandler(input: { ); } +function rejectRequest(message: string): { run: false; response: MessageStatusWrapper } { + return { + run: false, + response: { success: false, message }, + }; +} + /** * Run condition which ensures that the web3IdCredential request is valid. */ @@ -126,28 +140,40 @@ export const runIfValidWeb3IdCredentialRequest: RunCondition 31) { + return rejectRequest( + `The attribute [${attributeValue}] for key [${attributeKey}] is greater than 31 bytes.` + ); + } + if (typeof attributeValue === 'bigint' && (attributeValue < 0 || attributeValue > MAX_U64)) { + return rejectRequest( + `The attribute [${attributeValue}] for key [${attributeKey}] is out of bounds for a u64 integer.` + ); + } + if ( + attributeValue instanceof Date && + (attributeValue.getTime() < MIN_DATE_TIMESTAMP || attributeValue.getTime() > MAX_DATE_TIMESTAMP) + ) { + return rejectRequest( + `The attribute [${attributeValue}] for key [${attributeKey}] is out of bounds for a Date. The Date must be between ${MIN_DATE_ISO} and ${MAX_DATE_ISO}` + ); + } + } + return { run: true }; } catch (e) { - return { - run: false, - response: { success: false, message: `Credential is not well-formed: ${(e as Error).message}` }, - }; + return rejectRequest(`Credential is not well-formed: ${(e as Error).message}`); } }; @@ -168,10 +194,7 @@ export const createWeb3IdProofHandler: ExtensionMessageHandler = (msg, _sender, export const runIfValidWeb3IdProof: RunCondition> = async (msg) => { if (!isHex(msg.payload.challenge)) { - return { - run: false, - response: { success: false, message: `Challenge is invalid, it should be a HEX encoded string` }, - }; + return rejectRequest(`Challenge is invalid, it should be a HEX encoded string`); } try { const statements: CredentialStatements = parse(msg.payload.statements); @@ -184,20 +207,11 @@ export const runIfValidWeb3IdProof: RunCondition const noEmptyQualifier = statements.every((credStatement) => credStatement.idQualifier.issuers.length > 0); if (!noEmptyQualifier) { - return { - run: false, - response: { - success: false, - message: `Statements must have at least 1 possible identity provider / issuer`, - }, - }; + return rejectRequest(`Statements must have at least 1 possible identity provider / issuer`); } return { run: true }; } catch (e) { - return { - run: false, - response: { success: false, message: `Statement is not well-formed: ${(e as Error).message}` }, - }; + return rejectRequest(`Statement is not well-formed: ${(e as Error).message}`); } }; From e5890f3ee065317ea1abf1f8b8f142a389c2ffa7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 16:17:55 +0200 Subject: [PATCH 176/231] Throw error in replacer if date is invalid --- packages/browser-wallet-api/src/util.ts | 3 +++ packages/browser-wallet/CHANGELOG.md | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/browser-wallet-api/src/util.ts b/packages/browser-wallet-api/src/util.ts index a8555728..eddc629c 100644 --- a/packages/browser-wallet-api/src/util.ts +++ b/packages/browser-wallet-api/src/util.ts @@ -32,6 +32,9 @@ function replacer(this: any, k: string, value: any) { } const rawValue = this[k]; if (rawValue instanceof Date) { + if (Number.isNaN(rawValue.getTime())) { + throw new Error(`Received a Date instance that was an invalid Date. Raw value was: [${rawValue}]`); + } return { '@type': serializationTypes.Date, value }; } if (Buffer.isBuffer(rawValue)) { diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index b72c5700..5b433f80 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - An issue where changing the credential metadata URL to an invalid URL, or a URL that does not contain a credential metadata file, would result in an empty screen. +- An issue where an invalid Date would result in the epoch timestamp instead of returning an error. ## 1.1.3 From 31757fdf838dd15db1bff6ce6bfe1295c6c9201c Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 10:14:46 +0200 Subject: [PATCH 177/231] Refactor according to review comments --- .../VerifiableCredentialCard.tsx | 17 ++++++----------- .../VerifiableCredentialDetails.tsx | 7 ++----- .../VerifiableCredentialHooks.tsx | 10 +++------- .../VerifiableCredentialList.tsx | 8 ++------ .../browser-wallet/src/shared/storage/types.ts | 2 -- .../utils/verifiable-credential-helpers.ts | 2 ++ 6 files changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 86af2b23..36276339 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -1,15 +1,13 @@ import React, { PropsWithChildren } from 'react'; import { ClassName } from 'wallet-common-helpers'; import clsx from 'clsx'; -import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers'; -import Img from '@popup/shared/Img'; -import { AttributeType, CredentialSubject } from '@concordium/web-sdk'; import { - VerifiableCredentialStatus, - MetadataUrl, - VerifiableCredentialSchema, + VerifiableCredentialMetadata, VerifiableCredentialSchemaWithFallback, -} from '@shared/storage/types'; +} from '@shared/utils/verifiable-credential-helpers'; +import Img from '@popup/shared/Img'; +import { AttributeType, CredentialSubject } from '@concordium/web-sdk'; +import { VerifiableCredentialStatus, MetadataUrl, VerifiableCredentialSchema } from '@shared/storage/types'; import { useTranslation } from 'react-i18next'; import StatusIcon from './VerifiableCredentialStatus'; @@ -110,10 +108,7 @@ function applySchemaAndLocalization( ): (value: [string, AttributeType]) => { title: string; key: string; value: AttributeType } { return (value: [string, AttributeType]) => { const attributeSchema = schema.properties.credentialSubject.properties.attributes.properties[value[0]]; - let title = value[0]; - if (attributeSchema) { - title = attributeSchema.title; - } + let title = attributeSchema ? attributeSchema.title : value[0]; if (localization) { const localizedTitle = localization[value[0]]; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index f255da1b..c7bdee68 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -1,10 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useAtomValue } from 'jotai'; -import { - VerifiableCredential, - VerifiableCredentialSchemaWithFallback, - VerifiableCredentialStatus, -} from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; import { AccountTransactionType } from '@concordium/web-sdk'; @@ -21,6 +17,7 @@ import { getCredentialRegistryContractAddress, getRevokeTransactionExecutionEnergyEstimate, getContractAddressFromIssuerDID, + VerifiableCredentialSchemaWithFallback, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; import { TimeStampUnit, dateFromTimestamp, ClassName } from 'wallet-common-helpers'; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 3f97bdaf..753a0a13 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,10 +1,5 @@ import { grpcClientAtom } from '@popup/store/settings'; -import { - VerifiableCredential, - VerifiableCredentialStatus, - VerifiableCredentialSchema, - VerifiableCredentialSchemaWithFallback, -} from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { CredentialQueryResponse, IssuerMetadata, @@ -16,6 +11,7 @@ import { getCredentialRegistryMetadata, getVerifiableCredentialEntry, getVerifiableCredentialStatus, + VerifiableCredentialSchemaWithFallback, } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; @@ -60,7 +56,7 @@ export function useCredentialStatus(credential: VerifiableCredential) { export function useCredentialSchema( credential?: VerifiableCredential ): VerifiableCredentialSchemaWithFallback | undefined { - const [schema, setSchema] = useState(); + const [schema, setSchema] = useState(); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const client = useAtomValue(grpcClientAtom); diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 74cd26ca..57f640c3 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -7,14 +7,10 @@ import { import { useAtomValue, useAtom } from 'jotai'; import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; -import { - VerifiableCredential, - VerifiableCredentialSchema, - VerifiableCredentialSchemaWithFallback, - VerifiableCredentialStatus, -} from '@shared/storage/types'; +import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types'; import { VerifiableCredentialMetadata, + VerifiableCredentialSchemaWithFallback, getChangesToCredentialMetadata, getChangesToCredentialSchemas, } from '@shared/utils/verifiable-credential-helpers'; diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index b208e731..10241c4e 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -345,5 +345,3 @@ export interface VerifiableCredentialSchema { properties: SchemaProperties; required: string[]; } - -export type VerifiableCredentialSchemaWithFallback = VerifiableCredentialSchema & { usingFallback: boolean }; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 854e0f1c..3c07ef5d 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -13,6 +13,8 @@ import { applyExecutionNRGBuffer, getContractName } from './contract-helpers'; import { getNet } from './network-helpers'; import { logError } from './log-helpers'; +export type VerifiableCredentialSchemaWithFallback = VerifiableCredentialSchema & { usingFallback: boolean }; + /** * Extracts the credential holder id from a verifiable credential id (did). * @param credentialId the did for a credential From f794a5cfec64fc8e0df4acd9f45ae4ab87a5d553 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 10:24:41 +0200 Subject: [PATCH 178/231] Moved method out of the component --- .../AddWeb3IdCredential.tsx | 24 +----------- .../utils/verifiable-credential-helpers.ts | 38 ++++++++++++++++++- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index f781b84d..7eeb9889 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -22,15 +22,14 @@ import { fetchCredentialMetadata, fetchCredentialSchema, fetchLocalization, + findNextUnusedVerifiableCredentialIndex, getCredentialRegistryContractAddress, - isVerifiableCredentialInContract, } from '@shared/utils/verifiable-credential-helpers'; import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types'; import { parse } from '@shared/utils/payload-helpers'; import { logError } from '@shared/utils/log-helpers'; -import { ConcordiumHdWallet, ContractAddress } from '@concordium/web-sdk'; import { addToastAtom } from '@popup/state'; import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard'; @@ -172,25 +171,6 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { [metadata, i18n] ); - async function findNextUnusedVerifiableCredentialIndex( - localIndex: number, - issuer: ContractAddress, - hdWallet: ConcordiumHdWallet - ) { - let index = localIndex; - let credentialAlreadyExists = true; - - do { - const credentialHolderId = hdWallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); - credentialAlreadyExists = await isVerifiableCredentialInContract(client, credentialHolderId, issuer); - if (credentialAlreadyExists) { - index += 1; - } - } while (credentialAlreadyExists); - - return index; - } - async function addCredential(credentialSchema: VerifiableCredentialSchema) { if (!wallet) { throw new Error('Wallet is unexpectedly missing'); @@ -207,7 +187,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { // the next unused index based on that. let nextUnusedIndex: number; try { - nextUnusedIndex = await findNextUnusedVerifiableCredentialIndex(index, issuer, wallet); + nextUnusedIndex = await findNextUnusedVerifiableCredentialIndex(client, index, issuer, wallet); } catch (e) { addToast(t('error.findingNextIndex')); logError(e); diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 2792e505..f2851d87 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,4 +1,11 @@ -import { CcdAmount, UpdateContractPayload, ConcordiumGRPCClient, ContractAddress, sha256 } from '@concordium/web-sdk'; +import { + CcdAmount, + UpdateContractPayload, + ConcordiumGRPCClient, + ContractAddress, + sha256, + ConcordiumHdWallet, +} from '@concordium/web-sdk'; import * as ed from '@noble/ed25519'; import { MetadataUrl, @@ -1159,3 +1166,32 @@ export function getCredentialIdFromSubjectDID(did: string) { } return split[split.length - 1]; } + +/** + * Finds the next unused verifiable credential index, i.e. the next index that has not + * been used as a credential holder id in the provided issuer contract. + * @param client the GRPC client for accessing a node + * @param localIndex the currently best guess on the next index, based on what is currently stored locally + * @param issuer the issuer credential registry contract + * @param hdWallet the key derivation wallet + * @returns the next unused verifiable credential index + */ +export async function findNextUnusedVerifiableCredentialIndex( + client: ConcordiumGRPCClient, + localIndex: number, + issuer: ContractAddress, + hdWallet: ConcordiumHdWallet +): Promise { + let index = localIndex; + let credentialAlreadyExists = true; + + do { + const credentialHolderId = hdWallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex'); + credentialAlreadyExists = await isVerifiableCredentialInContract(client, credentialHolderId, issuer); + if (credentialAlreadyExists) { + index += 1; + } + } while (credentialAlreadyExists); + + return index; +} From 352c9266af1d4f9506eb6a39d7610595657afb4d Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 10:38:04 +0200 Subject: [PATCH 179/231] Fix off by one issue --- packages/browser-wallet/src/background/web3Id.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 94599ea8..e3548752 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -43,7 +43,7 @@ const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id'; const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given'; const MISSING_CREDENTIAL_PROOF = 'No credential proof given'; -const MAX_U64 = 2n ** 64n; +const MAX_U64 = 2n ** 64n - 1n; const MIN_DATE_ISO = '-262144-01-01T00:00:00Z'; const MAX_DATE_ISO = '+262143-12-31T23:59:59.999999999Z'; const MIN_DATE_TIMESTAMP = Date.parse(MIN_DATE_ISO); @@ -148,9 +148,7 @@ export const runIfValidWeb3IdCredentialRequest: RunCondition 31) { return rejectRequest( `The attribute [${attributeValue}] for key [${attributeKey}] is greater than 31 bytes.` From 8d2d3fc86d9d71abce368445b522c86410217553 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 10:52:44 +0200 Subject: [PATCH 180/231] Bump version --- packages/browser-wallet/CHANGELOG.md | 2 +- packages/browser-wallet/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 4043326f..0ede960c 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.1.4 ### Added diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e2384529..eca8bd5e 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.3", + "version": "1.1.4", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 2c84262a67bfacf67aa9a5b30a8bb9cce39776ab Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 31 Aug 2023 08:55:32 +0200 Subject: [PATCH 181/231] More UI --- .../src/assets/svg/warning-triangle.svg | 5 + .../ExternalRequestLayout.scss | 1 - .../ExternalRequestLayout.tsx | 7 +- .../popup/page-layouts/MainLayout/i18n/en.ts | 2 +- .../AddWeb3IdCredential.tsx | 2 +- .../ConnectAccountsRequest.tsx | 4 +- .../ConnectionRequest/ConnectionRequest.tsx | 2 +- .../ExternalAddTokens/ExternalAddTokens.tsx | 2 +- .../pages/IdProofRequest/IdProofRequest.tsx | 2 +- .../pages/SendTransaction/SendTransaction.tsx | 2 +- .../popup/pages/SignMessage/SignMessage.tsx | 2 +- .../Web3ProofRequest/AccountStatement.tsx | 59 ++++++--- .../Display/DisplayRevealStatements.tsx | 31 +++-- .../Display/DisplaySecretStatements.tsx | 58 +++++---- .../Display/DisplayStatementLine.tsx | 30 +++++ .../pages/Web3ProofRequest/Display/utils.ts | 15 ++- .../VerifiableCredentialStatement.tsx | 13 +- .../Web3ProofRequest/Web3ProofRequest.scss | 45 ++++++- .../Web3ProofRequest/Web3ProofRequest.tsx | 122 +++++++++--------- .../popup/pages/Web3ProofRequest/i18n/en.ts | 4 + .../browser-wallet/src/popup/shell/Root.tsx | 2 + .../src/popup/styles-new/config/_colors.scss | 4 + .../src/popup/styles/util/_flex.scss | 4 + .../src/shared/storage/types.ts | 21 ++- 24 files changed, 294 insertions(+), 145 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/warning-triangle.svg create mode 100644 packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayStatementLine.tsx diff --git a/packages/browser-wallet/src/assets/svg/warning-triangle.svg b/packages/browser-wallet/src/assets/svg/warning-triangle.svg new file mode 100644 index 00000000..a6409582 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/warning-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss index e4d26bdb..896ebb78 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.scss @@ -4,7 +4,6 @@ background-color: $color-bg; &__main { - padding: rem(10px); height: calc(100% - $account-page-details-height); overflow: overlay; } diff --git a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx index 50718d9c..a52ac889 100644 --- a/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx +++ b/packages/browser-wallet/src/popup/page-layouts/ExternalRequestLayout/ExternalRequestLayout.tsx @@ -8,6 +8,7 @@ import Toast from '@popup/shared/Toast/Toast'; import { useCredential } from '@popup/shared/utils/account-helpers'; import AccountDetails from '@popup/pages/Account/AccountDetails'; import clsx from 'clsx'; +import { ClassName } from 'wallet-common-helpers'; function Header() { const { t } = useTranslation('mainLayout'); @@ -61,7 +62,7 @@ interface Props { children?: ReactNode; } -export default function ExternalRequestLayout({ children }: Props) { +export default function ExternalRequestLayout({ children, className }: Props & ClassName) { const { state } = useLocation() as Location; const account = useCredential(state.payload.accountAddress); @@ -70,7 +71,9 @@ export default function ExternalRequestLayout({ children }: Props) {
    {account && } -
    {children}
    +
    + {children} +
    diff --git a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts index b4717f67..ca516e4b 100644 --- a/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts +++ b/packages/browser-wallet/src/popup/page-layouts/MainLayout/i18n/en.ts @@ -20,7 +20,7 @@ const t = { connectAccountsRequest: 'Connect accounts', addTokens: 'Add tokens', idProof: 'Proof of identity', - web3IdProof: 'Proof of identity', + web3IdProof: 'Proof of identity request', request: 'Signature Request', connect: 'New connection', allowlistingRequest: 'Allowlisting request', diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 818c85ae..bfd8ddef 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -214,7 +214,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const urlDisplay = displayUrl(url); return ( - +
    {error && (
    diff --git a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx index 271f3ad9..8f691a9e 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectAccountsRequest/ConnectAccountsRequest.tsx @@ -18,7 +18,7 @@ type Props = { }; function LoadingConnectAccountsRequest() { - return ; + return ; } export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { @@ -51,7 +51,7 @@ export default function ConnectAccountsRequest({ onAllow, onReject }: Props) { const urlDisplay = displayUrl(url); return ( - +

    {t('header', { url: urlDisplay })}

    diff --git a/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx b/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx index b60b8a7b..868325a2 100644 --- a/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/ConnectionRequest/ConnectionRequest.tsx @@ -52,7 +52,7 @@ export default function ConnectionRequest({ onAllow, onReject }: Props) { const urlDisplay = displayUrl(url); return ( - +
    {t('waiting')}
    diff --git a/packages/browser-wallet/src/popup/pages/ExternalAddTokens/ExternalAddTokens.tsx b/packages/browser-wallet/src/popup/pages/ExternalAddTokens/ExternalAddTokens.tsx index 5749c5f3..57511c66 100644 --- a/packages/browser-wallet/src/popup/pages/ExternalAddTokens/ExternalAddTokens.tsx +++ b/packages/browser-wallet/src/popup/pages/ExternalAddTokens/ExternalAddTokens.tsx @@ -118,7 +118,7 @@ export default function SignMessage({ respond }: Props) { const allExisting = addingTokens.every(({ status }) => status === ChoiceStatus.existing); return ( - + {detailView !== undefined && ( +

    {t('header', { dappName })}

    diff --git a/packages/browser-wallet/src/popup/pages/SendTransaction/SendTransaction.tsx b/packages/browser-wallet/src/popup/pages/SendTransaction/SendTransaction.tsx index 85b75274..8fe21c78 100644 --- a/packages/browser-wallet/src/popup/pages/SendTransaction/SendTransaction.tsx +++ b/packages/browser-wallet/src/popup/pages/SendTransaction/SendTransaction.tsx @@ -110,7 +110,7 @@ export default function SendTransaction({ onSubmit, onReject }: Props) { }, [payload, key, cost]); return ( - +

    {t('description', { dApp: displayUrl(url) })}

    diff --git a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx index 4617fed5..2907fb4d 100644 --- a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx +++ b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx @@ -112,7 +112,7 @@ export default function SignMessage({ onSubmit, onReject }: Props) { }, [state.payload.message, state.payload.accountAddress, key]); return ( - +

    {t('description', { dApp: displayUrl(url) })}

    diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index f636598c..08b3ec00 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -5,8 +5,9 @@ import { StatementTypes, AttributeKey, AttributeList, + IDENTITY_SUBJECT_SCHEMA, } from '@concordium/web-sdk'; -import { displaySplitAddress, useIdentityOf } from '@popup/shared/utils/account-helpers'; +import { displaySplitAddress, useIdentityName, useIdentityOf } from '@popup/shared/utils/account-helpers'; import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; import { WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; import React, { useCallback, useEffect, useState } from 'react'; @@ -23,6 +24,25 @@ import { useStatementName, useStatementValue, } from '../IdProofRequest/DisplayStatement/utils'; +import { DisplayRevealStatements } from './Display/DisplayRevealStatements'; +import { DisplaySecretStatements } from './Display/DisplaySecretStatements'; + +export function DisplayAccount({ option }: { option: WalletCredential }) { + const identityName = useIdentityName(option); + + if (!identityName) { + return null; + } + + return ( +
    +
    +
    {displaySplitAddress(option.address)}
    +
    {identityName}
    +
    +
    + ); +} type DisplaySecretStatementV2Props = ClassName & { identity?: ConfirmedIdentity; @@ -97,12 +117,14 @@ export default function AccountStatement({ setChosenId, net, }: DisplayCredentialStatementProps) { + const { t } = useTranslation('web3IdProofRequest'); const reveals = credentialStatement.statement.filter( (s) => s.type === StatementTypes.RevealAttribute ) as RevealStatementV2[]; const secrets = credentialStatement.statement.filter( (s) => s.type !== StatementTypes.RevealAttribute ) as SecretStatementV2[]; + const displayAttribute = useDisplayAttributeValue(); const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); // We do the type cast, because the check should have been done to filter validCredentials. @@ -120,31 +142,38 @@ export default function AccountStatement({ } }, []); + if (!identity) { + return null; + } + return (
    - {t('descriptions.accountCredential')}

    + options={validCredentials} - displayOption={(option) => displaySplitAddress(option.address)} + DisplayOption={DisplayAccount} onChange={onChange} + header={t('select.accountCredential')} /> {reveals.length !== 0 && ( - )} - {secrets.map((s, i) => ( - - ))} + )}
    ); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx index 00d9db7b..4d3cd74b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx @@ -1,21 +1,23 @@ import { AttributeType, RevealStatementV2 } from '@concordium/web-sdk'; -import { VerifiableCredentialSchema } from '@shared/storage/types'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { ClassName } from 'wallet-common-helpers'; -import { DisplayStatementLine } from '../../IdProofRequest/DisplayStatement/DisplayStatement'; +import WarningTriangleIcon from '@assets/svg/warning-triangle.svg'; +import { DisplayStatementLine } from './DisplayStatementLine'; import { DisplayBox } from './DisplayBox'; -import { getPropertyTitle } from './utils'; +import { DisplayProps, getPropertyTitle } from './utils'; -type RevealProps = ClassName & { +type Props = DisplayProps & { dappName: string; - statements: RevealStatementV2[]; - attributes: Record; - schema: VerifiableCredentialSchema; - className: string; }; -export function DisplayRevealStatements({ className, statements, attributes, dappName, schema }: RevealProps) { +export function DisplayRevealStatements({ + className, + statements, + attributes, + dappName, + schema, + formatAttribute = (_, value) => value.toString(), +}: Props) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.reveal'); @@ -24,7 +26,7 @@ export function DisplayRevealStatements({ className, statements, attributes, dap const title = getPropertyTitle(s.attributeTag, schema); return { attribute: title, - value: value.toString() ?? 'Unavailable', + value: formatAttribute(s.attributeTag, value) ?? 'Unavailable', isRequirementMet: value !== undefined, }; }); @@ -35,12 +37,13 @@ export function DisplayRevealStatements({ className, statements, attributes, dap header={header} infoBox={ <> +

    {t('revealTooltip.header')}

    {t('revealTooltip.body')}

    } > -
      +
        {lines.map((l, i) => ( ))}
      -
      +
      }} + components={{ 1: }} values={{ dappName }} />
      diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx index 1a447bb9..9617edf2 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -1,28 +1,25 @@ import { AttributeType, StatementTypes } from '@concordium/web-sdk'; -import { DisplayStatementLine } from '@popup/pages/IdProofRequest/DisplayStatement/DisplayStatement'; -import { VerifiableCredentialSchema } from '@shared/storage/types'; +import { CredentialSchemaSubject } from '@shared/storage/types'; import React from 'react'; import { TFunction, useTranslation } from 'react-i18next'; -import { ClassName } from 'wallet-common-helpers'; +import { DisplayStatementLine } from './DisplayStatementLine'; import { DisplayBox } from './DisplayBox'; import { SecretStatementV2 } from '../utils'; -import { getPropertyTitle } from './utils'; +import { DisplayProps, getPropertyTitle } from './utils'; -type SecretProps = ClassName & { - statements: SecretStatementV2[]; - attributes: Record; - schema: VerifiableCredentialSchema; - className: string; -}; - -function getStatementValue( +function getStatementValue( statement: SecretStatementV2, - schema: VerifiableCredentialSchema, - t: TFunction<'web3IdProofRequest', 'displayStatement'> + schema: CredentialSchemaSubject, + t: TFunction<'web3IdProofRequest', 'displayStatement'>, + formatAttribute: (key: string, value: Attribute) => string ): string { const name = getPropertyTitle(statement.attributeTag, schema); if (statement.type === StatementTypes.AttributeInRange) { - return t('proofs.range', { name, upper: statement.upper, lower: statement.lower }); + return t('proofs.range', { + name, + upper: formatAttribute(statement.attributeTag, statement.upper as Attribute), + lower: formatAttribute(statement.attributeTag, statement.lower as Attribute), + }); } if (statement.type === StatementTypes.AttributeInSet) { return t('proofs.membership', { name }); @@ -34,17 +31,23 @@ function getStatementValue( throw new Error('Unknown statement type'); } -function getStatementDescription( +function getStatementDescription( statement: SecretStatementV2, - schema: VerifiableCredentialSchema, - t: TFunction<'web3IdProofRequest', 'displayStatement'> + schema: CredentialSchemaSubject, + t: TFunction<'web3IdProofRequest', 'displayStatement'>, + formatAttribute: (key: string, value: Attribute) => string ) { const name = getPropertyTitle(statement.attributeTag, schema); - const listToString = (list: AttributeType[]) => list.map((member) => member.toString()).join(', '); + const listToString = (list: AttributeType[]) => + list.map((member) => formatAttribute(statement.attributeTag, member as Attribute)).join(', '); switch (statement.type) { case StatementTypes.AttributeInRange: - return t('descriptions.range', { name, lower: statement.lower, upper: statement.upper }); + return t('descriptions.range', { + name, + upper: formatAttribute(statement.attributeTag, statement.upper as Attribute), + lower: formatAttribute(statement.attributeTag, statement.lower as Attribute), + }); case StatementTypes.AttributeInSet: return t('descriptions.membership', { name, setNames: listToString(statement.set) }); case StatementTypes.AttributeNotInSet: @@ -54,14 +57,19 @@ function getStatementDescription( } } -export function DisplaySecretStatements({ schema, statements, className }: SecretProps) { +export function DisplaySecretStatements({ + schema, + statements, + className, + formatAttribute = (_, value) => value.toString(), +}: DisplayProps) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.secret'); const lines = statements.map((s) => { - const value = getStatementValue(s, schema, t); + const value = getStatementValue(s, schema, t, formatAttribute); const title = getPropertyTitle(s.attributeTag, schema); - const description = getStatementDescription(s, schema, t); + const description = getStatementDescription(s, schema, t, formatAttribute); return { attribute: title, value: value.toString() ?? 'Unavailable', @@ -81,7 +89,7 @@ export function DisplaySecretStatements({ schema, statements, className }: Secre } > -
        +
          {lines.map(({ description, ...l }, i) => (
          -
          {description}
          +
          {description}
          ))}
        diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayStatementLine.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayStatementLine.tsx new file mode 100644 index 00000000..2495101b --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayStatementLine.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; +import React from 'react'; +import { ClassName } from 'wallet-common-helpers'; + +import CheckmarkIcon from '@assets/svg/checkmark-dark-green.svg'; +import CrossIcon from '@assets/svg/cross.svg'; + +export type StatementLine = { + attribute: string; + value: string; + isRequirementMet: boolean; +}; + +type StatementLineProps = StatementLine & ClassName; + +export function DisplayStatementLine({ attribute, value, isRequirementMet, className }: StatementLineProps) { + return ( +
      • +
        {attribute}:
        +
        + {value} + {isRequirementMet ? ( + + ) : ( + + )} +
        +
      • + ); +} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts index 9a9b669d..802f1a87 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts @@ -1,7 +1,16 @@ -import { VerifiableCredentialSchema } from '@shared/storage/types'; +import { CredentialSchemaSubject } from '@shared/storage/types'; +import { ClassName } from 'wallet-common-helpers'; -export function getPropertyTitle(attributeTag: string, schema: VerifiableCredentialSchema) { +export function getPropertyTitle(attributeTag: string, schemaSubject: CredentialSchemaSubject) { // TODO use localization here - const property = schema.properties.credentialSubject.properties.attributes.properties[attributeTag]; + const property = schemaSubject.properties.attributes.properties[attributeTag]; return property ? property.title : attributeTag; } + +export type DisplayProps = ClassName & { + statements: StatementType[]; + attributes: Record; + schema: CredentialSchemaSubject; + className: string; + formatAttribute?: (key: string, value: Attribute) => string; +}; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 97358c2d..05e0c9be 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -73,31 +73,30 @@ export default function DisplayWeb3Statement({ return null; } - // TODO translate selector header return (
        -

        {t('descriptions.verifiableCredential')}

        +

        {t('descriptions.verifiableCredential')}

        options={validCredentials} DisplayOption={DisplayVC} onChange={onChange} - header="Select verifiable credential" + header={t('select.verifiableCredential')} /> {reveals.length !== 0 && ( )} {secrets.length !== 0 && ( )}
        diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 8cfdb182..ac3770f9 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -5,11 +5,20 @@ height: 100%; } + &__statement { + margin-top: 25px; + } + &__actions { + height: 115px; margin-top: auto; display: grid; grid-auto-flow: column; column-gap: 7px; + padding-top: 35px; + padding-left: 32px; + padding-right: 32px; + box-shadow: 0 0 30px 0 #0003; } &__loading-icon { @@ -18,9 +27,10 @@ &__credential-statement-container { height: calc(100% - 115px); - margin-bottom: rem(20px); - scrollbar-gutter: stable; - margin-right: 16px; + scrollbar-gutter: stable both-edges; + padding-right: 16px; + padding-left: 16px; + overflow-y: auto; } &__selector-icon { @@ -65,6 +75,13 @@ &__continue-button { flex-grow: 1; } + + &__description { + height: 120px; + padding-top: 32px; + color: $color-neutral-gray-50; + margin: 0; + } } .new-button-styling { @@ -95,14 +112,29 @@ } .display-reveal-statements { + &__body { + padding: 16px; + margin: 0; + } + &__line { &:not(:last-child) { margin-bottom: 8px; } } + + &__description { + color: $color-neutral-gray-30; + padding: 8px 16px 16px; + } } .display-secret-statements { + &__body { + padding: 16px; + margin: 0; + } + &__line { margin-top: 5px; @@ -110,12 +142,17 @@ border-bottom: 1px solid #e5e5e5; } } + + &__description { + color: $color-neutral-gray-30; + padding: 8px 16px 16px; + } } .verifiable-credential { &__selector { width: 100%; - margin: 10px 0; + margin: 0; height: 60px; border-radius: 12px; box-shadow: 0 0 15px 0 #0000001a; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 3926e19a..865c48ba 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -218,72 +218,70 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { } return ( - -
        - - setIds((currentIds) => { - const newIds = [...currentIds]; - newIds[currentStatementIndex] = newId; - return newIds; - }) - } - /> -
        + + + setIds((currentIds) => { + const newIds = [...currentIds]; + newIds[currentStatementIndex] = newId; + return newIds; + }) + } + /> +
        + + {currentStatementIndex > 0 && ( + )} + {currentStatementIndex === statements.length - 1 ? ( + + ) : ( + - {currentStatementIndex > 0 && ( - - )} - {currentStatementIndex === statements.length - 1 ? ( - - ) : ( - - )} -
        + )}
        ); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index e37b1e6f..9acb4ac4 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -33,6 +33,10 @@ export default { missingAttribute: 'The attribute cannot be found on the identity "{{identityName}}"', }, }, + select: { + verifiableCredential: 'Select verifiable credential', + accountCredential: 'Select Account', + }, descriptions: { verifiableCredential: 'Select a verifiable credential to reveal/prove the requested information.', accountCredential: diff --git a/packages/browser-wallet/src/popup/shell/Root.tsx b/packages/browser-wallet/src/popup/shell/Root.tsx index 4618bb4f..637fd833 100644 --- a/packages/browser-wallet/src/popup/shell/Root.tsx +++ b/packages/browser-wallet/src/popup/shell/Root.tsx @@ -45,6 +45,8 @@ function useScaling() { } if (dimensions && isSpawnedWindow) { + // TODO only for web3IdRequest? + dimensions = { width: 440, height: 870 }; // Send a message to the BG script to resize the window. popupMessageHandler.sendInternalMessage(InternalMessageType.SetViewSize, dimensions).catch(noOp); } diff --git a/packages/browser-wallet/src/popup/styles-new/config/_colors.scss b/packages/browser-wallet/src/popup/styles-new/config/_colors.scss index 66f9e78a..a04ccddc 100644 --- a/packages/browser-wallet/src/popup/styles-new/config/_colors.scss +++ b/packages/browser-wallet/src/popup/styles-new/config/_colors.scss @@ -54,3 +54,7 @@ $color-feedback-help-light: #b37cdf; $gradient-dark-mineral-blue-bg: linear-gradient(144deg, #005a78 0%, #2e8894 100%); $gradient-dark-mineral-blue-button: linear-gradient(161deg, #005a78 0%, #48a2ae 100%); $gradient-disable-button: linear-gradient(170deg, #ccc 0%, #e5e5e5 100%); + +.color-feedback-negative-dark { + color: $color-feedback-negative-dark; +} diff --git a/packages/browser-wallet/src/popup/styles/util/_flex.scss b/packages/browser-wallet/src/popup/styles/util/_flex.scss index a6fe43c3..a9a103a8 100644 --- a/packages/browser-wallet/src/popup/styles/util/_flex.scss +++ b/packages/browser-wallet/src/popup/styles/util/_flex.scss @@ -32,6 +32,10 @@ align-items: center; } +.align-start { + align-items: flex-start; +} + .flex-child-fill { flex: 1; } diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts index 1ef3c8c5..a3fe242e 100644 --- a/packages/browser-wallet/src/shared/storage/types.ts +++ b/packages/browser-wallet/src/shared/storage/types.ts @@ -5,6 +5,8 @@ import type { HexString, IdentityObjectV1, Network, + SimplePropertyDetails, + TimestampPropertyDetails, Versioned, } from '@concordium/web-sdk'; @@ -315,15 +317,28 @@ export type TimestampProperty = { description?: string; }; +type IdDetails = { + title: string; + description?: string; + type: 'string'; +}; + type CredentialSchemaAttributes = { - properties: Record; + properties: Record< + string, + CredentialSchemaProperty | TimestampProperty | SimplePropertyDetails | TimestampPropertyDetails + >; required: string[]; + title?: string; + type: 'object'; + description?: string; + format?: string; } & CredentialSchemaProperty; -interface CredentialSchemaSubject { +export interface CredentialSchemaSubject { type: string; properties: { - id: CredentialSchemaProperty; + id: IdDetails; attributes: CredentialSchemaAttributes; }; required: string[]; From bfce90d9c511871f43dc6a975d38fbb21a72518a Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 31 Aug 2023 14:21:41 +0200 Subject: [PATCH 182/231] Adjustment to selector / modals --- .../src/assets/svg/down-arrow.svg | 15 +++++++ .../Web3ProofRequest/CredentialSelector.tsx | 9 +++-- .../Web3ProofRequest/Display/DisplayBox.tsx | 1 + .../Web3ProofRequest/Web3ProofRequest.scss | 40 +++++++++++++------ .../src/popup/shared/Modal/Modal.scss | 6 +++ .../src/popup/shared/Modal/Modal.tsx | 4 +- 6 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/down-arrow.svg diff --git a/packages/browser-wallet/src/assets/svg/down-arrow.svg b/packages/browser-wallet/src/assets/svg/down-arrow.svg new file mode 100644 index 00000000..d5fbc19f --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/down-arrow.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index 4ba17e8a..5e4accca 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -1,7 +1,7 @@ import Button from '@popup/shared/Button'; import Modal from '@popup/shared/Modal'; import React, { ComponentType, useState } from 'react'; -import BackIcon from '@assets/svg/back-arrow.svg'; +import ArrowIcon from '@assets/svg/down-arrow.svg'; interface Props { options: T[]; @@ -41,15 +41,16 @@ export default function CredentialSelector({ onOpen={() => setOpen(true)} onClose={() => setOpen(false)} trigger={ - } - className="p-0" + className="verifiable-credential__selector-modal" >

        {header}

        - +
        {options.map((opt, index) => (
        @@ -271,7 +271,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { {creatingProof ? ( ) : ( - t('accept') + t('approve') )} ) : ( diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 9b030d36..bab7f6f1 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -2,7 +2,7 @@ import type en from './en'; const t: typeof en = { header: '{{dappName}} anmoder om følgende information om dig:', - accept: 'Godkend', + approve: 'Godkend', reject: 'Afvis', continue: 'Fortsæt', displayStatement: { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 9acb4ac4..c747a0ba 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -1,6 +1,6 @@ export default { header: '{{dappName}} requests the following information about you:', - accept: 'Accept', + approve: 'Approve', continue: 'Continue', back: 'Back', reject: 'Reject', From c97d2dcb67f1ca2f39dd23daa43abae2bd4a83a5 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 15:57:54 +0200 Subject: [PATCH 186/231] Fix tooltip help button --- packages/browser-wallet/src/assets/svg/help.svg | 5 +++++ .../popup/pages/Web3ProofRequest/Display/DisplayBox.tsx | 4 ++-- .../popup/pages/Web3ProofRequest/Web3ProofRequest.scss | 8 ++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/help.svg diff --git a/packages/browser-wallet/src/assets/svg/help.svg b/packages/browser-wallet/src/assets/svg/help.svg new file mode 100644 index 00000000..128a8b6b --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/help.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx index 2cab8e33..a2c4c5d0 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayBox.tsx @@ -3,7 +3,7 @@ import Modal from '@popup/shared/Modal'; import clsx from 'clsx'; import React, { ReactNode, useState } from 'react'; import { ClassName } from 'wallet-common-helpers'; -import InfoTooltipIcon from '@assets/svg/info-tooltip.svg'; +import InfoTooltipIcon from '@assets/svg/help.svg'; type DisplayBoxProps = ClassName & { header: string; @@ -24,7 +24,7 @@ export function DisplayBox({ className, children, header, infoBox }: DisplayBoxP onOpen={() => setOpen(true)} onClose={() => setOpen(false)} trigger={ - } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 3bb182e3..6b1354ea 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -102,8 +102,16 @@ color: $color-secondary-ocean-blue; } + &__tooltip-button { + align-items: center; + } + &__tooltip-icon { height: 24px; + + path { + fill: $color-secondary-ocean-blue; + } } } From 03209b51dd5f8d8c407018c361a59fd58716e2f0 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 31 Aug 2023 16:00:24 +0200 Subject: [PATCH 187/231] Fix errors --- .../popup/pages/Web3ProofRequest/AccountStatement.tsx | 6 +++--- .../src/popup/pages/Web3ProofRequest/i18n/da.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index 08b3ec00..0c890a8d 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -9,7 +9,7 @@ import { } from '@concordium/web-sdk'; import { displaySplitAddress, useIdentityName, useIdentityOf } from '@popup/shared/utils/account-helpers'; import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; -import { WalletCredential, ConfirmedIdentity } from '@shared/storage/types'; +import { WalletCredential, ConfirmedIdentity, CredentialSchemaSubject } from '@shared/storage/types'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ClassName } from 'wallet-common-helpers'; @@ -162,7 +162,7 @@ export default function AccountStatement({ attributes={identity.idObject.value.attributeList.chosenAttributes} statements={reveals} formatAttribute={displayAttribute} - schema={IDENTITY_SUBJECT_SCHEMA} + schema={IDENTITY_SUBJECT_SCHEMA as CredentialSchemaSubject} /> )} {secrets.length !== 0 && ( @@ -171,7 +171,7 @@ export default function AccountStatement({ attributes={identity.idObject.value.attributeList.chosenAttributes} statements={secrets} formatAttribute={displayAttribute} - schema={IDENTITY_SUBJECT_SCHEMA} + schema={IDENTITY_SUBJECT_SCHEMA as CredentialSchemaSubject} /> )}
      diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 9b030d36..8680c384 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -4,6 +4,7 @@ const t: typeof en = { header: '{{dappName}} anmoder om følgende information om dig:', accept: 'Godkend', reject: 'Afvis', + back: 'Tilbage', continue: 'Fortsæt', displayStatement: { requirementsMet: 'Du opfylder kravet', @@ -34,6 +35,15 @@ const t: typeof en = { missingAttribute: 'Denne Attribut kan ikke findes på identiteten "{{identityName}}"', }, }, + select: { + verifiableCredential: 'Vælg verifiable credential', + accountCredential: 'Væg Account', + }, + descriptions: { + verifiableCredential: 'Vælg en verifiable credential til at afsløre/bevise den anmodet information.', + accountCredential: + 'Vælg en account, hvis forbundne identity skal bruges til at afsløre/bevise den anmodet information.', + }, failedProof: 'Bevis kunne ikke oprettes', failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', unableToProve: From 73d4c15ceddb98cc09dab8fad742f4082056d3bc Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 31 Aug 2023 16:23:47 +0200 Subject: [PATCH 188/231] Format credential dates better --- .../pages/VerifiableCredential/VerifiableCredentialCard.tsx | 5 ++++- .../Web3ProofRequest/Display/DisplayRevealStatements.tsx | 4 ++-- .../Web3ProofRequest/Display/DisplaySecretStatements.tsx | 4 ++-- .../src/popup/pages/Web3ProofRequest/Display/utils.ts | 6 ++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx index 36276339..252b5a06 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialCard.tsx @@ -10,6 +10,7 @@ import { AttributeType, CredentialSubject } from '@concordium/web-sdk'; import { VerifiableCredentialStatus, MetadataUrl, VerifiableCredentialSchema } from '@shared/storage/types'; import { useTranslation } from 'react-i18next'; import StatusIcon from './VerifiableCredentialStatus'; +import { defaultFormatAttribute } from '../Web3ProofRequest/Display/utils'; function Logo({ logo }: { logo: MetadataUrl }) { return ; @@ -38,7 +39,9 @@ export function DisplayAttribute({ return (
      -
      {attributeValue.toString()}
      +
      + {defaultFormatAttribute(attributeKey, attributeValue)} +
      ); } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx index 4d3cd74b..aac0f3c9 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx @@ -4,7 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'; import WarningTriangleIcon from '@assets/svg/warning-triangle.svg'; import { DisplayStatementLine } from './DisplayStatementLine'; import { DisplayBox } from './DisplayBox'; -import { DisplayProps, getPropertyTitle } from './utils'; +import { DisplayProps, defaultFormatAttribute, getPropertyTitle } from './utils'; type Props = DisplayProps & { dappName: string; @@ -16,7 +16,7 @@ export function DisplayRevealStatements({ attributes, dappName, schema, - formatAttribute = (_, value) => value.toString(), + formatAttribute = defaultFormatAttribute, }: Props) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.reveal'); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx index 9617edf2..ad7d2c37 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -5,7 +5,7 @@ import { TFunction, useTranslation } from 'react-i18next'; import { DisplayStatementLine } from './DisplayStatementLine'; import { DisplayBox } from './DisplayBox'; import { SecretStatementV2 } from '../utils'; -import { DisplayProps, getPropertyTitle } from './utils'; +import { DisplayProps, defaultFormatAttribute, getPropertyTitle } from './utils'; function getStatementValue( statement: SecretStatementV2, @@ -61,7 +61,7 @@ export function DisplaySecretStatements({ schema, statements, className, - formatAttribute = (_, value) => value.toString(), + formatAttribute = defaultFormatAttribute, }: DisplayProps) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.secret'); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts index 802f1a87..f089d64f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts @@ -1,4 +1,6 @@ +import { AttributeType } from '@concordium/web-sdk'; import { CredentialSchemaSubject } from '@shared/storage/types'; +import { withDateAndTime } from '@shared/utils/time-helpers'; import { ClassName } from 'wallet-common-helpers'; export function getPropertyTitle(attributeTag: string, schemaSubject: CredentialSchemaSubject) { @@ -14,3 +16,7 @@ export type DisplayProps = ClassName & { className: string; formatAttribute?: (key: string, value: Attribute) => string; }; + +export function defaultFormatAttribute(_: string, value: Attribute) { + return value instanceof Date ? withDateAndTime(value) : value.toString(); +} From 8b8cbdd1f4b5e2a8fd6a980d93e6467b49726b33 Mon Sep 17 00:00:00 2001 From: Hjort Date: Thu, 31 Aug 2023 16:26:33 +0200 Subject: [PATCH 189/231] Improve page notProvable page --- examples/add-example-Web3Id/index.html | 1 + .../Web3ProofRequest/AccountStatement.tsx | 86 +------------------ .../Display/DisplayRevealStatements.tsx | 2 +- .../Display/DisplaySecretStatements.tsx | 1 + .../Web3ProofRequest/Web3ProofRequest.tsx | 50 ++++++++--- .../popup/pages/Web3ProofRequest/i18n/da.ts | 3 +- .../popup/pages/Web3ProofRequest/i18n/en.ts | 3 +- 7 files changed, 48 insertions(+), 98 deletions(-) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index 1ad680c0..17fd3070 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -44,6 +44,7 @@ .revealAttribute('degreeType') .revealAttribute('degreeName') .revealAttribute('graduationDate') + .revealAttribute('test') ) .addForVerifiableCredentials([{ index: 6105n, subindex: 0n }], (b) => b diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index 0c890a8d..f3d8dbf3 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -1,31 +1,19 @@ import { AccountCredentialStatement, createAccountDID, + IDENTITY_SUBJECT_SCHEMA, RevealStatementV2, StatementTypes, - AttributeKey, - AttributeList, - IDENTITY_SUBJECT_SCHEMA, } from '@concordium/web-sdk'; import { displaySplitAddress, useIdentityName, useIdentityOf } from '@popup/shared/utils/account-helpers'; -import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers'; -import { WalletCredential, ConfirmedIdentity, CredentialSchemaSubject } from '@shared/storage/types'; +import { useDisplayAttributeValue } from '@popup/shared/utils/identity-helpers'; +import { ConfirmedIdentity, CredentialSchemaSubject, WalletCredential } from '@shared/storage/types'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ClassName } from 'wallet-common-helpers'; -import { DisplayStatementView, StatementLine } from '../IdProofRequest/DisplayStatement/DisplayStatement'; import CredentialSelector from './CredentialSelector'; -import { DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; -import { - isoToCountryName, - SecretStatement, - useStatementDescription, - useStatementHeader, - useStatementName, - useStatementValue, -} from '../IdProofRequest/DisplayStatement/utils'; import { DisplayRevealStatements } from './Display/DisplayRevealStatements'; import { DisplaySecretStatements } from './Display/DisplaySecretStatements'; +import { DisplayCredentialStatementProps, SecretStatementV2 } from './utils'; export function DisplayAccount({ option }: { option: WalletCredential }) { const identityName = useIdentityName(option); @@ -44,72 +32,6 @@ export function DisplayAccount({ option }: { option: WalletCredential }) { ); } -type DisplaySecretStatementV2Props = ClassName & { - identity?: ConfirmedIdentity; - dappName: string; - statement: SecretStatementV2; -}; - -export function DisplaySecretStatementV2({ dappName, statement, identity, className }: DisplaySecretStatementV2Props) { - const v1Statement: SecretStatement = statement as SecretStatement; - const header = useStatementHeader(v1Statement); - const value = useStatementValue(v1Statement); - const description = useStatementDescription(v1Statement, identity); - const attribute = useStatementName(v1Statement); - - const lines: StatementLine[] = [ - { - attribute, - value, - isRequirementMet: identity !== undefined, - }, - ]; - - return ( - - ); -} - -type DisplayRevealStatementV2Props = ClassName & { - identity?: ConfirmedIdentity; - dappName: string; - statements: RevealStatementV2[]; -}; - -export function DisplayRevealStatementV2({ dappName, statements, identity, className }: DisplayRevealStatementV2Props) { - const { t, i18n } = useTranslation('idProofRequest', { keyPrefix: 'displayStatement' }); - const getAttributeName = useGetAttributeName(); - const displayAttribute = useDisplayAttributeValue(); - const header = t('headers.reveal'); - const attributes = identity - ? identity.idObject.value.attributeList.chosenAttributes - : ({} as AttributeList['chosenAttributes']); - - const lines: StatementLine[] = statements.map((s) => { - const stringTag = s.attributeTag as AttributeKey; - const raw = attributes[stringTag]; - let value = displayAttribute(stringTag, raw ?? ''); - - if (value && ['countryOfResidence', 'nationality', 'idDocIssuer'].includes(stringTag)) { - value = isoToCountryName(i18n.resolvedLanguage)(value); - } - - return { - attribute: getAttributeName(stringTag), - value: value ?? 'Unavailable', - isRequirementMet: raw !== undefined, - }; - }); - - return ; -} - export default function AccountStatement({ credentialStatement, validCredentials, diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx index 4d3cd74b..2eb2e709 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplayRevealStatements.tsx @@ -16,7 +16,7 @@ export function DisplayRevealStatements({ attributes, dappName, schema, - formatAttribute = (_, value) => value.toString(), + formatAttribute = (_, value) => value?.toString(), }: Props) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.reveal'); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx index 9617edf2..a0169641 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -73,6 +73,7 @@ export function DisplaySecretStatements({ return { attribute: title, value: value.toString() ?? 'Unavailable', + // TODO this is not enough for secrets isRequirementMet: value !== undefined, description, }; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 865c48ba..6c3c779f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -11,6 +11,8 @@ import { ConcordiumGRPCClient, CommitmentInput, isVerifiableCredentialStatement, + CredentialStatement, + Network, } from '@concordium/web-sdk'; import { InternalMessageType } from '@concordium/browser-wallet-message-hub'; @@ -25,7 +27,6 @@ import PendingArrows from '@assets/svg/pending-arrows.svg'; import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; import Button from '@popup/shared/Button'; -import ButtonGroup from '@popup/shared/ButtonGroup'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import { storedVerifiableCredentialsAtom, @@ -35,7 +36,7 @@ import { useConfirmedIdentities } from '@popup/shared/utils/identity-helpers'; import { parse } from '@shared/utils/payload-helpers'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { getVerifiableCredentialStatus } from '@shared/utils/verifiable-credential-helpers'; -import { useAsyncMemo } from 'wallet-common-helpers'; +import { noOp, useAsyncMemo } from 'wallet-common-helpers'; import { stringify } from '@concordium/browser-wallet-api/src/util'; import CloseIcon from '@assets/svg/cross.svg'; import { @@ -75,19 +76,39 @@ async function getAllCredentialStatuses( return Object.fromEntries(statuses); } -function DisplayNotProvable({ onClick, dappName }: { onClick: () => void; dappName: string }) { +function DisplayNotProvable({ + onClick, + dappName, + statement, + net, +}: { + onClick: () => void; + dappName: string; + statement: CredentialStatement; + net: Network; +}) { const { t } = useTranslation('web3IdProofRequest'); + const credentials = useAtomValue(credentialsAtom); + const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); + const validCredentials = isAccountCredentialStatement(statement) ? credentials : verifiableCredentials.value; + // TODO fix display of reject button return (
      -

      {t('unableToProve', { dappName })}

      - - - - +

      {t('descriptions.unableToProve')}

      + + +
      ); @@ -214,7 +235,14 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { } if (!canProve) { - return ; + return ( + !v.length)]} + /> + ); } return ( diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 8680c384..b0505ad7 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -43,11 +43,10 @@ const t: typeof en = { verifiableCredential: 'Vælg en verifiable credential til at afsløre/bevise den anmodet information.', accountCredential: 'Vælg en account, hvis forbundne identity skal bruges til at afsløre/bevise den anmodet information.', + unableToProve: 'One or more attributes does not meet the requirements from the verifier.', // TODO }, failedProof: 'Bevis kunne ikke oprettes', failedProofReason: 'Bevis kunne ikke oprettes: {{ reason }}', - unableToProve: - ' {{ dappName }} har anmodet et bevis for identitet fra dig, men du opfølger ikke kravene for beviset, så du kan ikke lave et bevis', }; export default t; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 9acb4ac4..4fe7cdf6 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -41,9 +41,8 @@ export default { verifiableCredential: 'Select a verifiable credential to reveal/prove the requested information.', accountCredential: 'Select an account associated with the identity whose credentials will be used to reveal/prove the requested information.', + unableToProve: 'One or more attributes does not meet the requirements from the verifier.', }, failedProof: 'Unable to create proof', failedProofReason: 'Unable to create proof due to: {{ reason }}', - unableToProve: - ' {{ dappName }} has requested a proof of identity from you, however you are unable to fulfill the request', }; From 80d296d9842c5cfbe70d451ecbe537049cc4f5eb Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 1 Sep 2023 08:53:33 +0200 Subject: [PATCH 190/231] Fix statement margins --- .../popup/pages/Web3ProofRequest/Web3ProofRequest.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 6b1354ea..43abfb4f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -7,6 +7,10 @@ &__statement { margin-top: 25px; + + &:last-child { + margin-bottom: 25px; + } } &__actions { @@ -140,7 +144,9 @@ } &__line { - margin-top: 5px; + &:not(:first-child) { + margin-top: 8px; + } &:not(:last-child) { border-bottom: 1px solid #e5e5e5; From ccd23814afc95006640457fe156ad7f72f79e090 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 1 Sep 2023 09:08:43 +0200 Subject: [PATCH 191/231] Fix selector scrollbar --- .../pages/Web3ProofRequest/Web3ProofRequest.scss | 2 +- .../browser-wallet/src/popup/shared/Modal/Modal.scss | 4 ++++ .../browser-wallet/src/popup/shared/Modal/Modal.tsx | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 43abfb4f..ed7f4249 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -178,7 +178,7 @@ margin-top: 178px; margin-left: 32px; margin-right: 32px; - width: calc(100% - 64px); + width: calc(100% - 48px); } &__selector-item { diff --git a/packages/browser-wallet/src/popup/shared/Modal/Modal.scss b/packages/browser-wallet/src/popup/shared/Modal/Modal.scss index bb5ca960..b9b01f40 100644 --- a/packages/browser-wallet/src/popup/shared/Modal/Modal.scss +++ b/packages/browser-wallet/src/popup/shared/Modal/Modal.scss @@ -19,6 +19,10 @@ justify-content: center; } + &--stable-scrollbar-gutter { + scrollbar-gutter: stable; + } + &__content { position: relative; width: 90%; diff --git a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx index 8391ee5d..bcb2d688 100644 --- a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx +++ b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx @@ -52,6 +52,7 @@ export type ModalProps = { onClose?(): void; bottom?: boolean; middle?: boolean; + stableScrollbarGutter?: boolean; /** * Used to overwrite styling for the modal content box */ @@ -77,6 +78,7 @@ export default function Modal({ onClose = noOp, bottom = false, middle = false, + stableScrollbarGutter = false, children, }: PropsWithChildren>): JSX.Element | null { const [{ isOpen, isExiting }, setOpenState] = useState({ isOpen: false, isExiting: false }); @@ -145,7 +147,14 @@ export default function Modal({ <> {triggerWithOpen} {isOpen && ( - + {!isExiting && ( Date: Fri, 1 Sep 2023 09:12:22 +0200 Subject: [PATCH 192/231] Fix linting issue --- .../pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx index d14d3c90..f0f71110 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/DisplayStatement/DisplayStatement.tsx @@ -38,7 +38,7 @@ type StatementLineProps = StatementLine & ClassName; export function DisplayStatementLine({ attribute, value, isRequirementMet, className }: StatementLineProps) { return ( -
    • +
    • {attribute}:
      {value} From 2b6349569a5197de6309ae5c5ffae45db33b9843 Mon Sep 17 00:00:00 2001 From: Hjort Date: Fri, 1 Sep 2023 09:41:58 +0200 Subject: [PATCH 193/231] Fixes for notProvablePage --- .../pages/Web3ProofRequest/AccountStatement.tsx | 5 ++++- .../Display/DisplaySecretStatements.tsx | 6 +++--- .../VerifiableCredentialStatement.tsx | 5 ++++- .../pages/Web3ProofRequest/Web3ProofRequest.scss | 15 +++++++++++++-- .../pages/Web3ProofRequest/Web3ProofRequest.tsx | 16 ++++++++++------ .../src/popup/pages/Web3ProofRequest/utils.ts | 1 + 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index f3d8dbf3..b4787b29 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -38,6 +38,7 @@ export default function AccountStatement({ dappName, setChosenId, net, + showDescription, }: DisplayCredentialStatementProps) { const { t } = useTranslation('web3IdProofRequest'); const reveals = credentialStatement.statement.filter( @@ -70,7 +71,9 @@ export default function AccountStatement({ return (
      -

      {t('descriptions.accountCredential')}

      + {showDescription && ( +

      {t('descriptions.accountCredential')}

      + )} options={validCredentials} DisplayOption={DisplayAccount} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx index a0169641..d8b44a74 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -1,4 +1,4 @@ -import { AttributeType, StatementTypes } from '@concordium/web-sdk'; +import { AttributeType, canProveAtomicStatement, StatementTypes } from '@concordium/web-sdk'; import { CredentialSchemaSubject } from '@shared/storage/types'; import React from 'react'; import { TFunction, useTranslation } from 'react-i18next'; @@ -60,6 +60,7 @@ function getStatementDescription( export function DisplaySecretStatements({ schema, statements, + attributes, className, formatAttribute = (_, value) => value.toString(), }: DisplayProps) { @@ -73,8 +74,7 @@ export function DisplaySecretStatements({ return { attribute: title, value: value.toString() ?? 'Unavailable', - // TODO this is not enough for secrets - isRequirementMet: value !== undefined, + isRequirementMet: canProveAtomicStatement(s, attributes), description, }; }); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 69334baa..67a91532 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -36,6 +36,7 @@ export default function DisplayWeb3Statement({ dappName, setChosenId, net, + showDescription, }: DisplayCredentialStatementProps) { const { t } = useTranslation('web3IdProofRequest'); const reveals = credentialStatement.statement.filter( @@ -68,7 +69,9 @@ export default function DisplayWeb3Statement({ return (
      -

      {t('descriptions.verifiableCredential')}

      + {showDescription && ( +

      {t('descriptions.verifiableCredential')}

      + )} options={validCredentials} DisplayOption={DisplayVC} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index 0cd76e47..aa3fbb24 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -59,9 +59,18 @@ &__reject-button { width: 56px; + } + + &__reject-button, + &__not-provable-button { height: 48px; border-radius: 16px; - background: #ab2b2b; + background: $color-feedback-negative-dark; + } + + &__not-provable-description { + margin-left: 32px; + margin-right: 32px; } &__back-button { @@ -72,11 +81,13 @@ flex-grow: 1; } + &__not-provable-description, &__description { height: 120px; padding-top: 32px; color: $color-neutral-gray-50; - margin: 0; + margin-top: 0; + margin-bottom: 0; } } diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 6c3c779f..40b96848 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -92,11 +92,12 @@ function DisplayNotProvable({ const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); const validCredentials = isAccountCredentialStatement(statement) ? credentials : verifiableCredentials.value; - // TODO fix display of reject button return (
      -

      {t('descriptions.unableToProve')}

      +

      + {t('descriptions.unableToProve')} +

      - - +
      + +
      ); @@ -261,6 +264,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { return newIds; }) } + showDescription />
      diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx index d0020bbf..e6cdcde4 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/DisplaySecretStatements.tsx @@ -63,19 +63,22 @@ export function DisplaySecretStatements({ attributes, className, formatAttribute = (_, value) => value.toString(), + overwriteSecretLine = () => ({}), }: DisplayProps) { const { t } = useTranslation('web3IdProofRequest', { keyPrefix: 'displayStatement' }); const header = t('headers.secret'); const lines = statements.map((s) => { const value = getStatementValue(s, schema, t, formatAttribute); - const title = getPropertyTitle(s.attributeTag, schema); + const attribute = getPropertyTitle(s.attributeTag, schema); const description = getStatementDescription(s, schema, t, formatAttribute); + return { - attribute: title, value, - isRequirementMet: canProveAtomicStatement(s, attributes), + attribute, description, + ...overwriteSecretLine(s), + isRequirementMet: canProveAtomicStatement(s, attributes), }; }); diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts index 802f1a87..4f0ccfd6 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts @@ -1,5 +1,6 @@ import { CredentialSchemaSubject } from '@shared/storage/types'; import { ClassName } from 'wallet-common-helpers'; +import { SecretStatementV2 } from '../utils'; export function getPropertyTitle(attributeTag: string, schemaSubject: CredentialSchemaSubject) { // TODO use localization here @@ -7,10 +8,17 @@ export function getPropertyTitle(attributeTag: string, schemaSubject: Credential return property ? property.title : attributeTag; } +export type OverwriteSecretLine = (statement: SecretStatementV2) => { + attribute?: string; + value?: string; + description?: string; +}; + export type DisplayProps = ClassName & { statements: StatementType[]; attributes: Record; schema: CredentialSchemaSubject; className: string; formatAttribute?: (key: string, value: Attribute) => string; + overwriteSecretLine?: OverwriteSecretLine; }; From cdb2a1a7f0ae626ddf96656ce407b503e9b572d3 Mon Sep 17 00:00:00 2001 From: Shjorty <201505261@post.au.dk> Date: Sat, 2 Sep 2023 14:03:45 +0200 Subject: [PATCH 205/231] Add progress indicator on web3Proofs --- packages/browser-wallet/CHANGELOG.md | 6 ++++ .../Web3ProofRequest/Web3ProofRequest.scss | 30 +++++++++++++++++++ .../Web3ProofRequest/Web3ProofRequest.tsx | 16 ++++++++++ 3 files changed, 52 insertions(+) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 0ede960c..cf7b202b 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- Indicator on proof request page, to show how many credential statements are requested, and the current position. + ## 1.1.4 ### Added diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index c293fc32..d3728037 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -89,6 +89,36 @@ margin-top: 0; margin-bottom: 0; } + + &__progress { + display: flex; + align-items: center; + justify-content: center; + height: 3px; + position: absolute; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + margin-top: -20px; + } + + &__progress-dot { + width: 10px; + height: 3px; + border-radius: 100px; + border: 0.5px solid $color-primary-mineral-blue; + background-color: transparent; + @include transition(background-color); + + &:not(:last-of-type) { + margin-right: rem(5px); + } + + &--active { + background-color: $color-primary-mineral-blue; + } + } } .new-button-styling { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx index 40b96848..ca3102c7 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import clsx from 'clsx'; import { useLocation } from 'react-router-dom'; import { useAtomValue, useSetAtom } from 'jotai'; import { useTranslation } from 'react-i18next'; @@ -267,6 +268,21 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { showDescription />
      +
      + {statements.length > 1 && + statements.map((_, i) => ( +
    • diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index cb326ee2..4a347558 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -267,3 +267,11 @@ $standard-box-shadow: 0 0 15px 0 rgb(0 0 0 / $color-shadow-alpha); fill: $color-feedback-positive-base; } } + +.display-statement-cross { + flex-shrink: 0; + + path { + fill: $color-feedback-negative-base; + } +} From 7f20c36988155b36b9a3b11f2ce299e0125a2fa8 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 13:52:53 +0200 Subject: [PATCH 212/231] Update schema for credential schemas --- packages/browser-wallet/CHANGELOG.md | 1 + .../utils/verifiable-credential-helpers.ts | 168 ++++++++++-------- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index d4b874e3..89a9e7b9 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Verifiable credentials are now validated according to the schema when being added. This will e.g. block setting an attribute as an integer if the schema defines it as a string. +- Refreshed the schema for credential schemas so that attribute types are now restricted as expected (`string`, `integer` and the special types are allowed). ## 1.1.5 diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index a5c136af..154448cb 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -396,107 +396,123 @@ const verifiableCredentialSchemaSchema = { $ref: '#/definitions/VerifiableCredentialSchema', $schema: 'http://json-schema.org/draft-07/schema#', definitions: { - SchemaProperties: { + CredentialSchemaProperty: { + anyOf: [ + { + $ref: '#/definitions/SimpleProperty', + }, + { + $ref: '#/definitions/TimestampProperty', + }, + ], + }, + CredentialSchemaSubject: { additionalProperties: false, properties: { - credentialSubject: { + properties: { additionalProperties: false, properties: { - properties: { + attributes: { additionalProperties: false, properties: { - attributes: { - additionalProperties: false, - properties: { - description: { - type: 'string', - }, - properties: { - additionalProperties: { - anyOf: [ - { - additionalProperties: false, - properties: { - description: { - type: 'string', - }, - format: { - type: 'string', - }, - title: { - type: 'string', - }, - type: { - type: 'string', - }, - }, - required: ['title', 'type', 'description'], - type: 'object', - }, - { - $ref: '#/definitions/TimestampProperty', - }, - ], + description: { + type: 'string', + }, + properties: { + additionalProperties: { + anyOf: [ + { + $ref: '#/definitions/CredentialSchemaProperty', }, - type: 'object', - }, - required: { - items: { - type: 'string', + { + $ref: '#/definitions/TimestampProperty', }, - type: 'array', - }, - title: { - type: 'string', - }, - type: { - const: 'object', - type: 'string', - }, + ], }, - required: ['type', 'properties', 'required'], type: 'object', }, - id: { - additionalProperties: false, - properties: { - description: { - type: 'string', - }, - format: { - type: 'string', - }, - title: { - type: 'string', - }, - type: { - type: 'string', - }, + required: { + items: { + type: 'string', }, - required: ['title', 'type', 'description'], - type: 'object', + type: 'array', + }, + title: { + type: 'string', + }, + type: { + const: 'object', + type: 'string', }, }, - required: ['id', 'attributes'], + required: ['type', 'properties', 'required'], type: 'object', }, - required: { - items: { - type: 'string', + id: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + title: { + type: 'string', + }, + type: { + const: 'string', + type: 'string', + }, }, - type: 'array', - }, - type: { - type: 'string', + required: ['title', 'type'], + type: 'object', }, }, - required: ['type', 'properties', 'required'], + required: ['id', 'attributes'], type: 'object', }, + required: { + items: { + type: 'string', + }, + type: 'array', + }, + type: { + const: 'object', + type: 'string', + }, + }, + required: ['type', 'properties', 'required'], + type: 'object', + }, + SchemaProperties: { + additionalProperties: false, + properties: { + credentialSubject: { + $ref: '#/definitions/CredentialSchemaSubject', + }, }, required: ['credentialSubject'], type: 'object', }, + SimpleProperty: { + additionalProperties: false, + properties: { + description: { + type: 'string', + }, + format: { + type: 'string', + }, + title: { + type: 'string', + }, + type: { + enum: ['string', 'integer'], + type: 'string', + }, + }, + required: ['title', 'type'], + type: 'object', + }, TimestampProperty: { additionalProperties: false, properties: { From 4cf1ab1f810244b0a52f8ef7bc80513c3eecdccf Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 14:19:09 +0200 Subject: [PATCH 213/231] Adjust to changes to AttributeType --- packages/browser-wallet/CHANGELOG.md | 4 ++++ .../VerifiableCredentialImport.tsx | 3 +-- .../src/popup/pages/VerifiableCredentialBackup/utils.ts | 3 +-- .../src/popup/pages/Web3ProofRequest/Display/utils.ts | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 89a9e7b9..931543dd 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- Update according to the change to AttributeType from the SDK. In particular the timestamp type is now explicit, and therefore we have removed hhe special serialization/parsing of Dates when exporting/import verifiable credentials. + ### Fixed - Verifiable credentials are now validated according to the schema when being added. This will e.g. block setting an attribute as an integer if the schema defines it as a string. diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx index 74dd36a9..a981c7e3 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/VerifiableCredentialImport.tsx @@ -15,7 +15,6 @@ import { decrypt } from '@shared/utils/crypto'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; import JSONBigInt from 'json-bigint'; import { FileInput } from '@popup/shared/Form/FileInput'; -import { reviveDateFromTimeStampAttribute } from '@concordium/web-sdk'; import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList'; import { ExportFormat, VerifiableCredentialExport } from './utils'; @@ -47,7 +46,7 @@ async function parseExport(data: EncryptedData, encryptionKey: string): Promise< const backup: ExportFormat = JSONBigInt({ alwaysParseAsBig: true, useNativeBigInt: true, - }).parse(decrypted, reviveDateFromTimeStampAttribute); + }).parse(decrypted); // Change index to number, due to parse changing all numbers to bigints. backup.value.verifiableCredentials = backup.value.verifiableCredentials.map((v) => ({ ...v, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts index 5fc8d814..56cd74ed 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredentialBackup/utils.ts @@ -12,7 +12,6 @@ import { import { networkConfigurationAtom } from '@popup/store/settings'; import { saveData } from '@popup/shared/utils/file-helpers'; import { stringify } from 'json-bigint'; -import { replaceDateWithTimeStampAttribute } from '@concordium/web-sdk'; export type VerifiableCredentialExport = { verifiableCredentials: VerifiableCredential[]; @@ -47,7 +46,7 @@ function createExport( // Use json-bigint to serialize bigints as json numbers. // Ensure that Dates are stored as timestamps to not lose typing (otherwise they are serialized as strings). - return encrypt(stringify(exportContent, replaceDateWithTimeStampAttribute), encryptionKey); + return encrypt(stringify(exportContent), encryptionKey); } export function useVerifiableCredentialExport() { diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts index 0d716bd6..2c367611 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Display/utils.ts @@ -1,4 +1,4 @@ -import { AttributeType, CredentialSchemaSubject } from '@concordium/web-sdk'; +import { AttributeType, CredentialSchemaSubject, isTimestampAttribute } from '@concordium/web-sdk'; import { withDateAndTime } from '@shared/utils/time-helpers'; import { ClassName } from 'wallet-common-helpers'; import { SecretStatementV2 } from '../utils'; @@ -25,5 +25,7 @@ export type DisplayProps = ClassName & { }; export function defaultFormatAttribute(_: string, value: Attribute) { - return value instanceof Date ? withDateAndTime(value) : value?.toString(); + return value !== undefined && isTimestampAttribute(value) + ? withDateAndTime(Date.parse(value.timestamp)) + : value?.toString(); } From fe0587588f8bc54b7fc9aa7c6a07b3eab19b1bf1 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 15:01:12 +0200 Subject: [PATCH 214/231] Use real URL as key for schema updates --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ .../src/shared/utils/verifiable-credential-helpers.ts | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 9733a172..d17d4b43 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- An issue where credential schemas were not updated with the correct key. + ## 1.1.5 ### Added diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index a5c136af..283273f1 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -916,7 +916,7 @@ export async function getCredentialSchemas( abortControllers: AbortController[], client: ConcordiumGRPCClient ) { - const onChainSchemas: VerifiableCredentialSchema[] = []; + const onChainSchemas: { schema: VerifiableCredentialSchema; url: string }[] = []; const allContractAddresses = credentials.map((vc) => getCredentialRegistryContractAddress(vc.id)); const issuerContractAddresses = new Set(allContractAddresses); @@ -937,7 +937,7 @@ export async function getCredentialSchemas( registryMetadata.credentialSchema.schema, controller ); - onChainSchemas.push(credentialSchema); + onChainSchemas.push({ schema: credentialSchema, url: registryMetadata.credentialSchema.schema.url }); } catch (e) { // Ignore errors that occur because we aborted, as that is expected to happen. if (!controller.signal.aborted) { @@ -1044,12 +1044,12 @@ export async function getChangesToCredentialSchemas( for (const updatedSchema of upToDateSchemas) { if (Object.keys(updatedSchemasInStorage).length === 0) { updatedSchemasInStorage = { - [updatedSchema.$id]: updatedSchema, + [updatedSchema.url]: updatedSchema.schema, }; updateReceived = true; } else { - updatedSchemasInStorage[updatedSchema.$id] = updatedSchema; - if (JSON.stringify(storedSchemas[updatedSchema.$id]) !== JSON.stringify(updatedSchema)) { + updatedSchemasInStorage[updatedSchema.url] = updatedSchema.schema; + if (JSON.stringify(storedSchemas[updatedSchema.url]) !== JSON.stringify(updatedSchema)) { updateReceived = true; } } From da4647885906444f376673e67db2e5860c1df33a Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 15:34:39 +0200 Subject: [PATCH 215/231] Update SDK dependency --- packages/browser-wallet/package.json | 2 +- yarn.lock | 37 +++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 74ab31ee..ed66edcd 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -19,7 +19,7 @@ "dependencies": { "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "@noble/ed25519": "^1.7.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index 6f0c55aa..30f49d44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,7 +1958,7 @@ __metadata: "@babel/core": ^7.18.2 "@concordium/browser-wallet-api": "workspace:^" "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 "@noble/ed25519": ^1.7.0 @@ -2048,6 +2048,27 @@ __metadata: languageName: node linkType: hard +"@concordium/common-sdk@npm:9.3.0": + version: 9.3.0 + resolution: "@concordium/common-sdk@npm:9.3.0" + dependencies: + "@concordium/rust-bindings": 1.2.0 + "@grpc/grpc-js": ^1.3.4 + "@noble/ed25519": ^1.7.1 + "@protobuf-ts/runtime-rpc": ^2.8.2 + "@scure/bip39": ^1.1.0 + big.js: ^6.2.0 + bs58check: ^2.1.2 + buffer: ^6.0.3 + cross-fetch: 3.1.5 + hash.js: ^1.1.7 + iso-3166-1: ^2.1.1 + json-bigint: ^1.0.0 + uuid: ^8.3.2 + checksum: 7457c54948feba0e6ad5e6e591c292300fe6457d77235f4aa0d63efc9328bb63a103144502962b53bac323d98af4407b7322bd8c74ad05252329d9ef9c1ad5f3 + languageName: node + linkType: hard + "@concordium/common-sdk@npm:^8.0.0": version: 8.0.0 resolution: "@concordium/common-sdk@npm:8.0.0" @@ -2140,6 +2161,20 @@ __metadata: languageName: node linkType: hard +"@concordium/web-sdk@npm:^6.3.0": + version: 6.3.0 + resolution: "@concordium/web-sdk@npm:6.3.0" + dependencies: + "@concordium/common-sdk": 9.3.0 + "@concordium/rust-bindings": 1.2.0 + "@grpc/grpc-js": ^1.3.4 + "@protobuf-ts/grpcweb-transport": ^2.8.2 + buffer: ^6.0.3 + process: ^0.11.10 + checksum: 86b069988c88e148490dd12b8d0c2bcbe954dee67c4074ec4965a491ab907c5ffba2a76da53479802605313f204fc6dfc0b0e3033fc186391ae3fe3a21ca6796 + languageName: node + linkType: hard + "@craftamap/esbuild-plugin-html@npm:^0.4.0": version: 0.4.0 resolution: "@craftamap/esbuild-plugin-html@npm:0.4.0" From 2fa2591a713edb2c8b520cf2b07b5968507e9f79 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 15:34:56 +0200 Subject: [PATCH 216/231] Do not require id when adding credential --- .../AddWeb3IdCredential.tsx | 4 ++- .../utils/verifiable-credential-helpers.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index fff2087c..23679e08 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -24,6 +24,7 @@ import { fetchLocalization, findNextUnusedVerifiableCredentialIndex, getCredentialRegistryContractAddress, + withIdRemovedFromSchema, } from '@shared/utils/verifiable-credential-helpers'; import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers'; import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; @@ -119,9 +120,10 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { // Use the schema to validate the credential. const validator = new Validator(); try { + const schemaWithNoId = withIdRemovedFromSchema(schema); const validationResult = validator.validate( { credentialSubject: credential.credentialSubject }, - schema as unknown as Schema + schemaWithNoId as unknown as Schema ); if (!validationResult.valid) { setError(t('error.schemaValidation', { errors: validationResult.errors.toString() })); diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 154448cb..3d2989dc 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1211,3 +1211,28 @@ export async function findNextUnusedVerifiableCredentialIndex( return index; } + +/** + * Creates a schema like the input with the id removed. This is used to be able to use the + * schema to validate an incoming credential being added, as the wallet is responsible for + * adding the id at a later point. + * @param schema the schema to create a version of without an id + * @returns the schema without the required id field + */ +export function withIdRemovedFromSchema(schema: VerifiableCredentialSchema) { + const propertiesWithNoId = { + attributes: schema.properties.credentialSubject.properties.attributes, + }; + const requiredAttributes = schema.properties.credentialSubject.required.filter((req) => req !== 'id'); + const credentialSubjectWithNoId = { + ...schema.properties.credentialSubject, + properties: propertiesWithNoId, + required: requiredAttributes, + }; + const schemaOuterPropertiesWithNoId = { + ...schema.properties, + credentialSubject: credentialSubjectWithNoId, + }; + + return { ...schema, properties: schemaOuterPropertiesWithNoId }; +} From c34d402673eec191f73473064b6f046ebe3afbd0 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 16:00:20 +0200 Subject: [PATCH 217/231] Bump all the versions --- examples/add-example-Web3Id/package.json | 2 +- examples/eSealing/package.json | 2 +- examples/nft-minting/package.json | 2 +- examples/piggybank/package.json | 2 +- examples/two-step-transfer/package.json | 2 +- examples/voting/package.json | 2 +- examples/wCCD/package.json | 2 +- .../browser-wallet-api-helpers/package.json | 2 +- packages/browser-wallet-api/package.json | 2 +- yarn.lock | 55 ++++--------------- 10 files changed, 19 insertions(+), 54 deletions(-) diff --git a/examples/add-example-Web3Id/package.json b/examples/add-example-Web3Id/package.json index 28449575..2793deb6 100644 --- a/examples/add-example-Web3Id/package.json +++ b/examples/add-example-Web3Id/package.json @@ -8,6 +8,6 @@ "start": "live-server ./index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.2.1" + "@concordium/web-sdk": "^6.3.0" } } diff --git a/examples/eSealing/package.json b/examples/eSealing/package.json index e0a8b836..58fea74d 100644 --- a/examples/eSealing/package.json +++ b/examples/eSealing/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.3.0", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "@thi.ng/leb128": "^2.1.18", "@types/sha256": "^0.2.0", "@walletconnect/types": "^2.1.4", diff --git a/examples/nft-minting/package.json b/examples/nft-minting/package.json index 0c19ae64..9f3ae96a 100644 --- a/examples/nft-minting/package.json +++ b/examples/nft-minting/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "cors": "^2.8.5", "express": "^4.18.1", "express-fileupload": "^1.4.0", diff --git a/examples/piggybank/package.json b/examples/piggybank/package.json index 24dc6d26..efd88d6c 100644 --- a/examples/piggybank/package.json +++ b/examples/piggybank/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "react": "^18.1.0", "react-dom": "^18.1.0" }, diff --git a/examples/two-step-transfer/package.json b/examples/two-step-transfer/package.json index 6ed259a5..15c1fcc1 100644 --- a/examples/two-step-transfer/package.json +++ b/examples/two-step-transfer/package.json @@ -8,6 +8,6 @@ "start": "live-server ../two-step-transfer/index.html --mount=/sdk.js:../../node_modules/@concordium/web-sdk/lib/concordium.min.js --mount=/helpers.js:../../packages/browser-wallet-api-helpers/lib/concordiumHelpers.min.js" }, "dependencies": { - "@concordium/web-sdk": "^6.2.1" + "@concordium/web-sdk": "^6.3.0" } } diff --git a/examples/voting/package.json b/examples/voting/package.json index 441681f1..6187a3bb 100644 --- a/examples/voting/package.json +++ b/examples/voting/package.json @@ -5,7 +5,7 @@ "packageManager": "yarn@3.2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "^2.0.0", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "bootstrap": "^5.2.1", "cross-env": "^7.0.3", "moment": "^2.29.4", diff --git a/examples/wCCD/package.json b/examples/wCCD/package.json index 2dc7924f..edb97172 100644 --- a/examples/wCCD/package.json +++ b/examples/wCCD/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/react-components": "^0.2.0", - "@concordium/web-sdk": "^6.2.1", + "@concordium/web-sdk": "^6.3.0", "@thi.ng/leb128": "^2.1.18", "@walletconnect/types": "^2.1.4", "mathjs": "^11.4.0", diff --git a/packages/browser-wallet-api-helpers/package.json b/packages/browser-wallet-api-helpers/package.json index 4afcbb53..3c609b62 100644 --- a/packages/browser-wallet-api-helpers/package.json +++ b/packages/browser-wallet-api-helpers/package.json @@ -19,7 +19,7 @@ "url": "https://concordium.com" }, "dependencies": { - "@concordium/web-sdk": "^6.2.1" + "@concordium/web-sdk": "^6.3.0" }, "devDependencies": { "@babel/core": "^7.17.10", diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index ea653959..40179bac 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@concordium/browser-wallet-api-helpers": "workspace:^", - "@concordium/common-sdk": "^9.2.1", + "@concordium/common-sdk": "^9.3.0", "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 30f49d44..3e4cc589 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1918,7 +1918,7 @@ __metadata: "@babel/plugin-transform-modules-commonjs": ^7.12.1 "@babel/plugin-transform-runtime": ^7.12.1 "@babel/preset-env": ^7.12.1 - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 typescript: ^4.3.5 webpack: ^5.72.0 webpack-cli: ^4.9.2 @@ -1930,7 +1930,7 @@ __metadata: resolution: "@concordium/browser-wallet-api@workspace:packages/browser-wallet-api" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/common-sdk": ^9.2.1 + "@concordium/common-sdk": ^9.3.0 "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 "@types/json-bigint": ^1.0.1 @@ -2027,28 +2027,7 @@ __metadata: languageName: unknown linkType: soft -"@concordium/common-sdk@npm:9.2.1, @concordium/common-sdk@npm:^9.2.1": - version: 9.2.1 - resolution: "@concordium/common-sdk@npm:9.2.1" - dependencies: - "@concordium/rust-bindings": 1.2.0 - "@grpc/grpc-js": ^1.3.4 - "@noble/ed25519": ^1.7.1 - "@protobuf-ts/runtime-rpc": ^2.8.2 - "@scure/bip39": ^1.1.0 - big.js: ^6.2.0 - bs58check: ^2.1.2 - buffer: ^6.0.3 - cross-fetch: 3.1.5 - hash.js: ^1.1.7 - iso-3166-1: ^2.1.1 - json-bigint: ^1.0.0 - uuid: ^8.3.2 - checksum: fd308fd31f9833a90e526c672bc8e0fd173ee41841350c2b477b39ac7c336f67b27c20d638416a0a807c631afdd196764f882e59ada5aa65d88f955901715d07 - languageName: node - linkType: hard - -"@concordium/common-sdk@npm:9.3.0": +"@concordium/common-sdk@npm:9.3.0, @concordium/common-sdk@npm:^9.3.0": version: 9.3.0 resolution: "@concordium/common-sdk@npm:9.3.0" dependencies: @@ -2147,20 +2126,6 @@ __metadata: languageName: node linkType: hard -"@concordium/web-sdk@npm:^6.2.1": - version: 6.2.1 - resolution: "@concordium/web-sdk@npm:6.2.1" - dependencies: - "@concordium/common-sdk": 9.2.1 - "@concordium/rust-bindings": 1.2.0 - "@grpc/grpc-js": ^1.3.4 - "@protobuf-ts/grpcweb-transport": ^2.8.2 - buffer: ^6.0.3 - process: ^0.11.10 - checksum: 6344d56fc8f71ab92c6913c4e2755a21660491964503d2f5709d59501e634c279611b620ec067937eb132f9a47109f54bcb0dcbceca87377a9de88e69bb61ed0 - languageName: node - linkType: hard - "@concordium/web-sdk@npm:^6.3.0": version: 6.3.0 resolution: "@concordium/web-sdk@npm:6.3.0" @@ -6793,7 +6758,7 @@ __metadata: resolution: "NFT-Minting@workspace:examples/nft-minting" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -6936,7 +6901,7 @@ __metadata: version: 0.0.0-use.local resolution: "add-example-web3-id@workspace:examples/add-example-Web3Id" dependencies: - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -10252,7 +10217,7 @@ __metadata: resolution: "e_sealing@workspace:examples/eSealing" dependencies: "@concordium/react-components": ^0.3.0 - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 @@ -17632,7 +17597,7 @@ __metadata: resolution: "piggybank@workspace:examples/piggybank" dependencies: "@concordium/browser-wallet-api-helpers": "workspace:^" - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -21549,7 +21514,7 @@ __metadata: version: 0.0.0-use.local resolution: "two-step-transfer@workspace:examples/two-step-transfer" dependencies: - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 live-server: ^1.2.2 languageName: unknown linkType: soft @@ -22247,7 +22212,7 @@ __metadata: resolution: "voting@workspace:examples/voting" dependencies: "@concordium/browser-wallet-api-helpers": ^2.0.0 - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@types/node": ^18.7.23 "@types/react": ^18.0.9 "@types/react-dom": ^18.0.5 @@ -22372,7 +22337,7 @@ __metadata: resolution: "wccd@workspace:examples/wCCD" dependencies: "@concordium/react-components": ^0.2.0 - "@concordium/web-sdk": ^6.2.1 + "@concordium/web-sdk": ^6.3.0 "@craftamap/esbuild-plugin-html": ^0.4.0 "@thi.ng/leb128": ^2.1.18 "@types/node": ^18.7.23 From 0ddf6d704d687e1f3160cfe8b9a97f25eebd7c67 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 16:07:58 +0200 Subject: [PATCH 218/231] Close selector when clicking outside --- .../popup/pages/Web3ProofRequest/CredentialSelector.tsx | 2 +- packages/browser-wallet/src/popup/shared/Modal/Modal.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index 90bb6c9e..bdd4261a 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -39,7 +39,7 @@ export default function CredentialSelector({ return ( setOpen(true)} onClose={() => setOpen(false)} diff --git a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx index bcb2d688..8d6983f7 100644 --- a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx +++ b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx @@ -53,6 +53,8 @@ export type ModalProps = { bottom?: boolean; middle?: boolean; stableScrollbarGutter?: boolean; + // Determines whether to show the close button if disableClose === true. + showCloseButton?: boolean; /** * Used to overwrite styling for the modal content box */ @@ -79,6 +81,7 @@ export default function Modal({ bottom = false, middle = false, stableScrollbarGutter = false, + showCloseButton = true, children, }: PropsWithChildren>): JSX.Element | null { const [{ isOpen, isExiting }, setOpenState] = useState({ isOpen: false, isExiting: false }); @@ -167,7 +170,9 @@ export default function Modal({ transition={defaultTransition} onClickOutside={close} > - {!disableClose && close()} />} + {!disableClose && showCloseButton && ( + close()} /> + )} {children}
      )} From 9711c1fab6bb81da919ad8fa61abffa495867f49 Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 4 Sep 2023 16:08:58 +0200 Subject: [PATCH 219/231] Fix credentials selector resetting --- packages/browser-wallet/CHANGELOG.md | 6 ++++ .../Web3ProofRequest/AccountStatement.tsx | 21 ++++++++------ .../VerifiableCredentialStatement.tsx | 17 +++++------ .../Web3ProofRequest/Web3ProofRequest.tsx | 29 +++++++++++++++++-- .../src/popup/pages/Web3ProofRequest/utils.ts | 1 + 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 9733a172..c5aadce2 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.6 + +### Fixed + +- On the proof request page, routing back to credential statements no longer resets the selection. + ## 1.1.5 ### Added diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx index fcf0261b..8e7e53f0 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/AccountStatement.tsx @@ -9,7 +9,8 @@ import { import { displaySplitAddress, useIdentityName, useIdentityOf } from '@popup/shared/utils/account-helpers'; import { useDisplayAttributeValue } from '@popup/shared/utils/identity-helpers'; import { ConfirmedIdentity, WalletCredential } from '@shared/storage/types'; -import React, { useCallback, useEffect, useState } from 'react'; +import { getCredentialIdFromSubjectDID } from '@shared/utils/verifiable-credential-helpers'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { SecretStatement, useStatementName, useStatementValue } from '../IdProofRequest/DisplayStatement/utils'; import CredentialSelector from './CredentialSelector'; @@ -39,6 +40,7 @@ export default function AccountStatement({ credentialStatement, validCredentials, dappName, + chosenId, setChosenId, net, showDescription, @@ -52,7 +54,14 @@ export default function AccountStatement({ ) as SecretStatementV2[]; const displayAttribute = useDisplayAttributeValue(); - const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + const initialIndex = useMemo( + () => validCredentials.findIndex((c) => c.credId === getCredentialIdFromSubjectDID(chosenId)), + [] + ); + const [chosenCredential, setChosenCredential] = useState( + validCredentials[initialIndex] + ); + // We do the type cast, because the check should have been done to filter validCredentials. const identity = useIdentityOf(chosenCredential) as ConfirmedIdentity | undefined; @@ -61,13 +70,6 @@ export default function AccountStatement({ setChosenId(createAccountDID(net, credential.credId)); }, []); - // Initially set chosenId - useEffect(() => { - if (chosenCredential) { - setChosenId(createAccountDID(net, chosenCredential.credId)); - } - }, []); - const accountCreateSecretLine: OverwriteSecretLine = (statement: SecretStatementV2) => { const value = useStatementValue(statement as SecretStatement); const attribute = useStatementName(statement as SecretStatement); @@ -88,6 +90,7 @@ export default function AccountStatement({ )} options={validCredentials} + initialIndex={initialIndex} DisplayOption={DisplayAccount} onChange={onChange} header={t('select.accountCredential')} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx index 67a91532..973635bd 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/VerifiableCredentialStatement.tsx @@ -1,7 +1,7 @@ import { RevealStatementV2, StatementTypes, VerifiableCredentialStatement } from '@concordium/web-sdk'; import Img from '@popup/shared/Img'; import { VerifiableCredential } from '@shared/storage/types'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useCredentialLocalization, @@ -34,6 +34,7 @@ export default function DisplayWeb3Statement({ credentialStatement, validCredentials, dappName, + chosenId, setChosenId, net, showDescription, @@ -45,20 +46,17 @@ export default function DisplayWeb3Statement({ const secrets = credentialStatement.statement.filter( (s) => s.type !== StatementTypes.RevealAttribute ) as SecretStatementV2[]; - const [chosenCredential, setChosenCredential] = useState(validCredentials[0]); + + const initialIndex = useMemo(() => validCredentials.findIndex((c) => c.id === chosenId), []); + const [chosenCredential, setChosenCredential] = useState( + validCredentials[initialIndex] + ); const onChange = useCallback((credential: VerifiableCredential) => { setChosenCredential(credential); setChosenId(createWeb3IdDIDFromCredential(credential, net)); }, []); - // Initially set chosenId - useEffect(() => { - if (chosenCredential) { - setChosenId(createWeb3IdDIDFromCredential(chosenCredential, net)); - } - }, []); - const schema = useCredentialSchema(chosenCredential); const metadata = useCredentialMetadata(chosenCredential); const localization = useCredentialLocalization(chosenCredential); @@ -77,6 +75,7 @@ export default function DisplayWeb3Statement({ DisplayOption={DisplayVC} onChange={onChange} header={t('select.verifiableCredential')} + initialIndex={initialIndex} /> {reveals.length !== 0 && ( @@ -169,7 +182,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { const statements: CredentialStatements = useMemo(() => parse(rawStatements), [rawStatements]); - const [ids, setIds] = useState(Array(statements.length).fill('')); + const [ids, setIds] = useState([]); const verifiableCredentialSchemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const identities = useConfirmedIdentities(); @@ -201,6 +214,16 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { }); }, [identities.loading, verifiableCredentials.loading, Boolean(statuses)]); + useEffect(() => { + if (validCredentials) { + setIds( + validCredentials.map((creds, index) => + creds[0] ? getIdFromCredential(creds[0], statements[index], net) : '' + ) + ); + } + }, [Boolean(validCredentials)]); + const canProve = useMemo( () => validCredentials && validCredentials.every((x) => x.length > 0), [Boolean(validCredentials)] @@ -268,7 +291,8 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { verifiableCredentials.loading || verifiableCredentialSchemas.loading || identities.loading || - !validCredentials + !validCredentials || + !ids.length ) { return null; } @@ -294,6 +318,7 @@ export default function Web3ProofRequest({ onReject, onSubmit }: Props) { credentialStatement={statements[currentStatementIndex]} net={net} key={currentStatementIndex} + chosenId={ids[currentStatementIndex]} setChosenId={(newId) => setIds((currentIds) => { const newIds = [...currentIds]; diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts index c7c6afeb..8446107b 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/utils.ts @@ -35,6 +35,7 @@ export interface DisplayCredentialStatementProps extends credentialStatement: Statement; validCredentials: Credential[]; dappName: string; + chosenId: string; setChosenId: (id: string) => void; net: Network; showDescription: boolean; From a850781503a17eb1cd931e9b9ede37fbaf2a4ac5 Mon Sep 17 00:00:00 2001 From: Hjort Date: Mon, 4 Sep 2023 16:24:14 +0200 Subject: [PATCH 220/231] Remove new button style that is only changes the weight and line height --- .../src/popup/styles-new/config/_typography.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/browser-wallet/src/popup/styles-new/config/_typography.scss b/packages/browser-wallet/src/popup/styles-new/config/_typography.scss index 3406c129..3d891681 100644 --- a/packages/browser-wallet/src/popup/styles-new/config/_typography.scss +++ b/packages/browser-wallet/src/popup/styles-new/config/_typography.scss @@ -7,7 +7,6 @@ $t-font-weight-regular: 400; $t-font-weight-medium: 500; $t-font-weight-semi-bold: 600; $t-font-weight-bold: 700; - $s-type-config: ( display: ( font-weight: $t-font-weight-bold, @@ -121,15 +120,6 @@ $s-type-config: ( ), ), ), - button: ( - font-weight: $t-font-weight-semi-bold, - styles: ( - button: ( - font-size: 14px, - line-height: 20px, - ), - ), - ), ); @each $cat-name, $cat-content in $s-type-config { From fabe72b0594d96ac61a0b4efce7db8f92cd2bfc7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 4 Sep 2023 16:32:37 +0200 Subject: [PATCH 221/231] Improve UI of credential selector --- packages/browser-wallet/CHANGELOG.md | 6 ++++++ .../popup/pages/Web3ProofRequest/CredentialSelector.tsx | 3 +++ .../popup/pages/Web3ProofRequest/Web3ProofRequest.scss | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 9733a172..1773fa2b 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- UI improvements to the credential selector. + ## 1.1.5 ### Added diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index bdd4261a..dc49d751 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -2,6 +2,7 @@ import Button from '@popup/shared/Button'; import Modal from '@popup/shared/Modal'; import React, { ComponentType, useState } from 'react'; import ArrowIcon from '@assets/svg/down-arrow.svg'; +import CheckmarkIcon from '@assets/svg/check_small.svg'; interface Props { /** @@ -39,6 +40,7 @@ export default function CredentialSelector({ return ( setOpen(true)} @@ -63,6 +65,7 @@ export default function CredentialSelector({ onClick={() => onClick(index)} > + {chosenIndex === index && } ))} diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss index cb326ee2..ec52398f 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/Web3ProofRequest.scss @@ -226,10 +226,19 @@ $standard-box-shadow: 0 0 15px 0 rgb(0 0 0 / $color-shadow-alpha); &__selector-item { width: calc(100% - 32px); margin: 5px 16px; + display: flex; + justify-content: space-between; + align-items: center; &:not(:last-child) { border-bottom: 1px solid $color-neutral-gray-10; } + + &-icon { + height: 20px; + width: 20px; + margin-right: 10px; + } } &__selector-header { From e958020f9f9cb8f9d1cab5b314b770834486fe9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20=C3=98rh=C3=B8j?= Date: Tue, 5 Sep 2023 08:19:42 +0200 Subject: [PATCH 222/231] Invert parameter to have false as default --- .../popup/pages/Web3ProofRequest/CredentialSelector.tsx | 2 +- packages/browser-wallet/src/popup/shared/Modal/Modal.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx index dc49d751..fe6f51d1 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/CredentialSelector.tsx @@ -41,7 +41,7 @@ export default function CredentialSelector({ return ( setOpen(true)} onClose={() => setOpen(false)} diff --git a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx index 8d6983f7..616e0af4 100644 --- a/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx +++ b/packages/browser-wallet/src/popup/shared/Modal/Modal.tsx @@ -53,8 +53,8 @@ export type ModalProps = { bottom?: boolean; middle?: boolean; stableScrollbarGutter?: boolean; - // Determines whether to show the close button if disableClose === true. - showCloseButton?: boolean; + // Determines whether to hide the close button if disableClose === true. + hideCloseButton?: boolean; /** * Used to overwrite styling for the modal content box */ @@ -81,7 +81,7 @@ export default function Modal({ bottom = false, middle = false, stableScrollbarGutter = false, - showCloseButton = true, + hideCloseButton = false, children, }: PropsWithChildren>): JSX.Element | null { const [{ isOpen, isExiting }, setOpenState] = useState({ isOpen: false, isExiting: false }); @@ -170,7 +170,7 @@ export default function Modal({ transition={defaultTransition} onClickOutside={close} > - {!disableClose && showCloseButton && ( + {!disableClose && !hideCloseButton && ( close()} /> )} {children} From 88c66e0632f75669485e845073d85e3bfd336e6c Mon Sep 17 00:00:00 2001 From: Hjort Date: Tue, 5 Sep 2023 10:22:27 +0200 Subject: [PATCH 223/231] Bump wallet version --- packages/browser-wallet/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index ed66edcd..527289a4 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.5", + "version": "1.1.6", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 4cc12a4ceab44c1cd925b3ffd7d3c42040709dcc Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 12:39:14 +0200 Subject: [PATCH 224/231] Fix image width of credential for smaller screens --- examples/add-example-Web3Id/index.html | 2 +- .../pages/VerifiableCredential/VerifiableCredential.scss | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html index 17fd3070..b2fd5213 100644 --- a/examples/add-example-Web3Id/index.html +++ b/examples/add-example-Web3Id/index.html @@ -96,7 +96,7 @@ const values = { degreeType: degreeType.value, degreeName: degreeName.value, - graduationDate: graduationDate.valueAsDate, + graduationDate: { type: 'date-time', timestamp: graduationDate.valueAsDate.toISOString() }, }; const metadataUrl = { url: web3metadataUrl.value, diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index ad7a30b7..4090e1d2 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -70,9 +70,10 @@ } &__image { - margin: auto; + margin-left: 8px; + margin-right: 8px; + width: calc(100% - 16px); height: 120px; - width: 308px; img { max-width: 100%; From 1ef67e421537b6e22b8307322463232c25f472e7 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 12:46:05 +0200 Subject: [PATCH 225/231] Fix texts with zero-knowledge proofs --- .../src/popup/pages/IdProofRequest/i18n/da.ts | 18 ++++++++--------- .../src/popup/pages/IdProofRequest/i18n/en.ts | 20 +++++++++---------- .../popup/pages/Web3ProofRequest/i18n/da.ts | 6 +++--- .../popup/pages/Web3ProofRequest/i18n/en.ts | 6 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts index 2da97ad1..de130f81 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/da.ts @@ -14,18 +14,18 @@ const da: typeof en = { body: 'Når du afslører information til en tredjepart, kan de beholde denne information. Dette betyder, at du kun bør afsløre information til dem hvis du er bekendt med deres databrugs- samt databeskyttelses-politik.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', }, secretTooltip: { - header: 'Zero Knowledge beviser', - body: 'Zero Knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + header: 'Zero-knowledge beviser', + body: 'Zero-knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', }, headers: { reveal: 'Information der afsløres', - age: 'Zero Knowledge bevis for alder', - dob: 'Zero Knowledge bevis for fødselsdato', - idValidity: 'Zero Knowledge bevis for ID validitet', - nationality: 'Zero Knowledge bevis for nationalitet', - residence: 'Zero Knowledge bevis for bopælsland', - idDocType: 'Zero Knowledge bevis for identitetsdokumenttype', - idDocIssuer: 'Zero Knowledge bevis for identitetsdokumentudsteder', + age: 'Zero-knowledge bevis for alder', + dob: 'Zero-knowledge bevis for fødselsdato', + idValidity: 'Zero-knowledge bevis for ID validitet', + nationality: 'Zero-knowledge bevis for nationalitet', + residence: 'Zero-knowledge bevis for bopælsland', + idDocType: 'Zero-knowledge bevis for identitetsdokumenttype', + idDocIssuer: 'Zero-knowledge bevis for identitetsdokumentudsteder', }, names: { age: 'Alder', diff --git a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts index 1d0ec3d0..7d17ad89 100644 --- a/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/IdProofRequest/i18n/en.ts @@ -6,24 +6,24 @@ export default { requirementsMet: 'You meet this requirement', requirementsNotMet: "You don't meet this requirement", revealDescription: - '<1>Important: {{dappName}} will be given all the information above. You should only accept, if you trust the service, and you are familiar with their privacy policy.', + '<1>Important: {{dappName}} will be given all the information above. You should only accept if you trust the service, and you are familiar with their privacy policy.', revealTooltip: { header: 'Revealing information <1 />', body: 'When you reveal information to a third party, you effectively hand over the information to them. This means you should only do this if you agree to their data usage and protection policies.\n\nYou can read more in\n<1>the developer documentation.', }, secretTooltip: { - header: 'Zero Knowledge proofs', - body: 'Zero Knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact age. Another example could be proving your residency is within a given set of countries without revealing which of those countries you reside within.\n\nYou can read more in\n<1>the developer documentation.', + header: 'Zero-knowledge proofs', + body: 'Zero-knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact age. Another example could be proving your residency is within a given set of countries without revealing which of those countries you reside within.\n\nYou can read more in\n<1>the developer documentation.', }, headers: { reveal: 'Information to reveal', - age: 'Zero Knowledge proof of age', - dob: 'Zero Knowledge proof of date of birth', - idValidity: 'Zero Knowledge proof of ID validity', - nationality: 'Zero Knowledge proof of nationality', - residence: 'Zero Knowledge proof of country of residence', - idDocType: 'Zero Knowledge proof of identity document type', - idDocIssuer: 'Zero Knowledge proof of identity document issuer', + age: 'Zero-knowledge proof of age', + dob: 'Zero-knowledge proof of date of birth', + idValidity: 'Zero-knowledge proof of ID validity', + nationality: 'Zero-knowledge proof of nationality', + residence: 'Zero-knowledge proof of country of residence', + idDocType: 'Zero-knowledge proof of identity document type', + idDocIssuer: 'Zero-knowledge proof of identity document issuer', }, names: { age: 'Age', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts index 2b2568ec..dd31d4ed 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/da.ts @@ -16,12 +16,12 @@ const t: typeof en = { body: 'Når du afslører information til en tredjepart, kan de beholde denne information. Dette betyder, at du kun bør afsløre information til dem hvis du er bekendt med deres databrugs- samt databeskyttelses-politik.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', }, secretTooltip: { - header: 'Zero Knowledge beviser', - body: 'Zero Knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', + header: 'Zero-knowledge beviser', + body: 'Zero-knowledge beviser er en måde at bevise noget overfor en service eller dApp, uden at afsløre den underliggende personlige information. Et eksempel kan være, at du beviser at du er over 18 år gammel, uden at bevise din specifikke fødselsdato. Et andet eksempel kan være, at du bor i ét ud af en række lande, uden at afsløre hvilken af disse lande du bor i.\n\nDu kan læse mere i\n<1>udvikler dokumentationen.', }, headers: { reveal: 'Information der afsløres', - secret: 'Zero Knowledge bevis', + secret: 'Zero-knowledge bevis', }, proofs: { range: 'Mellem {{ lower }} og {{ upper }}', diff --git a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts index 0188ef4a..ab27b3c6 100644 --- a/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts +++ b/packages/browser-wallet/src/popup/pages/Web3ProofRequest/i18n/en.ts @@ -8,18 +8,18 @@ export default { requirementsMet: 'You meet this requirement', requirementsNotMet: "You don't meet this requirement", revealDescription: - '<1>Important: {{dappName}} will be given all the information above. You should only accept, if you trust the service, and you are familiar with their privacy policy.', + '<1>Important: {{dappName}} will be given all the information above. You should only accept if you trust the service, and you are familiar with their privacy policy.', revealTooltip: { header: 'Information to reveal', body: 'When you reveal information for a third party, you effectively hand over the information to them. This means that you should only do this if you have absolute trust in them, and if you are familiar with their data usage and protection procedures.\n\nYou can read more on\ndeveloper.concordium.software', }, secretTooltip: { - header: 'Zero Knowledge proof', + header: 'Zero-knowledge proof', body: 'Zero-knowledge proofs are a way of proving something to a service or dApp without revealing the exact personal information. One example can be that you prove that you are over 18 years old without revealing your exact date of birth. Another example could be that you live in one of a range of countries without revealing exactly which country you live in.\n\nYou can read more on\ndeveloper.concordium.software', }, headers: { reveal: 'Information to reveal', - secret: 'Zero Knowledge proof', + secret: 'Zero-knowledge proof', }, proofs: { range: 'Between {{ lower }} and {{ upper }}', From 08380e8a62ef9b3e9517c14ce2b4f112868682bb Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 13:38:22 +0200 Subject: [PATCH 226/231] Fix image issue and typos --- packages/browser-wallet/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 4ad5bd20..702209d8 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixed + +- Changed 'Zero Knowledge' to 'Zero-knowledge' in display texts. +- An issue with images in verifiable credentials for lower resolutions. + ## 1.1.6 ### Fixed From 21cf72b7991680c20946ddfed16a039cfe323bde Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 15:11:34 +0200 Subject: [PATCH 227/231] Allow bigints as type integer --- packages/browser-wallet/CHANGELOG.md | 1 + .../AddWeb3IdCredential.tsx | 4 +++- .../utils/verifiable-credential-helpers.ts | 20 ++++++++++++++++++- .../verifiable-credential-helpers.test.ts | 20 +++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 702209d8..fb56793f 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -6,6 +6,7 @@ - Changed 'Zero Knowledge' to 'Zero-knowledge' in display texts. - An issue with images in verifiable credentials for lower resolutions. +- Schema validation of verifiable credentials with attributes that have `{ type: "integer" }` no longer rejects BigInt values. ## 1.1.6 diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx index 23679e08..7797fbc3 100644 --- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx +++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx @@ -17,6 +17,7 @@ import { useAsyncMemo } from 'wallet-common-helpers'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { displayUrl } from '@popup/shared/utils/string-helpers'; import { + coerceBigIntIntegerToNumber, createCredentialId, createPublicKeyIdentifier, fetchCredentialMetadata, @@ -123,7 +124,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) { const schemaWithNoId = withIdRemovedFromSchema(schema); const validationResult = validator.validate( { credentialSubject: credential.credentialSubject }, - schemaWithNoId as unknown as Schema + schemaWithNoId as unknown as Schema, + { preValidateProperty: coerceBigIntIntegerToNumber } ); if (!validationResult.valid) { setError(t('error.schemaValidation', { errors: validationResult.errors.toString() })); diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 571129b2..0a64f294 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -15,7 +15,7 @@ import { VerifiableCredentialStatus, } from '@shared/storage/types'; import { Buffer } from 'buffer/'; -import jsonschema from 'jsonschema'; +import jsonschema, { Schema } from 'jsonschema'; import { applyExecutionNRGBuffer, getContractName } from './contract-helpers'; import { getNet } from './network-helpers'; import { logError } from './log-helpers'; @@ -1236,3 +1236,21 @@ export function withIdRemovedFromSchema(schema: VerifiableCredentialSchema) { return { ...schema, properties: schemaOuterPropertiesWithNoId }; } + +/** + * BigInts do not validate against a json schema as being an integer. This pre validate function + * coerces any bigint values where { type: "integer" } into number, as that will ensure that + * they validate against the schema. + */ +export function coerceBigIntIntegerToNumber( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + instance: any, + key: string, + schema: Schema +) { + const value = instance[key]; + if (schema.type && schema.type === 'integer' && typeof value === 'bigint') { + // eslint-disable-next-line no-param-reassign + instance[key] = Number(value); + } +} diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index 5a8cc243..7e0bd9ec 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -1,3 +1,4 @@ +import { Schema, Validator } from 'jsonschema'; import { RevocationDataHolder, RevokeCredentialHolderParam, @@ -13,6 +14,7 @@ import { getCredentialIdFromSubjectDID, getContractAddressFromIssuerDID, getVerifiableCredentialPublicKeyfromSubjectDID, + coerceBigIntIntegerToNumber, } from '../src/shared/utils/verifiable-credential-helpers'; import { mainnet, testnet } from '../src/shared/constants/networkConfiguration'; @@ -184,3 +186,21 @@ test('getCredentialIdFromSubjectDID extracts credId without network', () => { 'aad98095db73b5b22f7f64823a495c6c57413947353646313dc453fa4604715d2f93b2c1f8cb4c9625edd6330e1d27fa' ); }); + +test('bigint with type integer validates against schema when coercing in pre validate', () => { + const validator = new Validator(); + const schema: Schema = { + type: 'object', + properties: { + age: { type: 'integer' }, + }, + }; + + const p = { + age: BigInt(97), + }; + + const validationResult = validator.validate(p, schema, { preValidateProperty: coerceBigIntIntegerToNumber }); + + expect(validationResult.valid).toBeTruthy(); +}); From bae4f9ad7addf53868a282892f57cd38516a9fbb Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 15:38:47 +0200 Subject: [PATCH 228/231] Restrict allowed attribute value types --- .../browser-wallet/src/background/web3Id.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index 086b5439..f472bf3a 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -12,6 +12,8 @@ import { IdStatement, StatementTypes, AttributeType, + isTimestampAttribute, + TimestampAttribute, } from '@concordium/web-sdk'; import { sessionVerifiableCredentials, @@ -126,10 +128,16 @@ function rejectRequest(message: string): { run: false; response: MessageStatusWr }; } +// TODO Expose function from SDK and re-use. +function timestampToDate(attribute: TimestampAttribute): Date { + return new Date(Date.parse(attribute.timestamp)); +} + function validateTimestampAttribute(attributeTag: string, attributeValue: AttributeType) { if ( - attributeValue instanceof Date && - (attributeValue.getTime() < MIN_DATE_TIMESTAMP || attributeValue.getTime() > MAX_DATE_TIMESTAMP) + isTimestampAttribute(attributeValue) && + (timestampToDate(attributeValue).getTime() < MIN_DATE_TIMESTAMP || + timestampToDate(attributeValue).getTime() > MAX_DATE_TIMESTAMP) ) { return `The attribute [${attributeValue}] for key [${attributeTag}] is out of bounds for a Date. The Date must be between ${MIN_DATE_ISO} and ${MAX_DATE_ISO}`; } @@ -154,6 +162,14 @@ function validateAttributeBounds( attributeTag: string, attributeValue: AttributeType ): { error: false } | { error: true; message: string } { + if ( + typeof attributeValue !== 'string' && + typeof attributeValue !== 'bigint' && + !isTimestampAttribute(attributeValue) + ) { + return { error: true, message: 'Unsupported attribute type' }; + } + const stringError = validateStringAttribute(attributeTag, attributeValue); if (stringError) { return { error: true, message: stringError }; From 84c4f36a6d8c709d72b4b8692d439789fd1cbfe0 Mon Sep 17 00:00:00 2001 From: orhoj Date: Tue, 5 Sep 2023 16:33:48 +0200 Subject: [PATCH 229/231] Fix validation of timestamp bounds --- packages/browser-wallet/src/background/web3Id.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts index f472bf3a..aea526b4 100644 --- a/packages/browser-wallet/src/background/web3Id.ts +++ b/packages/browser-wallet/src/background/web3Id.ts @@ -134,12 +134,15 @@ function timestampToDate(attribute: TimestampAttribute): Date { } function validateTimestampAttribute(attributeTag: string, attributeValue: AttributeType) { - if ( - isTimestampAttribute(attributeValue) && - (timestampToDate(attributeValue).getTime() < MIN_DATE_TIMESTAMP || - timestampToDate(attributeValue).getTime() > MAX_DATE_TIMESTAMP) - ) { - return `The attribute [${attributeValue}] for key [${attributeTag}] is out of bounds for a Date. The Date must be between ${MIN_DATE_ISO} and ${MAX_DATE_ISO}`; + if (isTimestampAttribute(attributeValue)) { + const timestamp = timestampToDate(attributeValue).getTime(); + if (Number.isNaN(timestamp)) { + return `The attribute [${attributeValue.timestamp}] for key [${attributeTag}] cannot be parsed as a Date.`; + } + + if (timestamp < MIN_DATE_TIMESTAMP || timestamp > MAX_DATE_TIMESTAMP) { + return `The attribute [${attributeValue.timestamp}] for key [${attributeTag}] is out of bounds for a Date. The Date must be between ${MIN_DATE_ISO} and ${MAX_DATE_ISO}`; + } } return undefined; } From 651af12d14678ff0bb294c9045459a1ad65bf902 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 6 Sep 2023 08:42:59 +0200 Subject: [PATCH 230/231] Bump version --- packages/browser-wallet/CHANGELOG.md | 2 +- packages/browser-wallet/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index fb56793f..6382b69f 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.1.7 ### Fixed diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 527289a4..f46979ce 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/browser-wallet", - "version": "1.1.6", + "version": "1.1.7", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", From 6f4d9a9752a964ce48af0c306bc1f315f87e0ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 6 Sep 2023 10:23:14 +0200 Subject: [PATCH 231/231] Aligned signature of method in wallet api interface with implementation --- packages/browser-wallet-api-helpers/src/wallet-api-types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index ac9208be..6baad679 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -152,8 +152,12 @@ interface MainWalletApi { * user will be prompted to select which accounts should be connected. The list of connected accounts is returned * to the caller. If a connection has already been accepted previously, then the returned promise will resolve * with the list of connected accounts. Note that the list of accounts may be empty. + * + * @throws If connection is rejected by the user + * + * @returns {string[]} The list of accounts connected to the application. */ - requestAccounts(): Promise; + requestAccounts(): Promise; /** * Returns some connected account, prioritizing the most recently selected. Resolves with account address or undefined if there are no connected account.