Skip to content

Commit a2e4be8

Browse files
committed
Add create account pages
1 parent 3b68429 commit a2e4be8

File tree

16 files changed

+354
-79
lines changed

16 files changed

+354
-79
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
@@ -124,6 +124,15 @@ export const relativeRoutes = {
124124
path: 'private-key/:account',
125125
},
126126
},
127+
createAccount: {
128+
path: 'create-account',
129+
confirm: {
130+
path: 'confirm/:identityProviderIndex/:identityIndex',
131+
},
132+
config: {
133+
backTitle: '',
134+
},
135+
},
127136
seedPhrase: {
128137
path: 'seedPhrase',
129138
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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React 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+
13+
export default function CreateAccount() {
14+
const { t } = useTranslation('x', { keyPrefix: 'createAccount' });
15+
const identities = useAtomValue(identitiesAtom);
16+
const nav = useNavigate();
17+
const navToCreateAccountConfirm = (identity: ConfirmedIdentity) => () =>
18+
nav(
19+
generatePath(absoluteRoutes.settings.createAccount.confirm.path, {
20+
identityProviderIndex: identity.providerIndex.toString(),
21+
identityIndex: identity.index.toString(),
22+
})
23+
);
24+
return (
25+
<Page className="create-account-x">
26+
<Page.Top heading={t('selectIdentity')} />
27+
<Page.Main>
28+
<Text.MainRegular>{t('selectIdentityDescription')}</Text.MainRegular>
29+
{identities.map((id) => {
30+
switch (id.status) {
31+
case CreationStatus.Confirmed:
32+
return (
33+
<Button.Base className="id-card-button" onClick={navToCreateAccountConfirm(id)}>
34+
<ConfirmedIdCard
35+
identity={id}
36+
key={`${id.providerIndex}:${id.index}`}
37+
shownAttributes={['idDocType', 'idDocNo']}
38+
hideAccounts
39+
/>
40+
</Button.Base>
41+
);
42+
case CreationStatus.Pending:
43+
return null;
44+
case CreationStatus.Rejected:
45+
return null;
46+
default:
47+
return null;
48+
}
49+
})}
50+
</Page.Main>
51+
</Page>
52+
);
53+
}
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const t = {
2+
selectIdentity: 'Select an identity',
3+
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.
4+
5+
Which identity do you want to use to create the account?`,
6+
confirmButton: 'Create account',
7+
};
8+
9+
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/IdCards/IdCards.tsx

Lines changed: 5 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,12 @@
1-
import React, { useMemo } from 'react';
1+
import React from 'react';
22
import Plus from '@assets/svgX/plus.svg';
33
import Button from '@popup/popupX/shared/Button';
44
import Page from '@popup/popupX/shared/Page';
55
import { useTranslation } from 'react-i18next';
6-
import IdCard from '@popup/popupX/shared/IdCard';
7-
import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity';
8-
import { useAtom, useAtomValue } from 'jotai';
9-
import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers';
10-
import { CreationStatus, ConfirmedIdentity, WalletCredential } from '@shared/storage/types';
11-
import { AttributeKey } from '@concordium/web-sdk';
12-
import { IdCardAccountInfo, IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard';
13-
import { credentialsAtomWithLoading } from '@popup/store/account';
14-
import { displayNameAndSplitAddress } from '@popup/shared/utils/account-helpers';
15-
import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext';
16-
import { compareAttributes, displayAsCcd } from 'wallet-common-helpers';
17-
18-
function CcdBalance({ credential }: { credential: WalletCredential }) {
19-
const accountInfo = useAccountInfo(credential);
20-
const balance =
21-
accountInfo === undefined ? '' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false, true);
22-
// eslint-disable-next-line react/jsx-no-useless-fragment
23-
return <>{balance}</>;
24-
}
25-
26-
function fallbackIdentityName(index: number) {
27-
return `Identity ${index + 1}`;
28-
}
29-
30-
type ConfirmedIdentityProps = { identity: ConfirmedIdentity; onNewName: (name: string) => void };
31-
32-
function ConfirmedIdCard({ identity, onNewName }: ConfirmedIdentityProps) {
33-
const displayAttribute = useDisplayAttributeValue();
34-
const getAttributeName = useGetAttributeName();
35-
const providers = useAtomValue(identityProvidersAtom);
36-
const credentials = useAtomValue(credentialsAtomWithLoading);
37-
const provider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex);
38-
const providerName = provider?.ipInfo.ipDescription.name ?? 'Unknown';
39-
const rowsIdInfo: IdCardAttributeInfo[] = useMemo(
40-
() =>
41-
Object.entries(identity.idObject.value.attributeList.chosenAttributes)
42-
.sort(([left], [right]) => compareAttributes(left, right))
43-
.map(([key, value]) => ({
44-
key: getAttributeName(key as AttributeKey),
45-
value: displayAttribute(key, value),
46-
})),
47-
[identity]
48-
);
49-
const rowsConnectedAccounts = useMemo(() => {
50-
const connectedAccounts = credentials.value.flatMap((cred): IdCardAccountInfo[] =>
51-
cred.identityIndex !== identity.index
52-
? []
53-
: [
54-
{
55-
address: displayNameAndSplitAddress(cred),
56-
amount: <CcdBalance credential={cred} />,
57-
},
58-
]
59-
);
60-
return connectedAccounts.length === 0 ? undefined : connectedAccounts;
61-
}, [credentials, identity]);
62-
return (
63-
<IdCard
64-
identityName={identity.name}
65-
onNewName={onNewName}
66-
identityNameFallback={fallbackIdentityName(identity.index)}
67-
idProviderName={providerName}
68-
rowsIdInfo={rowsIdInfo}
69-
rowsConnectedAccounts={rowsConnectedAccounts}
70-
/>
71-
);
72-
}
6+
import { identitiesAtom } from '@popup/store/identity';
7+
import { useAtom } from 'jotai';
8+
import { CreationStatus } from '@shared/storage/types';
9+
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';
7310

7411
export default function IdCards() {
7512
const { t } = useTranslation('x', { keyPrefix: 'idCards' });

0 commit comments

Comments
 (0)