Merge pull request #1201 from nymtech/feature/multi-address-wallet

Feature/multi address wallet
This commit is contained in:
Fouad
2022-04-08 13:13:09 +01:00
committed by GitHub
8 changed files with 382 additions and 7 deletions
+3
View File
@@ -42,7 +42,9 @@
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"string-to-color": "^2.2.2",
"use-clipboard-copy": "^0.2.0",
"uuid": "^8.3.2",
"yup": "^0.32.9"
},
"devDependencies": {
@@ -71,6 +73,7 @@
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.1.8",
"@types/semver": "^7.3.8",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2",
@@ -0,0 +1,29 @@
import React from 'react';
import { Box } from '@mui/material';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Accounts } from 'src/components/Accounts';
import { v4 as uuid4 } from 'uuid';
export default {
title: 'Wallet / Multi Account',
component: Accounts,
} as ComponentMeta<typeof Accounts>;
const Template: ComponentStory<typeof Accounts> = (args) => (
<Box display="flex" alignContent="center">
<Accounts {...args} />
</Box>
);
export const Default = Template.bind({});
Default.args = {
storedAccounts: [{ name: 'Account 1', address: uuid4() }],
};
export const MultipleAccounts = Template.bind({});
MultipleAccounts.args = {
storedAccounts: [
{ name: 'Account 1', address: uuid4() },
{ name: 'Account 2', address: uuid4() },
],
};
+295
View File
@@ -0,0 +1,295 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText,
TextField,
Typography,
} from '@mui/material';
import stc from 'string-to-color';
import { Add, CircleTwoTone, Close, Edit } from '@mui/icons-material';
import { v4 as uuidv4 } from 'uuid';
export type TAccount = {
name: string;
address: string;
};
type TDialog = 'Accounts' | 'Add' | 'Edit';
const AccountColor = ({ address }: { address: string }) => <CircleTwoTone sx={{ color: stc(address) }} />;
const AccountItem = ({
name,
address,
selected,
onSelect,
onEdit,
}: {
name: string;
address: string;
selected: boolean;
onSelect: () => void;
onEdit: () => void;
}) => (
<ListItem disablePadding disableGutters sx={selected ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}>
<ListItemButton disableRipple onClick={onSelect}>
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
<AccountColor address={address} />
</ListItemAvatar>
<ListItemText primary={name} secondary={address} />
<ListItemIcon>
<IconButton
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
>
<Edit />
</IconButton>
</ListItemIcon>
</ListItemButton>
</ListItem>
);
const AccountsModal = ({
show,
accounts,
selectedAccount,
onClose,
onAccountSelect,
onAddAccount,
onEditAccount,
}: {
show: boolean;
accounts: TAccount[];
selectedAccount: TAccount['address'];
onClose: () => void;
onAccountSelect: (account: TAccount) => void;
onAddAccount: () => void;
onEditAccount: (acc: TAccount) => void;
}) => (
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Accounts</Typography>
<IconButton onClick={onClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Switch between accounts
</Typography>
</DialogTitle>
<DialogContent sx={{ padding: 0 }}>
{accounts.map(({ name, address }) => (
<AccountItem
name={name}
address={address}
onSelect={() => {
onAccountSelect({ name, address });
onClose();
}}
onEdit={() => onEditAccount({ name, address })}
selected={selectedAccount === address}
key={address}
/>
))}
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
startIcon={<Add fontSize="small" />}
onClick={onAddAccount}
>
Add new account
</Button>
</DialogActions>
</Dialog>
);
const AddAccountModal = ({
show,
onClose,
onAdd,
}: {
show: boolean;
onClose: () => void;
onAdd: (accountName: string) => void;
}) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
if (!show) setAccountName('');
}, [show]);
return (
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Add new account</Typography>
<IconButton onClick={onClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
New wallet address
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
label="Account name"
fullWidth
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
autoFocus
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
startIcon={<Add fontSize="small" />}
onClick={() => onAdd(accountName)}
disabled={!accountName.length}
>
Add
</Button>
</DialogActions>
</Dialog>
);
};
const EditAccountModal = ({
account,
show,
onClose,
onEdit,
}: {
account?: TAccount;
show: boolean;
onClose: () => void;
onEdit: (account: TAccount) => void;
}) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
setAccountName(account ? account?.name : '');
}, [account]);
return (
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Edit account name</Typography>
<IconButton onClick={onClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
New wallet address
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
label="Account name"
fullWidth
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
autoFocus
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => account && onEdit({ ...account, name: accountName })}
disabled={!accountName?.length}
>
Edit
</Button>
</DialogActions>
</Dialog>
);
};
export const Accounts = ({ storedAccounts }: { storedAccounts: TAccount[] }) => {
const [accounts, setAccounts] = useState(storedAccounts);
const [selectedAccount, setSelectedAccount] = useState(accounts[0]);
const [accountToEdit, setAccountToEdit] = useState<TAccount>();
const [dialogToDisplasy, setDialogToDisplay] = useState<TDialog>();
useEffect(() => {
const selected = accounts.find((acc) => acc.address === selectedAccount.address);
if (selected) setSelectedAccount(selected);
}, [accounts]);
return (
<>
<Button
startIcon={<AccountColor address={selectedAccount.address} />}
color="inherit"
onClick={() => setDialogToDisplay('Accounts')}
size="large"
disableRipple
>
{selectedAccount.name}
</Button>
<AccountsModal
show={dialogToDisplasy === 'Accounts'}
onClose={() => setDialogToDisplay(undefined)}
accounts={accounts}
onAccountSelect={(acc) => setSelectedAccount(acc)}
selectedAccount={selectedAccount.address}
onAddAccount={() => {
setDialogToDisplay('Add');
}}
onEditAccount={(acc) => {
setAccountToEdit(acc);
setDialogToDisplay('Edit');
}}
/>
<AddAccountModal
show={dialogToDisplasy === 'Add'}
onClose={() => {
setDialogToDisplay(undefined);
}}
onAdd={(name) => {
setAccounts((accs) => [...accs, { address: uuidv4(), name }]);
setDialogToDisplay('Accounts');
}}
/>
<EditAccountModal
show={dialogToDisplasy === 'Edit'}
account={accountToEdit}
onClose={() => {
setDialogToDisplay('Accounts');
}}
onEdit={(account) => {
setAccounts((accs) => accs.map((acc) => (acc.address === account.address ? account : acc)));
setDialogToDisplay('Accounts');
}}
/>
</>
);
};
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Box, Typography } from '@mui/material';
import { Box } from '@mui/material';
import { ClientAddressDisplay } from './ClientAddress';
export default {
+4 -3
View File
@@ -14,10 +14,11 @@ export const NymCard: React.FC<{
title: string | React.ReactElement;
subheader?: string;
Action?: React.ReactNode;
Icon?: any;
Icon?: React.ReactNode;
noPadding?: boolean;
}> = ({ title, subheader, Action, Icon, noPadding, children }) => (
<Card variant="outlined" sx={{ overflow: 'auto' }}>
borderless?: boolean;
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children }) => (
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }) }}>
<CardHeader
sx={{ p: 3, color: 'nym.background.dark' }}
title={<Title title={title} Icon={Icon} />}
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => (
export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => (
<Box display="flex" alignItems="center">
{Icon && <Icon sx={{ mr: 1 }} />}{' '}
{Icon}
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{title}
</Typography>
+1 -1
View File
@@ -137,7 +137,7 @@ export const VestingCard = () => {
<NymCard
title="Vesting Schedule"
data-testid="check-unvested-tokens"
Icon={InfoOutlined}
Icon={<InfoOutlined />}
Action={
<IconButton
onClick={async () => {
+47
View File
@@ -5080,6 +5080,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/webpack-env@^1.16.0":
version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
@@ -7234,6 +7239,11 @@ colorette@^2.0.10, colorette@^2.0.14:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
colornames@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
colors@1.4.0, colors@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@@ -10258,6 +10268,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hex-rgb@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
highlight.js@^10.1.1, highlight.js@~10.7.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
@@ -12316,6 +12331,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.padend@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=
lodash.template@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
@@ -12331,11 +12351,21 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "^3.0.0"
lodash.trimstart@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz#8ff4dec532d82486af59573c39445914e944a7f1"
integrity sha1-j/TexTLYJIavWVc8OURZFOlEp/E=
lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash.words@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
integrity sha1-Xs/q+Oz4rKqODIOGKV8Zk8nPQDY=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -15666,6 +15696,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rgb-hex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-3.0.0.tgz#eab0168cc1279563b18a14605315389142e2e487"
integrity sha512-8h7ZcwxCBDKvchSWbWngJuSCqJGQ6nDuLLg+QcRyQDbX9jMWt+PpPeXAhSla0GOooEomk3lCprUpGkMdsLjKyg==
rifm@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4"
@@ -16468,6 +16503,18 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
string-to-color@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/string-to-color/-/string-to-color-2.2.2.tgz#46210bf7777dc9198dcdf997bd18ae6749cc9a73"
integrity sha512-XeA2goP7PNsSlz8RRn6KhYswnMf5Tl+38ajfy8n4oZJyMGC4qqKgHNHsZ/3qwvr42NRIjf9eSr721SyetDeMkA==
dependencies:
colornames "^1.1.1"
hex-rgb "^4.1.0"
lodash.padend "^4.6.1"
lodash.trimstart "^4.5.1"
lodash.words "^4.2.0"
rgb-hex "^3.0.0"
string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"