Skip to content

Commit

Permalink
Merge pull request #366 from Concordium/handle-schema-changes
Browse files Browse the repository at this point in the history
Improve handling of changes to schema
  • Loading branch information
orhoj authored Aug 31, 2023
2 parents 9aaf8b9 + be4b63a commit 4dd6ef1
Show file tree
Hide file tree
Showing 15 changed files with 91 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/browser-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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.
- The wallet now ensures that the verifiable credential index used when adding a credential has not already been used in the contract.
- An issue where an invalid Date would result in the epoch timestamp instead of returning an error.
- Enabled ID statement checks for Web3 ID proof requests containing account credential statements.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
<VerifiableCredentialCard
credentialSubject={credential.credentialSubject}
className="add-web3Id-credential__card"
schema={schema}
schema={{ ...schema, usingFallback: false }}
credentialStatus={VerifiableCredentialStatus.Pending}
metadata={metadata}
localization={localization}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import en from './en';

const t: typeof en = {
description: '{{ dapp }} anmoder at du tilføjer denne web3Id Credential til din wallet.',
description: '{{ dapp }} anmoder at du tilføjer denne Web3 ID credential til din wallet.',
accept: 'Tilføj credential',
reject: 'Annuller',
error: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const t = {
description: '{{ dapp }} requests that you add this web3Id Credential to the wallet.',
description: '{{ dapp }} requests that you add this Web3 ID credential to the wallet.',
accept: 'Add credential',
reject: 'Cancel',
error: {
initial:
'We are unable to add the web3Id credential to the wallet due to the following issue, please report this to the issuer:',
'We are unable to add the Web3 ID 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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const Primary: ComponentStory<typeof VerifiableCredentialCard> = () => {
<div style={{ width: 375 }}>
<VerifiableCredentialCard
credentialSubject={credentialSubject}
schema={schema}
schema={{ ...schema, usingFallback: false }}
credentialStatus={VerifiableCredentialStatus.Active}
metadata={metadata}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React, { PropsWithChildren } from 'react';
import { ClassName } from 'wallet-common-helpers';
import clsx from 'clsx';
import { VerifiableCredentialMetadata } from '@shared/utils/verifiable-credential-helpers';
import {
VerifiableCredentialMetadata,
VerifiableCredentialSchemaWithFallback,
} 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';

function Logo({ logo }: { logo: MetadataUrl }) {
Expand Down Expand Up @@ -71,24 +75,40 @@ 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<string, AttributeType>
) {
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,
localization?: Record<string, string>
): (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 } = attributeSchema;
let title = attributeSchema ? attributeSchema.title : value[0];

if (localization) {
const localizedTitle = localization[value[0]];
Expand Down Expand Up @@ -127,7 +147,7 @@ export function VerifiableCredentialCardHeader({

interface CardProps extends ClassName {
credentialSubject: Omit<CredentialSubject, 'id'>;
schema: VerifiableCredentialSchema;
schema: VerifiableCredentialSchemaWithFallback;
credentialStatus: VerifiableCredentialStatus;
metadata: VerifiableCredentialMetadata;
onClick?: () => void;
Expand All @@ -143,6 +163,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)
);
Expand All @@ -152,6 +175,8 @@ export function VerifiableCredentialCard({
<VerifiableCredentialCardHeader credentialStatus={credentialStatus} metadata={metadata} />
{metadata.image && <DisplayImage image={metadata.image} />}
<div className="verifiable-credential__body-attributes">
{schema.usingFallback && <div className="m-b-5">{t('errors.fallbackSchema')}</div>}
{!schemaMatchesCredentialAttributes && <div className="m-b-5">{t('errors.badSchema')}</div>}
{attributes &&
attributes.map((attribute) => (
<DisplayAttribute
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { VerifiableCredential, VerifiableCredentialSchema, 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';
Expand All @@ -17,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';
Expand Down Expand Up @@ -146,7 +147,7 @@ interface CredentialDetailsProps extends ClassName {
credential: VerifiableCredential;
status: VerifiableCredentialStatus;
metadata: VerifiableCredentialMetadata;
schema: VerifiableCredentialSchema;
schema: VerifiableCredentialSchemaWithFallback;
localization?: Record<string, string>;
backButtonOnClick: () => void;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { grpcClientAtom } from '@popup/store/settings';
import { VerifiableCredential, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types';
import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types';
import {
CredentialQueryResponse,
IssuerMetadata,
Expand All @@ -11,6 +11,7 @@ import {
getCredentialRegistryMetadata,
getVerifiableCredentialEntry,
getVerifiableCredentialStatus,
VerifiableCredentialSchemaWithFallback,
} from '@shared/utils/verifiable-credential-helpers';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
Expand Down Expand Up @@ -52,19 +53,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<VerifiableCredentialSchema>();
export function useCredentialSchema(
credential?: VerifiableCredential
): VerifiableCredentialSchemaWithFallback | undefined {
const [schema, setSchema] = useState<VerifiableCredentialSchemaWithFallback>();
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types';
import {
VerifiableCredentialMetadata,
VerifiableCredentialSchemaWithFallback,
getChangesToCredentialMetadata,
getChangesToCredentialSchemas,
} from '@shared/utils/verifiable-credential-helpers';
Expand Down Expand Up @@ -77,7 +78,7 @@ export function VerifiableCredentialCardWithStatusFromChain({
className: string;
onClick?: (
status: VerifiableCredentialStatus,
schema: VerifiableCredentialSchema,
schema: VerifiableCredentialSchemaWithFallback,
metadata: VerifiableCredentialMetadata,
localization?: Record<string, string>
) => void;
Expand Down Expand Up @@ -120,7 +121,7 @@ export default function VerifiableCredentialList() {
const [selected, setSelected] = useState<{
credential: VerifiableCredential;
status: VerifiableCredentialStatus;
schema: VerifiableCredentialSchema;
schema: VerifiableCredentialSchemaWithFallback;
metadata: VerifiableCredentialMetadata;
localization?: Record<string, string>;
}>();
Expand Down Expand Up @@ -192,7 +193,7 @@ export default function VerifiableCredentialList() {
credential={credential}
onClick={(
status: VerifiableCredentialStatus,
schema: VerifiableCredentialSchema,
schema: VerifiableCredentialSchemaWithFallback,
metadata: VerifiableCredentialMetadata,
localization?: Record<string, string>
) => setSelected({ credential, status, schema, metadata, localization })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<VerifiableCredential | undefined>(validCredentials[0]);

const onChange = useCallback((credential: VerifiableCredential) => {
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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
Expand Down Expand Up @@ -49,7 +51,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
*/
Expand Down

0 comments on commit 4dd6ef1

Please sign in to comment.