Skip to content

Commit

Permalink
Add create account pages
Browse files Browse the repository at this point in the history
  • Loading branch information
limemloh committed Nov 8, 2024
1 parent 3b68429 commit a2e4be8
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 79 deletions.
6 changes: 6 additions & 0 deletions packages/browser-wallet/src/assets/svgX/fat-arrow-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/browser-wallet/src/popup/popupX/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ export const relativeRoutes = {
path: 'private-key/:account',
},
},
createAccount: {
path: 'create-account',
confirm: {
path: 'confirm/:identityProviderIndex/:identityIndex',
},
config: {
backTitle: '',
},
},
seedPhrase: {
path: 'seedPhrase',
config: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import Plus from '@assets/svgX/plus.svg';
import Arrows from '@assets/svgX/arrows-down-up.svg';
import MagnifyingGlass from '@assets/svgX/magnifying-glass.svg';
Expand Down Expand Up @@ -112,12 +112,14 @@ function AccountListItem({ credential }: AccountListItemProps) {
export default function Accounts() {
const { t } = useTranslation('x', { keyPrefix: 'accounts' });
const accounts = useAtomValue(credentialsAtom);
const nav = useNavigate();
const navToCreateAccount = useCallback(() => nav(absoluteRoutes.settings.createAccount.path), []);
return (
<Page className="accounts-x">
<Page.Top heading={t('accounts')}>
<Button.Icon icon={<Arrows />} />
<Button.Icon icon={<MagnifyingGlass />} />
<Button.Icon icon={<Plus />} />
<Button.Icon icon={<Plus />} onClick={navToCreateAccount} />
</Page.Top>
<Page.Main>
{accounts.map((item) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.create-account-x {
.id-card-button {
margin-top: rem(14px);
padding: unset;
background-color: unset;
border: none;
text-align: left;
cursor: pointer;
}

.justify-content-center {
display: flex;
justify-content: center;
}

.page__footer {
gap: rem(24px) !important;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import { useTranslation } from 'react-i18next';
import { identitiesAtom } from '@popup/store/identity';
import { useAtomValue } from 'jotai';
import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types';
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';
import Button from '@popup/popupX/shared/Button';
import { generatePath, useNavigate } from 'react-router-dom';
import { absoluteRoutes } from '@popup/popupX/constants/routes';

export default function CreateAccount() {
const { t } = useTranslation('x', { keyPrefix: 'createAccount' });
const identities = useAtomValue(identitiesAtom);
const nav = useNavigate();
const navToCreateAccountConfirm = (identity: ConfirmedIdentity) => () =>
nav(
generatePath(absoluteRoutes.settings.createAccount.confirm.path, {
identityProviderIndex: identity.providerIndex.toString(),
identityIndex: identity.index.toString(),
})
);
return (
<Page className="create-account-x">
<Page.Top heading={t('selectIdentity')} />
<Page.Main>
<Text.MainRegular>{t('selectIdentityDescription')}</Text.MainRegular>
{identities.map((id) => {
switch (id.status) {
case CreationStatus.Confirmed:
return (
<Button.Base className="id-card-button" onClick={navToCreateAccountConfirm(id)}>
<ConfirmedIdCard
identity={id}
key={`${id.providerIndex}:${id.index}`}
shownAttributes={['idDocType', 'idDocNo']}
hideAccounts
/>
</Button.Base>
);
case CreationStatus.Pending:
return null;
case CreationStatus.Rejected:
return null;
default:
return null;
}
})}
</Page.Main>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useCallback } from 'react';
import Page from '@popup/popupX/shared/Page';
import { useTranslation } from 'react-i18next';
import { Navigate, useNavigate, useParams } from 'react-router-dom';
import Button from '@popup/popupX/shared/Button';
import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';
import { ConfirmedIdentity, CreationStatus } from '@shared/storage/types';
import FatArrowUp from '@assets/svgX/fat-arrow-up.svg';
import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings';
import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers';
import { addToastAtom } from '@popup/state';
import { creatingCredentialRequestAtom, credentialsAtom } from '@popup/store/account';
import { getGlobal, getNet } from '@shared/utils/network-helpers';
import { isIdentityOfCredential } from '@shared/utils/identity-helpers';
import { getNextEmptyCredNumber } from '@popup/shared/utils/account-helpers';
import { popupMessageHandler } from '@popup/shared/message-handler';
import { InternalMessageType } from '@messaging';
import { CredentialDeploymentBackgroundResponse } from '@shared/utils/types';
import { absoluteRoutes } from '@popup/popupX/constants/routes';

/**
* Hook providing function for sending credential deployments.
*/
function useSendCredentialDeployment() {
const providers = useAtomValue(identityProvidersAtom);
const credentials = useAtomValue(credentialsAtom);
const network = useAtomValue(networkConfigurationAtom);
const addToast = useSetAtom(addToastAtom);
const seedPhrase = useDecryptedSeedPhrase((e) => addToast(e.message));
const client = useAtomValue(grpcClientAtom);

const loading = !seedPhrase || !network || !providers || !credentials;
const sendCredentialDeployment = useCallback(
async (identity: ConfirmedIdentity) => {
if (loading) {
throw new Error('Still loading relevant information');
}

const identityProvider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex);
if (!identityProvider) {
throw new Error('provider not found');
}
const global = await getGlobal(client);

// Make request
const credsOfCurrentIdentity = credentials.filter(isIdentityOfCredential(identity));
const credNumber = getNextEmptyCredNumber(credsOfCurrentIdentity);

const response: CredentialDeploymentBackgroundResponse = await popupMessageHandler.sendInternalMessage(
InternalMessageType.SendCredentialDeployment,
{
globalContext: global,
ipInfo: identityProvider.ipInfo,
arsInfos: identityProvider.arsInfos,
seedAsHex: seedPhrase,
net: getNet(network),
idObject: identity.idObject.value,
revealedAttributes: [],
identityIndex: identity.index,
credNumber,
}
);
return response;
},
[seedPhrase, network, providers, credentials, loading]
);

return { loading, sendCredentialDeployment };
}

type CreateAccountConfirmProps = {
identityProviderIndex: number;
identityIndex: number;
};

function ConfirmInfo({ identityProviderIndex, identityIndex }: CreateAccountConfirmProps) {
const { t } = useTranslation('x', { keyPrefix: 'createAccount' });
const identities = useAtomValue(identitiesAtom);
const identity = identities.find((id) => id.providerIndex === identityProviderIndex && id.index === identityIndex);
const [creatingCredentialRequest, setCreatingRequest] = useAtom(creatingCredentialRequestAtom);
const deployment = useSendCredentialDeployment();
const nav = useNavigate();
const onCreateAccount = useCallback(async () => {
if (identity === undefined || identity.status !== CreationStatus.Confirmed) {
throw new Error(`Invalid identity: ${identity}`);
}
setCreatingRequest(true);
deployment
.sendCredentialDeployment(identity)
.catch(() => {})
.then(() => {
nav(absoluteRoutes.home.path);
})
.finally(() => {
setCreatingRequest(false);
});
}, [deployment.sendCredentialDeployment]);

if (identity === undefined) {
return null;
}
if (identity.status !== CreationStatus.Confirmed) {
return <Navigate to="../" />;
}
const loading = creatingCredentialRequest.loading || creatingCredentialRequest.value || deployment.loading;
return (
<>
<div className="justify-content-center">
<FatArrowUp />
</div>
<ConfirmedIdCard identity={identity} shownAttributes={['idDocType', 'idDocNo']} hideAccounts />
<Button.Main disabled={loading} type="submit" label={t('confirmButton')} onClick={onCreateAccount} />
</>
);
}

export default function CreateAccountConfirm() {
const params = useParams();
if (params.identityProviderIndex === undefined || params.identityIndex === undefined) {
// No account address passed in the url.
return <Navigate to="../" />;
}
const identityIndex = parseInt(params.identityIndex, 10);
const identityProviderIndex = parseInt(params.identityProviderIndex, 10);
if (Number.isNaN(identityProviderIndex) || Number.isNaN(identityIndex)) {
return <Navigate to="../" />;
}
return (
<Page className="create-account-x">
<Page.Footer>
<ConfirmInfo identityIndex={identityIndex} identityProviderIndex={identityProviderIndex} />
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const t = {
selectIdentity: 'Select an identity',
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.
Which identity do you want to use to create the account?`,
confirmButton: 'Create account',
};

export default t;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './CreateAccount';
export { default as CreateAccountConfirm } from './CreateAccountConfirm';
73 changes: 5 additions & 68 deletions packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,12 @@
import React, { useMemo } from 'react';
import React from 'react';
import Plus from '@assets/svgX/plus.svg';
import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
import { useTranslation } from 'react-i18next';
import IdCard from '@popup/popupX/shared/IdCard';
import { identitiesAtom, identityProvidersAtom } from '@popup/store/identity';
import { useAtom, useAtomValue } from 'jotai';
import { useDisplayAttributeValue, useGetAttributeName } from '@popup/shared/utils/identity-helpers';
import { CreationStatus, ConfirmedIdentity, WalletCredential } from '@shared/storage/types';
import { AttributeKey } from '@concordium/web-sdk';
import { IdCardAccountInfo, IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard';
import { credentialsAtomWithLoading } from '@popup/store/account';
import { displayNameAndSplitAddress } from '@popup/shared/utils/account-helpers';
import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext';
import { compareAttributes, displayAsCcd } from 'wallet-common-helpers';

function CcdBalance({ credential }: { credential: WalletCredential }) {
const accountInfo = useAccountInfo(credential);
const balance =
accountInfo === undefined ? '' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false, true);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{balance}</>;
}

function fallbackIdentityName(index: number) {
return `Identity ${index + 1}`;
}

type ConfirmedIdentityProps = { identity: ConfirmedIdentity; onNewName: (name: string) => void };

function ConfirmedIdCard({ identity, onNewName }: ConfirmedIdentityProps) {
const displayAttribute = useDisplayAttributeValue();
const getAttributeName = useGetAttributeName();
const providers = useAtomValue(identityProvidersAtom);
const credentials = useAtomValue(credentialsAtomWithLoading);
const provider = providers.find((p) => p.ipInfo.ipIdentity === identity.providerIndex);
const providerName = provider?.ipInfo.ipDescription.name ?? 'Unknown';
const rowsIdInfo: IdCardAttributeInfo[] = useMemo(
() =>
Object.entries(identity.idObject.value.attributeList.chosenAttributes)
.sort(([left], [right]) => compareAttributes(left, right))
.map(([key, value]) => ({
key: getAttributeName(key as AttributeKey),
value: displayAttribute(key, value),
})),
[identity]
);
const rowsConnectedAccounts = useMemo(() => {
const connectedAccounts = credentials.value.flatMap((cred): IdCardAccountInfo[] =>
cred.identityIndex !== identity.index
? []
: [
{
address: displayNameAndSplitAddress(cred),
amount: <CcdBalance credential={cred} />,
},
]
);
return connectedAccounts.length === 0 ? undefined : connectedAccounts;
}, [credentials, identity]);
return (
<IdCard
identityName={identity.name}
onNewName={onNewName}
identityNameFallback={fallbackIdentityName(identity.index)}
idProviderName={providerName}
rowsIdInfo={rowsIdInfo}
rowsConnectedAccounts={rowsConnectedAccounts}
/>
);
}
import { identitiesAtom } from '@popup/store/identity';
import { useAtom } from 'jotai';
import { CreationStatus } from '@shared/storage/types';
import { ConfirmedIdCard } from '@popup/popupX/shared/IdCard';

export default function IdCards() {
const { t } = useTranslation('x', { keyPrefix: 'idCards' });
Expand Down
Loading

0 comments on commit a2e4be8

Please sign in to comment.