diff --git a/src/components/Account/AccountList.tsx b/src/components/Account/AccountList.tsx index 16e8e939..9c279690 100644 --- a/src/components/Account/AccountList.tsx +++ b/src/components/Account/AccountList.tsx @@ -9,13 +9,13 @@ import { ArrowCircledTopRight, ArrowDownLine, ArrowUpLine, TrashBin } from 'comp import Radio from 'components/Radio' import SwitchWithLabel from 'components/SwitchWithLabel' import Text from 'components/Text' -import useToggle from 'hooks/useToggle' import useStore from 'store' import { calculateAccountDeposits } from 'utils/accounts' import { hardcodedFee } from 'utils/constants' import { BN } from 'utils/helpers' import { getPage, getRoute } from 'utils/route' import usePrices from 'hooks/usePrices' +import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds' interface Props { setShowFundAccount: (showFundAccount: boolean) => void @@ -32,10 +32,9 @@ export default function AccountList(props: Props) { const { pathname } = useLocation() const { accountId, address } = useParams() const { data: prices } = usePrices() - + const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds() const deleteAccount = useStore((s) => s.deleteAccount) - const [isLending, setIsLending] = useToggle() const accountSelected = !!accountId && !isNaN(Number(accountId)) const selectedAccountDetails = props.accounts.find((account) => account.id === accountId) const selectedAccountBalance = selectedAccountDetails @@ -50,11 +49,6 @@ export default function AccountList(props: Props) { } } - function onChangeLendSwitch() { - setIsLending(!isLending) - /* TODO: handle lending assets */ - } - useEffect(() => { const element = document.getElementById(`account-${accountId}`) if (element) { @@ -69,6 +63,8 @@ export default function AccountList(props: Props) { {props.accounts.map((account) => { const positionBalance = calculateAccountDeposits(account, prices) const isActive = accountId === account.id + const isAutoLendEnabled = autoLendEnabledAccountIds.includes(account.id) + return (
toggleAutoLend(account.id)} tooltip={`Fund your account and lend assets effortlessly! By lending, you'll earn attractive interest (APY) without impacting your LTV. It's a win-win situation - don't miss out on this easy opportunity to grow your holdings!`} />
diff --git a/src/components/Account/AccountMenuContent.tsx b/src/components/Account/AccountMenuContent.tsx index cf7780a4..6a79c437 100644 --- a/src/components/Account/AccountMenuContent.tsx +++ b/src/components/Account/AccountMenuContent.tsx @@ -16,6 +16,7 @@ import { hardcodedFee } from 'utils/constants' import { isNumber } from 'utils/parsers' const menuClasses = 'absolute isolate flex w-full flex-wrap scrollbar-hide' +const ACCOUNT_MENU_BUTTON_ID = 'account-menu-button' interface Props { accounts: Account[] @@ -57,6 +58,7 @@ export default function AccountMenuContent(props: Props) { return (
) } + +export { ACCOUNT_MENU_BUTTON_ID } diff --git a/src/components/Account/FundAccount.tsx b/src/components/Account/FundAccount.tsx index a6878545..858413d8 100644 --- a/src/components/Account/FundAccount.tsx +++ b/src/components/Account/FundAccount.tsx @@ -13,6 +13,7 @@ import useStore from 'store' import { getAmount } from 'utils/accounts' import { hardcodedFee } from 'utils/constants' import { BN } from 'utils/helpers' +import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds' interface Props { setShowFundAccount: (show: boolean) => void @@ -26,7 +27,7 @@ export default function FundAccount(props: Props) { const [amount, setAmount] = useState(BN(0)) const [asset, setAsset] = useState(ASSETS[0]) - const [isLending, setIsLending] = useToggle() + const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds() const [isFunding, setIsFunding] = useToggle() const max = getAmount(asset.denom, balances ?? []) @@ -39,14 +40,6 @@ export default function FundAccount(props: Props) { setAsset(asset) }, []) - const handleLendAssets = useCallback( - (val: boolean) => { - setIsLending(val) - /* TODO: handle lending assets */ - }, - [setIsLending], - ) - async function onDeposit() { if (!accountId) return setIsFunding(true) @@ -65,6 +58,8 @@ export default function FundAccount(props: Props) { } } + if (!accountId) return null + return ( <>
@@ -100,8 +95,8 @@ export default function FundAccount(props: Props) { handleLendAssets(!isLending)} + value={autoLendEnabledAccountIds.includes(accountId)} + onChange={() => toggleAutoLend(accountId)} className='mb-4' tooltip="Fund your account and lend assets effortlessly! By lending, you'll earn attractive interest (APY) without impacting your LTV. It's a win-win situation - don't miss out on this easy opportunity to grow your holdings!" disabled={isFunding || amount.isEqualTo(0)} diff --git a/src/components/Earn/Lend/LendingActionButtons.tsx b/src/components/Earn/Lend/LendingActionButtons.tsx index 32871faf..366a0b2e 100644 --- a/src/components/Earn/Lend/LendingActionButtons.tsx +++ b/src/components/Earn/Lend/LendingActionButtons.tsx @@ -1,11 +1,16 @@ +import { useCallback } from 'react' + import Button from 'components/Button' -import { ArrowDownLine, ArrowUpLine } from 'components/Icons' +import { ArrowDownLine, ArrowUpLine, Enter } from 'components/Icons' import Text from 'components/Text' import { Tooltip } from 'components/Tooltip' import ConditionalWrapper from 'hocs/ConditionalWrapper' +import useAlertDialog from 'hooks/useAlertDialog' import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits' import useLendAndReclaimModal from 'hooks/useLendAndReclaimModal' import { byDenom } from 'utils/array' +import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds' +import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent' interface Props { data: LendingMarketTableData @@ -18,8 +23,32 @@ function LendingActionButtons(props: Props) { const { asset, accountLentValue: accountLendValue } = props.data const accountDeposits = useCurrentAccountDeposits() const { openLend, openReclaim } = useLendAndReclaimModal() + const { open: showAlertDialog } = useAlertDialog() + const { isAutoLendEnabledForCurrentAccount } = useAutoLendEnabledAccountIds() const assetDepositAmount = accountDeposits.find(byDenom(asset.denom))?.amount + const handleWithdraw = useCallback(() => { + if (isAutoLendEnabledForCurrentAccount) { + showAlertDialog({ + title: 'Disable Automatically Lend Assets', + description: + "Your auto-lend feature is currently enabled. To recover your funds, please confirm if you'd like to disable this feature in order to continue.", + positiveButton: { + onClick: () => document.getElementById(ACCOUNT_MENU_BUTTON_ID)?.click(), + text: 'Continue to Account Settings', + icon: , + }, + negativeButton: { + text: 'Cancel', + }, + }) + + return + } + + openReclaim(props.data) + }, [isAutoLendEnabledForCurrentAccount, openReclaim, props.data, showAlertDialog]) + return (
{accountLendValue && ( @@ -27,7 +56,7 @@ function LendingActionButtons(props: Props) { leftIcon={} iconClassName={iconClassnames} color='secondary' - onClick={() => openReclaim(props.data)} + onClick={handleWithdraw} className={buttonClassnames} > Withdraw diff --git a/src/components/Earn/Lend/LendingMarketsTable.tsx b/src/components/Earn/Lend/LendingMarketsTable.tsx index 8245066c..208ac095 100644 --- a/src/components/Earn/Lend/LendingMarketsTable.tsx +++ b/src/components/Earn/Lend/LendingMarketsTable.tsx @@ -117,7 +117,7 @@ function LendingMarketsTable(props: Props) { header: 'Manage', cell: ({ row }) => (
-
{row.getIsExpanded() ? : }
+
{row.getIsExpanded() ? : }
), }, diff --git a/src/components/Modals/AlertDialog/ButtonIcons.tsx b/src/components/Modals/AlertDialog/ButtonIcons.tsx new file mode 100644 index 00000000..f4e028f7 --- /dev/null +++ b/src/components/Modals/AlertDialog/ButtonIcons.tsx @@ -0,0 +1,17 @@ +import { Enter } from 'components/Icons' + +export function NoIcon() { + return ( +
+ ESC +
+ ) +} + +export function YesIcon() { + return ( +
+ +
+ ) +} diff --git a/src/components/Modals/AlertDialog/index.tsx b/src/components/Modals/AlertDialog/index.tsx new file mode 100644 index 00000000..8ea3605e --- /dev/null +++ b/src/components/Modals/AlertDialog/index.tsx @@ -0,0 +1,66 @@ +import Button from 'components/Button' +import { ExclamationMarkCircled } from 'components/Icons' +import Modal from 'components/Modal' +import Text from 'components/Text' +import useAlertDialog from 'hooks/useAlertDialog' +import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons' + +function AlertDialogController() { + const { config, close } = useAlertDialog() + + if (!config) return null + + return +} + +interface Props { + config: AlertDialogConfig + close: () => void +} + +function AlertDialog(props: Props) { + const { title, icon, description, negativeButton, positiveButton } = props.config + + const handleButtonClick = (button?: AlertDialogButton) => { + button?.onClick && button.onClick() + props.close() + } + + return ( + + {icon ?? } +
+ } + modalClassName='w-[577px]' + headerClassName='p-8' + contentClassName='px-8 pb-8' + hideCloseBtn + > + {title} + {description} +
+
+ + ) +} + +export default AlertDialogController diff --git a/src/components/Modals/ModalsContainer.tsx b/src/components/Modals/ModalsContainer.tsx index 7e983d77..4ffb42fc 100644 --- a/src/components/Modals/ModalsContainer.tsx +++ b/src/components/Modals/ModalsContainer.tsx @@ -1,5 +1,6 @@ import { AddVaultBorrowAssetsModal, + AlertDialogController, BorrowModal, FundAndWithdrawModal, LendAndReclaimModalController, @@ -18,6 +19,7 @@ export default function ModalsContainer() { + ) } diff --git a/src/components/Modals/Unlock/UnlockModalContent.tsx b/src/components/Modals/Unlock/UnlockModalContent.tsx index b552afc5..f502f538 100644 --- a/src/components/Modals/Unlock/UnlockModalContent.tsx +++ b/src/components/Modals/Unlock/UnlockModalContent.tsx @@ -2,32 +2,16 @@ import { useState } from 'react' import { useParams } from 'react-router-dom' import Button from 'components/Button' -import { Enter } from 'components/Icons' import Text from 'components/Text' import useStore from 'store' import { hardcodedFee } from 'utils/constants' +import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons' interface Props { depositedVault: DepositedVault onClose: () => void } -function NoIcon() { - return ( -
- ESC -
- ) -} - -function YesIcon() { - return ( -
- -
- ) -} - export default function UnlockModalContent(props: Props) { const unlock = useStore((s) => s.unlock) const [isWating, setIsConfirming] = useState(false) diff --git a/src/components/Modals/index.tsx b/src/components/Modals/index.tsx index cb57c53e..293f7ee3 100644 --- a/src/components/Modals/index.tsx +++ b/src/components/Modals/index.tsx @@ -5,3 +5,4 @@ export { default as LendAndReclaimModalController } from 'components/Modals/Lend export { default as UnlockModal } from 'components/Modals/Unlock/UnlockModal' export { default as VaultModal } from 'components/Modals/Vault' export { default as WithdrawFromVaults } from 'components/Modals/WithdrawFromVaults/WithdrawFromVaults' +export { default as AlertDialogController } from 'components/Modals/AlertDialog' diff --git a/src/constants/assets.ts b/src/constants/assets.ts index 8156f7eb..b0b727bb 100644 --- a/src/constants/assets.ts +++ b/src/constants/assets.ts @@ -13,6 +13,7 @@ export const ASSETS: Asset[] = [ isEnabled: true, isMarket: true, isDisplayCurrency: true, + isAutoLendEnabled: true, }, { symbol: 'ATOM', @@ -28,6 +29,7 @@ export const ASSETS: Asset[] = [ isEnabled: true, isMarket: true, isDisplayCurrency: true, + isAutoLendEnabled: true, }, { symbol: 'stATOM', diff --git a/src/constants/localStore.ts b/src/constants/localStore.ts index 4705f93a..314671e7 100644 --- a/src/constants/localStore.ts +++ b/src/constants/localStore.ts @@ -1,3 +1,4 @@ export const DISPLAY_CURRENCY_KEY = 'displayCurrency' export const ENABLE_ANIMATIONS_KEY = 'enableAnimations' export const FAVORITE_ASSETS = 'favoriteAssets' +export const AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY = 'autoLendEnabledAccountIds' diff --git a/src/hooks/useAlertDialog.ts b/src/hooks/useAlertDialog.ts new file mode 100644 index 00000000..48196fa2 --- /dev/null +++ b/src/hooks/useAlertDialog.ts @@ -0,0 +1,19 @@ +import { useCallback } from 'react' + +import useStore from 'store' + +function useAlertDialog() { + const config = useStore((s) => s.alertDialog) + + const open = useCallback((config: AlertDialogConfig) => { + useStore.setState({ alertDialog: config }) + }, []) + + const close = useCallback(() => { + useStore.setState({ alertDialog: null }) + }, []) + + return { config, open, close } +} + +export default useAlertDialog diff --git a/src/hooks/useAutoLendEnabledAccountIds.ts b/src/hooks/useAutoLendEnabledAccountIds.ts new file mode 100644 index 00000000..7cd36470 --- /dev/null +++ b/src/hooks/useAutoLendEnabledAccountIds.ts @@ -0,0 +1,33 @@ +import useLocalStorage from 'hooks/useLocalStorage' +import { AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY } from 'constants/localStore' +import useCurrentAccount from 'hooks/useCurrentAccount' + +function useAutoLendEnabledAccountIds(): { + autoLendEnabledAccountIds: string[] + toggleAutoLend: (accountId: string) => void + isAutoLendEnabledForCurrentAccount: boolean +} { + const currentAccount = useCurrentAccount() + const [autoLendEnabledAccountIds, setAutoLendEnabledAccountIds] = useLocalStorage( + AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY, + [], + ) + + const toggleAutoLend = (accountId: string) => { + const setOfAccountIds = new Set(autoLendEnabledAccountIds) + + setOfAccountIds.has(accountId) + ? setOfAccountIds.delete(accountId) + : setOfAccountIds.add(accountId) + + setAutoLendEnabledAccountIds(Array.from(setOfAccountIds)) + } + + const isAutoLendEnabledForCurrentAccount = currentAccount + ? autoLendEnabledAccountIds.includes(currentAccount.id) + : false + + return { autoLendEnabledAccountIds, toggleAutoLend, isAutoLendEnabledForCurrentAccount } +} + +export default useAutoLendEnabledAccountIds diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 00000000..19ec23b5 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,45 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +export default function useLocalStorage(key: string, defaultValue: T): [T, (value: T) => void] { + const keyRef = useRef(key) + const defaultValueRef = useRef(defaultValue) + const [value, _setValue] = useState(defaultValueRef.current) + + useEffect(() => { + const savedItem = localStorage.getItem(keyRef.current) + + if (!savedItem) { + localStorage.setItem(keyRef.current, JSON.stringify(defaultValueRef.current)) + } + + _setValue(savedItem ? JSON.parse(savedItem) : defaultValueRef.current) + + function handler(e: StorageEvent) { + if (e.key !== keyRef.current) return + + const item = localStorage.getItem(keyRef.current) + _setValue(JSON.parse(item ?? JSON.stringify(defaultValueRef.current))) + } + + window.addEventListener('storage', handler) + + return () => { + window.removeEventListener('storage', handler) + } + }, []) + + const setValue = useCallback((value: T) => { + try { + _setValue(value) + + localStorage.setItem(keyRef.current, JSON.stringify(value)) + if (typeof window !== 'undefined') { + window.dispatchEvent(new StorageEvent('storage', { key: keyRef.current })) + } + } catch (e) { + console.error(e) + } + }, []) + + return [value, setValue] +} diff --git a/src/store/slices/modal.ts b/src/store/slices/modal.ts index d242d077..c7096ca9 100644 --- a/src/store/slices/modal.ts +++ b/src/store/slices/modal.ts @@ -12,5 +12,6 @@ export default function createModalSlice(set: SetState, get: GetStat lendAndReclaimModal: null, vaultModal: null, withdrawFromVaultsModal: null, + alertDialog: null, } } diff --git a/src/types/interfaces/asset.d.ts b/src/types/interfaces/asset.d.ts index a20c7229..c6ed1d75 100644 --- a/src/types/interfaces/asset.d.ts +++ b/src/types/interfaces/asset.d.ts @@ -15,6 +15,7 @@ interface Asset { isDisplayCurrency?: boolean isStable?: boolean isFavorite?: boolean + isAutoLendEnabled?: boolean } interface OtherAsset extends Omit { diff --git a/src/types/interfaces/store/modals.d.ts b/src/types/interfaces/store/modals.d.ts index 5d36fb86..0cf745ec 100644 --- a/src/types/interfaces/store/modals.d.ts +++ b/src/types/interfaces/store/modals.d.ts @@ -9,6 +9,21 @@ interface ModalSlice { withdrawFromVaultsModal: DepositedVault[] | null unlockModal: UnlockModal | null lendAndReclaimModal: LendAndReclaimModalConfig | null + alertDialog: AlertDialogConfig | null +} + +interface AlertDialogButton { + text?: string + icon?: JSX.Element + onClick?: () => void +} + +interface AlertDialogConfig { + icon?: JSX.Element + title: JSX.Element | string + description: JSX.Element | string + negativeButton?: AlertDialogButton + positiveButton: AlertDialogButton } type LendAndReclaimModalAction = 'lend' | 'reclaim'