Skip to content

Commit e20aeef

Browse files
authored
Merge pull request #578 from Concordium/ui-update/web3id-management
UI update/web3id management
2 parents 42b38b9 + 800886a commit e20aeef

File tree

12 files changed

+291
-41
lines changed

12 files changed

+291
-41
lines changed
Lines changed: 3 additions & 0 deletions
Loading

packages/browser-wallet/src/popup/popupX/pages/SendFunds/Confirm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { logError } from '@shared/utils/log-helpers';
3434
import { submittedTransactionRoute } from '@popup/popupX/constants/routes';
3535

3636
import { CIS2_TRANSFER_NRG_OFFSET, showToken, useTokenMetadata } from './util';
37-
import { UpdateContractSubmittedLocationState } from '../SubmittedTransaction/SubmittedTransaction';
37+
import { CIS2TransferSubmittedLocationState } from '../SubmittedTransaction/SubmittedTransaction';
3838

3939
type Props = {
4040
sender: AccountAddress.Type;
@@ -108,7 +108,7 @@ export default function SendFundsConfirm({ values, fee, sender }: Props) {
108108
}
109109

110110
const tx = await submitTransaction(payload, fee);
111-
const state: UpdateContractSubmittedLocationState = { type: 'cis2.transfer', amount: values.amount, tokenName };
111+
const state: CIS2TransferSubmittedLocationState = { type: 'cis2.transfer', amount: values.amount, tokenName };
112112
nav(submittedTransactionRoute(TransactionHash.fromHexString(tx)), {
113113
state,
114114
});

packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,18 @@ function TransferBody({ transfer }: TransferBodyProps) {
104104
);
105105
}
106106

107-
export type UpdateContractSubmittedLocationState = {
107+
export type CIS2TransferSubmittedLocationState = {
108108
type: 'cis2.transfer';
109109
/** formatted amount */
110110
amount: string;
111111
tokenName: string;
112112
};
113+
114+
export type CIS4RevokeSubmittedLocationState = {
115+
type: 'cis4.revoke';
116+
};
117+
118+
type UpdateContractSubmittedLocationState = CIS2TransferSubmittedLocationState | CIS4RevokeSubmittedLocationState;
113119
function UpdateContractBody() {
114120
const { t } = useTranslation('x', { keyPrefix: 'submittedTransaction.success' });
115121
const { state } = useLocation() as Location & { state: UpdateContractSubmittedLocationState };
@@ -122,6 +128,8 @@ function UpdateContractBody() {
122128
<Text.Capture>{state.tokenName}</Text.Capture>
123129
</>
124130
);
131+
case 'cis4.revoke':
132+
return <Text.Capture>{t('web3Revoke')}</Text.Capture>;
125133
default:
126134
throw new Error('Unsupported');
127135
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ const t = {
66
updated: "You've updated your delegation settings",
77
},
88
configureValidator: {
9-
changeStake: "You've validating with",
10-
removed: "You've stopped validating",
11-
updated: "You've updated your delegation settings",
9+
changeStake: "You're validating with",
10+
removed: "You've removed your validation stake",
11+
updated: "You've updated your validation settings",
1212
},
1313
transfer: {
1414
label: "You've sent",
1515
},
16+
web3Revoke: 'Your credential has been successfully revoked',
1617
},
1718
pending: {
1819
label: 'Transaction in progress',

packages/browser-wallet/src/popup/popupX/pages/Web3Id/Web3Id.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,16 @@
4444
}
4545
}
4646
}
47+
48+
.web3-id-details-x {
49+
&__info {
50+
svg {
51+
width: rem(16px) !important;
52+
height: rem(16px) !important;
53+
54+
& path {
55+
fill: $color-white;
56+
}
57+
}
58+
}
59+
}

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: 184 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,155 @@
1-
import React from 'react';
2-
import Page from '@popup/popupX/shared/Page';
1+
import React, { useEffect, useMemo, useState } from 'react';
32
import { useTranslation } from 'react-i18next';
4-
import Web3IdCard from '@popup/popupX/shared/Web3IdCard';
3+
import { Navigate, useNavigate, useParams } from 'react-router-dom';
54
import { useAtomValue } from 'jotai';
5+
import {
6+
AccountAddress,
7+
AccountTransactionType,
8+
ContractAddress,
9+
HexString,
10+
TransactionHash,
11+
} from '@concordium/web-sdk';
12+
13+
import Page from '@popup/popupX/shared/Page';
14+
import Web3IdCard from '@popup/popupX/shared/Web3IdCard';
615
import { storedVerifiableCredentialsAtom } from '@popup/store/verifiable-credential';
7-
import { ContractAddress, HexString } from '@concordium/web-sdk';
8-
import { Navigate, useParams } from 'react-router-dom';
9-
import { absoluteRoutes } from '@popup/popupX/constants/routes';
10-
import { networkConfigurationAtom } from '@popup/store/settings';
11-
import { createCredentialId } from '@shared/utils/verifiable-credential-helpers';
16+
import { absoluteRoutes, submittedTransactionRoute } from '@popup/popupX/constants/routes';
17+
import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings';
18+
import {
19+
CredentialQueryResponse,
20+
buildRevokeTransaction,
21+
buildRevokeTransactionParameters,
22+
createCredentialId,
23+
getCredentialHolderId,
24+
getCredentialRegistryContractAddress,
25+
getRevokeTransactionExecutionEnergyEstimate,
26+
} from '@shared/utils/verifiable-credential-helpers';
27+
import Button from '@popup/popupX/shared/Button';
28+
import Stop from '@assets/svgX/stop.svg';
29+
import Info from '@assets/svgX/info.svg';
30+
import { useCredentialEntry, useCredentialStatus } from '@popup/popupX/shared/utils/verifiable-credentials';
31+
import FullscreenNotice, { FullscreenNoticeProps } from '@popup/popupX/shared/FullscreenNotice';
32+
import { displayNameOrSplitAddress, useHdWallet, useSelectedCredential } from '@popup/shared/utils/account-helpers';
33+
import { fetchContractName } from '@shared/utils/token-helpers';
34+
import Text from '@popup/popupX/shared/Text';
35+
import {
36+
ConfirmedCredential,
37+
CreationStatus,
38+
VerifiableCredential,
39+
VerifiableCredentialStatus,
40+
} from '@shared/storage/types';
41+
import { displayAsCcd, noOp, useAsyncMemo } from 'wallet-common-helpers';
42+
import {
43+
TransactionSubmitError,
44+
TransactionSubmitErrorType,
45+
useGetTransactionFee,
46+
useTransactionSubmit,
47+
} from '@popup/shared/utils/transaction-helpers';
48+
import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage';
49+
import { CIS4RevokeSubmittedLocationState } from '../SubmittedTransaction/SubmittedTransaction';
50+
51+
type ConfirmRevocationProps = FullscreenNoticeProps & {
52+
credential: VerifiableCredential;
53+
entry: CredentialQueryResponse | undefined;
54+
walletCred: ConfirmedCredential;
55+
};
56+
57+
function ConfirmRevocation({ credential, entry, walletCred, ...props }: ConfirmRevocationProps) {
58+
const { t } = useTranslation('x', { keyPrefix: 'web3Id.details.confirmRevoke' });
59+
const hdWallet = useHdWallet();
60+
const client = useAtomValue(grpcClientAtom);
61+
const getFee = useGetTransactionFee();
62+
const nav = useNavigate();
63+
const submitTransaction = useTransactionSubmit(
64+
AccountAddress.fromBase58(walletCred.address),
65+
AccountTransactionType.Update
66+
);
67+
const [error, setError] = useState<Error>();
68+
69+
const payload = useAsyncMemo(
70+
async () => {
71+
if (entry === undefined || hdWallet === undefined || credential === undefined) {
72+
return undefined;
73+
}
74+
75+
const contractAddress = getCredentialRegistryContractAddress(credential.id);
76+
const credentialId = getCredentialHolderId(credential.id);
77+
const contractName = await fetchContractName(client, contractAddress.index, contractAddress.subindex);
78+
if (contractName === undefined) {
79+
throw new Error(`Unable to find contract name for address: ${contractAddress}`);
80+
}
81+
82+
const signingKey = hdWallet
83+
.getVerifiableCredentialSigningKey(contractAddress, credential.index)
84+
.toString('hex');
85+
86+
const parameters = await buildRevokeTransactionParameters(
87+
contractAddress,
88+
credentialId,
89+
entry.revocationNonce,
90+
signingKey
91+
);
92+
const maxExecutionEnergy = await getRevokeTransactionExecutionEnergyEstimate(
93+
client,
94+
contractName,
95+
parameters
96+
);
97+
return buildRevokeTransaction(contractAddress, contractName, credentialId, maxExecutionEnergy, parameters);
98+
},
99+
noOp,
100+
[client, credential, hdWallet, entry]
101+
);
102+
const fee = useMemo(
103+
() => (payload === undefined ? undefined : getFee(AccountTransactionType.Update, payload)),
104+
[payload, getFee]
105+
);
106+
107+
const submit = async () => {
108+
if (fee === undefined || payload === undefined) {
109+
throw Error('Fee could not be calculated');
110+
}
111+
try {
112+
const tx = await submitTransaction(payload, fee);
113+
const submittedState: CIS4RevokeSubmittedLocationState = { type: 'cis4.revoke' };
114+
nav(submittedTransactionRoute(TransactionHash.fromHexString(tx)), { state: submittedState });
115+
} catch (e) {
116+
if (e instanceof Error) {
117+
setError(e);
118+
}
119+
}
120+
};
121+
122+
useEffect(() => {
123+
setError(undefined);
124+
}, [props.open]);
125+
126+
return (
127+
<FullscreenNotice {...props}>
128+
<Page>
129+
<Page.Top heading={t('title')} />
130+
<Text.Capture>{t('description')}</Text.Capture>
131+
<Text.Capture className="m-t-30">
132+
{t('fee')}
133+
<br />
134+
{fee === undefined ? '...' : displayAsCcd(fee, false, true)}
135+
</Text.Capture>
136+
<Text.Capture className="m-t-5">
137+
{t('account')}
138+
<br />
139+
{displayNameOrSplitAddress(walletCred)}
140+
</Text.Capture>
141+
{error instanceof TransactionSubmitError &&
142+
error.type === TransactionSubmitErrorType.InsufficientFunds && (
143+
<ErrorMessage className="m-t-10 text-center">{t('error.insufficientFunds')}</ErrorMessage>
144+
)}
145+
<Page.Footer>
146+
<Button.Main label={t('buttonContinue')} onClick={submit} disabled={payload === undefined} />
147+
<Button.Main label={t('buttonCancel')} onClick={props.onClose} />
148+
</Page.Footer>
149+
</Page>
150+
</FullscreenNotice>
151+
);
152+
}
12153

13154
type Props = {
14155
contract: ContractAddress.Type;
@@ -20,17 +161,46 @@ function Web3IdDetailsParsed({ id, contract }: Props) {
20161
const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom);
21162
const net = useAtomValue(networkConfigurationAtom);
22163
const credential = verifiableCredentials.value.find((c) => c.id === createCredentialId(id, contract, net));
164+
const [showInfo, setShowInfo] = useState(false);
165+
const [showConfirm, setShowConfirm] = useState(false);
166+
const credentialEntry = useCredentialEntry(credential);
167+
const walletCredential = useSelectedCredential();
168+
const status = useCredentialStatus(credential);
23169

24170
if (verifiableCredentials.loading) return null;
25171
if (credential === undefined) throw new Error('Expected to find credential');
26172

173+
const canRevoke =
174+
walletCredential?.status === CreationStatus.Confirmed &&
175+
credentialEntry?.credentialInfo.holderRevocable &&
176+
status !== undefined &&
177+
[VerifiableCredentialStatus.Active, VerifiableCredentialStatus.NotActivated].includes(status);
178+
27179
return (
28-
<Page>
29-
<Page.Top heading={t('title')} />
30-
<Page.Main>
31-
<Web3IdCard credential={credential} />
32-
</Page.Main>
33-
</Page>
180+
<>
181+
{canRevoke && (
182+
<ConfirmRevocation
183+
open={showConfirm}
184+
onClose={() => setShowConfirm(false)}
185+
credential={credential}
186+
entry={credentialEntry}
187+
walletCred={walletCredential}
188+
/>
189+
)}
190+
<Page>
191+
<Page.Top heading={t('title')}>
192+
{canRevoke && <Button.Icon icon={<Stop />} onClick={() => setShowConfirm(true)} />}
193+
<Button.Icon
194+
className="web3-id-details-x__info"
195+
icon={<Info />}
196+
onClick={() => setShowInfo((v) => !v)}
197+
/>
198+
</Page.Top>
199+
<Page.Main>
200+
<Web3IdCard showInfo={showInfo} credential={credential} />
201+
</Page.Main>
202+
</Page>
203+
</>
34204
);
35205
}
36206

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ const t = {
1010
},
1111
details: {
1212
title: 'Credential details',
13+
confirmRevoke: {
14+
title: 'Revoke credential',
15+
description: "You're about to revoke your Web3 ID. This is an irreversible action.",
16+
buttonContinue: 'Revoke credential',
17+
buttonCancel: 'Cancel',
18+
fee: 'Estimated transaction fee',
19+
account: 'Selected account',
20+
error: { insufficientFunds: 'Insufficient funds on selected account to cover transaction fee' },
21+
},
1322
},
1423
};
1524

0 commit comments

Comments
 (0)