Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI update/id issuance #572

Merged
merged 12 commits into from
Nov 18, 2024
9 changes: 3 additions & 6 deletions packages/browser-wallet/src/background/identity-issuance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createIdentityRequest, IdentityRequestInput } from '@concordium/web-sdk';
import { createIdentityRequest } from '@concordium/web-sdk';
import { IdentityIssuanceBackgroundResponse } from '@shared/utils/identity-helpers';
import { ExtensionMessageHandler, InternalMessageType } from '@messaging';
import { BackgroundResponseStatus } from '@shared/utils/types';
import { BackgroundResponseStatus, IdentityIssuanceRequestPayload } from '@shared/utils/types';
import { sessionIdpTab, sessionPendingIdentity, storedCurrentNetwork } from '@shared/storage/access';
import { CreationStatus, PendingIdentity } from '@shared/storage/types';
import { buildURLwithSearchParameters } from '@shared/utils/url-helpers';
Expand Down Expand Up @@ -150,10 +150,7 @@ function launchExternalIssuance(url: string) {
});
}

async function startIdentityIssuance({
baseUrl,
...identityRequestInputs
}: IdentityRequestInput & { baseUrl: string }) {
async function startIdentityIssuance({ baseUrl, ...identityRequestInputs }: IdentityIssuanceRequestPayload) {
const idObjectRequest = createIdentityRequest(identityRequestInputs);

const params = {
Expand Down
29 changes: 27 additions & 2 deletions packages/browser-wallet/src/popup/popupX/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,30 @@ export const relativeRoutes = {
},
settings: {
path: 'settings',
idCards: {
path: 'id-cards',
identities: {
path: 'identities',
create: {
path: 'create',
externalFlow: {
path: 'external-flow',
config: {
hideMenu: true,
},
},
submitted: {
path: 'submitted',
config: {
hideMenu: true,
hideBackArrow: true,
},
},
failed: {
path: 'failed',
config: {
hideMenu: true,
},
},
},
},
about: {
path: 'about',
Expand Down Expand Up @@ -279,6 +301,9 @@ export const relativeRoutes = {
connectionRequest: {
path: 'connectionRequest',
},
endIdentityIssuance: {
path: 'end-identity-issuance',
},
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function MenuTiles({ menuOpen, setMenuOpen }: MenuTilesProps) {
className="main-header__menu-tiles_container"
onClick={() => setMenuOpen(false)}
>
<Link to={absoluteRoutes.settings.idCards.path}>
<Link to={absoluteRoutes.settings.identities.path}>
<IconButton className="main-header__menu-tiles_tile wide">
<Identification />
<Text.Capture>{t('identities')}</Text.Capture>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function AccountListItem({ credential }: AccountListItemProps) {
nav(generatePath(absoluteRoutes.settings.accounts.privateKey.path, { account: credential.address }));
const navToConnectedSites = () =>
nav(generatePath(absoluteRoutes.settings.accounts.connectedSites.path, { account: credential.address }));
const navToIdCards = () => nav(absoluteRoutes.settings.idCards.path);
const navToIdCards = () => nav(absoluteRoutes.settings.identities.path);
const identityName = useIdentityName(credential);
const accountInfo = useAccountInfo(credential);
const setAccount = useWritableSelectedAccount(credential.address);
Expand Down
21 changes: 10 additions & 11 deletions packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAtom } from 'jotai';

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 { identitiesAtom } from '@popup/store/identity';
import { useAtom } from 'jotai';
import { CreationStatus } from '@shared/storage/types';
import { ConfirmedIdCard, RejectedIdCard } from '@popup/popupX/shared/IdCard';
import { absoluteRoutes } from '@popup/popupX/constants/routes';
import PendingIdCard from '@popup/popupX/shared/IdCard/PendingIdCard';

export default function IdCards() {
const { t } = useTranslation('x', { keyPrefix: 'idCards' });
const nav = useNavigate();
const [identities, setIdentities] = useAtom(identitiesAtom);
const onNewName = (index: number) => (newName: string) => {
const identitiesClone = [...identities];
Expand All @@ -19,7 +24,7 @@ export default function IdCards() {
return (
<Page className="id-cards-x">
<Page.Top heading={t('idCards')}>
<Button.Icon icon={<Plus />} />
<Button.Icon icon={<Plus />} onClick={() => nav(absoluteRoutes.settings.identities.create.path)} />
</Page.Top>
<Page.Main>
{identities.map((id, index) => {
Expand All @@ -33,15 +38,9 @@ export default function IdCards() {
/>
);
case CreationStatus.Pending:
return null;
return <PendingIdCard identity={id} key={`${id.providerIndex}:${id.index}`} />;
case CreationStatus.Rejected:
return (
<RejectedIdCard
identity={id}
key={`${id.providerIndex}:${id.index}`}
onNewName={onNewName(index)}
/>
);
return <RejectedIdCard identity={id} key={`${id.providerIndex}:${id.index}`} />;
default:
return <>Unsupported</>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useCallback, useEffect } from 'react';
import { Location, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useSetAtom } from 'jotai';

import { InternalMessageType } from '@messaging';
import { absoluteRoutes } from '@popup/popupX/constants/routes';
import { LoaderInline } from '@popup/popupX/shared/Loader';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import { popupMessageHandler } from '@popup/shared/message-handler';
import { useDecryptedSeedPhrase } from '@popup/shared/utils/seed-phrase-helpers';
import { logError } from '@shared/utils/log-helpers';
import { IdentityIssuanceRequestPayload } from '@shared/utils/types';
import { pendingIdentityAtom } from '@popup/store/identity';
import { getNet } from '@shared/utils/network-helpers';
import Button from '@popup/popupX/shared/Button';

import { IdIssuanceFailedLocationState } from './Failed';
import { IdIssuanceExternalFlowLocationState } from './util';

export default function IdIssuanceExternalFlow() {
const { state } = useLocation() as Location & { state: IdIssuanceExternalFlowLocationState };
const { t } = useTranslation('x', { keyPrefix: 'idIssuance.externalFlow' });
const updatePendingIdentity = useSetAtom(pendingIdentityAtom);
const nav = useNavigate();

const handleError = useCallback(
(message: string) => {
const messageState: IdIssuanceFailedLocationState = { message, backState: state };
nav(absoluteRoutes.settings.identities.create.failed.path, { state: messageState, replace: true });
},
[state]
);

const seedPhrase = useDecryptedSeedPhrase((e) => handleError(e.message));

const start = useCallback(async () => {
if (seedPhrase === undefined) throw new Error('Seed phrase not available');

updatePendingIdentity(state.pendingIdentity);

const issuanceRequest: IdentityIssuanceRequestPayload = {
globalContext: state.global,
ipInfo: state.provider.ipInfo,
arsInfos: state.provider.arsInfos,
net: getNet(state.pendingIdentity.network),
identityIndex: state.pendingIdentity.identity.index,
arThreshold: Math.min(Object.keys(state.provider.arsInfos).length - 1, 255),
baseUrl: state.provider.metadata.issuanceStart,
seed: seedPhrase,
};

const response = await popupMessageHandler.sendInternalMessage(
InternalMessageType.StartIdentityIssuance,
issuanceRequest
);

if (!response) {
logError('Failed to issue identity due to internal error');
handleError('Internal error, please try again.');
} else {
nav(absoluteRoutes.settings.identities.path, { replace: true });
}
}, [state, seedPhrase, handleError]);

useEffect(() => {
if (state !== null && seedPhrase !== undefined) {
start();
}
}, [start]);

return (
<Page>
<Page.Top />
<Text.Capture className="text-center">{t('description')}</Text.Capture>
<LoaderInline className="m-t-50 margin-center" />
<Page.Footer>
<Button.Main label={t('buttonReset')} />
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Location, useLocation, useNavigate } from 'react-router-dom';

import { absoluteRoutes } from '@popup/popupX/constants/routes';
import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';

import { IdIssuanceExternalFlowLocationState } from './util';

export type IdIssuanceFailedLocationState = { message: string; backState?: IdIssuanceExternalFlowLocationState };

export default function IdIssuanceFailed() {
const { t } = useTranslation('x', { keyPrefix: 'idIssuance.failed' });
const { state } = useLocation() as Location & { state: IdIssuanceFailedLocationState };
const nav = useNavigate();

const handleRetry = useCallback(() => {
if (state.backState !== undefined) {
nav(absoluteRoutes.settings.identities.create.externalFlow.path, { state: state.backState, replace: true });
} else {
nav(absoluteRoutes.settings.identities.create.path, { replace: true });
}
}, [state.backState]);

return (
<Page>
<Page.Top heading={t('title')} />
<Text.Capture>{state.message}</Text.Capture>
<Page.Footer>
<Button.Main label={t('buttonRetry')} onClick={handleRetry} />
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.id-issuance {
&__issuer-btn {
display: flex;
color: $color-white;
align-items: center;
padding: rem(16px);
gap: rem(16px);
border: unset;
border-radius: rem(12px);
background: $color-transaction-bg;
width: 100%;

&:not(:first-child) {
margin-top: rem(8px);
}

.identity-provider-icon {
width: rem(64px);
height: rem(32px);
object-position: center;
object-fit: contain;
background: $color-white;
border-radius: rem(4px);
padding: rem(4px);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useAtom, useAtomValue } from 'jotai';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';

import { absoluteRoutes } from '@popup/popupX/constants/routes';
import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import IdentityProviderIcon from '@popup/shared/IdentityProviderIcon';
import { getIdentityProviders } from '@popup/shared/utils/wallet-proxy';
import { identitiesAtom, identityProvidersAtom, pendingIdentityAtom } from '@popup/store/identity';
import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings';
import { CreationStatus, IdentityProvider, SessionPendingIdentity } from '@shared/storage/types';
import { getGlobal } from '@shared/utils/network-helpers';
import { logErrorMessage } from '@shared/utils/log-helpers';

import { IdIssuanceExternalFlowLocationState } from './util';

export default function IdIssuance() {
const { t } = useTranslation('x', { keyPrefix: 'idIssuance.idIssuer' });
const [providers, setProviders] = useAtom(identityProvidersAtom);
const network = useAtomValue(networkConfigurationAtom);
const identities = useAtomValue(identitiesAtom);
const client = useAtomValue(grpcClientAtom);
const nav = useNavigate();
const [buttonDisabled, setButtonDisabled] = useState(false);
const [pendingIdentity, setPendingIdentity] = useAtom(pendingIdentityAtom);

useEffect(() => {
// TODO: only load once per session?
getIdentityProviders()
.then(setProviders)
// eslint-disable-next-line no-console
.catch(() => logErrorMessage('Unable to update identity provider list'));
}, []);

const startIssuance = useCallback(
async (provider: IdentityProvider) => {
setButtonDisabled(true);
try {
if (!network) {
throw new Error('Network is not specified');
}

const global = await getGlobal(client);
const providerIndex = provider.ipInfo.ipIdentity;
const identityIndex = identities.reduce(
(maxIndex, identity) =>
identity.providerIndex === providerIndex ? Math.max(maxIndex, identity.index + 1) : maxIndex,
0
);

const identity: SessionPendingIdentity = {
identity: {
status: CreationStatus.Pending,
index: identityIndex,
name: `Identity ${identities.length + 1}`,
providerIndex,
},
network,
};

const issuanceParams: IdIssuanceExternalFlowLocationState = {
global,
provider,
pendingIdentity: identity,
};
nav(absoluteRoutes.settings.identities.create.externalFlow.path, { state: issuanceParams });
} catch {
setButtonDisabled(false);
}
},
[network]
);

return (
<Page>
<Page.Top heading={t('title')} />
{pendingIdentity !== undefined ? (
<Text.Capture>{t('descriptionOngoing')}</Text.Capture>
) : (
<Text.Capture>{t('description')}</Text.Capture>
)}
{pendingIdentity === undefined && (
<div className="m-t-20">
{providers.map((p) => (
<Button.Base
className="id-issuance__issuer-btn"
key={p.ipInfo.ipDescription.url}
disabled={buttonDisabled}
onClick={() => startIssuance(p)}
>
<IdentityProviderIcon provider={p} />
<span>{p.metadata.display ?? p.ipInfo.ipDescription.name}</span>
</Button.Base>
))}
</div>
)}
{pendingIdentity !== undefined && (
<Page.Footer>
<Button.Main label={t('buttonReset')} onClick={() => setPendingIdentity(undefined)} />
</Page.Footer>
)}
</Page>
);
}
Loading
Loading