Skip to content

Commit

Permalink
Merge pull request #544 from Concordium/x-accounts
Browse files Browse the repository at this point in the history
Hook up list of accounts page
  • Loading branch information
limemloh authored Oct 24, 2024
2 parents 768026a + 8cf8eef commit efce054
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 77 deletions.
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/assets/svgX/checkmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@
color: $color-white;
}

.wrap-anywhere {
overflow-wrap: anywhere;
}

input.editable {
background: none;
color: inherit;
border: none;
font-weight: inherit;
font-size: inherit;
font-family: inherit;
padding: 0;
margin: 0;

&:focus {
outline: none;
}
}

.gap-16 {
gap: rem(16px);
}

.width-16 {
width: rem(16px);
}

.width-12 {
width: rem(12px);
}

.button__icon {
gap: rem(16px);

Expand Down
214 changes: 143 additions & 71 deletions packages/browser-wallet/src/popup/popupX/pages/Accounts/Accounts.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';
import React, { ChangeEvent, KeyboardEvent, useState } 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';
import Pencil from '@assets/svgX/pencil-simple.svg';
import Checkmark from '@assets/svgX/checkmark.svg';
import Close from '@assets/svgX/close.svg';
import Copy from '@assets/svgX/copy.svg';
import ArrowRight from '@assets/svgX/arrow-right.svg';
import Page from '@popup/popupX/shared/Page';
Expand All @@ -13,34 +15,151 @@ import Text from '@popup/popupX/shared/Text';
import { useNavigate } from 'react-router-dom';
import { absoluteRoutes } from '@popup/popupX/constants/routes';
import { copyToClipboard } from '@popup/popupX/shared/utils/helpers';
import { useAtomValue } from 'jotai';
import { credentialsAtom } from '@popup/store/account';
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';

const ACCOUNT_LIST = [
{
account: 'Account 1',
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
balance: '4,227.38',
attached: 'Identity 1',
},
{
account: 'Account 2',
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
balance: '4,227.38',
attached: 'Identity 1',
},
{
account: 'Account 3',
address: 'tt2kgdygjrsqtzq2n0yrf2493p83kkfjh50eo',
balance: '1,195.41',
attached: 'Identity 2',
},
];
type EditableAccountNameProps = {
currentName: string;
fallbackName: string;
onNewName: (newName: string) => void;
};

export default function Accounts() {
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} />
</>
);
}

type AccountListItemProps = {
credential: WalletCredential;
};

function AccountListItem({ credential }: AccountListItemProps) {
const { t } = useTranslation('x', { keyPrefix: 'accounts' });
const nav = useNavigate();
const navToPrivateKey = () => nav(absoluteRoutes.settings.accounts.privateKey.path);
const navToConnectedSites = () => nav(absoluteRoutes.settings.accounts.connectedSites.path);
const navToIdCards = () => nav(absoluteRoutes.settings.idCards.path);
const identityName = useIdentityName(credential);
const accountInfo = useAccountInfo(credential);
const setAccount = useWritableSelectedAccount(credential.address);
const fallbackName = displaySplitAddress(credential.address);
const accountName = credential.credName !== '' ? credential.credName : fallbackName;
const { address } = credential;
const ccdBalance =
accountInfo === undefined ? 'Loading' : displayAsCcd(accountInfo.accountAmount.microCcdAmount, false);
const onNewAccountName = (newName: string) => setAccount({ credName: newName });
return (
<Card key={accountName}>
<Card.Row>
<EditableAccountName
currentName={accountName}
onNewName={onNewAccountName}
fallbackName={fallbackName}
/>
</Card.Row>
<Card.Row>
<Text.Capture className="wrap-anywhere">{address}</Text.Capture>
<Button.Icon className="transparent" onClick={() => copyToClipboard(address)} icon={<Copy />} />
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('ccdBalance')}</Text.MainRegular>
<Text.MainMedium>{ccdBalance}</Text.MainMedium>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('connectedSites')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToConnectedSites}
label={t('seeList')}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('privateKey')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToPrivateKey}
label={t('export')}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('attachedTo')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToIdCards}
label={identityName}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
</Card>
);
}

export default function Accounts() {
const { t } = useTranslation('x', { keyPrefix: 'accounts' });
const accounts = useAtomValue(credentialsAtom);
return (
<Page className="accounts-x">
<Page.Top heading={t('accounts')}>
Expand All @@ -49,55 +168,8 @@ export default function Accounts() {
<Button.Icon icon={<Plus />} />
</Page.Top>
<Page.Main>
{ACCOUNT_LIST.map(({ account, address, balance, attached }) => (
<Card key={account}>
<Card.Row>
<Text.Main>{account}</Text.Main>
<Button.Icon className="transparent" icon={<Pencil />} />
</Card.Row>
<Card.Row>
<Text.Capture>{address}</Text.Capture>
<Button.Icon
className="transparent"
onClick={() => copyToClipboard(address)}
icon={<Copy />}
/>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('totalBalance')}</Text.MainRegular>
<Text.MainMedium>{balance} USD</Text.MainMedium>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('connectedSites')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToConnectedSites}
label={t('seeList')}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('privateKey')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToPrivateKey}
label={t('export')}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
<Card.Row>
<Text.MainRegular>{t('attachedTo')}</Text.MainRegular>
<Button.IconText
className="transparent"
onClick={navToIdCards}
label={attached}
icon={<ArrowRight />}
leftLabel
/>
</Card.Row>
</Card>
{accounts.map((item) => (
<AccountListItem credential={item} key={item.address} />
))}
</Page.Main>
</Page>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const t = {
accounts: 'Accounts',
totalBalance: 'Total Balance',
ccdBalance: 'CCD Balance',
connectedSites: 'Connected sites',
seeList: 'See list',
privateKey: 'Private key',
Expand Down
16 changes: 11 additions & 5 deletions packages/browser-wallet/src/popup/shared/utils/account-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import { isIdentityOfCredential } from '@shared/utils/identity-helpers';
import { getNextUnused } from '@shared/utils/number-helpers';
import { useDecryptedSeedPhrase } from './seed-phrase-helpers';

/** Format an account address for display by showing the 4 first and last characters in the base58check representation. */
export function displaySplitAddress(address: string) {
return `${address.slice(0, 4)}...${address.slice(-4)}`;
}

export const displayNameOrSplitAddress = (account: WalletCredential | undefined) => {
const { credName, address } = account || { address: '' };
return credName || `${address.slice(0, 4)}...${address.slice(address.length - 4)}`;
return credName || displaySplitAddress(address);
};

export const displayNameAndSplitAddress = (account: WalletCredential | undefined) => {
const { credName, address } = account || { address: '' };
const splitAddress = `${address.slice(0, 4)}...${address.slice(address.length - 4)}`;
return `${credName ? `${credName} / ` : ''}${splitAddress}`;
return `${credName ? `${credName} / ` : ''}${displaySplitAddress(address)}`;
};

export function useIdentityOf(cred?: WalletCredential) {
Expand Down Expand Up @@ -51,9 +55,11 @@ export function useIdentityName(credential: WalletCredential, fallback?: string)

export function useWritableSelectedAccount(accountAddress: string) {
const [accounts, setAccounts] = useAtom(writableCredentialAtom);
const setAccount = (update: WalletCredential) =>
const setAccount = (update: Partial<WalletCredential>) =>
setAccounts(
accounts.map((account) => (account.address === accountAddress ? { ...account, ...update } : account))
accounts.map((account) =>
account.address === accountAddress ? ({ ...account, ...update } as WalletCredential) : account
)
);

return setAccount;
Expand Down

0 comments on commit efce054

Please sign in to comment.