Skip to content

Commit

Permalink
Improve handling of changes to schema
Browse files Browse the repository at this point in the history
  • Loading branch information
orhoj committed Aug 30, 2023
1 parent ea91d98 commit c518f13
Show file tree
Hide file tree
Showing 16 changed files with 107 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 @@ -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

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 @@ -229,7 +229,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
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -71,24 +77,43 @@ 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 = value[0];
if (attributeSchema) {
title = attributeSchema.title;
}
let { title } = attributeSchema;

if (localization) {
const localizedTitle = localization[value[0]];
Expand Down Expand Up @@ -127,7 +152,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 +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)
);
Expand All @@ -152,6 +180,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,10 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types';
import {
VerifiableCredential,
VerifiableCredentialSchemaWithFallback,
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 Down Expand Up @@ -146,7 +150,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,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,
Expand Down Expand Up @@ -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<VerifiableCredentialSchema>();
export function useCredentialSchema(
credential?: VerifiableCredential
): VerifiableCredentialSchemaWithFallback | undefined {
const [schema, setSchema] = useState<VerifiableCredentialSchema & { usingFallback: boolean }>();
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 @@ -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,
Expand Down Expand Up @@ -77,7 +82,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 +125,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 +197,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
2 changes: 2 additions & 0 deletions packages/browser-wallet/src/shared/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,5 @@ export interface VerifiableCredentialSchema {
properties: SchemaProperties;
required: string[];
}

export type VerifiableCredentialSchemaWithFallback = VerifiableCredentialSchema & { usingFallback: boolean };
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down

0 comments on commit c518f13

Please sign in to comment.