diff --git a/package-lock.json b/package-lock.json index 7cf7795..183cf84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "realtoken-dashboard-v2", - "version": "2.0.2", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "realtoken-dashboard-v2", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { "@apollo/client": "^3.7.3", "@emotion/react": "^11.10.5", diff --git a/package.json b/package.json index 010cc53..e73e280 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "realtoken-dashboard-v2", - "version": "2.0.2", + "version": "2.1.0", "scripts": { "dev": "next dev", "build": "next build", diff --git a/src/components/assetPage/assetPageTransfersTab.tsx b/src/components/assetPage/assetPageTransfersTab.tsx index 4292cd5..c1cc93d 100644 --- a/src/components/assetPage/assetPageTransfersTab.tsx +++ b/src/components/assetPage/assetPageTransfersTab.tsx @@ -7,7 +7,10 @@ import { RealTokenTransfer, TransferOrigin, } from 'src/repositories/transferts.repository' -import { selectAddressList } from 'src/store/features/settings/settingsSelector' +import { + selectAllUserAddressList, + selectUserAddressList, +} from 'src/store/features/settings/settingsSelector' import { UserRealtoken } from 'src/store/features/wallets/walletsSelector' function getTransferTitle(item: RealTokenTransfer) { @@ -80,15 +83,17 @@ const TransferRow: FC<{ item: RealTokenTransfer }> = ({ item }) => { export const AssetPageTransfersTab: FC<{ data: UserRealtoken }> = ({ data, }) => { - const addressList = useSelector(selectAddressList) + const addressList = useSelector(selectUserAddressList) + const allAddressList = useSelector(selectAllUserAddressList) const [transfers, setTransfers] = useState([]) useEffect(() => { GetRealTokenTransfers({ addressList: addressList, + allAddressList: allAddressList, realtokenList: [data], }).then((item) => setTransfers(item)) - }, [data, addressList]) + }, [data, addressList, allAddressList]) return ( diff --git a/src/components/layouts/MainLayout.tsx b/src/components/layouts/MainLayout.tsx index ad34aba..ed13858 100644 --- a/src/components/layouts/MainLayout.tsx +++ b/src/components/layouts/MainLayout.tsx @@ -5,8 +5,8 @@ import { MediaQuery, createStyles } from '@mantine/core' import { useModals } from '@mantine/modals' import { - selectCleanedAddressList, selectIsInitialized, + selectUserAddressList, } from 'src/store/features/settings/settingsSelector' import { Footer } from './Footer' @@ -43,7 +43,7 @@ export const MainLayout: FC = ({ children }) => { const { classes } = useStyles() const isInitialized = useSelector(selectIsInitialized) - const addressList = useSelector(selectCleanedAddressList) + const addressList = useSelector(selectUserAddressList) const modals = useModals() useEffect(() => { diff --git a/src/components/layouts/WalletMenu.tsx b/src/components/layouts/WalletMenu.tsx index 726c398..22d4ed7 100644 --- a/src/components/layouts/WalletMenu.tsx +++ b/src/components/layouts/WalletMenu.tsx @@ -23,17 +23,18 @@ import { selectIsInitialized, selectUser, } from 'src/store/features/settings/settingsSelector' -import { setUserAddress } from 'src/store/features/settings/settingsSlice' +import { User, setUserAddress } from 'src/store/features/settings/settingsSlice' import { IntegerField, StringField } from '../commons' interface WalletItemProps { address: string + isVisible?: boolean } const useStyles = createStyles({ address: { - span: { fontFamily: 'monospace', fontSize: '12px' }, + span: { fontFamily: 'monospace', fontSize: '11px' }, }, }) @@ -45,7 +46,11 @@ const WalletItem: FC = (props) => { variant={'dot'} fullWidth={true} className={classes.address} - style={{ textTransform: 'none', justifyContent: 'space-between' }} + style={{ + textTransform: 'none', + justifyContent: 'space-between', + opacity: props.isVisible ? 0.5 : 1, + }} > {EthersUtils.getAddress(props.address)} @@ -53,13 +58,52 @@ const WalletItem: FC = (props) => { } WalletItem.displayName = 'WalletItem' -const WalletItemList: FC<{ addressList: string[] }> = (props) => { +const AddWalletButton: FC<{ onClick?: () => void }> = (props) => { + const modals = useModals() + const { t } = useTranslation('common', { keyPrefix: 'manageWalletModal' }) + + function openModal() { + props.onClick?.() + modals.openContextModal('manageWalletModal', { + innerProps: {}, + closeOnClickOutside: false, + closeOnEscape: false, + }) + } + + return ( + <> + + + ) +} +AddWalletButton.displayName = 'AddWalletButton' + +const WalletItemList: FC<{ user: User }> = (props) => { + const addresses = [ + ...(props.user?.addressList ?? []), + ...(props.user?.customAddressList ?? []), + ] + + const hiddenAddressList = props.user?.hiddenAddressList ?? [] + return ( - {props.addressList + {addresses .filter((item) => item) - .map((address) => ( - + .map((item) => ( + ))} ) @@ -140,7 +184,11 @@ export const WalletMenu: FC = () => { - + + + + + diff --git a/src/components/modals/ManageWalletModal.tsx b/src/components/modals/ManageWalletModal.tsx new file mode 100644 index 0000000..56dda61 --- /dev/null +++ b/src/components/modals/ManageWalletModal.tsx @@ -0,0 +1,242 @@ +import { FC, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { + Button, + Flex, + Modal, + Stack, + TextInput, + createStyles, +} from '@mantine/core' +import { useDisclosure } from '@mantine/hooks' +import { ContextModalProps } from '@mantine/modals' + +import { utils as EthersUtils } from 'ethers' + +import { useAppDispatch } from 'src/hooks/react-hooks' +import { selectUser } from 'src/store/features/settings/settingsSelector' +import { + setCustomAddressList, + setHiddenAddressList, +} from 'src/store/features/settings/settingsSlice' + +const AddAddressButton: FC<{ + onChange: (value: string) => void + addresses: string[] +}> = (props) => { + const { t } = useTranslation('common', { keyPrefix: 'addCustomAddress' }) + + const [opened, { open, close }] = useDisclosure(false) + const [address, setAddress] = useState('') + const [isDirty, setIsDirty] = useState(false) + + const isInvalidAddress = !EthersUtils.isAddress(address) + const isAddressAlreadyAdded = props.addresses + .map((address) => address.toLowerCase()) + .includes(address.toLowerCase()) + + const currentError = isInvalidAddress + ? t('invalidAddress') + : isAddressAlreadyAdded + ? t('addressAlreadyAdded') + : undefined + + function resetAndClose() { + setAddress('') + setIsDirty(false) + close() + } + + function submit() { + if (currentError) { + return setIsDirty(true) + } + props.onChange(address) + resetAndClose() + } + + return ( + <> + + setAddress(event.currentTarget.value)} + /> + + + + + + + + + + ) +} +AddAddressButton.displayName = 'AddAddressButton' + +const useStyles = createStyles({ + address: { + border: '1px solid rgb(92, 95, 102)', + borderRadius: '10px', + fontFamily: 'monospace', + fontSize: '13px', + display: 'inline-block', + padding: '4px 8px', + maxWidth: '100%', + overflowWrap: 'break-word', + }, +}) + +const WalletItem: FC<{ + address: string + isVisible: boolean + onToggle: (address: string) => void + onRemove?: (address: string) => void +}> = (props) => { + const { classes } = useStyles() + const { t } = useTranslation('common', { + keyPrefix: 'manageWalletModal.item', + }) + + const address = EthersUtils.getAddress(props.address) + + return ( +
+
{address}
+ + + {props.onRemove ? ( + + ) : undefined} + +
+ ) +} +WalletItem.displayName = 'WalletItem' + +export const ManageWalletModal: FC = ({ context, id }) => { + const { t } = useTranslation('common', { keyPrefix: 'manageWalletModal' }) + const dispatch = useAppDispatch() + + const onClose = useCallback(() => { + context.closeModal(id) + }, [context, id]) + + const user = useSelector(selectUser) + const [customAddresses, setCustomAddresses] = useState( + user?.customAddressList ?? [] + ) + const [hiddenAddresses, setHiddenAddresses] = useState( + user?.hiddenAddressList ?? [] + ) + + const addresses = [...(user?.addressList ?? []), ...(customAddresses ?? [])] + + function addAddress(address: string) { + setCustomAddresses([...customAddresses, address]) + } + + function removeAddress(address: string) { + setCustomAddresses(customAddresses.filter((item) => item !== address)) + } + + function toggleAddressVisibility(address: string) { + if (hiddenAddresses.includes(address)) { + setHiddenAddresses(hiddenAddresses.filter((item) => item !== address)) + } else { + setHiddenAddresses([...hiddenAddresses, address]) + } + } + + const onSubmit = async () => { + dispatch(setCustomAddressList(customAddresses)) + dispatch(setHiddenAddressList(hiddenAddresses)) + onClose() + } + + return ( + + +
{t('open')}
+ {user?.addressList.map((address) => ( + + ))} + {customAddresses.map((address) => ( + + ))} + +
+ + +
+
+ + + + +
+ + + ) +} diff --git a/src/components/modals/index.ts b/src/components/modals/index.ts index 6bf0079..bec5f21 100644 --- a/src/components/modals/index.ts +++ b/src/components/modals/index.ts @@ -1,8 +1,10 @@ import { WalletModal } from '@realtoken/realt-commons' import { AssetsViewFilterModal } from '../assetsView/filters/AssetsViewFilterModal' +import { ManageWalletModal } from './ManageWalletModal' export const modals = { web3Wallets: WalletModal, assetsViewFilterModal: AssetsViewFilterModal, + manageWalletModal: ManageWalletModal, } diff --git a/src/hooks/useInitStore.ts b/src/hooks/useInitStore.ts index d6aa44e..0533177 100644 --- a/src/hooks/useInitStore.ts +++ b/src/hooks/useInitStore.ts @@ -6,7 +6,10 @@ import { useWeb3React } from '@web3-react/core' import { fetchCurrenciesRates } from 'src/store/features/currencies/currenciesSlice' import { selectRealtokens } from 'src/store/features/realtokens/realtokensSelector' import { fetchRealtokens } from 'src/store/features/realtokens/realtokensSlice' -import { selectCleanedAddressList } from 'src/store/features/settings/settingsSelector' +import { + selectAllUserAddressList, + selectUserAddressList, +} from 'src/store/features/settings/settingsSelector' import { initializeSettings, setUserAddress, @@ -21,16 +24,17 @@ import { useAppDispatch } from './react-hooks' export default function useInitStore() { const dispatch = useAppDispatch() - const addressList = useSelector(selectCleanedAddressList) + const allAddressList = useSelector(selectAllUserAddressList) + const addressList = useSelector(selectUserAddressList) const realtokens = useSelector(selectRealtokens) const { account } = useWeb3React() useEffect(() => { const accountAddress = account?.toLowerCase() - if (accountAddress && !addressList.includes(accountAddress)) { + if (accountAddress && !allAddressList.includes(accountAddress)) { dispatch(setUserAddress(accountAddress)) } - }, [account, addressList]) + }, [account, allAddressList]) useEffect(() => { dispatch(initializeSettings()) diff --git a/src/i18next/locales/en/common.json b/src/i18next/locales/en/common.json index ef36343..61e05ee 100644 --- a/src/i18next/locales/en/common.json +++ b/src/i18next/locales/en/common.json @@ -23,6 +23,24 @@ "addresses": "Addresses", "whitelists": "Whitelists" }, + "manageWalletModal": { + "open": "Manage my wallets", + "close": "Close", + "submit": "Submit", + "item": { + "hide": "Hide", + "show": "Show", + "remove": "Remove" + } + }, + "addCustomAddress": { + "open": "Add an address", + "title": "Add an address", + "close": "Close", + "submit": "Submit", + "invalidAddress": "Invalid address", + "addressAlreadyAdded": "Address already added" + }, "numbers": { "currency": "{{value, number(maximumFractionDigits: 2; minimumFractionDigits: 2)}} {{symbol}}", "percent": "{{value, number(maximumFractionDigits: 2)}} %", diff --git a/src/i18next/locales/fr/common.json b/src/i18next/locales/fr/common.json index 141d8c0..5336da7 100644 --- a/src/i18next/locales/fr/common.json +++ b/src/i18next/locales/fr/common.json @@ -23,6 +23,24 @@ "addresses": "Adresses", "whitelists": "Whitelists" }, + "manageWalletModal": { + "open": "Gérer mes adresses", + "close": "Fermer", + "submit": "Valider", + "item": { + "hide": "Masquer", + "show": "Afficher", + "remove": "Supprimer" + } + }, + "addCustomAddress": { + "open": "Ajouter une adresse", + "title": "Ajouter une adresse", + "close": "Fermer", + "submit": "Valider", + "invalidAddress": "Adresse invalide", + "addressAlreadyAdded": "Cette adresse est déjà ajoutée" + }, "numbers": { "currency": "{{value, number(maximumFractionDigits: 2; minimumFractionDigits: 2)}} {{symbol}}", "percent": "{{value, number(maximumFractionDigits: 2)}} %", diff --git a/src/repositories/transferts.repository.ts b/src/repositories/transferts.repository.ts index 4ed7be3..1d20fae 100644 --- a/src/repositories/transferts.repository.ts +++ b/src/repositories/transferts.repository.ts @@ -30,9 +30,10 @@ export interface RealTokenTransfer { export async function GetRealTokenTransfers(params: { addressList: string[] + allAddressList: string[] realtokenList: APIRealToken[] }) { - const addressList = params.addressList.map((item) => item.toLowerCase()) + const allAddressList = params.allAddressList.map((item) => item.toLowerCase()) const transferEvents = await getRealTokenTransfers( params.addressList, @@ -42,12 +43,12 @@ export async function GetRealTokenTransfers(params: { return transferEvents.map((transfer) => ({ timestamp: Number(transfer.timestamp), amount: Number(transfer.amount), - direction: addressList.includes(transfer.source) + direction: allAddressList.includes(transfer.source) ? TransferDirection.out : TransferDirection.in, origin: getTransferOrigin( transfer, - addressList, + allAddressList, params.realtokenList.find( (item) => item.blockchainAddresses.xDai.contract.toLowerCase() === diff --git a/src/store/features/settings/settingsSelector.ts b/src/store/features/settings/settingsSelector.ts index 1194151..ddbda68 100644 --- a/src/store/features/settings/settingsSelector.ts +++ b/src/store/features/settings/settingsSelector.ts @@ -13,13 +13,21 @@ export const selectIsLoading = (state: RootState): boolean => state.wallets.isLoading || state.currencies.isLoading -export const selectAddressList = (state: RootState): string[] => - state.settings.user?.addressList ?? [] - -export const selectCleanedAddressList = createSelector( - selectAddressList, - (addressList) => - Array.from(new Set(addressList.map((item) => item.toLowerCase()))) +export const selectAllUserAddressList = (state: RootState) => { + const addressList = state.settings.user?.addressList ?? [] + const customAddressList = state.settings.user?.customAddressList ?? [] + return Array.from( + new Set( + [...addressList, ...customAddressList].map((item) => item.toLowerCase()) + ) + ) +} + +export const selectUserAddressList = createSelector( + (state: RootState) => state.settings.user, + selectAllUserAddressList, + (user, addressList) => + addressList.filter((item) => !user?.hiddenAddressList?.includes(item)) ) export const selectUserCurrency = (state: RootState): string => diff --git a/src/store/features/settings/settingsSlice.ts b/src/store/features/settings/settingsSlice.ts index 3f6d53e..441c1ce 100644 --- a/src/store/features/settings/settingsSlice.ts +++ b/src/store/features/settings/settingsSlice.ts @@ -5,7 +5,7 @@ import { createAction, createReducer } from '@reduxjs/toolkit' import { t } from 'i18next' import { UserRepository } from 'src/repositories/user.repository' -import { AppDispatch } from 'src/store/store' +import { AppDispatch, RootState } from 'src/store/store' import { Currency } from 'src/types/Currencies' const USER_LS_KEY = 'store:settings/user' @@ -15,6 +15,8 @@ export interface User { id: string mainAddress: string addressList: string[] + customAddressList: string[] + hiddenAddressList: string[] whitelistAttributeKeys: string[] } @@ -63,6 +65,8 @@ export function setUserAddress(address: string) { type: userChangedDispatchType, payload: { mainAddress: address.toLowerCase(), + customAddressList: [], + hiddenAddressList: [], ...user, }, }) @@ -72,15 +76,69 @@ export function setUserAddress(address: string) { } } +export function setCustomAddressList(addressList: string[]) { + return async (dispatch: AppDispatch, getState: () => RootState) => { + const user = getState().settings.user + dispatch({ + type: userChangedDispatchType, + payload: { + ...user, + customAddressList: addressList.map((item) => item.toLowerCase()), + }, + }) + } +} + +export function setHiddenAddressList(addressList: string[]) { + return async (dispatch: AppDispatch, getState: () => RootState) => { + const user = getState().settings.user + dispatch({ + type: userChangedDispatchType, + payload: { + ...user, + hiddenAddressList: addressList.map((item) => item.toLowerCase()), + }, + }) + } +} + export const settingsReducers = createReducer( settingsInitialState, (builder) => { builder .addCase(userChanged, (state, action) => { - state.user = action.payload - action.payload - ? localStorage.setItem(USER_LS_KEY, JSON.stringify(action.payload)) - : localStorage.removeItem(USER_LS_KEY) + const user = action.payload + if (!user) { + state.user = undefined + localStorage.removeItem(USER_LS_KEY) + return + } + const mainAddress = user.mainAddress.toLowerCase() + const addressList = + user.addressList?.map((item) => item.toLowerCase()) ?? [] + + const customAddressList = + user.customAddressList?.map((item) => item.toLowerCase()) ?? [] + + const hiddenAddressList = + user.hiddenAddressList?.map((item) => item.toLowerCase()) ?? [] + + if (!addressList.includes(mainAddress)) { + addressList.unshift(mainAddress) + } + + const addresses = [...addressList, ...customAddressList] + + state.user = { + ...user, + mainAddress, + addressList, + customAddressList, + hiddenAddressList: hiddenAddressList.filter((item) => + addresses.includes(item) + ), + } + localStorage.setItem(USER_LS_KEY, JSON.stringify(action.payload)) }) .addCase(userCurrencyChanged, (state, action) => { state.userCurrency = action.payload diff --git a/src/store/features/wallets/walletsSlice.ts b/src/store/features/wallets/walletsSlice.ts index 9a445d1..72b0f1a 100644 --- a/src/store/features/wallets/walletsSlice.ts +++ b/src/store/features/wallets/walletsSlice.ts @@ -8,7 +8,7 @@ import { import { AppDispatch, RootState } from 'src/store/store' import { Realtoken } from '../realtokens/realtokensSelector' -import { selectCleanedAddressList } from '../settings/settingsSelector' +import { selectUserAddressList } from '../settings/settingsSelector' interface WalletsInitialStateType { balances: WalletBalances @@ -46,7 +46,7 @@ export function fetchWallets(realtokens: Realtoken[]) { return async (dispatch: AppDispatch, getState: () => RootState) => { const state = getState() const isLoading = state.wallets.isLoading - const addressList = selectCleanedAddressList(state) + const addressList = selectUserAddressList(state) if (isLoading) return dispatch({ type: isLoadingDispatchType, payload: true }) diff --git a/src/theme/theme.ts b/src/theme/theme.ts index e347415..17037df 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -8,6 +8,7 @@ export const modalStyles: ModalProps['styles'] = { padding: '10px !important', overflowY: 'unset !important' as 'unset', }, + body: { padding: '0' }, } export const theme: MantineThemeOverride = {