Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display pending verifiable credentials #348

Merged
merged 6 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -10,6 +10,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 @@ -144,18 +146,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
Loading