From c518f138ed636c5634d041ab8eadcffff0c27b2f Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 30 Aug 2023 10:38:39 +0200 Subject: [PATCH] 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 */