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

Hook up IdCards page with actual data #556

Merged
merged 5 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const relativeRoutes = {
settings: {
path: 'settings',
idCards: {
path: 'idCards',
path: 'id-cards',
},
about: {
path: 'about',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
import React 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 All @@ -21,71 +21,7 @@ import { WalletCredential } from '@shared/storage/types';
import { displaySplitAddress, useIdentityName, useWritableSelectedAccount } from '@popup/shared/utils/account-helpers';
import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext';
import { displayAsCcd } from 'wallet-common-helpers';

type EditableAccountNameProps = {
currentName: string;
fallbackName: string;
onNewName: (newName: string) => void;
};

function EditableAccountName({ currentName, fallbackName, onNewName }: EditableAccountNameProps) {
const [isEditingName, setIsEditingName] = useState(false);
const [editedName, setEditedName] = useState(currentName);
// Using editedName instead of currentName to avoid flickering after completing.
const displayName = editedName === '' ? fallbackName : editedName;
const onAbort = () => {
setIsEditingName(false);
setEditedName(currentName);
};
const onComplete = () => {
onNewName(editedName.trim());
setIsEditingName(false);
};
const onEdit = () => {
setEditedName(currentName);
setIsEditingName(true);
};
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setEditedName(event.target.value);
};
const onKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
onComplete();
}
};
if (isEditingName) {
return (
<>
<Text.Main>
<input
autoFocus
className="editable"
value={editedName}
placeholder={fallbackName}
onChange={onInputChange}
onKeyUp={onKeyUp}
maxLength={25}
/>
</Text.Main>
<div className="row gap-16">
<Button.Icon
className="transparent"
icon={<Checkmark className="width-12" />}
onClick={onComplete}
/>
<Button.Icon className="transparent" icon={<Close className="width-16" />} onClick={onAbort} />
</div>
</>
);
}
return (
<>
<Text.Main>{displayName}</Text.Main>
<Button.Icon className="transparent" icon={<Pencil />} onClick={onEdit} />
</>
);
}
import useEditableValue from '@popup/popupX/shared/EditableValue';

type AccountListItemProps = {
credential: WalletCredential;
Expand All @@ -108,14 +44,28 @@ function AccountListItem({ credential }: AccountListItemProps) {
const ccdBalance =
accountInfo === undefined ? 'Loading' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false);
const onNewAccountName = (newName: string) => setAccount({ credName: newName });
const editable = useEditableValue(accountName, fallbackName, onNewAccountName);

return (
<Card key={accountName}>
<Card.Row>
<EditableAccountName
currentName={accountName}
onNewName={onNewAccountName}
fallbackName={fallbackName}
/>
<Text.Main>{editable.value}</Text.Main>
{editable.isEditing ? (
<div className="row gap-16">
<Button.Icon
className="transparent"
icon={<Checkmark className="width-12" />}
onClick={editable.onComplete}
/>
<Button.Icon
className="transparent"
icon={<Close className="width-16" />}
onClick={editable.onAbort}
/>
</div>
) : (
<Button.Icon className="transparent" icon={<Pencil />} onClick={editable.onEdit} />
)}
</Card.Row>
<Card.Row>
<Text.Capture className="wrap-anywhere">{address}</Text.Capture>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.id-cards-x {
.page__main {
gap: rem(16px);
}
}
107 changes: 90 additions & 17 deletions packages/browser-wallet/src/popup/popupX/pages/IdCards/IdCards.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,108 @@
import React from 'react';
import React, { useMemo } 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';

const rowsIdInfo: [string, string][] = [
['Identity document type', 'Drivers licence'],
['Identity document number', 'BXM680515'],
['First name', 'Lewis'],
['Last name', 'Hamilton'],
['Date of birth', '13 August 1992'],
['Identity document issuer', 'New Zeland'],
['ID valid until', '30 October 2051'],
];
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}
/>
);
}

const rowsConnectedAccounts: [string, string][] = [
['Accout 1 / 6gk...Fk7o', '4,227.38 USD'],
['Accout 2 / tt2...50eo', '1,195.41 USD'],
['Accout 3 / bnh...JJ76', '123.38 USD'],
['Accout 4 / rijf...8h7T', '7,200.41 USD'],
];
export default function IdCards() {
const { t } = useTranslation('x', { keyPrefix: 'idCards' });
const [identities, setIdentities] = useAtom(identitiesAtom);
const onNewName = (index: number) => (newName: string) => {
const identitiesClone = [...identities];
identitiesClone[index] = { ...identities[index], name: newName };
setIdentities(identitiesClone);
};
return (
<Page className="id-cards-x">
<Page.Top heading={t('idCards')}>
<Button.Icon icon={<Plus />} />
</Page.Top>
<Page.Main>
<IdCard rowsIdInfo={rowsIdInfo} rowsConnectedAccounts={rowsConnectedAccounts} />
{identities.map((id, index) => {
switch (id.status) {
case CreationStatus.Confirmed:
return (
<ConfirmedIdCard
identity={id}
key={`${id.providerIndex}:${id.index}`}
onNewName={onNewName(index)}
/>
);
case CreationStatus.Pending:
return null;
case CreationStatus.Rejected:
return null;
default:
return <>Unsupported</>;
}
})}
</Page.Main>
</Page>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import { useTranslation } from 'react-i18next';
import IdCard from '@popup/popupX/shared/IdCard';
import { IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard';

const rowsIdInfo: [string, string][] = [
['Identity document type', 'Drivers licence'],
['Identity document number', 'BXM680515'],
const rowsIdInfo: IdCardAttributeInfo[] = [
{ key: 'Identity document type', value: 'Drivers licence' },
{ key: 'Identity document number', value: 'BXM680515' },
];

export default function IdSubmitted() {
Expand All @@ -20,7 +21,7 @@ export default function IdSubmitted() {
<Page.Top heading={t('yourId')} />
<Page.Main>
<Text.Capture>{t('idSubmitInfo')}</Text.Capture>
<IdCard rowsIdInfo={rowsIdInfo} />
<IdCard rowsIdInfo={rowsIdInfo} idProviderName="TODO" identityName="TODO" />
soerenbf marked this conversation as resolved.
Show resolved Hide resolved
</Page.Main>
<Page.Footer>
<Button.Main label={t('done')} onClick={() => navToNext()} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { ReactNode } from 'react';
import React from 'react';
import ArrowRight from '@assets/svgX/arrow-right.svg';
import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
import Text from '@popup/popupX/shared/Text';
import { useTranslation } from 'react-i18next';
import IdCard from '@popup/popupX/shared/IdCard';
import { IdCardAttributeInfo } from '@popup/popupX/shared/IdCard/IdCard';

function AccountLink({ account, balance }: { account: string; balance: string }) {
return (
Expand All @@ -16,9 +17,9 @@ function AccountLink({ account, balance }: { account: string; balance: string })
);
}

const rowsIdInfo: [string, ReactNode][] = [
['', <AccountLink account="Accout 1 / 6gk...k7o" balance="1,285,700 CCD" />],
['', <AccountLink account="Accout 2 / tt2...50eo" balance="90,800 CCD" />],
const rowsIdInfo: IdCardAttributeInfo[] = [
{ key: '', value: <AccountLink account="Accout 1 / 6gk...k7o" balance="1,285,700 CCD" /> },
{ key: '', value: <AccountLink account="Accout 2 / tt2...50eo" balance="90,800 CCD" /> },
];

export default function RestoreResult() {
Expand All @@ -29,7 +30,7 @@ export default function RestoreResult() {
<Page.Top heading={t('result')} />
<Page.Main>
<Text.Capture>{t('recoveredIds')}</Text.Capture>
<IdCard rowsIdInfo={rowsIdInfo} />
<IdCard rowsIdInfo={rowsIdInfo} idProviderName="TODO" identityName="TODO" />
soerenbf marked this conversation as resolved.
Show resolved Hide resolved
</Page.Main>
<Page.Footer>
<Button.Main label={t('continue')} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
input.editable-value-x {
background: none;
color: inherit;
border: none;
font-weight: inherit;
font-size: inherit;
font-family: inherit;
padding: 0;
margin: 0;

&:focus {
outline: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { ChangeEvent, useState, KeyboardEvent, useCallback } from 'react';

export default function useEditableValue(current: string, fallback: string, onNewValue: (newValue: string) => void) {
const [isEditing, setIsEditing] = useState(false);
const [edited, setEdited] = useState(current);
// Using edited instead of currentValue to avoid flickering after completing.
const displayName = edited === '' ? fallback : edited;
const onAbort = useCallback(() => {
setIsEditing(false);
setEdited(current);
}, []);
const onComplete = useCallback(() => {
onNewValue(edited.trim());
setIsEditing(false);
}, []);
const onEdit = useCallback(() => {
setEdited(current);
setIsEditing(true);
}, []);
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setEdited(event.target.value);
};
const onKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
onComplete();
}
};
const value = isEditing ? (
<input
className="editable-value-x"
autoFocus
maxLength={25}
value={edited}
placeholder={fallback}
onChange={onInputChange}
onKeyUp={onKeyUp}
/>
) : (
displayName
);
return { value, isEditing, onAbort, onComplete, onEdit };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './EditableValue';
Loading