Skip to content

Commit 361dc95

Browse files
authored
Merge pull request #566 from Concordium/x-create-account
Add create account pages
2 parents 6257935 + ad9e6b6 commit 361dc95

File tree

18 files changed

+403
-80
lines changed

18 files changed

+403
-80
lines changed
Lines changed: 6 additions & 0 deletions
Loading

packages/browser-wallet/src/popup/popupX/constants/routes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ export const relativeRoutes = {
133133
path: 'private-key/:account',
134134
},
135135
},
136+
createAccount: {
137+
path: 'create-account',
138+
confirm: {
139+
path: 'confirm/:identityProviderIndex/:identityIndex',
140+
},
141+
config: {
142+
backTitle: '',
143+
},
144+
},
136145
seedPhrase: {
137146
path: 'seedPhrase',
138147
config: {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useCallback } from 'react';
22
import Plus from '@assets/svgX/plus.svg';
33
import Arrows from '@assets/svgX/arrows-down-up.svg';
44
import MagnifyingGlass from '@assets/svgX/magnifying-glass.svg';
@@ -112,12 +112,14 @@ function AccountListItem({ credential }: AccountListItemProps) {
112112
export default function Accounts() {
113113
const { t } = useTranslation('x', { keyPrefix: 'accounts' });
114114
const accounts = useAtomValue(credentialsAtom);
115+
const nav = useNavigate();
116+
const navToCreateAccount = useCallback(() => nav(absoluteRoutes.settings.createAccount.path), []);
115117
return (
116118
<Page className="accounts-x">
117119
<Page.Top heading={t('accounts')}>
118120
<Button.Icon icon={<Arrows />} />
119121
<Button.Icon icon={<MagnifyingGlass />} />
120-
<Button.Icon icon={<Plus />} />
122+
<Button.Icon icon={<Plus />} onClick={navToCreateAccount} />
121123
</Page.Top>
122124
<Page.Main>
123125
{accounts.map((item) => (
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.create-account-x {
2+
.id-card-button {
3+
margin-top: rem(14px);
4+
padding: unset;
5+
background-color: unset;
6+
border: none;
7+
text-align: left;
8+
cursor: pointer;
9+
}
10+
11+
.justify-content-center {
12+
display: flex;
13+
justify-content: center;
14+
}
15+
16+
.page__footer {
17+
gap: rem(24px) !important;
18+
}
19+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useMemo } from 'react';
2+
import Page from '@popup/popupX/shared/Page';
3+
import Text from '@popup/popupX/shared/Text';
4+
import { useTranslation } from 'react-i18next';
5+
import { identitiesAtom } from '@popup/store/identity';
6+
import { useAtomValue } from 'jotai';
7+
import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types';
8+
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';
9+
import Button from '@popup/popupX/shared/Button';
10+
import { generatePath, useNavigate } from 'react-router-dom';
11+
import { absoluteRoutes } from '@popup/popupX/constants/routes';
12+
import { compareYearMonth, getCurrentYearMonth } from 'wallet-common-helpers';
13+
14+
/**
15+
* Get the valid identities, which is Identities that are confirmed by the ID provider and are not
16+
* expired.
17+
* This is not recomputed by change of current time, meaning the returned identities might become
18+
* expired over time.
19+
*/
20+
function useValidIdentities(): ConfirmedIdentity[] {
21+
const identities = useAtomValue(identitiesAtom);
22+
return useMemo(() => {
23+
const now = getCurrentYearMonth();
24+
return identities.flatMap((id) => {
25+
if (id.status !== CreationStatus.Confirmed) {
26+
return [];
27+
}
28+
// Negative number is indicating that `validTo` is before `now`, therefore expired.
29+
const isExpired = compareYearMonth(id.idObject.value.attributeList.validTo, now) < 0;
30+
if (isExpired) {
31+
return [];
32+
}
33+
return [id];
34+
});
35+
}, [identities]);
36+
}
37+
38+
export default function CreateAccount() {
39+
const { t } = useTranslation('x', { keyPrefix: 'createAccount' });
40+
const nav = useNavigate();
41+
const navToCreateAccountConfirm = (identity: ConfirmedIdentity) => () =>
42+
nav(
43+
generatePath(absoluteRoutes.settings.createAccount.confirm.path, {
44+
identityProviderIndex: identity.providerIndex.toString(),
45+
identityIndex: identity.index.toString(),
46+
})
47+
);
48+
const validIdentities = useValidIdentities();
49+
50+
return (
51+
<Page className="create-account-x">
52+
<Page.Top heading={t('selectIdentity')} />
53+
<Page.Main>
54+
<Text.MainRegular>{t('selectIdentityDescription')}</Text.MainRegular>
55+
{validIdentities.length === 0 ? (
56+
<p className="m-t-40">
57+
<Text.Capture>{t('noValidIdentities')}</Text.Capture>
58+
</p>
59+
) : (
60+
validIdentities.map((id) => (
61+
<Button.Base
62+
className="id-card-button"
63+
key={`${id.providerIndex}:${id.index}`}
64+
onClick={navToCreateAccountConfirm(id)}
65+
>
66+
<ConfirmedIdCard identity={id} shownAttributes={['idDocType', 'idDocNo']} hideAccounts />
67+
</Button.Base>
68+
))
69+
)}
70+
</Page.Main>
71+
</Page>
72+
);
73+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { useCallback } from 'react';
2+
import Page from '@popup/popupX/shared/Page';
3+
import { useTranslation } from 'react-i18next';
4+
import { Navigate, useNavigate, useParams } from 'react-router-dom';
5+
import Button from '@popup/popupX/shared/Button';
6+
import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity';
7+
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
8+
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';
9+
import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types';
10+
import FatArrowUp from '@assets/svgX/fat-arrow-up.svg';
11+
import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings';
12+
import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers';
13+
import { addToastAtom } from '@popup/state';
14+
import { creatingCredentialRequestAtom, credentialsAtom } from '@popup/store/account';
15+
import { getGlobal, getNet } from '@shared/utils/network-helpers';
16+
import { isIdentityOfCredential } from '@shared/utils/identity-helpers';
17+
import { getNextEmptyCredNumber } from '@popup/shared/utils/account-helpers';
18+
import { popupMessageHandler } from '@popup/shared/message-handler';
19+
import { InternalMessageType } from '@messaging';
20+
import { CredentialDeploymentBackgroundResponse } from '@shared/utils/types';
21+
import { absoluteRoutes } from '@popup/popupX/constants/routes';
22+
23+
/**
24+
* Hook providing function for sending credential deployments.
25+
*/
26+
function useSendCredentialDeployment() {
27+
const providers = useAtomValue(identityProvidersAtom);
28+
const credentials = useAtomValue(credentialsAtom);
29+
const network = useAtomValue(networkConfigurationAtom);
30+
const addToast = useSetAtom(addToastAtom);
31+
const seedPhrase = useDecryptedSeedPhrase((e) => addToast(e.message));
32+
const client = useAtomValue(grpcClientAtom);
33+
34+
const loading = !seedPhrase || !network || !providers || !credentials;
35+
const sendCredentialDeployment = useCallback(
36+
async (identity: ConfirmedIdentity) => {
37+
if (loading) {
38+
throw new Error('Still loading relevant information');
39+
}
40+
41+
const identityProvider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex);
42+
if (!identityProvider) {
43+
throw new Error('provider not found');
44+
}
45+
const global = await getGlobal(client);
46+
47+
// Make request
48+
const credsOfCurrentIdentity = credentials.filter(isIdentityOfCredential(identity));
49+
const credNumber = getNextEmptyCredNumber(credsOfCurrentIdentity);
50+
51+
const response: CredentialDeploymentBackgroundResponse = await popupMessageHandler.sendInternalMessage(
52+
InternalMessageType.SendCredentialDeployment,
53+
{
54+
globalContext: global,
55+
ipInfo: identityProvider.ipInfo,
56+
arsInfos: identityProvider.arsInfos,
57+
seedAsHex: seedPhrase,
58+
net: getNet(network),
59+
idObject: identity.idObject.value,
60+
revealedAttributes: [],
61+
identityIndex: identity.index,
62+
credNumber,
63+
}
64+
);
65+
return response;
66+
},
67+
[seedPhrase, network, providers, credentials, loading]
68+
);
69+
70+
return { loading, sendCredentialDeployment };
71+
}
72+
73+
type CreateAccountConfirmProps = {
74+
identityProviderIndex: number;
75+
identityIndex: number;
76+
};
77+
78+
function ConfirmInfo({ identityProviderIndex, identityIndex }: CreateAccountConfirmProps) {
79+
const { t } = useTranslation('x', { keyPrefix: 'createAccount' });
80+
const identities = useAtomValue(identitiesAtom);
81+
const identity = identities.find((id) => id.providerIndex === identityProviderIndex && id.index === identityIndex);
82+
const [creatingCredentialRequest, setCreatingRequest] = useAtom(creatingCredentialRequestAtom);
83+
const deployment = useSendCredentialDeployment();
84+
const nav = useNavigate();
85+
const onCreateAccount = useCallback(async () => {
86+
if (identity === undefined || identity.status !== CreationStatus.Confirmed) {
87+
throw new Error(`Invalid identity: ${identity}`);
88+
}
89+
setCreatingRequest(true);
90+
deployment
91+
.sendCredentialDeployment(identity)
92+
.catch(() => {})
93+
.then(() => {
94+
nav(absoluteRoutes.home.path);
95+
})
96+
.finally(() => {
97+
setCreatingRequest(false);
98+
});
99+
}, [deployment.sendCredentialDeployment]);
100+
101+
if (identity === undefined) {
102+
return null;
103+
}
104+
if (identity.status !== CreationStatus.Confirmed) {
105+
return <Navigate to="../" />;
106+
}
107+
const loading = creatingCredentialRequest.loading || creatingCredentialRequest.value || deployment.loading;
108+
return (
109+
<>
110+
<div className="justify-content-center">
111+
<FatArrowUp />
112+
</div>
113+
<ConfirmedIdCard identity={identity} shownAttributes={['idDocType', 'idDocNo']} hideAccounts />
114+
<Button.Main disabled={loading} type="submit" label={t('confirmButton')} onClick={onCreateAccount} />
115+
</>
116+
);
117+
}
118+
119+
export default function CreateAccountConfirm() {
120+
const params = useParams();
121+
if (params.identityProviderIndex === undefined || params.identityIndex === undefined) {
122+
// No account address passed in the url.
123+
return <Navigate to="../" />;
124+
}
125+
const identityIndex = parseInt(params.identityIndex, 10);
126+
const identityProviderIndex = parseInt(params.identityProviderIndex, 10);
127+
if (Number.isNaN(identityProviderIndex) || Number.isNaN(identityIndex)) {
128+
return <Navigate to="../" />;
129+
}
130+
return (
131+
<Page className="create-account-x">
132+
<Page.Footer>
133+
<ConfirmInfo identityIndex={identityIndex} identityProviderIndex={identityProviderIndex} />
134+
</Page.Footer>
135+
</Page>
136+
);
137+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const t = {
2+
selectIdentity: 'Select an identity',
3+
noValidIdentities:
4+
'There are currently no confirmed and non-expired identities. Please issue a new identity first.',
5+
selectIdentityDescription: `The ID Documents (e.g. Passport pictures) that are used for the ID verification, are held exclusively by our trusted, third-party identity providers in their own off-chain records. This means your ID data will not be share on-chain.
6+
7+
Which identity do you want to use to create the account?`,
8+
confirmButton: 'Create account',
9+
};
10+
11+
export default t;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './CreateAccount';
2+
export { default as CreateAccountConfirm } from './CreateAccountConfirm';

packages/browser-wallet/src/popup/popupX/pages/CreateAccount/useSendCredentialDeployment.tsx

Whitespace-only changes.

packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers';
2020
import { submittedTransactionRoute } from '@popup/popupX/constants/routes';
2121
import Text from '@popup/popupX/shared/Text';
2222
import { useSelectedAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext';
23+
import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage';
2324
import {
2425
isRange,
2526
showCommissionRate,
2627
showValidatorAmount,
2728
showValidatorOpenStatus,
2829
showValidatorRestake,
2930
} from '../util';
30-
import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage';
3131

3232
export type ValidationResultLocationState = {
3333
payload: ConfigureBakerPayload;

0 commit comments

Comments
 (0)