diff --git a/src/extension/components/ARC0300AccountImportModalContent/ARC0300AccountImportModalContent.tsx b/src/extension/components/ARC0300AccountImportModalContent/ARC0300AccountImportModalContent.tsx index cffbde3e..1cfa3f21 100644 --- a/src/extension/components/ARC0300AccountImportModalContent/ARC0300AccountImportModalContent.tsx +++ b/src/extension/components/ARC0300AccountImportModalContent/ARC0300AccountImportModalContent.tsx @@ -17,9 +17,9 @@ import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; // components -import AccountItem from '@extension/components/AccountItem'; import Button from '@extension/components/Button'; import EmptyState from '@extension/components/EmptyState'; +import NewAccountItem from '@extension/components/NewAccountItem'; // constants import { @@ -216,7 +216,7 @@ const ARC0300AccountImportModalContent: FC< py={DEFAULT_GAP / 3} w="full" > - diff --git a/src/extension/components/AccountAvatar/AccountAvatar.tsx b/src/extension/components/AccountAvatar/AccountAvatar.tsx index 83bb93f7..865961b3 100644 --- a/src/extension/components/AccountAvatar/AccountAvatar.tsx +++ b/src/extension/components/AccountAvatar/AccountAvatar.tsx @@ -1,20 +1,57 @@ import { Avatar, Icon } from '@chakra-ui/react'; -import React, { FC, PropsWithChildren } from 'react'; -import { IoWalletOutline } from 'react-icons/io5'; +import React, { type FC } from 'react'; // hooks import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; -const AccountAvatar: FC = ({ children }) => { +// types +import type { IProps } from './types'; + +// utils +import parseAccountIcon from '@extension/utils/parseAccountIcon'; + +const AccountAvatar: FC = ({ account, children }) => { // hooks const primaryButtonTextColor = usePrimaryButtonTextColor(); const primaryColor = usePrimaryColor(); + // misc + let iconColor = primaryButtonTextColor; + + switch (account.color) { + case 'yellow.300': + case 'yellow.500': + case 'orange.300': + case 'orange.500': + case 'red.300': + case 'red.500': + iconColor = 'gray.800'; + break; + case 'black': + case 'blue.300': + case 'blue.500': + case 'green.300': + case 'green.500': + case 'teal.300': + case 'teal.500': + iconColor = 'white'; + break; + case 'primary': + default: + break; + } return ( } + bg={ + !account.color || account.color === 'primary' + ? primaryColor + : account.color + } + icon={parseAccountIcon({ + accountIcon: account.icon, + color: iconColor, + })} size="sm" > {children} diff --git a/src/extension/components/AccountAvatar/types/IProps.ts b/src/extension/components/AccountAvatar/types/IProps.ts new file mode 100644 index 00000000..b0e8a05f --- /dev/null +++ b/src/extension/components/AccountAvatar/types/IProps.ts @@ -0,0 +1,10 @@ +import type { PropsWithChildren } from 'react'; + +// types +import type { IAccountWithExtendedProps } from '@extension/types'; + +interface IProps extends PropsWithChildren { + account: IAccountWithExtendedProps; +} + +export default IProps; diff --git a/src/extension/components/AccountAvatar/types/index.ts b/src/extension/components/AccountAvatar/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/AccountAvatar/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/AccountAvatarWithBadges/AccountAvatarWithBadges.tsx b/src/extension/components/AccountAvatarWithBadges/AccountAvatarWithBadges.tsx index d8540957..d31630cc 100644 --- a/src/extension/components/AccountAvatarWithBadges/AccountAvatarWithBadges.tsx +++ b/src/extension/components/AccountAvatarWithBadges/AccountAvatarWithBadges.tsx @@ -65,7 +65,7 @@ const AccountAvatarWithBadges: FC = ({ }; return ( - + {/*polis account badge*/} {systemInfo && systemInfo.polisAccountID === account.id && ( = ({ - address, - name, - subTextColor, - textColor, -}) => { +const AccountItem: FC = ({ account, subTextColor, textColor }) => { // hooks const defaultSubTextColor = useSubTextColor(); const defaultTextColor = useDefaultTextColor(); + // misc + const address = convertPublicKeyToAVMAddress(account.publicKey); return ( {/*avatar*/}
- +
- {name ? ( + {account.name ? ( = ({ noOfLines={1} textAlign="left" > - {name} + {account.name} = ({ _context, @@ -117,10 +116,7 @@ const AccountSelect: FC = ({ w="full" > - + diff --git a/src/extension/components/NewAccountItem/NewAccountItem.tsx b/src/extension/components/NewAccountItem/NewAccountItem.tsx new file mode 100644 index 00000000..b1d548e5 --- /dev/null +++ b/src/extension/components/NewAccountItem/NewAccountItem.tsx @@ -0,0 +1,88 @@ +import { Avatar, Center, HStack, Icon, Text, VStack } from '@chakra-ui/react'; +import React, { type FC } from 'react'; +import { IoWalletOutline } from 'react-icons/io5'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import usePrimaryColor from '@extension/hooks/usePrimaryColor'; +import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import type { IProps } from './types'; + +// utils +import ellipseAddress from '@extension/utils/ellipseAddress'; + +const NewAccountItem: FC = ({ + address, + name, + subTextColor, + textColor, +}) => { + // hooks + const defaultSubTextColor = useSubTextColor(); + const defaultTextColor = useDefaultTextColor(); + const primaryButtonTextColor = usePrimaryButtonTextColor(); + const primaryColor = usePrimaryColor(); + + return ( + + {/*avatar*/} +
+ } + size="sm" + /> +
+ + {name ? ( + + + {name} + + + + {ellipseAddress(address, { + end: 10, + start: 10, + })} + + + ) : ( + + {ellipseAddress(address, { + end: 10, + start: 10, + })} + + )} +
+ ); +}; + +export default NewAccountItem; diff --git a/src/extension/components/NewAccountItem/index.ts b/src/extension/components/NewAccountItem/index.ts new file mode 100644 index 00000000..90a58f02 --- /dev/null +++ b/src/extension/components/NewAccountItem/index.ts @@ -0,0 +1 @@ +export { default } from './NewAccountItem'; diff --git a/src/extension/components/NewAccountItem/types/IProps.ts b/src/extension/components/NewAccountItem/types/IProps.ts new file mode 100644 index 00000000..0342eec4 --- /dev/null +++ b/src/extension/components/NewAccountItem/types/IProps.ts @@ -0,0 +1,8 @@ +interface IProps { + address: string; + name?: string; + subTextColor?: string; + textColor?: string; +} + +export default IProps; diff --git a/src/extension/components/NewAccountItem/types/index.ts b/src/extension/components/NewAccountItem/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/NewAccountItem/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/ScrollableContainer/ScrollableContainer.tsx b/src/extension/components/ScrollableContainer/ScrollableContainer.tsx index eb115b1a..7e577251 100644 --- a/src/extension/components/ScrollableContainer/ScrollableContainer.tsx +++ b/src/extension/components/ScrollableContainer/ScrollableContainer.tsx @@ -7,6 +7,7 @@ import type { IProps } from './types'; const ScrollableContainer: FC = ({ children, onScrollEnd, + showScrollBars = false, ...stackProps }) => { const scrollContainerRef = useRef(null); @@ -32,6 +33,15 @@ const ScrollableContainer: FC = ({ ref={scrollContainerRef} spacing={0} w="full" + {...(showScrollBars && { + sx: { + scrollbarWidth: __TARGET__ === 'chrome' ? 'thin' : 'auto', + msOverflowStyle: 'auto', + ['::-webkit-scrollbar']: { + display: 'contents', + }, + }, + })} > {children} diff --git a/src/extension/components/ScrollableContainer/types/IProps.ts b/src/extension/components/ScrollableContainer/types/IProps.ts index 04a94c93..91eccc7b 100644 --- a/src/extension/components/ScrollableContainer/types/IProps.ts +++ b/src/extension/components/ScrollableContainer/types/IProps.ts @@ -2,6 +2,7 @@ import type { StackProps } from '@chakra-ui/react'; interface IProps extends StackProps { onScrollEnd?: () => void; + showScrollBars?: boolean; } export default IProps; diff --git a/src/extension/components/VoiIcon/VoiIcon.tsx b/src/extension/components/VoiIcon/VoiIcon.tsx new file mode 100644 index 00000000..c72f30a4 --- /dev/null +++ b/src/extension/components/VoiIcon/VoiIcon.tsx @@ -0,0 +1,13 @@ +import { Icon, type IconProps } from '@chakra-ui/react'; +import React, { type FC } from 'react'; + +const VoiIcon: FC = (props: IconProps) => ( + + + +); + +export default VoiIcon; diff --git a/src/extension/components/VoiIcon/index.ts b/src/extension/components/VoiIcon/index.ts new file mode 100644 index 00000000..1e6bd50d --- /dev/null +++ b/src/extension/components/VoiIcon/index.ts @@ -0,0 +1 @@ +export { default } from './VoiIcon'; diff --git a/src/extension/features/accounts/enums/ThunkEnum.ts b/src/extension/features/accounts/enums/ThunkEnum.ts index b0357748..1237e630 100644 --- a/src/extension/features/accounts/enums/ThunkEnum.ts +++ b/src/extension/features/accounts/enums/ThunkEnum.ts @@ -5,7 +5,7 @@ enum ThunkEnum { RemoveAccountById = 'accounts/removeAccountById', RemoveARC0200AssetHoldings = 'accounts/removeARC0200AssetHoldings', RemoveStandardAssetHoldings = 'accounts/removeStandardAssetHoldings', - SaveAccountName = 'accounts/saveAccountName', + SaveAccountDetails = 'accounts/saveAccountDetails', SaveAccounts = 'accounts/saveAccounts', SaveActiveAccountDetails = 'accounts/saveActiveAccountDetails', SaveNewAccounts = 'accounts/saveNewAccounts', diff --git a/src/extension/features/accounts/slice.ts b/src/extension/features/accounts/slice.ts index 17ae22c9..d88b8703 100644 --- a/src/extension/features/accounts/slice.ts +++ b/src/extension/features/accounts/slice.ts @@ -14,7 +14,7 @@ import { removeAccountByIdThunk, removeARC0200AssetHoldingsThunk, removeStandardAssetHoldingsThunk, - saveAccountNameThunk, + saveAccountDetailsThunk, saveAccountsThunk, saveActiveAccountDetails, saveNewAccountsThunk, @@ -221,20 +221,24 @@ const slice = createSlice({ ); } ); - /** save account name **/ - builder.addCase(saveAccountNameThunk.fulfilled, (state: IState, action) => { - if (action.payload) { - state.items = upsertItemsById(state.items, [ - action.payload, - ]); - } + /** save account details **/ + builder.addCase( + saveAccountDetailsThunk.fulfilled, + (state: IState, action) => { + if (action.payload) { + state.items = upsertItemsById( + state.items, + [action.payload] + ); + } - state.saving = false; - }); - builder.addCase(saveAccountNameThunk.pending, (state: IState) => { + state.saving = false; + } + ); + builder.addCase(saveAccountDetailsThunk.pending, (state: IState) => { state.saving = true; }); - builder.addCase(saveAccountNameThunk.rejected, (state: IState) => { + builder.addCase(saveAccountDetailsThunk.rejected, (state: IState) => { state.saving = false; }); /** save accounts **/ diff --git a/src/extension/features/accounts/thunks/index.ts b/src/extension/features/accounts/thunks/index.ts index b144ab5a..70c9ca1c 100644 --- a/src/extension/features/accounts/thunks/index.ts +++ b/src/extension/features/accounts/thunks/index.ts @@ -4,7 +4,7 @@ export { default as fetchAccountsFromStorageThunk } from './fetchAccountsFromSto export { default as removeAccountByIdThunk } from './removeAccountByIdThunk'; export { default as removeARC0200AssetHoldingsThunk } from './removeARC0200AssetHoldingsThunk'; export { default as removeStandardAssetHoldingsThunk } from './removeStandardAssetHoldingsThunk'; -export { default as saveAccountNameThunk } from './saveAccountNameThunk'; +export { default as saveAccountDetailsThunk } from './saveAccountDetailsThunk'; export { default as saveAccountsThunk } from './saveAccountsThunk'; export { default as saveActiveAccountDetails } from './saveActiveAccountDetails'; export { default as saveNewAccountsThunk } from './saveNewAccountsThunk'; diff --git a/src/extension/features/accounts/thunks/saveAccountDetailsThunk.ts b/src/extension/features/accounts/thunks/saveAccountDetailsThunk.ts new file mode 100644 index 00000000..6da33e6e --- /dev/null +++ b/src/extension/features/accounts/thunks/saveAccountDetailsThunk.ts @@ -0,0 +1,67 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { ThunkEnum } from '../enums'; + +// repositories +import AccountRepository from '@extension/repositories/AccountRepository'; + +// types +import type { + IAccountWithExtendedProps, + IBaseAsyncThunkConfig, + IMainRootState, +} from '@extension/types'; +import type { ISaveAccountDetailsPayload } from '../types'; + +// utils +import isWatchAccount from '@extension/utils/isWatchAccount/isWatchAccount'; +import serialize from '@extension/utils/serialize'; +import { findAccountWithoutExtendedProps } from '../utils'; + +const saveAccountNameThunk: AsyncThunk< + IAccountWithExtendedProps | null, // return + ISaveAccountDetailsPayload, // args + IBaseAsyncThunkConfig +> = createAsyncThunk< + IAccountWithExtendedProps | null, + ISaveAccountDetailsPayload, + IBaseAsyncThunkConfig +>( + ThunkEnum.SaveAccountDetails, + async ({ accountId, color, icon, name }, { getState }) => { + const logger = getState().system.logger; + const accounts = getState().accounts.items; + let account = serialize( + findAccountWithoutExtendedProps(accountId, accounts) + ); + + if (!account) { + logger.debug( + `${ThunkEnum.SaveAccountDetails}: no account found for "${accountId}", ignoring` + ); + + return null; + } + + logger.debug( + `${ThunkEnum.SaveAccountDetails}: updating account "${accountId}" details "${icon}"` + ); + + account = { + ...account, + color, + icon, + name, + }; + + await new AccountRepository().saveMany([account]); + + return { + ...account, + watchAccount: await isWatchAccount(account), + }; + } +); + +export default saveAccountNameThunk; diff --git a/src/extension/features/accounts/thunks/saveAccountNameThunk.ts b/src/extension/features/accounts/thunks/saveAccountNameThunk.ts deleted file mode 100644 index f018c1b3..00000000 --- a/src/extension/features/accounts/thunks/saveAccountNameThunk.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; - -// enums -import { ThunkEnum } from '../enums'; - -// repositories -import AccountRepository from '@extension/repositories/AccountRepository'; - -// types -import type { - IAccountWithExtendedProps, - IBaseAsyncThunkConfig, - IMainRootState, -} from '@extension/types'; -import type { ISaveAccountNamePayload } from '../types'; - -// utils -import isWatchAccount from '@extension/utils/isWatchAccount/isWatchAccount'; -import serialize from '@extension/utils/serialize'; -import { findAccountWithoutExtendedProps } from '../utils'; - -const saveAccountNameThunk: AsyncThunk< - IAccountWithExtendedProps | null, // return - ISaveAccountNamePayload, // args - IBaseAsyncThunkConfig -> = createAsyncThunk< - IAccountWithExtendedProps | null, - ISaveAccountNamePayload, - IBaseAsyncThunkConfig ->(ThunkEnum.SaveAccountName, async ({ accountId, name }, { getState }) => { - const logger = getState().system.logger; - const accounts = getState().accounts.items; - let account = serialize(findAccountWithoutExtendedProps(accountId, accounts)); - - if (!account) { - logger.debug( - `${ThunkEnum.SaveAccountName}: no account found for "${accountId}", ignoring` - ); - - return null; - } - - logger.debug( - `${ThunkEnum.SaveAccountName}: ${ - name - ? `updating account "${accountId}" with new name "${name}"` - : `removing account name for account "${accountId}"` - }` - ); - - account = { - ...account, - name, - }; - - await new AccountRepository().saveMany([account]); - - return { - ...account, - watchAccount: await isWatchAccount(account), - }; -}); - -export default saveAccountNameThunk; diff --git a/src/extension/features/accounts/types/ISaveAccountDetailsPayload.ts b/src/extension/features/accounts/types/ISaveAccountDetailsPayload.ts new file mode 100644 index 00000000..e01d048d --- /dev/null +++ b/src/extension/features/accounts/types/ISaveAccountDetailsPayload.ts @@ -0,0 +1,11 @@ +// types +import type { TAccountColors, TAccountIcons } from '@extension/types'; + +interface ISaveAccountDetailsPayload { + accountId: string; + color: TAccountColors | null; + icon: TAccountIcons | null; + name: string; +} + +export default ISaveAccountDetailsPayload; diff --git a/src/extension/features/accounts/types/ISaveAccountNamePayload.ts b/src/extension/features/accounts/types/ISaveAccountNamePayload.ts deleted file mode 100644 index 48172d33..00000000 --- a/src/extension/features/accounts/types/ISaveAccountNamePayload.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface ISaveAccountNamePayload { - accountId: string; - name: string | null; -} - -export default ISaveAccountNamePayload; diff --git a/src/extension/features/accounts/types/index.ts b/src/extension/features/accounts/types/index.ts index 389e214b..ebc0a253 100644 --- a/src/extension/features/accounts/types/index.ts +++ b/src/extension/features/accounts/types/index.ts @@ -1,6 +1,6 @@ export type { default as IAccountUpdateRequest } from './IAccountUpdateRequest'; export type { default as IFetchAccountsFromStorageResult } from './IFetchAccountsFromStorageResult'; -export type { default as ISaveAccountNamePayload } from './ISaveAccountNamePayload'; +export type { default as ISaveAccountDetailsPayload } from './ISaveAccountDetailsPayload'; export type { default as ISaveNewAccountsPayload } from './ISaveNewAccountsPayload'; export type { default as ISaveNewWatchAccountPayload } from './ISaveNewWatchAccountPayload'; export type { default as IState } from './IState'; diff --git a/src/extension/modals/EditAccountModal/EditAccountModal.tsx b/src/extension/modals/EditAccountModal/EditAccountModal.tsx index 1d63cfa8..b63aa9e1 100644 --- a/src/extension/modals/EditAccountModal/EditAccountModal.tsx +++ b/src/extension/modals/EditAccountModal/EditAccountModal.tsx @@ -1,4 +1,5 @@ import { + Button as ChakraButton, Heading, HStack, Modal, @@ -9,8 +10,10 @@ import { Text, Tooltip, VStack, + Wrap, + WrapItem, } from '@chakra-ui/react'; -import React, { type FC, useEffect } from 'react'; +import React, { type FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IoSaveOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; @@ -18,6 +21,8 @@ import { useDispatch } from 'react-redux'; // components import Button from '@extension/components/Button'; import GenericInput from '@extension/components/GenericInput'; +import ModalSubHeading from '@extension/components/ModalSubHeading'; +import ScrollableContainer from '@extension/components/ScrollableContainer'; // constants import { @@ -27,12 +32,14 @@ import { } from '@extension/constants'; // features -import { saveAccountNameThunk } from '@extension/features/accounts'; +import { saveAccountDetailsThunk } from '@extension/features/accounts'; import { create as createNotification } from '@extension/features/notifications'; // hooks +import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useGenericInput from '@extension/hooks/useGenericInput'; +import usePrimaryColor from '@extension/hooks/usePrimaryColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors @@ -49,20 +56,25 @@ import type { IAccountWithExtendedProps, IAppThunkDispatch, IMainRootState, + TAccountColors, + TAccountIcons, } from '@extension/types'; import type { IProps } from './types'; // utils import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; +import parseAccountIcon from '@extension/utils/parseAccountIcon'; const EditAccountModal: FC = ({ isOpen, onClose }) => { + const _context = 'account-icon-modal'; const { t } = useTranslation(); const dispatch = useDispatch>(); // selectors const account = useSelectActiveAccount(); const saving = useSelectAccountsSaving(); // hooks + const buttonHoverBackgroundColor = useButtonHoverBackgroundColor(); const defaultTextColor = useDefaultTextColor(); const { charactersRemaining: nameCharactersRemaining, @@ -82,15 +94,167 @@ const EditAccountModal: FC = ({ isOpen, onClose }) => { defaultValue: account.name, }), }); + const primaryColor = usePrimaryColor(); const subTextColor = useSubTextColor(); + // states + const [color, setColor] = useState( + account?.color || null + ); + const [icon, setIcon] = useState(account?.icon || null); + // misc + const accountColors: TAccountColors[] = [ + 'primary', + 'black', + 'blue.300', + 'blue.500', + 'teal.300', + 'teal.500', + 'green.300', + 'green.500', + 'yellow.300', + 'yellow.500', + 'orange.300', + 'orange.500', + 'red.300', + 'red.500', + ]; + const accountIcons: TAccountIcons[] = [ + 'voi', + 'algorand', + 'airplane', + 'american-football', + 'balloon', + 'baseball', + 'basketball', + 'beer', + 'bicycle', + 'bitcoin', + 'boat', + 'briefcase', + 'brush', + 'bug', + 'bulb', + 'buoy', + 'bus', + 'business', + 'cafe', + 'car', + 'cart', + 'cash', + 'circle', + 'cloud', + 'code', + 'compass', + 'construct', + 'credit-card', + 'cube', + 'database', + 'diamond', + 'dice', + 'earth', + 'egg', + 'ethereum', + 'euro', + 'female', + 'file-tray', + 'film', + 'fingerprint', + 'fire', + 'fish', + 'fitness', + 'flag', + 'flash', + 'flashlight', + 'flask', + 'flower', + 'football', + 'footsteps', + 'gaming', + 'glasses', + 'globe', + 'golf', + 'hammer', + 'heart', + 'home', + 'key', + 'leaf', + 'library', + 'male', + 'moon', + 'music-note', + 'palette', + 'paw', + 'people', + 'person', + 'pizza', + 'planet', + 'prism', + 'puzzle', + 'rainy', + 'receipt', + 'restaurant', + 'rocket', + 'rose', + 'school', + 'shield', + 'shirt', + 'shopping-bag', + 'skull', + 'snow', + 'sparkles', + 'star', + 'storefront', + 'sun', + 'telescope', + 'tennis', + 'terminal', + 'thermometer', + 'thumbs-up', + 'ticket', + 'time', + 'train', + 'transgender', + 'trash', + 'trophy', + 'umbrella', + 'usd', + 'wallet', + 'water', + 'wine', + 'wrench', + 'yen', + ]; + const reset = () => { + resetName(); + setColor(null); + setIcon(null); + }; // handlers const handleCancelClick = () => handleClose(); const handleClose = () => { // reset inputs - resetName(); + reset(); // close onClose && onClose(); }; + const handleOnColorChange = (value: TAccountColors) => () => { + if (value === color) { + setColor(null); + + return; + } + + setColor(value); + }; + const handleOnIconChange = (value: TAccountIcons) => () => { + if (value === icon) { + setIcon(null); + + return; + } + + setIcon(value); + }; const handleSaveClick = async () => { let _account: IAccountWithExtendedProps | null; @@ -102,15 +266,22 @@ const EditAccountModal: FC = ({ isOpen, onClose }) => { return; } - if (account && account.name === nameValue) { + if ( + account.color === color && + account.icon === icon && + account && + account.name === nameValue + ) { handleClose(); return; } _account = await dispatch( - saveAccountNameThunk({ + saveAccountDetailsThunk({ accountId: account.id, + color, + icon, name: nameValue, }) ).unwrap(); @@ -128,9 +299,11 @@ const EditAccountModal: FC = ({ isOpen, onClose }) => { handleClose(); }; - // update the input with the name of the active account + // update the state with the previous values when the modal is opened useEffect(() => { if (isOpen) { + account?.color && setColor(account.color); + account?.icon && setIcon(account.icon); account?.name && setNameValue(account?.name); } }, [isOpen]); @@ -204,6 +377,64 @@ const EditAccountModal: FC = ({ isOpen, onClose }) => { validate={validateName} value={nameValue} /> + + ('headings.selectColor')} /> + + + {accountColors.map((value, index) => ( + + + + ))} + + + ('headings.selectIcon')} /> + + {/*icons*/} + + + {accountIcons.map((value, index) => ( + + + {parseAccountIcon({ + accountIcon: value, + color: defaultTextColor, + size: 'sm', + })} + + + ))} + + diff --git a/src/extension/modals/RegistrationImportAccountViaQRCodeModal/RegistrationImportAccountViaQRCodeModal.tsx b/src/extension/modals/RegistrationImportAccountViaQRCodeModal/RegistrationImportAccountViaQRCodeModal.tsx index 779fe8af..a6600e93 100644 --- a/src/extension/modals/RegistrationImportAccountViaQRCodeModal/RegistrationImportAccountViaQRCodeModal.tsx +++ b/src/extension/modals/RegistrationImportAccountViaQRCodeModal/RegistrationImportAccountViaQRCodeModal.tsx @@ -14,8 +14,8 @@ import { useTranslation } from 'react-i18next'; import { IoArrowBackOutline, IoDownloadOutline } from 'react-icons/io5'; // components -import AccountItem from '@extension/components/AccountItem'; import Button from '@extension/components/Button'; +import NewAccountItem from '@extension/components/NewAccountItem'; import ScanModeModalContent from '@extension/components/ScanModeModalContent'; import ScanQRCodeViaCameraModalContent from '@extension/components/ScanQRCodeViaCameraModalContent'; import ScanQRCodeViaScreenCaptureModalContent from '@extension/components/ScanQRCodeViaScreenCaptureModalContent'; @@ -178,7 +178,7 @@ const RegistrationImportAccountViaQRCodeModal: FC = ({ py={DEFAULT_GAP / 3} w="full" > - diff --git a/src/extension/modals/SignMessageModal/SignMessageModal.tsx b/src/extension/modals/SignMessageModal/SignMessageModal.tsx index cf619755..632db759 100644 --- a/src/extension/modals/SignMessageModal/SignMessageModal.tsx +++ b/src/extension/modals/SignMessageModal/SignMessageModal.tsx @@ -220,10 +220,7 @@ const SignMessageModal: FC = ({ onClose }) => { 'labels.addressToSign' )}:`} - + ) : ( <> diff --git a/src/extension/modals/WhatsNewModal/WhatsNewModal.tsx b/src/extension/modals/WhatsNewModal/WhatsNewModal.tsx index 1cb4b102..57698ad5 100644 --- a/src/extension/modals/WhatsNewModal/WhatsNewModal.tsx +++ b/src/extension/modals/WhatsNewModal/WhatsNewModal.tsx @@ -64,7 +64,10 @@ const WhatsNewModal: FC = ({ onClose }) => { const primaryColorScheme = usePrimaryColorScheme(); const subTextColor = useSubTextColor(); // misc - const features = ['🖼️ Add Voi mainnet indexers and explorer.']; + const features = [ + '💅 Change account icon.', + '💅 Change account background color.', + ]; const fixes: string[] = []; // handlers const handleClose = () => { diff --git a/src/extension/pages/AccountPage/AccountPage.tsx b/src/extension/pages/AccountPage/AccountPage.tsx index 2c8db47c..bdb72976 100644 --- a/src/extension/pages/AccountPage/AccountPage.tsx +++ b/src/extension/pages/AccountPage/AccountPage.tsx @@ -609,7 +609,6 @@ const AccountPage: FC = () => { isOpen={isEditAccountModalOpen} onClose={onEditAccountModalClose} /> - >( @@ -193,7 +195,9 @@ export default class AccountRepository extends BaseRepository { */ private _sanitize(account: IAccount): IAccount { return { + color: account.color, createdAt: account.createdAt, + icon: account.icon, id: account.id, name: account.name, networkInformation: Object.keys(account.networkInformation).reduce< diff --git a/src/extension/translations/en.ts b/src/extension/translations/en.ts index eafd55c8..47d888d6 100644 --- a/src/extension/translations/en.ts +++ b/src/extension/translations/en.ts @@ -410,6 +410,8 @@ const translation: IResourceLanguage = { selectAssets: 'Select Assets', selectANetwork: 'Select A Network', selectAnOption: 'Select An Option', + selectColor: 'Select Color', + selectIcon: 'Select Icon', selectReceiverAccount: 'Select Receiver Account', selectSenderAccount: 'Select Sender Account', sendAsset: 'Send {{asset}}', diff --git a/src/extension/types/accounts/IAccount.ts b/src/extension/types/accounts/IAccount.ts index 7c6ce73e..9ab518d8 100644 --- a/src/extension/types/accounts/IAccount.ts +++ b/src/extension/types/accounts/IAccount.ts @@ -1,9 +1,13 @@ // types import IAccountInformation from './IAccountInformation'; import IAccountTransactions from './IAccountTransactions'; +import TAccountColors from './TAccountColors'; +import TAccountIcons from './TAccountIcons'; /** + * @property {TAccountColors | null} color - The background color. * @property {number} createdAt - a timestamp (in milliseconds) when this account was created in storage. + * @property {TAccountIcons | null} icon - An icon for the account. * @property {string} id - a unique identifier (in UUID). * @property {number | null} index - the position of the account as it appears in a list. * @property {string | null} name - a canonical name given to this account. @@ -15,7 +19,9 @@ import IAccountTransactions from './IAccountTransactions'; * @property {number} updatedAt - a timestamp (in milliseconds) for when this account was last saved to storage. */ interface IAccount { + color: TAccountColors | null; createdAt: number; + icon: TAccountIcons | null; id: string; index: number | null; name: string | null; diff --git a/src/extension/types/accounts/TAccountColors.ts b/src/extension/types/accounts/TAccountColors.ts new file mode 100644 index 00000000..60e47601 --- /dev/null +++ b/src/extension/types/accounts/TAccountColors.ts @@ -0,0 +1,17 @@ +type TAccountColors = + | 'primary' + | 'black' + | 'blue.300' + | 'blue.500' + | 'teal.300' + | 'teal.500' + | 'green.300' + | 'green.500' + | 'yellow.300' + | 'yellow.500' + | 'orange.300' + | 'orange.500' + | 'red.300' + | 'red.500'; + +export default TAccountColors; diff --git a/src/extension/types/accounts/TAccountIcons.ts b/src/extension/types/accounts/TAccountIcons.ts new file mode 100644 index 00000000..8bb76b16 --- /dev/null +++ b/src/extension/types/accounts/TAccountIcons.ts @@ -0,0 +1,107 @@ +type TAccountIcons = + | 'airplane' + | 'algorand' + | 'american-football' + | 'balloon' + | 'baseball' + | 'basketball' + | 'beer' + | 'bicycle' + | 'bitcoin' + | 'boat' + | 'briefcase' + | 'brush' + | 'bug' + | 'bulb' + | 'buoy' + | 'bus' + | 'business' + | 'cafe' + | 'car' + | 'cart' + | 'cash' + | 'circle' + | 'cloud' + | 'code' + | 'compass' + | 'construct' + | 'credit-card' + | 'cube' + | 'database' + | 'diamond' + | 'dice' + | 'earth' + | 'egg' + | 'ethereum' + | 'euro' + | 'female' + | 'file-tray' + | 'film' + | 'fingerprint' + | 'fire' + | 'fish' + | 'fitness' + | 'flag' + | 'flash' + | 'flashlight' + | 'flask' + | 'flower' + | 'football' + | 'footsteps' + | 'gaming' + | 'glasses' + | 'globe' + | 'golf' + | 'hammer' + | 'heart' + | 'home' + | 'key' + | 'leaf' + | 'library' + | 'male' + | 'moon' + | 'music-note' + | 'palette' + | 'paw' + | 'people' + | 'person' + | 'pizza' + | 'planet' + | 'prism' + | 'puzzle' + | 'rainy' + | 'receipt' + | 'restaurant' + | 'rocket' + | 'rose' + | 'school' + | 'shield' + | 'shirt' + | 'shopping-bag' + | 'skull' + | 'snow' + | 'sparkles' + | 'star' + | 'storefront' + | 'sun' + | 'telescope' + | 'tennis' + | 'terminal' + | 'thermometer' + | 'thumbs-up' + | 'ticket' + | 'time' + | 'train' + | 'transgender' + | 'trash' + | 'trophy' + | 'umbrella' + | 'usd' + | 'voi' + | 'wallet' + | 'water' + | 'wine' + | 'wrench' + | 'yen'; + +export default TAccountIcons; diff --git a/src/extension/types/accounts/index.ts b/src/extension/types/accounts/index.ts index 276fe9fd..d483f4cc 100644 --- a/src/extension/types/accounts/index.ts +++ b/src/extension/types/accounts/index.ts @@ -5,3 +5,5 @@ export type { default as IAccountWithExtendedProps } from './IAccountWithExtende export type { default as IActiveAccountDetails } from './IActiveAccountDetails'; export type { default as IInitializeAccountOptions } from './IInitializeAccountOptions'; export type { default as INewAccount } from './INewAccount'; +export type { default as TAccountColors } from './TAccountColors'; +export type { default as TAccountIcons } from './TAccountIcons'; diff --git a/src/extension/utils/mapAccountWithExtendedPropsToAccount/mapAccountWithExtendedPropsToAccount.ts b/src/extension/utils/mapAccountWithExtendedPropsToAccount/mapAccountWithExtendedPropsToAccount.ts index 7024c424..2b2cda4e 100644 --- a/src/extension/utils/mapAccountWithExtendedPropsToAccount/mapAccountWithExtendedPropsToAccount.ts +++ b/src/extension/utils/mapAccountWithExtendedPropsToAccount/mapAccountWithExtendedPropsToAccount.ts @@ -8,7 +8,9 @@ import type { IAccount, IAccountWithExtendedProps } from '@extension/types'; * @returns {IAccount} the account object without the extended props. */ export default function mapAccountWithExtendedPropsToAccount({ + color, createdAt, + icon, id, name, networkInformation, @@ -18,7 +20,9 @@ export default function mapAccountWithExtendedPropsToAccount({ updatedAt, }: IAccountWithExtendedProps): IAccount { return { + color, createdAt, + icon, id, name, networkInformation, diff --git a/src/extension/utils/parseAccountIcon/index.ts b/src/extension/utils/parseAccountIcon/index.ts new file mode 100644 index 00000000..7b3edd04 --- /dev/null +++ b/src/extension/utils/parseAccountIcon/index.ts @@ -0,0 +1 @@ +export { default } from './parseAccountIcon'; diff --git a/src/extension/utils/parseAccountIcon/parseAccountIcon.tsx b/src/extension/utils/parseAccountIcon/parseAccountIcon.tsx new file mode 100644 index 00000000..1e398618 --- /dev/null +++ b/src/extension/utils/parseAccountIcon/parseAccountIcon.tsx @@ -0,0 +1,455 @@ +import { Icon, type IconProps } from '@chakra-ui/react'; +import React, { type ReactElement } from 'react'; +import { + IoAirplaneOutline, + IoAmericanFootballOutline, + IoBagHandleOutline, + IoBalloonOutline, + IoBaseballOutline, + IoBasketballOutline, + IoBeerOutline, + IoBicycleOutline, + IoBoatOutline, + IoBriefcaseOutline, + IoBrushOutline, + IoBugOutline, + IoBuildOutline, + IoBulbOutline, + IoBusOutline, + IoBusinessOutline, + IoCafeOutline, + IoCarOutline, + IoCardOutline, + IoCartOutline, + IoCashOutline, + IoCloudOutline, + IoCodeSlashOutline, + IoColorPaletteOutline, + IoCompassOutline, + IoConstructOutline, + IoCubeOutline, + IoDiamondOutline, + IoDiceOutline, + IoEarthOutline, + IoEggOutline, + IoEllipseOutline, + IoExtensionPuzzleOutline, + IoFemaleOutline, + IoFileTrayOutline, + IoFilmOutline, + IoFingerPrintOutline, + IoFishOutline, + IoFitnessOutline, + IoFlagOutline, + IoFlameOutline, + IoFlashOutline, + IoFlashlightOutline, + IoFlaskOutline, + IoFlowerOutline, + IoFootballOutline, + IoFootstepsOutline, + IoGameControllerOutline, + IoGlasses, + IoGlobeOutline, + IoGolfOutline, + IoHammerOutline, + IoHeartOutline, + IoHelpBuoyOutline, + IoHomeOutline, + IoKeyOutline, + IoLeafOutline, + IoLibraryOutline, + IoMaleOutline, + IoMoonOutline, + IoMusicalNotes, + IoPawOutline, + IoPeopleOutline, + IoPersonOutline, + IoPizzaOutline, + IoPlanetOutline, + IoPrismOutline, + IoRainyOutline, + IoReceiptOutline, + IoRestaurantOutline, + IoRocketOutline, + IoRoseOutline, + IoSchoolOutline, + IoServerOutline, + IoShieldOutline, + IoShirtOutline, + IoSkullOutline, + IoSnowOutline, + IoSparklesOutline, + IoStarOutline, + IoStorefrontOutline, + IoSunnyOutline, + IoTelescopeOutline, + IoTennisballOutline, + IoTerminalOutline, + IoThermometerOutline, + IoThumbsUpOutline, + IoTicketOutline, + IoTimeOutline, + IoTrainOutline, + IoTransgenderOutline, + IoTrashOutline, + IoTrophyOutline, + IoUmbrellaOutline, + IoWalletOutline, + IoWaterOutline, + IoWine, +} from 'react-icons/io5'; +import { + FaBitcoin, + FaDollarSign, + FaEthereum, + FaEuroSign, + FaYenSign, +} from 'react-icons/fa'; +import type { IconType } from 'react-icons'; + +// components +import AlgorandIcon from '@extension/components/AlgorandIcon'; +import VoiIcon from '@extension/components/VoiIcon'; + +// types +import type { IOptions } from './types'; + +// utils +import calculateIconSize from '@extension/utils/calculateIconSize'; + +/** + * Parses the account icon to an JSX Icon. If the account icon is null, it defaults to the wallet icon. + * @param {IOptions} options - The account icon, the color and size. + * @returns {ReactElement} The react-icons IconType for the account icon. + */ +export default function parseAccountIcon({ + accountIcon, + color, + size, +}: IOptions): ReactElement { + const defaultProps: Partial = { + boxSize: calculateIconSize(size), + color, + }; + let icon: IconType = IoWalletOutline; + + if (accountIcon === 'algorand') { + return ; + } + + if (accountIcon === 'voi') { + return ; + } + + switch (accountIcon) { + case 'airplane': + icon = IoAirplaneOutline; + break; + case 'american-football': + icon = IoAmericanFootballOutline; + break; + case 'balloon': + icon = IoBalloonOutline; + break; + case 'baseball': + icon = IoBaseballOutline; + break; + case 'basketball': + icon = IoBasketballOutline; + break; + case 'beer': + icon = IoBeerOutline; + break; + case 'bicycle': + icon = IoBicycleOutline; + break; + case 'bitcoin': + icon = FaBitcoin; + break; + case 'boat': + icon = IoBoatOutline; + break; + case 'briefcase': + icon = IoBriefcaseOutline; + break; + case 'brush': + icon = IoBrushOutline; + break; + case 'bug': + icon = IoBugOutline; + break; + case 'bulb': + icon = IoBulbOutline; + break; + case 'buoy': + icon = IoHelpBuoyOutline; + break; + case 'bus': + icon = IoBusOutline; + break; + case 'business': + icon = IoBusinessOutline; + break; + case 'cafe': + icon = IoCafeOutline; + break; + case 'car': + icon = IoCarOutline; + break; + case 'cart': + icon = IoCartOutline; + break; + case 'cash': + icon = IoCashOutline; + break; + case 'circle': + icon = IoEllipseOutline; + break; + case 'cloud': + icon = IoCloudOutline; + break; + case 'code': + icon = IoCodeSlashOutline; + break; + case 'compass': + icon = IoCompassOutline; + break; + case 'construct': + icon = IoConstructOutline; + break; + case 'credit-card': + icon = IoCardOutline; + break; + case 'cube': + icon = IoCubeOutline; + break; + case 'database': + icon = IoServerOutline; + break; + case 'diamond': + icon = IoDiamondOutline; + break; + case 'dice': + icon = IoDiceOutline; + break; + case 'earth': + icon = IoEarthOutline; + break; + case 'egg': + icon = IoEggOutline; + break; + case 'ethereum': + icon = FaEthereum; + break; + case 'euro': + icon = FaEuroSign; + break; + case 'female': + icon = IoFemaleOutline; + break; + case 'file-tray': + icon = IoFileTrayOutline; + break; + case 'film': + icon = IoFilmOutline; + break; + case 'fingerprint': + icon = IoFingerPrintOutline; + break; + case 'fire': + icon = IoFlameOutline; + break; + case 'fish': + icon = IoFishOutline; + break; + case 'fitness': + icon = IoFitnessOutline; + break; + case 'flag': + icon = IoFlagOutline; + break; + case 'flash': + icon = IoFlashOutline; + break; + case 'flashlight': + icon = IoFlashlightOutline; + break; + case 'flask': + icon = IoFlaskOutline; + break; + case 'flower': + icon = IoFlowerOutline; + break; + case 'football': + icon = IoFootballOutline; + break; + case 'footsteps': + icon = IoFootstepsOutline; + break; + case 'gaming': + icon = IoGameControllerOutline; + break; + case 'glasses': + icon = IoGlasses; + break; + case 'globe': + icon = IoGlobeOutline; + break; + case 'golf': + icon = IoGolfOutline; + break; + case 'hammer': + icon = IoHammerOutline; + break; + case 'heart': + icon = IoHeartOutline; + break; + case 'home': + icon = IoHomeOutline; + break; + case 'key': + icon = IoKeyOutline; + break; + case 'leaf': + icon = IoLeafOutline; + break; + case 'library': + icon = IoLibraryOutline; + break; + case 'male': + icon = IoMaleOutline; + break; + case 'moon': + icon = IoMoonOutline; + break; + case 'music-note': + icon = IoMusicalNotes; + break; + case 'palette': + icon = IoColorPaletteOutline; + break; + case 'paw': + icon = IoPawOutline; + break; + case 'people': + icon = IoPeopleOutline; + break; + case 'person': + icon = IoPersonOutline; + break; + case 'pizza': + icon = IoPizzaOutline; + break; + case 'planet': + icon = IoPlanetOutline; + break; + case 'prism': + icon = IoPrismOutline; + break; + case 'puzzle': + icon = IoExtensionPuzzleOutline; + break; + case 'rainy': + icon = IoRainyOutline; + break; + case 'receipt': + icon = IoReceiptOutline; + break; + case 'restaurant': + icon = IoRestaurantOutline; + break; + case 'rocket': + icon = IoRocketOutline; + break; + case 'rose': + icon = IoRoseOutline; + break; + case 'school': + icon = IoSchoolOutline; + break; + case 'shield': + icon = IoShieldOutline; + break; + case 'shirt': + icon = IoShirtOutline; + break; + case 'shopping-bag': + icon = IoBagHandleOutline; + break; + case 'skull': + icon = IoSkullOutline; + break; + case 'snow': + icon = IoSnowOutline; + break; + case 'sparkles': + icon = IoSparklesOutline; + break; + case 'star': + icon = IoStarOutline; + break; + case 'storefront': + icon = IoStorefrontOutline; + break; + case 'sun': + icon = IoSunnyOutline; + break; + case 'telescope': + icon = IoTelescopeOutline; + break; + case 'tennis': + icon = IoTennisballOutline; + break; + case 'terminal': + icon = IoTerminalOutline; + break; + case 'thermometer': + icon = IoThermometerOutline; + break; + case 'thumbs-up': + icon = IoThumbsUpOutline; + break; + case 'ticket': + icon = IoTicketOutline; + break; + case 'time': + icon = IoTimeOutline; + break; + case 'train': + icon = IoTrainOutline; + break; + case 'transgender': + icon = IoTransgenderOutline; + break; + case 'trash': + icon = IoTrashOutline; + break; + case 'trophy': + icon = IoTrophyOutline; + break; + case 'umbrella': + icon = IoUmbrellaOutline; + break; + case 'usd': + icon = FaDollarSign; + break; + case 'water': + icon = IoWaterOutline; + break; + case 'wine': + icon = IoWine; + break; + case 'wrench': + icon = IoBuildOutline; + break; + case 'yen': + icon = FaYenSign; + break; + case 'wallet': + default: + break; + } + + return ; +} diff --git a/src/extension/utils/parseAccountIcon/types/IOptions.ts b/src/extension/utils/parseAccountIcon/types/IOptions.ts new file mode 100644 index 00000000..b5128dc5 --- /dev/null +++ b/src/extension/utils/parseAccountIcon/types/IOptions.ts @@ -0,0 +1,9 @@ +import type { TAccountIcons, TSizes } from '@extension/types'; + +interface IOptions { + accountIcon: TAccountIcons | null; + color?: string; + size?: TSizes; +} + +export default IOptions; diff --git a/src/extension/utils/parseAccountIcon/types/index.ts b/src/extension/utils/parseAccountIcon/types/index.ts new file mode 100644 index 00000000..68e70016 --- /dev/null +++ b/src/extension/utils/parseAccountIcon/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions';