Skip to content

Commit d32c9a3

Browse files
committed
Show credential info
1 parent 090eaf4 commit d32c9a3

File tree

5 files changed

+72
-23
lines changed

5 files changed

+72
-23
lines changed

packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdCredentials.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
import React from 'react';
2-
import Plus from '@assets/svgX/plus.svg';
32
import { useTranslation } from 'react-i18next';
3+
import { useNavigate } from 'react-router-dom';
4+
import { useAtomValue } from 'jotai';
5+
6+
import Plus from '@assets/svgX/plus.svg';
47
import Page from '@popup/popupX/shared/Page';
58
import Button from '@popup/popupX/shared/Button';
69
import Web3IdCard from '@popup/popupX/shared/Web3IdCard';
7-
import { useNavigate } from 'react-router-dom';
810
import { relativeRoutes, web3IdDetailsRoute } from '@popup/popupX/constants/routes';
9-
import { useAtomValue } from 'jotai';
1011
import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential';
1112
import { VerifiableCredential } from '@shared/storage/types';
12-
import { ContractAddress } from '@concordium/web-sdk';
13+
import { parseCredentialDID } from '@shared/utils/verifiable-credential-helpers';
1314

1415
export default function Web3IdCredentials() {
1516
const { t } = useTranslation('x', { keyPrefix: 'web3Id.credentials' });
1617
const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom);
1718
const nav = useNavigate();
1819

1920
const toDetails = (vc: VerifiableCredential) => {
20-
const [, index, subindex, id] =
21-
vc.id.match(/.*:sci:(\d*):(\d*)\/credentialEntry\/(.*)$/) ??
22-
(() => {
23-
throw new Error('Invalid ID found in verifiable credential');
24-
})();
25-
const contract = ContractAddress.create(BigInt(index), BigInt(subindex));
21+
const [contract, id] = parseCredentialDID(vc.id);
2622
nav(web3IdDetailsRoute(contract, id));
2723
};
2824

packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3IdDetails.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { Navigate, useParams } from 'react-router-dom';
44
import { useAtomValue } from 'jotai';
@@ -24,6 +24,7 @@ function Web3IdDetailsParsed({ id, contract }: Props) {
2424
const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom);
2525
const net = useAtomValue(networkConfigurationAtom);
2626
const credential = verifiableCredentials.value.find((c) => c.id === createCredentialId(id, contract, net));
27+
const [showInfo, setShowInfo] = useState(false);
2728

2829
if (verifiableCredentials.loading) return null;
2930
if (credential === undefined) throw new Error('Expected to find credential');
@@ -32,10 +33,14 @@ function Web3IdDetailsParsed({ id, contract }: Props) {
3233
<Page>
3334
<Page.Top heading={t('title')}>
3435
<Button.Icon icon={<Stop />} />
35-
<Button.Icon className="web3-id-details-x__info" icon={<Info />} />
36+
<Button.Icon
37+
className="web3-id-details-x__info"
38+
icon={<Info />}
39+
onClick={() => setShowInfo((v) => !v)}
40+
/>
3641
</Page.Top>
3742
<Page.Main>
38-
<Web3IdCard credential={credential} />
43+
<Web3IdCard showInfo={showInfo} credential={credential} />
3944
</Page.Main>
4045
</Page>
4146
);

packages/browser-wallet/src/popup/popupX/shared/Web3IdCard/Web3IdCard.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { ClassName } from 'wallet-common-helpers';
1+
import React, { useMemo } from 'react';
2+
import { ClassName, TimeStampUnit, dateFromTimestamp } from 'wallet-common-helpers';
33
import { AttributeType } from '@concordium/web-sdk';
44
import clsx from 'clsx';
55
import { useTranslation } from 'react-i18next';
@@ -13,15 +13,18 @@ import {
1313
VerifiableCredentialSchema,
1414
VerifiableCredentialStatus,
1515
} from '@shared/storage/types';
16-
16+
import { parseCredentialDID } from '@shared/utils/verifiable-credential-helpers';
1717
import Img from '@popup/shared/Img';
18+
1819
import {
1920
defaultFormatAttribute,
21+
useCredentialEntry,
2022
useCredentialLocalization,
2123
useCredentialMetadata,
2224
useCredentialSchema,
2325
useCredentialStatus,
2426
} from '../utils/verifiable-credentials';
27+
import { withDateAndTime } from '@shared/utils/time-helpers';
2528

2629
/**
2730
* Component for displaying the status of a verifiable credential.
@@ -147,14 +150,16 @@ function applySchemaAndLocalization(
147150

148151
type Props = Pick<ViewProps, 'className'> & {
149152
credential: VerifiableCredential;
153+
showInfo?: boolean;
150154
};
151155

152-
export default function Web3IdCard({ credential, ...viewProps }: Props) {
153-
const { t } = useTranslation('x', { keyPrefix: 'sharedX.web3IdCard.warning' });
156+
export default function Web3IdCard({ credential, showInfo = false, ...viewProps }: Props) {
157+
const { t } = useTranslation('x', { keyPrefix: 'sharedX.web3IdCard' });
154158
const status = useCredentialStatus(credential);
155159
const schema = useCredentialSchema(credential);
156160
const metadata = useCredentialMetadata(credential);
157161
const localization = useCredentialLocalization(credential);
162+
const entry = useCredentialEntry(credential);
158163

159164
// Render nothing until all the required data is available.
160165
if (!schema || !metadata || localization.loading || status === undefined) {
@@ -165,15 +170,38 @@ export default function Web3IdCard({ credential, ...viewProps }: Props) {
165170
schema,
166171
credential.credentialSubject.attributes
167172
);
168-
const attributes = Object.entries(credential.credentialSubject.attributes).map(
169-
applySchemaAndLocalization(schema, localization.result)
170-
);
173+
174+
let attributes: AttributeView[] = [];
175+
if (showInfo && entry !== undefined) {
176+
const [contract, id] = parseCredentialDID(credential.id);
177+
if (!entry) {
178+
return null;
179+
}
180+
181+
const validFrom = dateFromTimestamp(entry.credentialInfo.validFrom, TimeStampUnit.milliSeconds);
182+
const validFromFormatted = withDateAndTime(validFrom);
183+
attributes = [
184+
{ title: t('details.id'), value: id },
185+
{ title: t('details.contract'), value: contract.toString() },
186+
{ title: t('details.validFrom'), value: validFromFormatted },
187+
];
188+
189+
if (entry.credentialInfo.validUntil !== undefined) {
190+
const validUntil = dateFromTimestamp(entry.credentialInfo.validUntil, TimeStampUnit.milliSeconds);
191+
const validUntilFormatted = withDateAndTime(validUntil);
192+
attributes.push({ title: t('details.validUntil'), value: validUntilFormatted });
193+
}
194+
} else if (!showInfo) {
195+
attributes = Object.entries(credential.credentialSubject.attributes).map(
196+
applySchemaAndLocalization(schema, localization.result)
197+
);
198+
}
171199

172200
let warning: string | undefined;
173201
if (!schemaMatchesCredentialAttributes) {
174-
warning = t('schemaMismatch');
202+
warning = t('warning.schemaMismatch');
175203
} else if (schema.usingFallback) {
176-
warning = t('fallback');
204+
warning = t('warning.fallback');
177205
}
178206

179207
return (

packages/browser-wallet/src/popup/popupX/shared/i18n/en.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ const t = {
5959
schemaMismatch: 'Attributes found do not match credential schema',
6060
fallback: 'Using fallback credential chema',
6161
},
62+
details: {
63+
id: 'Credential holder ID',
64+
contract: 'Contract address',
65+
validFrom: 'Valid from',
66+
validUntil: 'Valid until',
67+
},
6268
},
6369
};
6470

packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Energy,
1010
Parameter,
1111
ReturnValue,
12+
HexString,
1213
} from '@concordium/web-sdk';
1314
import * as ed from '@noble/ed25519';
1415
import {
@@ -1132,6 +1133,19 @@ export function createCredentialId(
11321133
}/credentialEntry/${credentialHolderId}`;
11331134
}
11341135

1136+
/**
1137+
* Parse the {@linkcode ContractAddress.Type} and holder ID from the given DID string
1138+
*/
1139+
export function parseCredentialDID(did: string): [ContractAddress.Type, HexString] {
1140+
const [, index, subindex, id] =
1141+
did.match(/.*:sci:(\d*):(\d*)\/credentialEntry\/(.*)$/) ??
1142+
(() => {
1143+
throw new Error('Invalid ID found in verifiable credential');
1144+
})();
1145+
const contract = ContractAddress.create(BigInt(index), BigInt(subindex));
1146+
return [contract, id];
1147+
}
1148+
11351149
/**
11361150
* Extracts the network from any concordium DID identitifer.
11371151
* Note that if the network is not present in the DID, then 'mainnet' is returned, per the specifiction, see https://proposals.concordium.software/ID/concordium-did.html#identifier-syntax.

0 commit comments

Comments
 (0)