diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 151a13c9a1..8058728562 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -44,6 +44,7 @@ fn main() { mixnet::account::get_validator_api_urls, mixnet::account::get_validator_nymd_urls, mixnet::account::list_accounts, + mixnet::account::sign_in_decrypted_account, mixnet::account::logout, mixnet::account::remove_account_for_password, mixnet::account::remove_password, diff --git a/nym-wallet/src/components/Accounts/AccountAvatar.tsx b/nym-wallet/src/components/Accounts/AccountAvatar.tsx index fb2ba5da4b..95d1fcfd16 100644 --- a/nym-wallet/src/components/Accounts/AccountAvatar.tsx +++ b/nym-wallet/src/components/Accounts/AccountAvatar.tsx @@ -3,6 +3,6 @@ import { Avatar } from '@mui/material'; import stc from 'string-to-color'; import { TAccount } from 'src/types'; -export const AccountAvatar = ({ name, address }: TAccount) => ( - {name.split('')[0]} +export const AccountAvatar = ({ name }: Pick) => ( + {name?.split('')[0]} ); diff --git a/nym-wallet/src/components/Accounts/AccountContainer.tsx b/nym-wallet/src/components/Accounts/AccountContainer.tsx deleted file mode 100644 index eb61715f20..0000000000 --- a/nym-wallet/src/components/Accounts/AccountContainer.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { TAccount } from 'src/types'; -import { Accounts } from './Accounts'; -import { TDialog } from './types'; - -export const AccountsContainer = ({ storedAccounts }: { storedAccounts: TAccount[] }) => { - const [accounts, setAccounts] = useState(storedAccounts); - const [selectedAccount, setSelectedAccount] = useState(accounts[0]); - const [accountToEdit, setAccountToEdit] = useState(); - const [dialogToDisplay, setDialogToDisplay] = useState(); - - useEffect(() => { - const selected = accounts.find((acc) => acc.address === selectedAccount.address); - if (selected) setSelectedAccount(selected); - }, [accounts]); - - const addAccount = (account: TAccount) => setAccounts((accs) => [...accs, account]); - const editAccount = (account: TAccount) => - setAccounts((accs) => accs.map((acc) => (acc.address === account.address ? account : acc))); - const importAccount = (account: TAccount) => setAccounts((accs) => [...accs, account]); - - return ( - - ); -}; diff --git a/nym-wallet/src/components/Accounts/AccountItem.tsx b/nym-wallet/src/components/Accounts/AccountItem.tsx index b8dd61279c..8344d355a4 100644 --- a/nym-wallet/src/components/Accounts/AccountItem.tsx +++ b/nym-wallet/src/components/Accounts/AccountItem.tsx @@ -1,37 +1,68 @@ -import React from 'react'; -import { IconButton, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; -import { Edit } from '@mui/icons-material'; +import React, { useContext } from 'react'; +import { Box, ListItem, ListItemAvatar, ListItemButton, ListItemText, Tooltip, Typography } from '@mui/material'; +import { useClipboard } from 'use-clipboard-copy'; +import { AccountsContext } from 'src/context'; import { AccountAvatar } from './AccountAvatar'; -export const AccountItem = ({ - name, - address, - selected, - onSelect, - onEdit, -}: { - name: string; - address: string; - selected: boolean; - onSelect: () => void; - onEdit: () => void; -}) => ( - - - - - - - - { - e.stopPropagation(); - onEdit(); - }} - > - - - - - -); +export const AccountItem = ({ name, address }: { name: string; address: string }) => { + const { selectedAccount, handleSelectAccount, setDialogToDisplay, setAccountMnemonic } = useContext(AccountsContext); + const { copy, copied } = useClipboard({ copiedTimeout: 1000 }); + return ( + + handleSelectAccount(name)}> + + + + + + ) => { + e.stopPropagation(); + copy(address); + }} + sx={{ '&:hover': { color: 'grey.900' } }} + > + {address} + + + + ) => { + e.stopPropagation(); + setDialogToDisplay('Mnemonic'); + setAccountMnemonic((accountMnemonic) => ({ ...accountMnemonic, accountName: name })); + }} + > + Show mnemonic + + + + } + /> + {/* edit and remove accounts todo */} + {/* + { + e.stopPropagation(); + handleAccountToEdit(name); + }} + > + + + */} + + + ); +}; diff --git a/nym-wallet/src/components/Accounts/AccountOverview.tsx b/nym-wallet/src/components/Accounts/AccountOverview.tsx new file mode 100644 index 0000000000..17830ba319 --- /dev/null +++ b/nym-wallet/src/components/Accounts/AccountOverview.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Button } from '@mui/material'; +import { AccountEntry } from 'src/types'; +import { AccountAvatar } from './AccountAvatar'; + +export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( + +); diff --git a/nym-wallet/src/components/Accounts/Accounts.tsx b/nym-wallet/src/components/Accounts/Accounts.tsx index 194ec48b51..a59b59c95f 100644 --- a/nym-wallet/src/components/Accounts/Accounts.tsx +++ b/nym-wallet/src/components/Accounts/Accounts.tsx @@ -1,89 +1,36 @@ -import React from 'react'; -import { Button } from '@mui/material'; -import { v4 as uuidv4 } from 'uuid'; -import { TAccount } from 'src/types'; +import React, { useContext, useState } from 'react'; +import { AccountsContext, AppContext } from 'src/context'; import { EditAccountModal } from './EditAccountModal'; import { AddAccountModal } from './AddAccountModal'; import { AccountsModal } from './AccountsModal'; -import { ImportAccountModal } from './ImportAccountModal'; -import { AccountAvatar } from './AccountAvatar'; -import { TDialog } from './types'; +import { MnemonicModal } from './MnemonicModal'; +import { AccountOverview } from './AccountOverview'; +import { MultiAccountHowTo } from './MultiAccountHowTo'; -export const Accounts = ({ - accounts, - selectedAccount, - accountToEdit, - dialogToDisplay, - addAccount, - editAccount, - setAccountToEdit, - importAccount, - setSelectedAccount, - setDialogToDisplay, -}: { - accounts: TAccount[]; - selectedAccount: TAccount; - accountToEdit?: TAccount; - dialogToDisplay?: TDialog; - addAccount: (acc: TAccount) => void; - editAccount: (acc: TAccount) => void; - importAccount: (acc: TAccount) => void; - setAccountToEdit: (acc: TAccount) => void; - setSelectedAccount: (accs: TAccount) => void; - setDialogToDisplay: (dialog: TDialog | undefined) => void; -}) => ( - <> - - setDialogToDisplay(undefined)} - accounts={accounts} - onAccountSelect={(acc) => setSelectedAccount(acc)} - selectedAccount={selectedAccount.address} - onAdd={() => { - setDialogToDisplay('Add'); - }} - onEdit={(acc) => { - setAccountToEdit(acc); - setDialogToDisplay('Edit'); - }} - onImport={() => setDialogToDisplay('Import')} - /> - { - setDialogToDisplay('Accounts'); - }} - onAdd={(name) => { - addAccount({ name, address: uuidv4() }); - setDialogToDisplay('Accounts'); - }} - /> - { - setDialogToDisplay('Accounts'); - }} - onEdit={(account) => { - editAccount(account); - setDialogToDisplay('Accounts'); - }} - /> - setDialogToDisplay('Accounts')} - onImport={() => { - importAccount({ name: 'New Account', address: uuidv4() }); - setDialogToDisplay('Accounts'); - }} - /> - -); +export const Accounts = () => { + const { accounts, selectedAccount, setDialogToDisplay } = useContext(AccountsContext); + + return accounts && selectedAccount ? ( + <> + setDialogToDisplay('Accounts')} /> + + + + + + ) : null; +}; + +export const SingleAccount = () => { + const [showHowToDialog, setShowHowToDialog] = useState(false); + const { clientDetails } = useContext(AppContext); + return ( + <> + setShowHowToDialog(true)} + /> + setShowHowToDialog(false)} /> + + ); +}; diff --git a/nym-wallet/src/components/Accounts/AccountsModal.tsx b/nym-wallet/src/components/Accounts/AccountsModal.tsx index 4b4c9205aa..8210b3c562 100644 --- a/nym-wallet/src/components/Accounts/AccountsModal.tsx +++ b/nym-wallet/src/components/Accounts/AccountsModal.tsx @@ -1,62 +1,48 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material'; import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material'; -import { TAccount } from 'src/types'; +import { AccountsContext } from 'src/context'; import { AccountItem } from './AccountItem'; -export const AccountsModal = ({ - show, - accounts, - selectedAccount, - onClose, - onAccountSelect, - onAdd, - onEdit, - onImport, -}: { - show: boolean; - accounts: TAccount[]; - selectedAccount: TAccount['address']; - onClose: () => void; - onAccountSelect: (account: TAccount) => void; - onAdd: () => void; - onEdit: (acc: TAccount) => void; - onImport: () => void; -}) => ( - - - - Accounts - - - - - - Switch between accounts - - - - {accounts.map(({ name, address }) => ( - { - onAccountSelect({ name, address }); - onClose(); - }} - onEdit={() => onEdit({ name, address })} - selected={selectedAccount === address} - key={address} - /> - ))} - - - - - - -); +export const AccountsModal = ({ onClose }: { onClose?: () => void }) => { + const { accounts, dialogToDisplay, setDialogToDisplay } = useContext(AccountsContext); + + const handleClose = () => { + setDialogToDisplay(undefined); + onClose?.(); + }; + + return ( + + + + Accounts + + + + + + Switch between accounts + + + + {accounts?.map(({ id, address }) => ( + + ))} + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Accounts/AddAccountModal.tsx b/nym-wallet/src/components/Accounts/AddAccountModal.tsx index 60d1963618..3ad40f9209 100644 --- a/nym-wallet/src/components/Accounts/AddAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/AddAccountModal.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { - Alert, Box, Button, + CircularProgress, Dialog, DialogActions, DialogContent, @@ -12,75 +12,32 @@ import { TextField, Typography, } from '@mui/material'; -import { Check, Close, ContentCopySharp } from '@mui/icons-material'; +import { ArrowBackSharp } from '@mui/icons-material'; import { useClipboard } from 'use-clipboard-copy'; +import { createMnemonic } from 'src/requests'; +import { AccountsContext } from 'src/context'; +import { Mnemonic } from '../Mnemonic'; -const createAccountSteps = ['Save and copy mnemonic for your new account', 'Name your new account']; - -const passwordCreationSteps = [ - 'Log out', - 'During sign in screen click “Sign in with mnemonic” button', - 'On next screen click “Create a password for your account”', - 'Sign in to wallet with your new password', - 'Now you can create multiple accounts', +const createAccountSteps = [ + 'Copy and save mnemonic for your new account', + 'Name your new account', + 'Confirm the password used to login to your wallet', +]; +const importAccountSteps = [ + 'Provide mnemonic of account you want to import', + 'Name your new account', + 'Confirm the password used to login to your wallet', ]; -const NoPassword = ({ onClose }: { onClose: () => void }) => ( - - - - - - You can’t add new accounts if your wallet doesn’t have a password. - - Follow steps below to create password. - - How to create password to your account - {passwordCreationSteps.map((step, i) => ( - {`${i + 1}. ${step}`} - ))} - - - - - - -); - -const MnemonicStep = ({ mnemonic, onSave }: { mnemonic: string; onSave: () => void }) => { +const MnemonicStep = ({ mnemonic, onNext }: { mnemonic: string; onNext: () => void }) => { const { copy, copied } = useClipboard({ copiedTimeout: 5000 }); return ( - - - - Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the - future - - - - - - + - @@ -88,7 +45,44 @@ const MnemonicStep = ({ mnemonic, onSave }: { mnemonic: string; onSave: () => vo ); }; -const NameAccount = ({ onAdd }: { onAdd: (value: string) => void }) => { +const ImportMnemonic = ({ + value, + onChange, + onNext, +}: { + value: string; + onChange: (value: string) => void; + onNext: () => void; +}) => ( + + + + onChange(e.target.value)} + fullWidth + type="password" + /> + + + + + + +); + +const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => { const [value, setValue] = useState(''); return ( @@ -102,61 +96,128 @@ const NameAccount = ({ onAdd }: { onAdd: (value: string) => void }) => { disableElevation variant="contained" size="large" - onClick={() => onAdd(value)} + onClick={() => onNext(value)} > - Add + Next ); }; -export const AddAccountModal = ({ - show, - withoutPassword, - onClose, - onAdd, -}: { - show: boolean; - withoutPassword?: boolean; - onClose: () => void; - onAdd: (accountName: string) => void; -}) => { - const [step, setStep] = useState(0); +const ConfirmPassword = ({ onConfirm }: { onConfirm: (password: string) => void }) => { + const [value, setValue] = useState(''); + const { isLoading, error } = useContext(AccountsContext); return ( - - - - Add new account - - - - - {!withoutPassword && ( - - {`Step ${step + 1}/${createAccountSteps.length}`} + + + {error && ( + + {error} )} - {createAccountSteps[step]} + setValue(e.target.value)} fullWidth type="password" /> + + + + + + ); +}; + +export const AddAccountModal = () => { + const [step, setStep] = useState(0); + const [data, setData] = useState({ + mnemonic: '', + accountName: '', + }); + + const { dialogToDisplay, setDialogToDisplay, handleAddAccount, setError } = useContext(AccountsContext); + + const generateMnemonic = async () => { + const mnemon = await createMnemonic(); + setData((d) => ({ ...d, mnemonic: mnemon })); + }; + + const handleClose = () => { + setDialogToDisplay('Accounts'); + setData({ mnemonic: '', accountName: '' }); + setStep(0); + setError(undefined); + }; + + useEffect(() => { + if (dialogToDisplay === 'Add') generateMnemonic(); + }, [dialogToDisplay]); + + useEffect(() => { + setError(undefined); + }, [step]); + + return ( + + + + {`${dialogToDisplay} new account`} + (step === 0 ? handleClose() : setStep((s) => s - 1))}> + + + + + {dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]} + - {withoutPassword && } - {!withoutPassword && - (() => { - switch (step) { - case 0: - return ( - setStep((s) => s + 1)} - /> - ); - case 1: - return ; - default: - return null; - } - })()} + {(() => { + switch (step) { + case 0: + return dialogToDisplay === 'Add' ? ( + setStep((s) => s + 1)} /> + ) : ( + setData((d) => ({ ...d, mnemonic: value }))} + onNext={() => setStep((s) => s + 1)} + /> + ); + case 1: + return ( + { + setData((d) => ({ ...d, accountName })); + setStep((s) => s + 1); + }} + /> + ); + case 2: + return ( + { + if (data.accountName && data.mnemonic) { + handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password }); + } + }} + /> + ); + default: + return null; + } + })()} ); }; diff --git a/nym-wallet/src/components/Accounts/EditAccountModal.tsx b/nym-wallet/src/components/Accounts/EditAccountModal.tsx index 1f290c6f0f..c5fec1d28b 100644 --- a/nym-wallet/src/components/Accounts/EditAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/EditAccountModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { Box, Button, @@ -11,31 +11,23 @@ import { Typography, } from '@mui/material'; import { Close } from '@mui/icons-material'; -import { TAccount } from 'src/types'; +import { AccountsContext } from 'src/context'; -export const EditAccountModal = ({ - account, - show, - onClose, - onEdit, -}: { - account?: TAccount; - show: boolean; - onClose: () => void; - onEdit: (account: TAccount) => void; -}) => { +export const EditAccountModal = () => { const [accountName, setAccountName] = useState(''); + const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext); + useEffect(() => { - setAccountName(account ? account?.name : ''); - }, [account]); + setAccountName(accountToEdit ? accountToEdit?.id : ''); + }, [accountToEdit]); return ( - + setDialogToDisplay('Accounts')} fullWidth hideBackdrop> Edit account name - + setDialogToDisplay('Accounts')}> @@ -60,7 +52,12 @@ export const EditAccountModal = ({ disableElevation variant="contained" size="large" - onClick={() => account && onEdit({ ...account, name: accountName })} + onClick={() => { + if (accountToEdit) { + handleEditAccount({ ...accountToEdit, id: accountName }); + setDialogToDisplay('Accounts'); + } + }} disabled={!accountName?.length} > Edit diff --git a/nym-wallet/src/components/Accounts/ImportAccountModal.tsx b/nym-wallet/src/components/Accounts/ImportAccountModal.tsx index 8222f330bf..2e61066f71 100644 --- a/nym-wallet/src/components/Accounts/ImportAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/ImportAccountModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { Box, Button, @@ -11,28 +11,24 @@ import { Typography, } from '@mui/material'; import { Close } from '@mui/icons-material'; +import { AccountsContext } from 'src/context'; -export const ImportAccountModal = ({ - show, - onClose, - onImport, -}: { - show: boolean; - onClose: () => void; - onImport: (mnemonic: string) => void; -}) => { +export const ImportAccountModal = () => { const [mnemonic, setMnemonic] = useState(''); - useEffect(() => { - if (!show) setMnemonic(''); - }, [show]); + const { dialogToDisplay, setDialogToDisplay, handleImportAccount } = useContext(AccountsContext); + + const handleClose = () => { + setMnemonic(''); + setDialogToDisplay('Accounts'); + }; return ( - + Import account - + @@ -59,7 +55,7 @@ export const ImportAccountModal = ({ disableElevation variant="contained" size="large" - onClick={() => onImport(mnemonic)} + onClick={() => handleImportAccount({ id: '', address: '' })} disabled={!mnemonic.length} > Import account diff --git a/nym-wallet/src/components/Accounts/MnemonicModal.tsx b/nym-wallet/src/components/Accounts/MnemonicModal.tsx new file mode 100644 index 0000000000..55dc78a604 --- /dev/null +++ b/nym-wallet/src/components/Accounts/MnemonicModal.tsx @@ -0,0 +1,102 @@ +import React, { useContext, useState } from 'react'; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + TextField, + Typography, +} from '@mui/material'; +import { ArrowBackSharp } from '@mui/icons-material'; +import { AccountsContext } from 'src/context'; +import { useClipboard } from 'use-clipboard-copy'; +import { Mnemonic } from '../Mnemonic'; + +export const MnemonicModal = () => { + const [password, setPassword] = useState(''); + + const { copy, copied } = useClipboard({ copiedTimeout: 5000 }); + + const { + dialogToDisplay, + setDialogToDisplay, + accountMnemonic, + setAccountMnemonic, + handleGetAcccountMnemonic, + error, + setError, + isLoading, + } = useContext(AccountsContext); + + const handleClose = () => { + setAccountMnemonic({ value: undefined, accountName: undefined }); + setError(undefined); + setDialogToDisplay('Accounts'); + setPassword(''); + }; + + return ( + + + + Display mnemonic + + + + + + {`Display mnemonic for: ${accountMnemonic?.accountName}`} + + + + + {error && ( + + {error} + + )} + {!accountMnemonic.value ? ( + <> + Enter the password used to login to your wallet + ) => setPassword(e.target.value)} + autoFocus + /> + + ) : ( + + )} + + + + {!accountMnemonic.value && ( + + )} + + + ); +}; diff --git a/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx new file mode 100644 index 0000000000..4f9f514c47 --- /dev/null +++ b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Alert, Box, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material'; +import { Close } from '@mui/icons-material'; + +const passwordCreationSteps = [ + 'Log out', + 'When signing in, select “Sign in with mnemonic”', + 'On the next screen click “Create a password for your account”', + 'Sign in to wallet with your new password', + 'Now you can create multiple accounts', +]; + +export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => ( + + + + Multi accounts + + + + + + How to set up multiple accounts + + + + + + In order to create multiple accounts your wallet need password. + Follow steps below to create password. + + How to create a password for your account + {passwordCreationSteps.map((step, index) => ( + {`${index + 1}. ${step}`} + ))} + + + +); diff --git a/nym-wallet/src/components/Accounts/ShowMnemonicModal.tsx b/nym-wallet/src/components/Accounts/ShowMnemonicModal.tsx deleted file mode 100644 index 392bd4e841..0000000000 --- a/nym-wallet/src/components/Accounts/ShowMnemonicModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { - Alert, - AlertTitle, - Box, - Dialog, - DialogContent, - DialogTitle, - IconButton, - Stack, - Typography, -} from '@mui/material'; -import { Close } from '@mui/icons-material'; -import { CopyToClipboard } from '@nymproject/react'; - -export const ShowMnemonicModal = ({ - mnemonic, - show, - onClose, -}: { - mnemonic: string; - show: boolean; - onClose: () => void; -}) => ( - - - - Show mnemonic - - - - - - - - - DO NOT share this phrase with anyone! - These words can be used to steal all your accounts. - - } icon={false}> - Mnemonic - {mnemonic} - - - - -); diff --git a/nym-wallet/src/components/Accounts/index.tsx b/nym-wallet/src/components/Accounts/index.tsx new file mode 100644 index 0000000000..9e02c40d2b --- /dev/null +++ b/nym-wallet/src/components/Accounts/index.tsx @@ -0,0 +1,16 @@ +import React, { useContext } from 'react'; +import { AccountsProvider, AppContext } from 'src/context'; +import { Accounts, SingleAccount } from './Accounts'; + +export const MultiAccounts = () => { + const { loginType } = useContext(AppContext); + + if (loginType === 'password') { + return ( + + + + ); + } + return ; +}; diff --git a/nym-wallet/src/components/Accounts/mocks.ts b/nym-wallet/src/components/Accounts/mocks.ts new file mode 100644 index 0000000000..d3505b0b4c --- /dev/null +++ b/nym-wallet/src/components/Accounts/mocks.ts @@ -0,0 +1,10 @@ +export const accounts = [ + { + id: 'Account 1', + address: 'n107wsxkj08hycflnkp5ayfg6rt3pt0psm7w2t9r', + }, + { + id: 'Account 2', + address: 'n1dgp04lqaasnzaww66zwdp6u24smqe7ltuny8vk', + }, +]; diff --git a/nym-wallet/src/components/Accounts/stories/Accounts.stories.tsx b/nym-wallet/src/components/Accounts/stories/Accounts.stories.tsx index 4b8b87ee4d..4c6f6550a9 100644 --- a/nym-wallet/src/components/Accounts/stories/Accounts.stories.tsx +++ b/nym-wallet/src/components/Accounts/stories/Accounts.stories.tsx @@ -1,25 +1,18 @@ import React from 'react'; import { Box } from '@mui/material'; import { ComponentMeta, ComponentStory } from '@storybook/react'; - -import { v4 as uuid4 } from 'uuid'; -import { AccountsContainer } from '../AccountContainer'; +import { MockAccountsProvider } from 'src/context/mocks/accounts'; +import { Accounts } from '../Accounts'; export default { title: 'Wallet / Multi Account', - component: AccountsContainer, -} as ComponentMeta; + component: Accounts, +} as ComponentMeta; -const Template: ComponentStory = (args) => ( +export const Default: ComponentStory = () => ( - + + + ); - -export const Default = Template.bind({}); -Default.args = { - storedAccounts: [ - { name: 'Account 1', address: uuid4() }, - { name: 'Account 2', address: uuid4() }, - ], -}; diff --git a/nym-wallet/src/components/Accounts/stories/AddAccount.stories.tsx b/nym-wallet/src/components/Accounts/stories/AddAccount.stories.tsx deleted file mode 100644 index ac1289d443..0000000000 --- a/nym-wallet/src/components/Accounts/stories/AddAccount.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { AddAccountModal } from 'src/components/Accounts/AddAccountModal'; - -export default { - title: 'Wallet / Multi Account / Add Account', - component: AddAccountModal, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - - - -); - -export const Default = Template.bind({}); -Default.args = { - show: true, - onClose: () => {}, - onAdd: () => {}, -}; - -export const WithoutPassword = Template.bind({}); -WithoutPassword.args = { - show: true, - withoutPassword: true, - onClose: () => {}, - onAdd: () => {}, -}; diff --git a/nym-wallet/src/components/Accounts/stories/EditAccount.stories.tsx b/nym-wallet/src/components/Accounts/stories/EditAccount.stories.tsx deleted file mode 100644 index a75139e3c0..0000000000 --- a/nym-wallet/src/components/Accounts/stories/EditAccount.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { v4 as uuid } from 'uuid'; -import { EditAccountModal } from 'src/components/Accounts/EditAccountModal'; - -export default { - title: 'Wallet / Multi Account / Edit Account', - component: EditAccountModal, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - - - -); - -export const Default = Template.bind({}); -Default.args = { - account: { name: 'Account 1', address: uuid() }, - show: true, - onClose: () => {}, -}; diff --git a/nym-wallet/src/components/Accounts/stories/ImportAccount.stories.tsx b/nym-wallet/src/components/Accounts/stories/ImportAccount.stories.tsx deleted file mode 100644 index b203cc676c..0000000000 --- a/nym-wallet/src/components/Accounts/stories/ImportAccount.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ImportAccountModal } from 'src/components/Accounts/ImportAccountModal'; - -export default { - title: 'Wallet / Multi Account / Import Account', - component: ImportAccountModal, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - - - -); - -export const Default = Template.bind({}); -Default.args = { - show: true, - onClose: () => {}, - onImport: () => {}, -}; diff --git a/nym-wallet/src/components/Accounts/stories/ShowMnemonic.stories.tsx b/nym-wallet/src/components/Accounts/stories/ShowMnemonic.stories.tsx deleted file mode 100644 index 0db1a2046a..0000000000 --- a/nym-wallet/src/components/Accounts/stories/ShowMnemonic.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ShowMnemonicModal } from 'src/components/Accounts/ShowMnemonicModal'; - -export default { - title: 'Wallet / Multi Account / Show Mnemonic', - component: ShowMnemonicModal, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - - - -); - -export const Default = Template.bind({}); -Default.args = { - mnemonic: - 'lonely employ curtain skull gas swim pizza injury tail birth inmate apart giraffe behave caution hammer echo action best symptom skull toast beyond casino', - show: true, - onClose: () => {}, -}; diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index b5ee0ab55d..ce5feabdc2 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -1,19 +1,26 @@ import React, { useContext } from 'react'; import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material'; +import { useHistory } from 'react-router-dom'; import { Logout } from '@mui/icons-material'; -import { ClientContext } from '../context/main'; +import { AppContext } from '../context/main'; import { NetworkSelector } from './NetworkSelector'; import { Node as NodeIcon } from '../svg-icons/node'; +import { MultiAccounts } from './Accounts'; export const AppBar = () => { - const { showSettings, logOut, handleShowSettings } = useContext(ClientContext); - + const { showSettings, logOut, handleShowSettings } = useContext(AppContext); + const history = useHistory(); return ( - - + + + + + + + @@ -26,7 +33,14 @@ export const AppBar = () => { - + { + await logOut(); + history.push('/'); + }} + sx={{ color: 'nym.background.dark' }} + > diff --git a/nym-wallet/src/components/ClientAddress.tsx b/nym-wallet/src/components/ClientAddress.tsx index 25c32919db..816f8b6a3a 100644 --- a/nym-wallet/src/components/ClientAddress.tsx +++ b/nym-wallet/src/components/ClientAddress.tsx @@ -1,6 +1,6 @@ import React, { FC, useContext } from 'react'; import { Box, Typography, Tooltip } from '@mui/material'; -import { ClientContext } from '../context/main'; +import { AppContext } from '../context/main'; import { CopyToClipboard } from './CopyToClipboard'; import { splice } from '../utils'; @@ -53,6 +53,6 @@ export const ClientAddressDisplay: FC ); export const ClientAddress: FC = ({ ...props }) => { - const { clientDetails } = useContext(ClientContext); + const { clientDetails } = useContext(AppContext); return ; }; diff --git a/nym-wallet/src/components/Fee.tsx b/nym-wallet/src/components/Fee.tsx index d4a7b550ec..4b2ae45646 100644 --- a/nym-wallet/src/components/Fee.tsx +++ b/nym-wallet/src/components/Fee.tsx @@ -2,11 +2,11 @@ import React, { useState, useEffect, useContext } from 'react'; import { Typography } from '@mui/material'; import { Operation } from '../types'; import { getGasFee } from '../requests'; -import { ClientContext } from '../context/main'; +import { AppContext } from '../context/main'; export const Fee = ({ feeType }: { feeType: Operation }) => { const [fee, setFee] = useState(); - const { currency } = useContext(ClientContext); + const { currency } = useContext(AppContext); const getFee = async () => { const res = await getGasFee(feeType); diff --git a/nym-wallet/src/components/LoadingPage.tsx b/nym-wallet/src/components/LoadingPage.tsx new file mode 100644 index 0000000000..7143bf49a0 --- /dev/null +++ b/nym-wallet/src/components/LoadingPage.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Box, LinearProgress, Stack } from '@mui/material'; +import { NymWordmark } from '@nymproject/react'; +import { AuthTheme } from 'src/theme'; + +export const LoadingPage = () => ( + + + + + + + + + + + + +); diff --git a/nym-wallet/src/components/Mnemonic.tsx b/nym-wallet/src/components/Mnemonic.tsx new file mode 100644 index 0000000000..e49c2b8042 --- /dev/null +++ b/nym-wallet/src/components/Mnemonic.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Alert, Button, Stack, TextField, Typography } from '@mui/material'; +import { Check, ContentCopySharp } from '@mui/icons-material'; + +export const Mnemonic = ({ + mnemonic, + copied, + handleCopy, +}: { + mnemonic: string; + copied: boolean; + handleCopy: (text?: string) => void; +}) => ( + + + + Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future + + + + + + +); diff --git a/nym-wallet/src/components/Nav.tsx b/nym-wallet/src/components/Nav.tsx index 50d2b3e0b4..0ecc2d3f89 100644 --- a/nym-wallet/src/components/Nav.tsx +++ b/nym-wallet/src/components/Nav.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material'; -import { ADMIN_ADDRESS, ClientContext } from '../context/main'; +import { ADMIN_ADDRESS, AppContext } from '../context/main'; import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons'; const routesSchema = [ @@ -44,7 +44,7 @@ const routesSchema = [ ]; export const Nav = () => { - const { clientDetails, handleShowAdmin } = useContext(ClientContext); + const { clientDetails, handleShowAdmin } = useContext(AppContext); const location = useLocation(); useEffect(() => { diff --git a/nym-wallet/src/components/NetworkSelector.tsx b/nym-wallet/src/components/NetworkSelector.tsx index 2f72c47fbd..cdf84ebc68 100644 --- a/nym-wallet/src/components/NetworkSelector.tsx +++ b/nym-wallet/src/components/NetworkSelector.tsx @@ -1,7 +1,7 @@ import React, { useState, useContext } from 'react'; import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material'; import { ArrowDropDown, CheckSharp } from '@mui/icons-material'; -import { ClientContext, IS_DEV_MODE } from '../context/main'; +import { AppContext, IS_DEV_MODE } from '../context/main'; import { Network } from '../types'; const networks: { networkName: Network; name: string }[] = [ @@ -22,7 +22,7 @@ const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () = ); export const NetworkSelector = () => { - const { network, switchNetwork } = useContext(ClientContext); + const { network, switchNetwork } = useContext(AppContext); const [anchorEl, setAnchorEl] = useState(null); diff --git a/nym-wallet/src/components/TokenPoolSelector.tsx b/nym-wallet/src/components/TokenPoolSelector.tsx index 60e9c9788b..99da52f9dc 100644 --- a/nym-wallet/src/components/TokenPoolSelector.tsx +++ b/nym-wallet/src/components/TokenPoolSelector.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material'; -import { ClientContext } from '../context/main'; +import { AppContext } from '../context/main'; type TPoolOption = 'balance' | 'locked'; @@ -12,7 +12,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T const { userBalance: { tokenAllocation, balance, fetchBalance, fetchTokenAllocation }, currency, - } = useContext(ClientContext); + } = useContext(AppContext); useEffect(() => { (async () => { diff --git a/nym-wallet/src/components/index.ts b/nym-wallet/src/components/index.ts index 2b04937757..776aab98a6 100644 --- a/nym-wallet/src/components/index.ts +++ b/nym-wallet/src/components/index.ts @@ -15,3 +15,4 @@ export * from './ClientAddress'; export * from './InfoToolTip'; export * from './Title'; export * from './TokenPoolSelector'; +export * from './LoadingPage'; diff --git a/nym-wallet/src/context/accounts.tsx b/nym-wallet/src/context/accounts.tsx new file mode 100644 index 0000000000..53e7127124 --- /dev/null +++ b/nym-wallet/src/context/accounts.tsx @@ -0,0 +1,135 @@ +import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'; +import { AccountEntry } from 'src/types'; +import { addAccount as addAccountRequest, showMnemonicForAccount } from 'src/requests'; +import { useSnackbar } from 'notistack'; +import { AppContext } from './main'; + +type TAccounts = { + accounts?: AccountEntry[]; + selectedAccount?: AccountEntry; + accountToEdit?: AccountEntry; + dialogToDisplay?: TAccountsDialog; + isLoading: boolean; + error?: string; + accountMnemonic: TAccountMnemonic; + setError: Dispatch>; + setAccountMnemonic: Dispatch>; + handleAddAccount: (data: { accountName: string; mnemonic: string; password: string }) => void; + setDialogToDisplay: (dialog?: TAccountsDialog) => void; + handleSelectAccount: (accountId: string) => void; + handleAccountToEdit: (accountId: string) => void; + handleEditAccount: (account: AccountEntry) => void; + handleImportAccount: (account: AccountEntry) => void; + handleGetAcccountMnemonic: (data: { password: string; accountName: string }) => void; +}; + +export type TAccountsDialog = 'Accounts' | 'Add' | 'Edit' | 'Import' | 'Mnemonic'; +export type TAccountMnemonic = { value?: string; accountName?: string }; + +export const AccountsContext = createContext({} as TAccounts); + +export const AccountsProvider: React.FC = ({ children }) => { + const [accounts, setAccounts] = useState([]); + const [selectedAccount, setSelectedAccount] = useState(); + const [accountToEdit, setAccountToEdit] = useState(); + const [dialogToDisplay, setDialogToDisplay] = useState(); + const [accountMnemonic, setAccountMnemonic] = useState({ + value: undefined, + accountName: undefined, + }); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const { onAccountChange, storedAccounts } = useContext(AppContext); + const { enqueueSnackbar } = useSnackbar(); + + const handleAddAccount = async ({ + accountName, + mnemonic, + password, + }: { + accountName: string; + mnemonic: string; + password: string; + }) => { + setIsLoading(true); + try { + const newAccount = await addAccountRequest({ + accountName, + mnemonic, + password, + }); + setAccounts((accs) => [...accs, newAccount]); + enqueueSnackbar('New account created', { variant: 'success' }); + setDialogToDisplay('Accounts'); + } catch (e) { + setError(`Error adding account: ${e}`); + } finally { + setIsLoading(false); + } + }; + const handleEditAccount = (account: AccountEntry) => + setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc))); + + const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]); + + const handleAccountToEdit = (accountName: string) => + setAccountToEdit(accounts?.find((acc) => acc.id === accountName)); + + const handleSelectAccount = async (accountName: string) => { + if (accountName !== selectedAccount?.id) { + await onAccountChange(accountName); + const match = accounts?.find((acc) => acc.id === accountName); + setSelectedAccount(match); + } + }; + + const handleGetAcccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => { + try { + setIsLoading(true); + const mnemonic = await showMnemonicForAccount({ password, accountName }); + setAccountMnemonic({ value: mnemonic, accountName }); + } catch (e) { + setError(e as string); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (storedAccounts) { + setAccounts(storedAccounts); + } + + if (storedAccounts && !selectedAccount) { + setSelectedAccount(storedAccounts[0]); + } + }, [storedAccounts]); + + return ( + ({ + error, + setError, + accounts, + selectedAccount, + accountToEdit, + dialogToDisplay, + accountMnemonic, + setDialogToDisplay, + setAccountMnemonic, + isLoading, + handleAddAccount, + handleEditAccount, + handleAccountToEdit, + handleSelectAccount, + handleImportAccount, + handleGetAcccountMnemonic, + }), + [accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic], + )} + > + {children} + + ); +}; diff --git a/nym-wallet/src/context/sign-in.tsx b/nym-wallet/src/context/auth.tsx similarity index 79% rename from nym-wallet/src/context/sign-in.tsx rename to nym-wallet/src/context/auth.tsx index 4af1973e13..12798ef134 100644 --- a/nym-wallet/src/context/sign-in.tsx +++ b/nym-wallet/src/context/auth.tsx @@ -1,11 +1,10 @@ import React, { createContext, useEffect, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { createMnemonic } from 'src/requests'; -import { TMnemonicWords } from 'src/pages/sign-in/types'; +import { TMnemonicWords } from 'src/pages/auth/types'; -export const SignInContext = createContext({} as TSignInContent); +export const AuthContext = createContext({} as TAuthContext); -export type TSignInContent = { +export type TAuthContext = { error?: string; password: string; mnemonic: string; @@ -22,23 +21,17 @@ const mnemonicToArray = (mnemonic: string): TMnemonicWords => .split(' ') .reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords); -export const SignInProvider: React.FC = ({ children }) => { +export const AuthProvider: React.FC = ({ children }) => { const [password, setPassword] = useState(''); const [mnemonic, setMnemonic] = useState(''); const [mnemonicWords, setMnemonicWords] = useState([]); const [error, setError] = useState(); - const history = useHistory(); - const generateMnemonic = async () => { const mnemonicPhrase = await createMnemonic(); setMnemonic(mnemonicPhrase); }; - useEffect(() => { - history.push('/welcome'); - }, []); - useEffect(() => { if (mnemonic.length > 0) { const mnemonicArray = mnemonicToArray(mnemonic); @@ -54,7 +47,7 @@ export const SignInProvider: React.FC = ({ children }) => { }; return ( - ({ error, @@ -71,6 +64,6 @@ export const SignInProvider: React.FC = ({ children }) => { )} > {children} - + ); }; diff --git a/nym-wallet/src/context/index.tsx b/nym-wallet/src/context/index.tsx index fe68268d78..0077fb3147 100644 --- a/nym-wallet/src/context/index.tsx +++ b/nym-wallet/src/context/index.tsx @@ -1,2 +1,3 @@ export * from './main'; -export * from './sign-in'; +export * from './auth'; +export * from './accounts'; diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index dc6091c8de..b75048fcd2 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -1,11 +1,18 @@ import React, { useMemo, createContext, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { useSnackbar } from 'notistack'; -import { TLoginType } from 'src/pages/sign-in/types'; -import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types'; +import { Account, Network, TCurrency, TMixnodeBondDetails, AccountEntry } from '../types'; import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance'; import { config } from '../../config'; -import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut } from '../requests'; +import { + getMixnodeBondDetails, + listAccounts, + selectNetwork, + signInWithMnemonic, + signInWithPassword, + signOut, + switchAccount, +} from '../requests'; import { currencyMap } from '../utils'; import { Console } from '../utils/console'; @@ -22,9 +29,12 @@ export const urls = (networkName?: Network) => networkExplorer: `https://${networkName}-explorer.nymtech.net`, }; -type TClientContext = { +type TLoginType = 'mnemonic' | 'password'; + +type TAppContext = { mode: 'light' | 'dark'; clientDetails?: Account; + storedAccounts?: AccountEntry[]; mixnodeDetails?: TMixnodeBondDetails | null; userBalance: TUseuserBalance; showAdmin: boolean; @@ -33,27 +43,31 @@ type TClientContext = { currency?: TCurrency; isLoading: boolean; error?: string; + loginType?: TLoginType; setIsLoading: (isLoading: boolean) => void; setError: (value?: string) => void; switchNetwork: (network: Network) => void; getBondDetails: () => Promise; handleShowSettings: () => void; handleShowAdmin: () => void; - logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void; + logIn: (opts: { type: TLoginType; value: string }) => void; signInWithPassword: (password: string) => void; logOut: () => void; + onAccountChange: (accountId: string) => void; }; -export const ClientContext = createContext({} as TClientContext); +export const AppContext = createContext({} as TAppContext); -export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => { +export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [clientDetails, setClientDetails] = useState(); + const [storedAccounts, setStoredAccounts] = useState(); const [mixnodeDetails, setMixnodeDetails] = useState(); const [network, setNetwork] = useState(); const [currency, setCurrency] = useState(); const [showAdmin, setShowAdmin] = useState(false); const [showSettings, setShowSettings] = useState(false); const [mode] = useState<'light' | 'dark'>('light'); + const [loginType, setLoginType] = useState<'mnemonic' | 'password'>(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); @@ -61,6 +75,15 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); + const clearState = () => { + userBalance.clearAll(); + setStoredAccounts(undefined); + setNetwork(undefined); + setError(undefined); + setIsLoading(false); + setMixnodeDetails(undefined); + }; + const loadAccount = async (n: Network) => { try { const client = await selectNetwork(n); @@ -73,6 +96,11 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode } }; + const loadStoredAccounts = async () => { + const accounts = await listAccounts(); + setStoredAccounts(accounts); + }; + const getBondDetails = async () => { setMixnodeDetails(undefined); try { @@ -83,15 +111,22 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode } }; + const refreshAccount = async (_network: Network) => { + await loadAccount(_network); + if (loginType === 'password') { + await loadStoredAccounts(); + } + }; + useEffect(() => { - const refreshAccount = async () => { - if (network) { - await loadAccount(network); - await getBondDetails(); - await userBalance.fetchBalance(); - } - }; - refreshAccount(); + if (!clientDetails) { + clearState(); + history.push('/'); + } + }, [clientDetails]); + + useEffect(() => { + if (network) refreshAccount(network); }, [network]); const logIn = async ({ type, value }: { type: TLoginType; value: string }) => { @@ -103,8 +138,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode setIsLoading(true); if (type === 'mnemonic') { await signInWithMnemonic(value); + setLoginType('mnemonic'); } else { await signInWithPassword(value); + setLoginType('password'); } setNetwork('MAINNET'); history.push('/balance'); @@ -116,16 +153,26 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode }; const logOut = async () => { - userBalance.clearAll(); - setClientDetails(undefined); - setNetwork(undefined); - setError(undefined); - setIsLoading(false); - setMixnodeDetails(undefined); await signOut(); + setClientDetails(undefined); enqueueSnackbar('Successfully logged out', { variant: 'success' }); }; + const onAccountChange = async (accountId: string) => { + if (network) { + setIsLoading(true); + try { + await switchAccount(accountId); + await loadAccount(network); + enqueueSnackbar('Account switch success', { variant: 'success', preventDuplicate: true }); + } catch (e) { + enqueueSnackbar(`Error swtiching account: ${e}`, { variant: 'error' }); + } finally { + setIsLoading(false); + } + } + }; + const handleShowAdmin = () => setShowAdmin((show) => !show); const handleShowSettings = () => setShowSettings((show) => !show); const switchNetwork = (_network: Network) => setNetwork(_network); @@ -136,12 +183,14 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode isLoading, error, clientDetails, + storedAccounts, mixnodeDetails, userBalance, showAdmin, showSettings, network, currency, + loginType, setIsLoading, setError, signInWithPassword, @@ -151,9 +200,23 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode handleShowAdmin, logIn, logOut, + onAccountChange, }), - [mode, isLoading, error, clientDetails, mixnodeDetails, userBalance, showAdmin, showSettings, network, currency], + [ + loginType, + mode, + isLoading, + error, + clientDetails, + mixnodeDetails, + userBalance, + showAdmin, + showSettings, + network, + currency, + storedAccounts, + ], ); - return {children}; + return {children}; }; diff --git a/nym-wallet/src/context/mocks/accounts.tsx b/nym-wallet/src/context/mocks/accounts.tsx new file mode 100644 index 0000000000..8b85530b63 --- /dev/null +++ b/nym-wallet/src/context/mocks/accounts.tsx @@ -0,0 +1,84 @@ +import React, { useMemo, useState } from 'react'; +import { AccountEntry } from 'src/types'; +import { AccountsContext, TAccountMnemonic, TAccountsDialog } from '../accounts'; + +export const MockAccountsProvider: React.FC = ({ children }) => { + const [accounts, setAccounts] = useState([{ id: 'Account_1', address: 'abc123' }]); + const [selectedAccount, setSelectedAccount] = useState({ + id: 'Account_1', + address: 'abc123', + }); + const [accountToEdit, setAccountToEdit] = useState(); + const [dialogToDisplay, setDialogToDisplay] = useState(); + const [accountMnemonic, setAccountMnemonic] = useState({ + value: undefined, + accountName: undefined, + }); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const handleAddAccount = async ({ accountName }: { accountName: string; mnemonic: string; password: string }) => { + setIsLoading(true); + try { + setAccounts((accs) => [...accs, { address: 'abc123', id: accountName }]); + setDialogToDisplay('Accounts'); + } catch (e) { + setError(`Error adding account: ${e}`); + } finally { + setIsLoading(false); + } + }; + const handleEditAccount = (account: AccountEntry) => + setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc))); + + const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]); + + const handleAccountToEdit = (accountName: string) => + setAccountToEdit(accounts?.find((acc) => acc.id === accountName)); + + const handleSelectAccount = async (accountName: string) => { + if (accountName !== selectedAccount?.id) { + const match = accounts?.find((acc) => acc.id === accountName); + setSelectedAccount(match); + } + }; + + const handleGetAcccountMnemonic = async ({ accountName }: { password: string; accountName: string }) => { + try { + setIsLoading(true); + const mnemonic = 'test mnemonic'; + setAccountMnemonic({ value: mnemonic, accountName }); + } catch (e) { + setError(e as string); + } finally { + setIsLoading(false); + } + }; + return ( + ({ + error, + setError, + accounts, + selectedAccount, + accountToEdit, + dialogToDisplay, + accountMnemonic, + setDialogToDisplay, + setAccountMnemonic, + isLoading, + handleAddAccount, + handleEditAccount, + handleAccountToEdit, + handleSelectAccount, + handleImportAccount, + handleGetAcccountMnemonic, + }), + [accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic], + )} + > + {children} + + ); +}; diff --git a/nym-wallet/src/hooks/useCheckOwnership.ts b/nym-wallet/src/hooks/useCheckOwnership.ts index b9cf89ebf4..b47df00efd 100644 --- a/nym-wallet/src/hooks/useCheckOwnership.ts +++ b/nym-wallet/src/hooks/useCheckOwnership.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { Console } from '../utils/console'; -import { ClientContext } from '../context/main'; +import { AppContext } from '../context/main'; import { checkGatewayOwnership, checkMixnodeOwnership, getVestingPledgeInfo } from '../requests'; import { EnumNodeType, TNodeOwnership } from '../types'; @@ -11,7 +11,7 @@ const initial = { }; export const useCheckOwnership = () => { - const { clientDetails } = useContext(ClientContext); + const { clientDetails } = useContext(AppContext); const [ownership, setOwnership] = useState(initial); const [isLoading, setIsLoading] = useState(true); diff --git a/nym-wallet/src/hooks/useGetBalance.ts b/nym-wallet/src/hooks/useGetBalance.ts index 7c43c433f1..9056f1d4d2 100644 --- a/nym-wallet/src/hooks/useGetBalance.ts +++ b/nym-wallet/src/hooks/useGetBalance.ts @@ -92,9 +92,7 @@ export const useGetBalance = (address?: string): TUseuserBalance => { } catch (err) { setError(err as string); } finally { - setTimeout(() => { - setIsLoading(false); - }, 1000); + setIsLoading(false); } }, []); diff --git a/nym-wallet/src/index.tsx b/nym-wallet/src/index.tsx index e16fbcdab0..76d20f0ec0 100644 --- a/nym-wallet/src/index.tsx +++ b/nym-wallet/src/index.tsx @@ -1,58 +1,39 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { ErrorBoundary } from 'react-error-boundary'; import { BrowserRouter as Router } from 'react-router-dom'; import { SnackbarProvider } from 'notistack'; -import { AppRoutes, SignInRoutes } from './routes'; -import { ClientContext, ClientContextProvider } from './context/main'; -import { ApplicationLayout } from './layouts'; -import { Admin, Settings } from './pages'; +import { Routes } from './routes'; +import { AppProvider } from './context/main'; import { ErrorFallback } from './components'; -import { NymWalletTheme, WelcomeTheme } from './theme'; +import { NymWalletTheme } from './theme'; import { maximizeWindow } from './utils'; -import { SignInProvider } from './context'; const App = () => { - const { clientDetails } = useContext(ClientContext); - useEffect(() => { maximizeWindow(); }, []); - return !clientDetails ? ( - - - - - - ) : ( - - - - - - - + return ( + + + + + + + + + + + ); }; -const AppWrapper = () => ( - - - - - - - - - -); - const root = document.getElementById('root'); -ReactDOM.render(, root); +ReactDOM.render(, root); diff --git a/nym-wallet/src/layouts/AppLayout.tsx b/nym-wallet/src/layouts/AppLayout.tsx index d813f8c42b..c583b3be5e 100644 --- a/nym-wallet/src/layouts/AppLayout.tsx +++ b/nym-wallet/src/layouts/AppLayout.tsx @@ -1,44 +1,52 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { NymWordmark } from '@nymproject/react'; import { Box, Container } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { AppBar, Nav } from '../components'; +import { AppContext } from 'src/context'; +import { Settings } from 'src/pages'; +import { AppBar, LoadingPage, Nav } from '../components'; export const ApplicationLayout: React.FC = ({ children }) => { const theme = useTheme(); + const { isLoading, showSettings } = useContext(AppContext); + return ( - + <> + {isLoading && } + {showSettings && } - - - + + + + + +