Skip to content

Commit

Permalink
Merge pull request #348 from Concordium/show-pending-verifiable-crede…
Browse files Browse the repository at this point in the history
…ntial

Display pending verifiable credentials
  • Loading branch information
shjortConcordium authored Aug 24, 2023
2 parents cda5a3e + 6392ea9 commit 88e053b
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 57 deletions.
3 changes: 2 additions & 1 deletion packages/browser-wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## Unreleased
## 1.1.2

### Added

Expand All @@ -11,6 +11,7 @@

- Incorrect verifiable presentations created, due to incorrect identity/identityProviderIndex used.
- 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)
- Show verifiable credentials in overview before they are put on chain.

## 1.1.1

Expand Down
2 changes: 1 addition & 1 deletion packages/browser-wallet/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useLocation } from 'react-router-dom';
import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout';
import Button from '@popup/shared/Button';
import {
sessionTemporaryVerifiableCredentialMetadataUrlsAtom,
sessionTemporaryVerifiableCredentialsAtom,
storedVerifiableCredentialMetadataAtom,
storedVerifiableCredentialsAtom,
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
const { onClose, withClose } = useContext(fullscreenPromptContext);
const [acceptButtonDisabled, setAcceptButtonDisabled] = useState<boolean>(false);
const [web3IdCredentials, setWeb3IdCredentials] = useAtom(sessionTemporaryVerifiableCredentialsAtom);
const [metadataUrls, setMetadataUrls] = useAtom(sessionTemporaryVerifiableCredentialMetadataUrlsAtom);
const storedWeb3IdCredentials = useAtomValue(storedVerifiableCredentialsAtom);
const [verifiableCredentialMetadata, setVerifiableCredentialMetadata] = useAtom(
storedVerifiableCredentialMetadataAtom
Expand Down Expand Up @@ -163,18 +165,20 @@ 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]);
if (metadata) {
const newMetadata = { ...verifiableCredentialMetadata.value };
newMetadata[metadataUrl.url] = metadata;
await setVerifiableCredentialMetadata(newMetadata);
await setMetadataUrls({ ...metadataUrls.value, [credentialId]: metadataUrl.url });
}
return credentialSubjectId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 (
<div className="verifiable-credential__body-attributes">
<DisplayAttribute
attributeKey="credentialHolderId"
attributeTitle={t('details.id')}
attributeValue={credentialEntry.credentialInfo.credentialHolderId}
/>
<DisplayAttribute
attributeKey="validFrom"
attributeTitle={t('details.validFrom')}
attributeValue={validFromFormatted}
/>
{credentialEntry.credentialInfo.validUntil !== undefined && (
<DisplayAttribute
attributeKey="validUntil"
attributeTitle={t('details.validUntil')}
attributeValue={validUntilFormatted}
/>
)}
</div>
);
}

/**
* 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.
Expand All @@ -84,43 +126,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 (
<div className="verifiable-credential-wrapper">
<div className={`verifiable-credential ${className}`} style={{ backgroundColor: metadata.backgroundColor }}>
<VerifiableCredentialCardHeader credentialStatus={status} metadata={metadata} />
<div className="verifiable-credential__body-attributes">
<DisplayAttribute
attributeKey="credentialHolderId"
attributeTitle={t('details.id')}
attributeValue={credentialEntry.credentialInfo.credentialHolderId}
/>
<DisplayAttribute
attributeKey="validFrom"
attributeTitle={t('details.validFrom')}
attributeValue={validFromFormatted}
/>
{credentialEntry.credentialInfo.validUntil !== undefined && (
<DisplayAttribute
attributeKey="validUntil"
attributeTitle={t('details.validUntil')}
attributeValue={validUntilFormatted}
/>
)}
</div>
<DisplayCredentialEntryInfo credentialEntry={credentialEntry} />
<DisplayIssuerMetadata issuer={issuer} />
</div>
</div>
Expand Down Expand Up @@ -195,10 +210,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) {
Expand Down Expand Up @@ -227,9 +238,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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import { useEffect, useState } from 'react';
import {
storedVerifiableCredentialMetadataAtom,
storedVerifiableCredentialSchemasAtom,
sessionTemporaryVerifiableCredentialMetadataUrlsAtom,
} 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.
Expand Down Expand Up @@ -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]);

Expand All @@ -97,18 +101,31 @@ export function useCredentialMetadata(credential?: VerifiableCredential) {
const [metadata, setMetadata] = useState<VerifiableCredentialMetadata>();
const credentialEntry = useCredentialEntry(credential);
const storedMetadata = useAtomValue(storedVerifiableCredentialMetadataAtom);
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);
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!`
);
}
}, [storedMetadata, credentialEntry]);
setMetadata(storedCredentialMetadata);
}, [storedMetadata.loading, tempMetadata.loading, credentialEntry, credential?.id]);

return metadata;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/browser-wallet/src/popup/store/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
storedAllowlist,
storedVerifiableCredentialMetadata,
sessionVerifiableCredentials,
sessionVerifiableCredentialMetadataUrls,
} from '@shared/storage/access';
import { ChromeStorageKey } from '@shared/storage/types';
import { atom, PrimitiveAtom, WritableAtom } from 'jotai';
Expand Down Expand Up @@ -66,6 +67,10 @@ const accessorMap: Record<ChromeStorageKey, StorageAccessor<any>> = {
[ChromeStorageKey.VerifiableCredentialMetadata]: storedVerifiableCredentialMetadata,
[ChromeStorageKey.Allowlist]: storedAllowlist,
[ChromeStorageKey.TemporaryVerifiableCredentials]: useIndexedStorage(sessionVerifiableCredentials, getGenesisHash),
[ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls]: useIndexedStorage(
sessionVerifiableCredentialMetadataUrls,
getGenesisHash
),
};

export function resetOnUnmountAtom<V>(initial: V): PrimitiveAtom<V> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ export const storedVerifiableCredentialMetadataAtom = atomWithChromeStorage<
export const sessionTemporaryVerifiableCredentialsAtom = atomWithChromeStorage<
Omit<VerifiableCredential, 'signature' | 'randomness'>[]
>(ChromeStorageKey.TemporaryVerifiableCredentials, [], true);

export const sessionTemporaryVerifiableCredentialMetadataUrlsAtom = atomWithChromeStorage<Record<string, string>>(
ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls,
{},
true
);
5 changes: 5 additions & 0 deletions packages/browser-wallet/src/shared/storage/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,8 @@ export const sessionPendingTransactions = makeIndexedStorageAccessor<string[]>(
export const sessionVerifiableCredentials = makeSerializedAndIndexedStorageAccessor<
Omit<VerifiableCredential, 'signature' | 'randomness'>[]
>('session', ChromeStorageKey.TemporaryVerifiableCredentials);

export const sessionVerifiableCredentialMetadataUrls = makeIndexedStorageAccessor<Record<string, string>>(
'session',
ChromeStorageKey.TemporaryVerifiableCredentialMetadataUrls
);
1 change: 1 addition & 0 deletions packages/browser-wallet/src/shared/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum ChromeStorageKey {
VerifiableCredentialSchemas = 'verifiableCredentialSchemas',
VerifiableCredentialMetadata = 'verifiableCredentialMetadata',
TemporaryVerifiableCredentials = 'tempVerifiableCredentials',
TemporaryVerifiableCredentialMetadataUrls = 'tempVerifiableCredentialMetadataUrls',
Allowlist = 'allowlist',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down

0 comments on commit 88e053b

Please sign in to comment.