diff --git a/src/App.tsx b/src/App.tsx index e40cd0bf0..bec7c8b5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ import { useWeb3React } from '@web3-react/core'; import { useAutoConnect } from 'services/web3/wallet/hooks'; import { setUser } from 'services/observables/user'; import { BancorRouter } from 'router/BancorRouter'; +import { Modals } from 'modals'; import { handleRestrictedWalletCheck } from 'services/restrictedWallets'; import { RestrictedWallet } from 'pages/RestrictedWallet'; @@ -103,6 +104,7 @@ export const App = () => { + ); }; diff --git a/src/components/modal/Modal.stories.tsx b/src/components/modal/Modal.stories.tsx deleted file mode 100644 index 0996079a3..000000000 --- a/src/components/modal/Modal.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { Modal } from './Modal'; -import { useState } from 'react'; -import { withDesign } from 'storybook-addon-designs'; -import { Button } from 'components/button/Button'; - -export default { - title: 'Components/Modal/Normal', - component: Modal, - decorators: [withDesign], -} as ComponentMeta; - -const Template: ComponentStory = (args) => { - const [isOpen, setIsOpen] = useState(false); - - return ( - <> - - -

some content

-
- - ); -}; - -export const Open = Template.bind({}); -Open.args = { - isOpen: true, - setIsOpen: () => {}, - title: 'Modal title', -}; - -Open.parameters = { - design: { - type: 'figma', - url: 'https://www.figma.com/file/fwADI9wqDrRAdlMX8EddCw/Bancor-v3?node-id=7879%3A257081', - }, -}; diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx deleted file mode 100644 index dcf8801d8..000000000 --- a/src/components/modal/Modal.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Dialog, Transition } from '@headlessui/react'; -import { Fragment } from 'react'; -import { ReactComponent as IconTimes } from 'assets/icons/times.svg'; -import { ReactComponent as IconChevron } from 'assets/icons/chevronRight.svg'; - -interface ModalProps { - title?: string; - children: JSX.Element; - separator?: boolean; - titleElement?: any; - setIsOpen: Function; - isOpen: boolean; - onBackClick?: Function; - showBackButton?: boolean; - onClose?: Function; - large?: boolean; -} - -export const Modal = ({ - title, - children, - separator, - titleElement, - setIsOpen, - isOpen, - showBackButton, - onBackClick, - onClose, - large, -}: ModalProps) => { - return ( - <> - - (onClose ? onClose() : setIsOpen(false))} - > -
- - - - - {/* This element is to trick the browser into centering the modal contents. */} - - -
- - {showBackButton && ( - - )} - {titleElement && titleElement} - {title ? title :
 
} -
- -
-
-
- {separator &&
} - {children} -
-
-
-
-
-
- - ); -}; diff --git a/src/components/modal/ModalV3.tsx b/src/components/modal/ModalV3.tsx deleted file mode 100644 index d0770b7a1..000000000 --- a/src/components/modal/ModalV3.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Dialog, Transition } from '@headlessui/react'; -import { Fragment, ReactNode } from 'react'; -import { ReactComponent as IconTimes } from 'assets/icons/times.svg'; -import { ReactComponent as IconChevron } from 'assets/icons/chevronRight.svg'; - -interface Props { - title?: string; - children: JSX.Element; - separator?: boolean; - titleElement?: ReactNode; - setIsOpen: (isOpen: boolean) => void; - isOpen: boolean; - onBackClick?: Function; - showBackButton?: boolean; - onClose?: Function; - large?: boolean; -} - -export const ModalV3 = ({ - title, - children, - separator, - titleElement, - setIsOpen, - isOpen, - showBackButton, - onBackClick, - onClose, - large, -}: Props) => { - return ( - <> - - (onClose ? onClose() : setIsOpen(false))} - > -
- - - - - {/* This element is to trick the browser into centering the modal contents. */} - - -
- -
- {showBackButton && ( - - )} - {title && title} -
-
- {titleElement && titleElement} - -
-
-
- {separator &&
} - {children} -
-
-
-
-
-
- - ); -}; diff --git a/src/components/modalFullscreen/ModalFullscreen.stories.tsx b/src/components/modalFullscreen/ModalFullscreen.stories.tsx deleted file mode 100644 index 04f5fefe3..000000000 --- a/src/components/modalFullscreen/ModalFullscreen.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { ModalFullscreen } from './ModalFullscreen'; -import { useState } from 'react'; -import { withDesign } from 'storybook-addon-designs'; -import { Button } from 'components/button/Button'; - -export default { - title: 'Components/Modal/Fullscreen', - component: ModalFullscreen, - decorators: [withDesign], -} as ComponentMeta; - -const Template: ComponentStory = (args) => { - const [isOpen, setIsOpen] = useState(false); - - return ( - <> - - -

some content

-
- - ); -}; - -export const Open = Template.bind({}); -Open.args = { - isOpen: true, - setIsOpen: () => {}, - title: 'Modal title', -}; - -Open.parameters = { - design: { - type: 'figma', - url: 'https://www.figma.com/file/fwADI9wqDrRAdlMX8EddCw/Bancor-v3?node-id=7879%3A257081', - }, -}; diff --git a/src/components/modalFullscreen/ModalFullscreen.tsx b/src/components/modalFullscreen/ModalFullscreen.tsx deleted file mode 100644 index ef423538d..000000000 --- a/src/components/modalFullscreen/ModalFullscreen.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Dialog, Transition } from '@headlessui/react'; -import { Fragment } from 'react'; -import { ReactComponent as IconChevron } from 'assets/icons/chevronRight.svg'; -import { ReactComponent as IconBancor } from 'assets/icons/bancor.svg'; -import 'elements/layoutHeader/LayoutHeader.css'; -import { LayoutHeaderMobile } from 'elements/layoutHeader/LayoutHeaderMobile'; - -interface ModalFullscreenProps { - title?: string | JSX.Element | JSX.Element[]; - children: JSX.Element | JSX.Element[]; - setIsOpen: Function; - isOpen: boolean; - showHeader?: boolean; -} - -export const ModalFullscreen = ({ - title, - children, - setIsOpen, - isOpen, - showHeader, -}: ModalFullscreenProps) => { - return ( - <> - - setIsOpen(false)} - > -
- {showHeader && ( - - -
- -
-
- )} - -
- {title && ( -
- {title} -
- )} - - {children} -
-
-
-
- - ); -}; diff --git a/src/components/modalFullscreen/modalFullscreenV3.tsx b/src/components/modalFullscreen/modalFullscreenV3.tsx deleted file mode 100644 index 30e98e009..000000000 --- a/src/components/modalFullscreen/modalFullscreenV3.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Dialog, Transition } from '@headlessui/react'; -import { Fragment, memo, ReactNode } from 'react'; -import { ReactComponent as IconBancor } from 'assets/icons/bancor.svg'; -import { ReactComponent as IconTimes } from 'assets/icons/times.svg'; - -interface Props { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; - children: ReactNode; - title: string; - titleElement?: ReactNode; -} - -const ModalFullscreenV3 = ({ - isOpen, - setIsOpen, - children, - title, - titleElement, -}: Props) => { - return ( - <> - - setIsOpen(false)} - > -
-
-

- - {title} -

-
- {titleElement && titleElement} - -
-
- -
- {children} -
-
-
-
- - ); -}; - -export default memo(ModalFullscreenV3); diff --git a/src/components/selectPool/SelectPool.tsx b/src/components/selectPool/SelectPool.tsx index 204735497..19ce77953 100644 --- a/src/components/selectPool/SelectPool.tsx +++ b/src/components/selectPool/SelectPool.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; -import { SelectPoolModal } from 'components/selectPoolModal/SelectPoolModal'; import { ReactComponent as IconChevronDown } from 'assets/icons/chevronDown.svg'; import { Pool } from 'services/observables/pools'; import { Image } from 'components/image/Image'; +import { ModalNames } from 'modals'; +import { useModal } from 'hooks/useModal'; interface SelectPoolProps { pool: Pool; @@ -17,13 +17,18 @@ export const SelectPool = ({ label, onSelect, }: SelectPoolProps) => { - const [isModalOpen, setIsModalOpen] = useState(false); + const { pushModal } = useModal(); return (
{label} -
); }; diff --git a/src/components/selectPoolModal/SelectPoolModal.tsx b/src/components/selectPoolModal/SelectPoolModal.tsx deleted file mode 100644 index d90f15262..000000000 --- a/src/components/selectPoolModal/SelectPoolModal.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { isMobile } from 'react-device-detect'; -import { ModalFullscreen } from '../modalFullscreen/ModalFullscreen'; -import { Modal } from '../modal/Modal'; -import { SelectPoolModalContent } from './SelectPoolModalContent'; -import { Pool } from 'services/observables/pools'; - -interface Props { - pools: Pool[]; - isOpen: boolean; - setIsOpen: Function; - onSelect: Function; -} - -export const SelectPoolModal = ({ - pools, - isOpen, - setIsOpen, - onSelect, -}: Props) => { - const handleOnSelect = (pool: Pool) => { - setIsOpen(false); - onSelect(pool); - }; - - if (isMobile) { - return ( - - - - ); - } - - return ( - - - - ); -}; diff --git a/src/components/selectToken/SelectToken.tsx b/src/components/selectToken/SelectToken.tsx index 7281874e0..7ef3d3531 100644 --- a/src/components/selectToken/SelectToken.tsx +++ b/src/components/selectToken/SelectToken.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; import { classNameGenerator } from 'utils/pureFunctions'; -import { SearchableTokenList } from 'components/searchableTokenList/SearchableTokenList'; import { Token } from 'services/observables/tokens'; import { ReactComponent as IconChevronDown } from 'assets/icons/chevronDown.svg'; import { Image } from 'components/image/Image'; import { Button } from 'components/button/Button'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; interface SelectTokenProps { label?: string; @@ -27,8 +28,24 @@ export const SelectToken = ({ excludedTokens = [], includedTokens = [], }: SelectTokenProps) => { - const [isOpen, setIsOpen] = useState(false); const [showSelectToken, setSelectToken] = useState(!!startEmpty); + const { pushModal } = useModal(); + + const pushSearchableTokenList = () => { + pushModal({ + modalName: ModalNames.SearchableTokenList, + data: { + limit: true, + excludedTokens, + includedTokens, + tokens: tokens ? tokens : [], + onSelected: (token: Token) => { + if (setToken) setToken(token); + setSelectToken(false); + }, + }, + }); + }; return ( <> @@ -39,7 +56,7 @@ export const SelectToken = ({ className={`flex items-center ${classNameGenerator({ 'cursor-pointer': selectable, })}`} - onClick={() => (selectable ? setIsOpen(true) : {})} + onClick={() => (selectable ? pushSearchableTokenList() : {})} > {token ? ( <> @@ -65,7 +82,7 @@ export const SelectToken = ({ ) : ( )} - - { - if (setToken) setToken(token); - setSelectToken(false); - }} - isOpen={isOpen} - limit - setIsOpen={setIsOpen} - tokens={tokens ? tokens : []} - excludedTokens={excludedTokens} - includedTokens={includedTokens} - /> ); }; diff --git a/src/components/slideover/Slideover.tsx b/src/components/slideover/Slideover.tsx index 20469db39..a5a456b80 100644 --- a/src/components/slideover/Slideover.tsx +++ b/src/components/slideover/Slideover.tsx @@ -3,7 +3,7 @@ import { Dialog, Transition } from '@headlessui/react'; interface SlideoverProps { isOpen: boolean; - setIsOpen: Function; + setIsOpen: (isOpen: boolean) => void; children: JSX.Element; } diff --git a/src/components/tokenInputField/TokenInputField.tsx b/src/components/tokenInputField/TokenInputField.tsx index 5cf25e781..d593ac9bb 100644 --- a/src/components/tokenInputField/TokenInputField.tsx +++ b/src/components/tokenInputField/TokenInputField.tsx @@ -1,6 +1,4 @@ -import { useState } from 'react'; import { classNameGenerator, sanitizeNumberInput } from 'utils/pureFunctions'; -import { SearchableTokenList } from 'components/searchableTokenList/SearchableTokenList'; import { Token, TokenMinimal } from 'services/observables/tokens'; import { ReactComponent as IconChevronDown } from 'assets/icons/chevronDown.svg'; import 'components/tokenInputField/TokenInputField.css'; @@ -11,6 +9,8 @@ import { useAppSelector } from 'store'; import { Image } from 'components/image/Image'; import { getV2AndV3Tokens } from 'store/bancor/bancor'; import { TokenCurrency } from 'store/user/user'; +import { ModalNames } from 'modals'; +import { useModal } from 'hooks/useModal'; interface TokenInputFieldProps { label?: string; @@ -61,7 +61,7 @@ export const TokenInputField = ({ v3AndV2, balanceLabel = 'Balance', }: TokenInputFieldProps) => { - const [isOpen, setIsOpen] = useState(false); + const { pushModal } = useModal(); const balance = fieldBalance ? fieldBalance : token ? token.balance : null; const balanceUsd = @@ -141,6 +141,21 @@ export const TokenInputField = ({ '!border-error': errorMsg, })}`; + const pushSearchableTokenList = () => { + pushModal({ + modalName: ModalNames.SearchableTokenList, + data: { + excludedTokens, + includedTokens, + tokens, + onSelected: (token: Token) => { + if (setToken) setToken(token); + onInputChange(inputValue(), token); + }, + }, + }); + }; + return (
@@ -168,7 +183,7 @@ export const TokenInputField = ({
{startEmpty && !token ? (
)} - - { - if (setToken) setToken(token); - onInputChange(inputValue(), token); - }} - isOpen={isOpen} - setIsOpen={setIsOpen} - tokens={tokens} - excludedTokens={excludedTokens} - includedTokens={includedTokens} - /> ); }; diff --git a/src/elements/earn/pools/TopPools.tsx b/src/elements/earn/pools/TopPools.tsx index b97fc66bc..072a5bef7 100644 --- a/src/elements/earn/pools/TopPools.tsx +++ b/src/elements/earn/pools/TopPools.tsx @@ -3,10 +3,12 @@ import { useAppSelector } from 'store'; import { getTopPoolsV3 } from 'store/bancor/pool'; import { ReactComponent as IconGift } from 'assets/icons/gift.svg'; import { Image } from 'components/image/Image'; -import { DepositV3Modal } from './poolsTable/v3/DepositV3Modal'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; export const TopPools = () => { const pools = useAppSelector(getTopPoolsV3); + const { pushModal } = useModal(); return (
@@ -16,33 +18,33 @@ export const TopPools = () => { {pools.length ? pools.map((pool, index) => { return ( - ( - - )} - /> + onClick={() => + pushModal({ + modalName: ModalNames.DepositV3, + data: { pool }, + }) + } + className="flex items-center justify-center min-w-[170px] h-[75px] rounded-[6px] bg-white dark:bg-charcoal border border-silver dark:border-grey transition-all duration-300" + > + Token Logo +
+
+ {pool.reserveToken.symbol} +
+
+ {pool.apr7d.total.toFixed(2)}% + {pool.latestProgram?.isActive && ( + + )} +
+
+ ); }) : [...Array(20)].map((_, index) => ( diff --git a/src/elements/earn/pools/addLiquidity/empty/AddLiquidityEmptyCTA.tsx b/src/elements/earn/pools/addLiquidity/empty/AddLiquidityEmptyCTA.tsx index 0d210e786..4597ee4ba 100644 --- a/src/elements/earn/pools/addLiquidity/empty/AddLiquidityEmptyCTA.tsx +++ b/src/elements/earn/pools/addLiquidity/empty/AddLiquidityEmptyCTA.tsx @@ -1,5 +1,4 @@ import { useDispatch } from 'react-redux'; -import { useApproveModal } from 'hooks/useApproveModal'; import { Token } from 'services/observables/tokens'; import { addLiquidity } from 'services/web3/liquidity/liquidity'; import { @@ -14,6 +13,7 @@ import { useAppSelector } from 'store'; import { Pool } from 'services/observables/pools'; import { useNavigation } from 'hooks/useNavigation'; import { useWalletConnect } from 'elements/walletConnect/useWalletConnect'; +import { useApproval } from 'hooks/useApproval'; interface Props { pool: Pool; @@ -83,7 +83,7 @@ export const AddLiquidityEmptyCTA = ({ goToPage, ]); - const [onStart, ModalApprove] = useApproveModal( + const startApprove = useApproval( [ { amount: amountBnt, token: bnt }, { amount: amountTkn, token: tkn }, @@ -121,7 +121,7 @@ export const AddLiquidityEmptyCTA = ({ if (!account) { handleWalletButtonClick(); } else { - onStart(); + startApprove(); } }; @@ -136,7 +136,6 @@ export const AddLiquidityEmptyCTA = ({ > {btn.label} - {ModalApprove} ); }; diff --git a/src/elements/earn/pools/addLiquidity/single/AddLiquiditySingle.tsx b/src/elements/earn/pools/addLiquidity/single/AddLiquiditySingle.tsx index 2e051086d..2a2cb6055 100644 --- a/src/elements/earn/pools/addLiquidity/single/AddLiquiditySingle.tsx +++ b/src/elements/earn/pools/addLiquidity/single/AddLiquiditySingle.tsx @@ -6,7 +6,6 @@ import { AddLiquiditySingleSpaceAvailable } from 'elements/earn/pools/addLiquidi import { useAppSelector } from 'store'; import { AddLiquiditySingleAmount } from 'elements/earn/pools/addLiquidity/single/AddLiquiditySingleAmount'; import { useCallback, useState } from 'react'; -import { useApproveModal } from 'hooks/useApproveModal'; import { AddLiquiditySingleCTA } from 'elements/earn/pools/addLiquidity/single/AddLiquiditySingleCTA'; import { useDispatch } from 'react-redux'; import { prettifyNumber } from 'utils/helperFunctions'; @@ -31,6 +30,7 @@ import { useNavigation } from 'hooks/useNavigation'; import { fetchProtectedPositions } from 'services/web3/protection/positions'; import { setProtectedPositions } from 'store/liquidity/liquidity'; import { Events } from 'services/api/googleTagManager'; +import { useApproval } from 'hooks/useApproval'; import { TokenCurrency } from 'store/user/user'; interface Props { @@ -115,7 +115,7 @@ export const AddLiquiditySingle = ({ pool }: Props) => { ); }; - const [onStart, ModalApprove] = useApproveModal( + const startApprove = useApproval( [{ amount, token: selectedToken }], addV2Protection, ApprovalContract.LiquidityProtection, @@ -167,8 +167,15 @@ export const AddLiquiditySingle = ({ pool }: Props) => { isCurrency ); sendLiquidityEvent(Events.click); - onStart(); - }, [amount, amountUsd, isCurrency, onStart, pool.name, selectedToken.symbol]); + startApprove(); + }, [ + startApprove, + amount, + amountUsd, + isCurrency, + pool.name, + selectedToken.symbol, + ]); if (!tkn) { goToPage.notFound(); @@ -214,7 +221,6 @@ export const AddLiquiditySingle = ({ pool }: Props) => { errorMsg={handleError()} isBNTSelected={isBNTSelected} /> - {ModalApprove} ); }; diff --git a/src/elements/earn/pools/poolsTable/PoolsTable.tsx b/src/elements/earn/pools/poolsTable/PoolsTable.tsx index ba029f1c3..2177c98fc 100644 --- a/src/elements/earn/pools/poolsTable/PoolsTable.tsx +++ b/src/elements/earn/pools/poolsTable/PoolsTable.tsx @@ -15,8 +15,9 @@ import { sortNumbersByKey } from 'utils/pureFunctions'; import { Navigate } from 'components/navigate/Navigate'; import { PopoverV3 } from 'components/popover/PopoverV3'; import { Image } from 'components/image/Image'; -import { DepositV3Modal } from './v3/DepositV3Modal'; -import { SnapshotLink } from 'elements/earn/pools/SnapshotLink'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; +import { SnapshotLink } from '../SnapshotLink'; import { config } from 'config'; import { BaseCurrency } from 'store/user/user'; @@ -40,6 +41,7 @@ export const PoolsTable = ({ setLowEarnRate: Function; }) => { const pools = useAppSelector((state) => state.pool.v3Pools); + const { pushModal } = useModal(); const baseCurrency = useAppSelector((state) => state.user.baseCurrency); const isUSD = baseCurrency === BaseCurrency.USD; @@ -184,25 +186,25 @@ export const PoolsTable = ({ Header: '', accessor: 'poolDltId', Cell: (cellData) => ( - ( - - )} - /> + ), width: 50, minWidth: 50, disableSortBy: true, }, ], - [toolTip] + [toolTip, pushModal] ); const defaultSort: SortingRule = { diff --git a/src/elements/earn/pools/poolsTable/PoolsTableCellActions.tsx b/src/elements/earn/pools/poolsTable/PoolsTableCellActions.tsx index bcda0766b..81b198763 100644 --- a/src/elements/earn/pools/poolsTable/PoolsTableCellActions.tsx +++ b/src/elements/earn/pools/poolsTable/PoolsTableCellActions.tsx @@ -1,17 +1,23 @@ import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; import { PopoverV3 } from 'components/popover/PopoverV3'; -import { DepositDisabledModal } from 'elements/earn/pools/poolsTable/v3/DepositDisabledModal'; -import { Navigate } from 'components/navigate/Navigate'; -import { BancorURL } from 'router/bancorURL.service'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; +import { useNavigation } from 'hooks/useNavigation'; export const PoolsTableCellActions = (id: string) => { + const { pushModal } = useModal(); + const { goToPage } = useNavigation(); const enableDeposit = false; - const button = (onClick: () => void) => ( + return ( ( + ); }; diff --git a/src/elements/earn/pools/poolsTable/v3/DepositFAQ.tsx b/src/elements/earn/portfolio/v3/DepositFAQ.tsx similarity index 97% rename from src/elements/earn/pools/poolsTable/v3/DepositFAQ.tsx rename to src/elements/earn/portfolio/v3/DepositFAQ.tsx index ef80bce73..0a0f5152b 100644 --- a/src/elements/earn/pools/poolsTable/v3/DepositFAQ.tsx +++ b/src/elements/earn/portfolio/v3/DepositFAQ.tsx @@ -1,7 +1,7 @@ import { ExpandableSection } from 'components/expandableSection/ExpandableSection'; import { ReactComponent as IconChevron } from 'assets/icons/chevronRight.svg'; import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; -import { Navigate } from '../../../../../components/navigate/Navigate'; +import { Navigate } from 'components/navigate/Navigate'; const faq = [ { diff --git a/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx b/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx index 00ada31e9..9c7c4e6f7 100644 --- a/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx +++ b/src/elements/earn/portfolio/v3/V3AvailableToStake.tsx @@ -8,37 +8,33 @@ import { getAvailableToStakeTokens } from 'store/bancor/token'; import { Token } from 'services/observables/tokens'; import { PoolV3 } from 'services/observables/pools'; import 'swiper/css'; -import { DepositV3Modal } from 'elements/earn/pools/poolsTable/v3/DepositV3Modal'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; const AvailableItem = ({ token, pool }: { token: Token; pool: PoolV3 }) => { + const { pushModal } = useModal(); + return ( - ( - - )} - /> + ); }; diff --git a/src/elements/earn/portfolio/v3/bonuses/V3BonusesModal.stories.tsx b/src/elements/earn/portfolio/v3/bonuses/V3BonusesModal.stories.tsx index 73f255c9e..a527fcbb6 100644 --- a/src/elements/earn/portfolio/v3/bonuses/V3BonusesModal.stories.tsx +++ b/src/elements/earn/portfolio/v3/bonuses/V3BonusesModal.stories.tsx @@ -2,7 +2,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { withDesign } from 'storybook-addon-designs'; import { store } from 'store'; import { Provider } from 'react-redux'; -import { V3BonusesModal } from 'elements/earn/portfolio/v3/bonuses/V3BonusesModal'; +import { V3BonusesModal } from 'modals/V3BonusesModal'; export default { title: 'Elements/V3/Portfolio/Bonuses', diff --git a/src/elements/earn/portfolio/v3/bonuses/V3ClaimBonuses.tsx b/src/elements/earn/portfolio/v3/bonuses/V3ClaimBonuses.tsx index 3849ecb0a..2c3b0d5b5 100644 --- a/src/elements/earn/portfolio/v3/bonuses/V3ClaimBonuses.tsx +++ b/src/elements/earn/portfolio/v3/bonuses/V3ClaimBonuses.tsx @@ -1,5 +1,4 @@ import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; -import { V3BonusesModal } from 'elements/earn/portfolio/v3/bonuses/V3BonusesModal'; import { useV3Bonuses } from 'elements/earn/portfolio/v3/bonuses/useV3Bonuses'; import { prettifyNumber } from 'utils/helperFunctions'; import { @@ -13,9 +12,12 @@ import { getCurrency, } from 'services/api/googleTagManager'; import { pool } from 'store/bancor/pool'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; export const V3ClaimBonuses = () => { - const { setBonusModalOpen, bonusUsdTotal, isLoading } = useV3Bonuses(); + const { bonusUsdTotal, isLoading } = useV3Bonuses(); + const { pushModal } = useModal(); return ( <> @@ -42,7 +44,7 @@ export const V3ClaimBonuses = () => { withdraw_display_currency: getCurrency(), }); sendWithdrawBonusEvent(WithdrawBonusEvent.CTAClick); - setBonusModalOpen(true); + pushModal({ modalName: ModalNames.V3Bonuses }); }} disabled={bonusUsdTotal === 0} > @@ -55,7 +57,6 @@ export const V3ClaimBonuses = () => { )}
- ); }; diff --git a/src/elements/earn/portfolio/v3/bonuses/useV3Bonuses.ts b/src/elements/earn/portfolio/v3/bonuses/useV3Bonuses.ts index 928fc04c7..c682284cf 100644 --- a/src/elements/earn/portfolio/v3/bonuses/useV3Bonuses.ts +++ b/src/elements/earn/portfolio/v3/bonuses/useV3Bonuses.ts @@ -3,7 +3,6 @@ import { useDispatch } from 'react-redux'; import { getIsLoadingStandardRewards, getStandardRewards, - openBonusesModal, } from 'store/portfolio/v3Portfolio'; import { useCallback, useMemo } from 'react'; import BigNumber from 'bignumber.js'; @@ -20,22 +19,18 @@ import { sendWithdrawBonusEvent, WithdrawBonusEvent, } from 'services/api/googleTagManager/withdraw'; +import { useModal } from 'hooks/useModal'; export const useV3Bonuses = () => { const account = useAppSelector((state) => state.user.account); const dispatch = useDispatch(); - const isBonusModalOpen = useAppSelector( - (state) => state.v3Portfolio.bonusesModal - ); + const { popModal } = useModal(); const bonuses = useAppSelector(getStandardRewards); const isLoading = useAppSelector(getIsLoadingStandardRewards); - const setBonusModalOpen = useCallback( - (state: boolean) => { - dispatch(openBonusesModal(state)); - }, - [dispatch] - ); + const closeBonusesModal = useCallback(() => { + popModal(); + }, [popModal]); const bonusUsdTotal = useMemo( () => @@ -61,7 +56,7 @@ export const useV3Bonuses = () => { const tx = await ContractsApi.StandardRewards.write.claimRewards(ids); sendWithdrawBonusEvent(WithdrawBonusEvent.WalletConfirm); confirmClaimNotification(dispatch, tx.hash); - setBonusModalOpen(false); + closeBonusesModal(); await tx.wait(); sendWithdrawBonusEvent( WithdrawBonusEvent.Success, @@ -78,10 +73,10 @@ export const useV3Bonuses = () => { } else { genericFailedNotification(dispatch, 'Claim rewards failed'); } - setBonusModalOpen(false); + closeBonusesModal(); } }, - [account, dispatch, setBonusModalOpen] + [account, dispatch, closeBonusesModal] ); const handleClaimAndEarn = useCallback( @@ -93,7 +88,7 @@ export const useV3Bonuses = () => { try { const tx = await ContractsApi.StandardRewards.write.stakeRewards(ids); confirmClaimNotification(dispatch, tx.hash); - setBonusModalOpen(false); + closeBonusesModal(); await tx.wait(); await updatePortfolioData(dispatch); } catch (e: any) { @@ -103,16 +98,14 @@ export const useV3Bonuses = () => { } else { genericFailedNotification(dispatch, 'Restake rewards failed'); } - setBonusModalOpen(false); + closeBonusesModal(); } }, - [account, dispatch, setBonusModalOpen] + [account, dispatch, closeBonusesModal] ); return { bonuses, - isBonusModalOpen, - setBonusModalOpen, handleClaim, handleClaimAndEarn, bonusUsdTotal, diff --git a/src/elements/earn/portfolio/v3/earningsTable/V3EarningTable.tsx b/src/elements/earn/portfolio/v3/earningsTable/V3EarningTable.tsx index 89df15c0b..154421c88 100644 --- a/src/elements/earn/portfolio/v3/earningsTable/V3EarningTable.tsx +++ b/src/elements/earn/portfolio/v3/earningsTable/V3EarningTable.tsx @@ -1,7 +1,6 @@ import { DataTable, TableColumn } from 'components/table/DataTable'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { TokenBalance } from 'components/tokenBalance/TokenBalance'; -import V3WithdrawModal from 'elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal'; import { V3EarningTableMenu } from 'elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu'; import { useAppSelector } from 'store'; import { @@ -9,15 +8,14 @@ import { getPortfolioHoldings, } from 'store/portfolio/v3Portfolio'; import { Holding } from 'store/portfolio/v3Portfolio.types'; -// import { DepositV3Modal } from 'elements/earn/pools/poolsTable/v3/DepositV3Modal'; import { SortingRule } from 'react-table'; import { shrinkToken } from 'utils/formulas'; import { prettifyNumber } from 'utils/helperFunctions'; -import { DepositV3Modal } from 'elements/earn/pools/poolsTable/v3/DepositV3Modal'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; export const V3EarningTable = () => { - const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); - const [holdingToWithdraw, setHoldingToWithdraw] = useState(); + const { pushModal } = useModal(); const holdings = useAppSelector(getPortfolioHoldings); const isLoadingHoldings = useAppSelector(getIsLoadingHoldings); @@ -73,16 +71,14 @@ export const V3EarningTable = () => { accessor: 'poolTokenBalance', Cell: ({ cell }) => (
- ( - - )} + + pushModal({ + modalName: ModalNames.DepositV3, + data: { pool: cell.row.original.pool }, + }) + } />
), @@ -112,14 +108,6 @@ export const V3EarningTable = () => { defaultSort={defaultSort} /> - - {holdingToWithdraw && ( - - )} ); }; diff --git a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.stories.tsx b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.stories.tsx index a71aa79ec..8b3dad9d4 100644 --- a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.stories.tsx +++ b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.stories.tsx @@ -20,10 +20,6 @@ export const Menu: ComponentStory = ( ); }; -Menu.args = { - setIsWithdrawModalOpen: () => {}, -}; - Menu.parameters = { design: { type: 'figma', diff --git a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.tsx b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.tsx index 6130e0204..898c448e5 100644 --- a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.tsx +++ b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenu.tsx @@ -5,7 +5,6 @@ import { usePopper } from 'react-popper'; import { V3EarningsTableMenuContent } from 'elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenuContent'; import { Placement } from '@popperjs/core'; import { Holding } from 'store/portfolio/v3Portfolio.types'; -import { useApproveModal } from 'hooks/useApproveModal'; import { ContractsApi } from 'services/web3/v3/contractsApi'; import { expandToken } from 'utils/formulas'; import { updatePortfolioData } from 'services/web3/v3/portfolio/helpers'; @@ -16,25 +15,18 @@ import { rejectNotification, } from 'services/notifications/notifications'; import { ErrorCode } from 'services/web3/types'; +import { useApproval } from 'hooks/useApproval'; export type EarningTableMenuState = 'main' | 'bonus' | 'rate'; interface Props { - setIsWithdrawModalOpen: (isOpen: boolean) => void; holding: Holding; - setHoldingToWithdraw: (holding: Holding) => void; handleDepositClick: () => void; placement?: Placement; } export const V3EarningTableMenu = memo( - ({ - holding, - setHoldingToWithdraw, - setIsWithdrawModalOpen, - handleDepositClick, - placement = 'left-start', - }: Props) => { + ({ holding, handleDepositClick, placement = 'left-start' }: Props) => { const popperElRef = useRef(null); const [targetElement, setTargetElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -82,7 +74,7 @@ export const V3EarningTableMenu = memo( } }; - const [onStart, ApproveModal] = useApproveModal( + const startApprove = useApproval( [ { amount: holding.poolTokenBalance, @@ -100,12 +92,11 @@ export const V3EarningTableMenu = memo( const onStartJoin = useCallback(() => { setTxJoinBusy(true); - onStart(); - }, [onStart]); + startApprove(); + }, [startApprove]); return ( <> - {ApproveModal} {({ open }) => ( <> @@ -141,8 +132,6 @@ export const V3EarningTableMenu = memo(
void; - setHoldingToWithdraw: (holding: Holding) => void; handleDepositClick: () => void; holding: Holding; onStartJoin: Function; @@ -15,14 +13,7 @@ interface Props { } export const V3EarningsTableMenuContent = memo( - ({ - holding, - setIsWithdrawModalOpen, - setHoldingToWithdraw, - handleDepositClick, - onStartJoin, - txJoinBusy, - }: Props) => { + ({ holding, handleDepositClick, onStartJoin, txJoinBusy }: Props) => { const [currentMenu, setCurrentMenu] = useState('main'); @@ -31,9 +22,7 @@ export const V3EarningsTableMenuContent = memo( {currentMenu === 'main' && ( )} diff --git a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenuMain.tsx b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenuMain.tsx index efc64d908..db1c79691 100644 --- a/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenuMain.tsx +++ b/src/elements/earn/portfolio/v3/earningsTable/menu/V3EarningTableMenuMain.tsx @@ -6,40 +6,34 @@ import { EarningTableMenuState } from 'elements/earn/portfolio/v3/earningsTable/ import { useV3Bonuses } from 'elements/earn/portfolio/v3/bonuses/useV3Bonuses'; import { Holding } from 'store/portfolio/v3Portfolio.types'; import { shrinkToken } from 'utils/formulas'; +import { ModalNames } from 'modals'; +import { useModal } from 'hooks/useModal'; interface Props { setCurrentMenu: (menu: EarningTableMenuState) => void; - setIsWithdrawModalOpen: (isOpen: boolean) => void; - setHoldingToWithdraw: (holding: Holding) => void; handleDepositClick: () => void; holding: Holding; } export const V3EarningTableMenuMain = memo( - ({ - holding, - setHoldingToWithdraw, - setCurrentMenu, - setIsWithdrawModalOpen, - handleDepositClick, - }: Props) => { - const { setBonusModalOpen, bonusUsdTotal } = useV3Bonuses(); + ({ holding, setCurrentMenu, handleDepositClick }: Props) => { + const { bonusUsdTotal } = useV3Bonuses(); const { latestProgram } = holding; + const { pushModal } = useModal(); const handleWithdrawClick = useCallback(() => { - setHoldingToWithdraw(holding); - setIsWithdrawModalOpen(true); - }, [holding, setHoldingToWithdraw, setIsWithdrawModalOpen]); + pushModal({ modalName: ModalNames.V3Withdraw, data: { holding } }); + }, [holding, pushModal]); const handleBonusClick = useCallback(() => { - setBonusModalOpen(true); + pushModal({ modalName: ModalNames.V3Bonuses }); // TODO - add logic for what action to perform // if (true) { // // } else { // setCurrentMenu('bonus'); // } - }, [setBonusModalOpen]); + }, []); return (
diff --git a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx index 8de3208b2..97a5240b7 100644 --- a/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx +++ b/src/elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsItem.tsx @@ -1,16 +1,17 @@ -import { memo, useState } from 'react'; +import { memo } from 'react'; import { prettifyNumber } from 'utils/helperFunctions'; import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; import { ExternalHolding } from 'elements/earn/portfolio/v3/externalHoldings/externalHoldings.types'; -import { V3ExternalHoldingsModal } from 'elements/earn/portfolio/v3/externalHoldings/V3ExternalHoldingsModal'; import { TokensOverlap } from 'components/tokensOverlap/TokensOverlap'; +import { ModalNames } from 'modals'; +import { useModal } from 'hooks/useModal'; interface Props { position: ExternalHolding; } const V3ExternalHoldingsItem = ({ position }: Props) => { - const [isOpen, setIsOpen] = useState(false); + const { pushModal } = useModal(); return (
@@ -29,16 +30,15 @@ const V3ExternalHoldingsItem = ({ position }: Props) => { variant={ButtonVariant.Secondary} size={ButtonSize.Full} className="mt-20" - onClick={() => setIsOpen(true)} + onClick={() => + pushModal({ + modalName: ModalNames.V3ExternalHoldings, + data: { position }, + }) + } > Protect and earn - -
); }; diff --git a/src/elements/earn/portfolio/v3/holdings/V3HoldingPage.tsx b/src/elements/earn/portfolio/v3/holdings/V3HoldingPage.tsx index d1f21e3dd..ec57d4540 100644 --- a/src/elements/earn/portfolio/v3/holdings/V3HoldingPage.tsx +++ b/src/elements/earn/portfolio/v3/holdings/V3HoldingPage.tsx @@ -17,19 +17,18 @@ import { ContractsApi } from 'services/web3/v3/contractsApi'; import { fetchWithdrawalRequestOutputBreakdown } from 'services/web3/v3/portfolio/withdraw'; import useAsyncEffect from 'use-async-effect'; import { expandToken, shrinkToken } from 'utils/formulas'; -import { DepositV3Modal } from 'elements/earn/pools/poolsTable/v3/DepositV3Modal'; -import V3WithdrawModal from '../initWithdraw/V3WithdrawModal'; import BigNumber from 'bignumber.js'; import { getTokensByIdV2V3 } from 'store/bancor/bancor'; import { getV3byID } from 'store/bancor/pool'; import { WalletConnectRequest } from 'elements/walletConnect/WalletConnectRequest'; -import { V3ManageProgramsModal } from './V3ManageProgramsModal'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; import { TokenCurrency } from 'store/user/user'; export const V3HoldingPage = () => { const { id } = useParams(); const { goToPage } = useNavigation(); - const [isOpen, setIsOpen] = useState(false); + const { pushModal } = useModal(); const account = useAppSelector((state) => state.user.account); const holdings = useAppSelector(getPortfolioHoldings); @@ -222,18 +221,18 @@ export const V3HoldingPage = () => { {prettifyNumber(token.balance ?? 0)}
- ( - - )} - /> + ) : ( @@ -255,18 +254,18 @@ export const V3HoldingPage = () => { {prettifyNumber(totalPTAllPrograms)} - ( - - )} - /> +
@@ -279,15 +278,15 @@ export const V3HoldingPage = () => { <> - + + ); +}; diff --git a/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemUnstaked.tsx b/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemUnstaked.tsx new file mode 100644 index 000000000..c17e164d3 --- /dev/null +++ b/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemUnstaked.tsx @@ -0,0 +1,136 @@ +import { Holding } from 'store/portfolio/v3Portfolio.types'; +import { prettifyNumber, toBigNumber } from 'utils/helperFunctions'; +import { useAppSelector } from 'store/index'; +import { useDispatch } from 'react-redux'; +import { useCallback, useState } from 'react'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { expandToken } from 'utils/formulas'; +import { + confirmJoinNotification, + genericFailedNotification, + rejectNotification, +} from 'services/notifications/notifications'; +import { updatePortfolioData } from 'services/web3/v3/portfolio/helpers'; +import { ErrorCode } from 'services/web3/types'; +import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; +import { PopoverV3 } from 'components/popover/PopoverV3'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; +import { useApproval } from 'hooks/useApproval'; + +export const V3HoldingsItemUnstaked = ({ holding }: { holding: Holding }) => { + const { pool } = holding; + const isDisabled = toBigNumber(holding.tokenBalance).isZero(); + const { pushModal } = useModal(); + + const account = useAppSelector((state) => state.user.account); + const dispatch = useDispatch(); + const [txJoinBusy, setTxJoinBusy] = useState(false); + + const handleJoinClick = async () => { + if (!pool.latestProgram?.isActive || !account) { + console.error('rewardProgram is not defined or active'); + return; + } + + try { + const tx = await ContractsApi.StandardRewards.write.join( + pool.latestProgram.id, + expandToken(holding.poolTokenBalance, pool.decimals) + ); + confirmJoinNotification( + dispatch, + tx.hash, + holding.tokenBalance, + holding.pool.reserveToken.symbol + ); + await tx.wait(); + await updatePortfolioData(dispatch); + setTxJoinBusy(false); + } catch (e: any) { + console.error('handleJoinClick', e); + setTxJoinBusy(false); + if (e.code === ErrorCode.DeniedTx) { + rejectNotification(dispatch); + } else { + genericFailedNotification(dispatch, 'Joining rewards failed'); + } + } + }; + + const startApprove = useApproval( + [ + { + amount: holding.poolTokenBalance, + token: { + ...holding.pool.reserveToken, + address: holding.pool.poolTokenDltId, + symbol: `bn${holding.pool.reserveToken.symbol}`, + }, + }, + ], + handleJoinClick, + ContractsApi.StandardRewards.contractAddress + ); + + const onStartJoin = useCallback(() => { + setTxJoinBusy(true); + startApprove(); + }, [startApprove]); + + return ( + <> +
+
Available Balance
+
+ ( +
+ {prettifyNumber(holding.tokenBalance)}{' '} + {pool.reserveToken.symbol} +
+ )} + > + {holding.tokenBalance} {pool.reserveToken.symbol} +
+ +
+ ({prettifyNumber(holding.poolTokenBalance)} bn + {pool.reserveToken.symbol}) +
+
+ +
+ + + {holding.pool.latestProgram?.isActive && ( + + )} +
+
+ + ); +}; diff --git a/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemWithdraw.tsx b/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemWithdraw.tsx new file mode 100644 index 000000000..df1d2944d --- /dev/null +++ b/src/elements/earn/portfolio/v3/holdings/V3HoldingsItemWithdraw.tsx @@ -0,0 +1,38 @@ +import { Holding } from 'store/portfolio/v3Portfolio.types'; +import { prettifyNumber, toBigNumber } from 'utils/helperFunctions'; +import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; + +export const V3HoldingsItemWithdraw = ({ holding }: { holding: Holding }) => { + const { pool } = holding; + const { pushModal } = useModal(); + + const isDisabled = toBigNumber(holding.tokenBalance).isZero(); + + return ( + <> +
+
Withdrawal
+
+ {prettifyNumber(holding.tokenBalance)} {pool.reserveToken.symbol} +
+
+ +
+
+ + ); +}; diff --git a/src/elements/earn/portfolio/v3/holdings/V3ManageProgramsModal.tsx b/src/elements/earn/portfolio/v3/holdings/V3ManageProgramsModal.tsx deleted file mode 100644 index 647281ae8..000000000 --- a/src/elements/earn/portfolio/v3/holdings/V3ManageProgramsModal.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { useAppSelector } from 'store'; -import { ModalV3 } from 'components/modal/ModalV3'; -import { Holding } from 'store/portfolio/v3Portfolio.types'; -import { prettifyNumber, toBigNumber } from 'utils/helperFunctions'; -import { expandToken, shrinkToken } from 'utils/formulas'; -import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; -import { ContractsApi } from 'services/web3/v3/contractsApi'; -import { - confirmJoinNotification, - confirmLeaveNotification, - genericFailedNotification, - rejectNotification, -} from 'services/notifications/notifications'; -import { updatePortfolioData } from 'services/web3/v3/portfolio/helpers'; -import { ErrorCode } from 'services/web3/types'; -import { ReactComponent as IconClock } from 'assets/icons/time.svg'; -import { useTknFiatInput } from 'elements/trade/useTknFiatInput'; -import BigNumber from 'bignumber.js'; -import dayjs from 'dayjs'; -import { TradeWidgetInput } from 'elements/trade/TradeWidgetInput'; -import { useApproveModal } from 'hooks/useApproveModal'; - -interface Props { - holding: Holding; - renderButton: (onClick: () => void) => React.ReactNode; -} - -export const V3ManageProgramsModal = ({ holding, renderButton }: Props) => { - const account = useAppSelector((state) => state.user.account); - const [isOpen, setIsOpen] = useState(false); - const [txBusy, setTxBusy] = useState(false); - const [joinRewards, setJoinRewards] = useState(false); - const [amount, setAmount] = useState(''); - const [inputFiat, setInputFiat] = useState(''); - const [txJoinBusy, setTxJoinBusy] = useState(false); - const tokenInputField = useTknFiatInput({ - token: { - ...holding.pool.reserveToken, - symbol: `bn${holding.pool.name}`, - balance: holding.poolTokenBalance, - balanceUsd: toBigNumber(holding.tokenBalance) - .times(holding.pool.reserveToken.usdPrice) - .toNumber(), - usdPrice: toBigNumber(holding.tokenBalance) - .times(holding.pool.reserveToken.usdPrice) - .div(holding.poolTokenBalance) - .toString(), - }, - setInputTkn: setAmount, - setInputFiat: setInputFiat, - inputTkn: amount, - inputFiat: inputFiat, - }); - const inputErrorMsg = useMemo( - () => - !!account && new BigNumber(holding.poolTokenBalance || 0).lt(amount) - ? 'Insufficient balance' - : '', - [account, amount, holding.poolTokenBalance] - ); - - const onClose = async () => { - setIsOpen(false); - setJoinRewards(false); - setAmount(''); - setInputFiat(''); - }; - - const dispatch = useDispatch(); - - const programs = holding.programs.filter((p) => - toBigNumber(p.tokenAmountWei).gt(0) - ); - - const programsMerged = programs.map((p) => ({ - ...p, - ...holding.pool.programs.find((pp) => pp.id === p.id), - })); - - const programsSorted = programsMerged.sort((a, _) => (a.isActive ? 0 : -1)); - - const handleJoinClick = async () => { - if (!holding.pool.latestProgram?.isActive || !account) { - console.error('rewardProgram is not defined or inactive'); - return; - } - - if (amount === '') { - return; - } - - try { - const tx = await ContractsApi.StandardRewards.write.join( - holding.pool.latestProgram.id, - expandToken(amount, holding.pool.decimals) - ); - confirmJoinNotification( - dispatch, - tx.hash, - amount, - `bn${holding.pool.reserveToken.symbol}` - ); - setTxJoinBusy(false); - setJoinRewards(false); - setAmount(''); - setInputFiat(''); - await tx.wait(); - await updatePortfolioData(dispatch); - } catch (e: any) { - console.error('handleJoinClick', e); - setTxJoinBusy(false); - if (e.code === ErrorCode.DeniedTx) { - rejectNotification(dispatch); - } - } - }; - - const [onStart, ApproveModal] = useApproveModal( - [ - { - amount: expandToken(amount, holding.pool.decimals), - token: { - ...holding.pool.reserveToken, - address: holding.pool.poolTokenDltId, - symbol: `bn${holding.pool.reserveToken.symbol}`, - }, - }, - ], - handleJoinClick, - ContractsApi.StandardRewards.contractAddress - ); - - const onStartJoin = useCallback(() => { - setTxJoinBusy(true); - onStart(); - }, [onStart]); - - const handleLeaveClick = async ( - id: string, - poolTokenAmountWei: string, - tokenAmountWei: string - ) => { - if (!account) { - console.error('handleLeaveClick because arguments are not defined'); - return; - } - try { - setTxBusy(true); - const tx = await ContractsApi.StandardRewards.write.leave( - id, - poolTokenAmountWei - ); - confirmLeaveNotification( - dispatch, - tx.hash, - shrinkToken(tokenAmountWei, holding.pool.decimals), - holding.pool.reserveToken.symbol - ); - onClose(); - setTxBusy(false); - await tx.wait(); - await updatePortfolioData(dispatch); - } catch (e: any) { - console.error('handleLeaveClick', e); - onClose(); - setTxBusy(false); - if (e.code === ErrorCode.DeniedTx) { - rejectNotification(dispatch); - } else { - genericFailedNotification(dispatch, 'Leave rewards failed'); - } - } - }; - - return ( - <> - {renderButton(() => setIsOpen(true))} - setJoinRewards(false)} - separator - large - > - {joinRewards ? ( -
-
Join BNT rewards
- -
-
BNT Rewards
-
- {holding.pool.apr7d.total.toFixed(2)}% -
- {dayjs( - (holding.pool.latestProgram?.endTime ?? 0) * 1000 - ).format('MMM D, YYYY')} -
-
-
- - {ApproveModal} -
- ) : ( -
-
- Earn BNT rewards on your bn{holding.pool.reserveToken.symbol}, - there are no cooldowns or withdrawal fees for adding or removing - bnETH to reward programs -
- -
- Available bn{holding.pool.reserveToken.symbol} -
-
-
- {prettifyNumber( - new BigNumber(holding.tokenBalance).times( - holding.pool.reserveToken.usdPrice - ), - true - )} -
-
- {prettifyNumber(holding.poolTokenBalance)} bn - {holding.pool.reserveToken.symbol} -
-
- {holding.pool.latestProgram?.isActive && ( - - )} -
-
- {programsSorted.length > 0 && ( -
- )} - {programsSorted.length > 0 && ( - <> -
- bn{holding.pool.reserveToken.symbol} in BNT Rewards -
- -
- {programsSorted.map((program) => ( -
-
-
-
- {prettifyNumber( - new BigNumber( - shrinkToken( - program.tokenAmountWei, - holding.pool.decimals - ) - ).times(holding.pool.reserveToken.usdPrice), - true - )} -
-
- {prettifyNumber( - shrinkToken( - program.poolTokenAmountWei, - holding.pool.decimals - ) - )}{' '} - bn{holding.pool.reserveToken.symbol} -
-
- {program.isActive ? ( -
-
- {holding.pool.apr7d.standardRewards.toFixed(2)}% - APR -
-
- - {dayjs((program.startTime ?? 0) * 1000).format( - 'MMM D, YYYY' - )}{' '} - -{' '} - {dayjs((program.endTime ?? 0) * 1000).format( - 'MMM D, YYYY' - )} -
-
- ) : ( -
Inactive
- )} - -
-
- ))} -
- - )} -
- )} -
- - ); -}; diff --git a/src/elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal.stories.tsx b/src/elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal.stories.tsx index 4d9935ecf..eeed4e095 100644 --- a/src/elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal.stories.tsx +++ b/src/elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal.stories.tsx @@ -1,7 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import { withDesign } from 'storybook-addon-designs'; -import { useState } from 'react'; -import V3WithdrawModal from 'elements/earn/portfolio/v3/initWithdraw/V3WithdrawModal'; +import V3WithdrawModal from 'modals/V3WithdrawModal'; import { store } from 'store'; import { Provider } from 'react-redux'; @@ -12,11 +11,9 @@ export default { } as ComponentMeta; export const WithdrawModal: ComponentStory = (args) => { - const [isOpen, setIsOpen] = useState(true); - return ( - + ); }; diff --git a/src/elements/earn/portfolio/v3/initWithdraw/step1/V3WithdrawStep1.tsx b/src/elements/earn/portfolio/v3/initWithdraw/step1/V3WithdrawStep1.tsx index 3e2b477b5..1096ddc72 100644 --- a/src/elements/earn/portfolio/v3/initWithdraw/step1/V3WithdrawStep1.tsx +++ b/src/elements/earn/portfolio/v3/initWithdraw/step1/V3WithdrawStep1.tsx @@ -111,7 +111,7 @@ const V3WithdrawStep1 = ({ const [isConfirmed, setIsConfirmed] = useState(false); - const { handleButtonClick, ModalApprove, txBusy } = useV3WithdrawStep3({ + const { handleButtonClick, txBusy } = useV3WithdrawStep3({ holding, amount, setStep, @@ -158,7 +158,6 @@ const V3WithdrawStep1 = ({ return (
- {ModalApprove}

How much {token.symbol} do you want to withdraw?

diff --git a/src/elements/earn/portfolio/v3/initWithdraw/step3/V3WithdrawStep3.tsx b/src/elements/earn/portfolio/v3/initWithdraw/step3/V3WithdrawStep3.tsx index 8e375a1c9..2e0a3dee3 100644 --- a/src/elements/earn/portfolio/v3/initWithdraw/step3/V3WithdrawStep3.tsx +++ b/src/elements/earn/portfolio/v3/initWithdraw/step3/V3WithdrawStep3.tsx @@ -22,14 +22,12 @@ const V3WithdrawStep3 = ({ isFiat, setRequestId, }: Props) => { - const { token, handleButtonClick, ModalApprove, txBusy } = useV3WithdrawStep3( - { - holding, - amount, - setStep, - setRequestId, - } - ); + const { token, handleButtonClick, txBusy } = useV3WithdrawStep3({ + holding, + amount, + setStep, + setRequestId, + }); return ( <> @@ -58,7 +56,6 @@ const V3WithdrawStep3 = ({
- {ModalApprove} ); }; diff --git a/src/elements/earn/portfolio/v3/initWithdraw/step3/useV3WithdrawStep3.ts b/src/elements/earn/portfolio/v3/initWithdraw/step3/useV3WithdrawStep3.ts index d8cb93e70..ff61f5a93 100644 --- a/src/elements/earn/portfolio/v3/initWithdraw/step3/useV3WithdrawStep3.ts +++ b/src/elements/earn/portfolio/v3/initWithdraw/step3/useV3WithdrawStep3.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useApproveModal } from 'hooks/useApproveModal'; +import { useApproval } from 'hooks/useApproval'; import { ContractsApi } from 'services/web3/v3/contractsApi'; import { Holding } from 'store/portfolio/v3Portfolio.types'; import BigNumber from 'bignumber.js'; @@ -157,7 +157,7 @@ export const useV3WithdrawStep3 = ({ if (initiatedWithdraw.current) setStep(4); }, [withdrawalRequests, setStep]); - const [onStart, ModalApprove] = useApproveModal( + const startApprove = useApproval( approveTokens, (approvalHash?: string) => initWithdraw(approvalHash), ContractsApi.BancorNetwork.contractAddress, @@ -181,13 +181,12 @@ export const useV3WithdrawStep3 = ({ return; } hasStarted.current = true; - onStart(); - }, [onStart, poolTokenAmountWei]); + startApprove(); + }, [startApprove, poolTokenAmountWei]); return { token: reserveToken, handleButtonClick, - ModalApprove, txBusy, }; }; diff --git a/src/elements/earn/portfolio/v3/initWithdraw/step4/V3WithdrawStep4.tsx b/src/elements/earn/portfolio/v3/initWithdraw/step4/V3WithdrawStep4.tsx index 2e2a487dd..efa1050ef 100644 --- a/src/elements/earn/portfolio/v3/initWithdraw/step4/V3WithdrawStep4.tsx +++ b/src/elements/earn/portfolio/v3/initWithdraw/step4/V3WithdrawStep4.tsx @@ -7,7 +7,7 @@ import { } from 'datebook'; import { openNewTab } from 'utils/pureFunctions'; import dayjs from 'dayjs'; -import { V3WithdrawConfirmContent } from '../../pendingWithdraw/confirm/V3WithdrawConfirmModal'; +import { V3WithdrawConfirmContent } from '../../../../../../modals/V3WithdrawConfirmModal'; import { useAppSelector } from 'store'; import { getPortfolioWithdrawalRequests } from 'store/portfolio/v3Portfolio'; diff --git a/src/elements/earn/portfolio/v3/pendingWithdraw/V3Withdraw.tsx b/src/elements/earn/portfolio/v3/pendingWithdraw/V3Withdraw.tsx index 986228e8e..bbe2d5b18 100644 --- a/src/elements/earn/portfolio/v3/pendingWithdraw/V3Withdraw.tsx +++ b/src/elements/earn/portfolio/v3/pendingWithdraw/V3Withdraw.tsx @@ -1,19 +1,11 @@ import { useV3Withdraw } from 'elements/earn/portfolio/v3/pendingWithdraw/useV3Withdraw'; import { WithdrawItem } from 'elements/earn/portfolio/v3/pendingWithdraw/V3WithdrawItem'; -import { V3WithdrawCancelModal } from 'elements/earn/portfolio/v3/pendingWithdraw/V3WithdrawCancelModal'; -import { V3WithdrawConfirmModal } from 'elements/earn/portfolio/v3/pendingWithdraw/confirm/V3WithdrawConfirmModal'; export const V3Withdraw = () => { const { withdrawalRequests, - cancelWithdrawal, openCancelModal, isLoadingWithdrawalRequests, - isModalCancelOpen, - setIsModalCancelOpen, - isModalConfirmOpen, - setIsModalConfirmOpen, - selected, openConfirmModal, } = useV3Withdraw(); @@ -40,22 +32,6 @@ export const V3Withdraw = () => { )} - {selected && ( - <> - - - - )} ); }; diff --git a/src/elements/earn/portfolio/v3/pendingWithdraw/V3WithdrawCancelModal.tsx b/src/elements/earn/portfolio/v3/pendingWithdraw/V3WithdrawCancelModal.tsx deleted file mode 100644 index 45754cefd..000000000 --- a/src/elements/earn/portfolio/v3/pendingWithdraw/V3WithdrawCancelModal.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { WithdrawalRequest } from 'store/portfolio/v3Portfolio.types'; -import { memo, useCallback, useMemo, useState } from 'react'; -import { Modal } from 'components/modal/Modal'; -import { Button, ButtonSize } from 'components/button/Button'; -import { TokenBalanceLarge } from 'components/tokenBalance/TokenBalanceLarge'; -import { toBigNumber } from 'utils/helperFunctions'; -import { - sendWithdrawEvent, - setCurrentWithdraw, - WithdrawEvent, -} from 'services/api/googleTagManager/withdraw'; -import BigNumber from 'bignumber.js'; -import { - getBlockchain, - getBlockchainNetwork, - getCurrency, -} from 'services/api/googleTagManager'; - -interface Props { - isModalOpen: boolean; - setIsModalOpen: (isOpen: boolean) => void; - withdrawRequest: WithdrawalRequest; - cancelWithdrawal: () => Promise; -} - -export const V3WithdrawCancelModal = memo( - ({ - isModalOpen, - setIsModalOpen, - withdrawRequest, - cancelWithdrawal, - }: Props) => { - const [txBusy, setTxBusy] = useState(false); - const { pool } = withdrawRequest; - const token = pool.reserveToken; - - const handleCTAClick = useCallback(async () => { - setCurrentWithdraw({ - withdraw_pool: pool.name, - withdraw_blockchain: getBlockchain(), - withdraw_blockchain_network: getBlockchainNetwork(), - withdraw_token: pool.name, - withdraw_token_amount: withdrawRequest.reserveTokenAmount, - withdraw_token_amount_usd: new BigNumber( - withdrawRequest.reserveTokenAmount - ) - .times(withdrawRequest.pool.reserveToken.usdPrice) - .toString(), - withdraw_display_currency: getCurrency(), - }); - sendWithdrawEvent(WithdrawEvent.WithdrawCancelApproveClick); - setTxBusy(true); - await cancelWithdrawal(); - setIsModalOpen(false); - setTxBusy(false); - }, [ - cancelWithdrawal, - setIsModalOpen, - pool.name, - withdrawRequest.pool.reserveToken.usdPrice, - withdrawRequest.reserveTokenAmount, - ]); - - const compoundingApr = useMemo( - () => - toBigNumber(pool.apr7d.autoCompounding) - .plus(pool.apr7d.tradingFees) - .toFixed(2), - [pool.apr7d.autoCompounding, pool.apr7d.tradingFees] - ); - - return ( - -
- - -
-
- Compounding rewards{' '} - {token.symbol} -
-
{compoundingApr} %
-
- - -
-
- ); - } -); diff --git a/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/useV3WithdrawConfirm.ts b/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/useV3WithdrawConfirm.ts index ed765bf0d..a9c3488b8 100644 --- a/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/useV3WithdrawConfirm.ts +++ b/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/useV3WithdrawConfirm.ts @@ -7,7 +7,7 @@ import BigNumber from 'bignumber.js'; import useAsyncEffect from 'use-async-effect'; import { fetchWithdrawalRequestOutputBreakdown } from 'services/web3/v3/portfolio/withdraw'; import { wait } from 'utils/pureFunctions'; -import { useApproveModal } from 'hooks/useApproveModal'; +import { useApproval } from 'hooks/useApproval'; import { ContractsApi } from 'services/web3/v3/contractsApi'; import { WithdrawalRequest } from 'store/portfolio/v3Portfolio.types'; import { @@ -176,7 +176,7 @@ export const useV3WithdrawConfirm = ({ return tokensToApprove; }, [govToken?.address, govToken?.symbol, poolTokenAmount, token]); - const [onStart, ModalApprove] = useApproveModal( + const startApprove = useApproval( approveTokens, (approvalHash?: string) => withdraw(approvalHash), ContractsApi.BancorNetwork.contractAddress, @@ -204,9 +204,9 @@ export const useV3WithdrawConfirm = ({ }); sendWithdrawACEvent(WithdrawACEvent.ApproveClick); setTxBusy(true); - onStart(); + startApprove(); }, [ - onStart, + startApprove, pool.name, withdrawRequest.pool.reserveToken.usdPrice, withdrawRequest.reserveTokenAmount, @@ -220,7 +220,6 @@ export const useV3WithdrawConfirm = ({ return { onModalClose, - ModalApprove, token, withdrawAmounts, missingGovTokenBalance, diff --git a/src/elements/earn/portfolio/v3/pendingWithdraw/useV3Withdraw.ts b/src/elements/earn/portfolio/v3/pendingWithdraw/useV3Withdraw.ts index fc6c8465d..35bdedba3 100644 --- a/src/elements/earn/portfolio/v3/pendingWithdraw/useV3Withdraw.ts +++ b/src/elements/earn/portfolio/v3/pendingWithdraw/useV3Withdraw.ts @@ -20,6 +20,8 @@ import { WithdrawACEvent, WithdrawEvent, } from 'services/api/googleTagManager/withdraw'; +import { ModalNames } from 'modals'; +import { useModal } from 'hooks/useModal'; export const useV3Withdraw = () => { const withdrawalRequests = useAppSelector(getPortfolioWithdrawalRequests); @@ -27,17 +29,10 @@ export const useV3Withdraw = () => { getIsLoadingWithdrawalRequests ); const dispatch = useDispatch(); + const { pushModal, popModal } = useModal(); const account = useAppSelector((state) => state.user.account); const [selected, setSelected] = useState(null); - const [isModalCancelOpen, setIsModalCancelOpen] = useState(false); - const [isModalConfirmOpen, setIsModalConfirmOpen] = useState(false); - - const openCancelModal = useCallback(async (req: WithdrawalRequest) => { - sendWithdrawEvent(WithdrawEvent.WithdrawCancelClick); - setSelected(req); - setIsModalCancelOpen(true); - }, []); const cancelWithdrawal = useCallback(async () => { if (!selected || !account) { @@ -56,7 +51,7 @@ export const useV3Withdraw = () => { selected.reserveTokenAmount, selected.pool.reserveToken.symbol ); - setIsModalCancelOpen(false); + popModal(); await tx.wait(); sendWithdrawEvent( WithdrawEvent.WithdrawCancelSuccess, @@ -72,7 +67,7 @@ export const useV3Withdraw = () => { undefined, e.message ); - setIsModalCancelOpen(false); + popModal(); console.error('cancelWithdrawal failed: ', e); if (e.code === ErrorCode.DeniedTx) { rejectNotification(dispatch); @@ -80,24 +75,40 @@ export const useV3Withdraw = () => { genericFailedNotification(dispatch, 'Cancel withdrawal failed'); } } - }, [account, dispatch, selected]); + }, [account, dispatch, selected, popModal]); - const openConfirmModal = useCallback(async (req: WithdrawalRequest) => { - sendWithdrawACEvent(WithdrawACEvent.CTAClick); - setSelected(req); - setIsModalConfirmOpen(true); - }, []); + const openCancelModal = useCallback( + async (req: WithdrawalRequest) => { + sendWithdrawEvent(WithdrawEvent.WithdrawCancelClick); + setSelected(req); + dispatch( + pushModal({ + modalName: ModalNames.V3WithdrawCancel, + data: { cancelWithdrawal, withdrawRequest: selected }, + }) + ); + }, + [cancelWithdrawal, dispatch, selected, pushModal] + ); + + const openConfirmModal = useCallback( + async (req: WithdrawalRequest) => { + sendWithdrawACEvent(WithdrawACEvent.CTAClick); + setSelected(req); + pushModal({ + modalName: ModalNames.V3WithdrawConfirm, + data: { openCancelModal, withdrawRequest: selected }, + }); + }, + [openCancelModal, selected, pushModal] + ); return { withdrawalRequests, cancelWithdrawal, openCancelModal, isLoadingWithdrawalRequests, - isModalCancelOpen, - setIsModalCancelOpen, selected, - isModalConfirmOpen, - setIsModalConfirmOpen, openConfirmModal, }; }; diff --git a/src/elements/layoutHeader/LayoutHeader.tsx b/src/elements/layoutHeader/LayoutHeader.tsx index 775270f39..9a4b168b2 100644 --- a/src/elements/layoutHeader/LayoutHeader.tsx +++ b/src/elements/layoutHeader/LayoutHeader.tsx @@ -2,17 +2,17 @@ import { NotificationsMenu } from 'elements/notifications/NotificationsMenu'; import { SettingsMenu } from 'elements/settings/SettingsMenu'; import { ReactComponent as IconBancor } from 'assets/icons/bancor.svg'; import { useWalletConnect } from '../walletConnect/useWalletConnect'; -import { WalletConnectModal } from '../walletConnect/WalletConnectModal'; +import { WalletConnectModal } from '../../modals/WalletConnectModal'; import { WalletConnectButton } from '../walletConnect/WalletConnectButton'; import { useEffect, useState } from 'react'; import { NetworkIndicator } from './NetworkIndicator'; import { isForkAvailable } from 'services/web3/config'; -import 'elements/layoutHeader/LayoutHeader.css'; import { useAppSelector } from 'store/index'; import { getIsAppBusy } from 'store/bancor/bancor'; import { BancorURL } from 'router/bancorURL.service'; import { Navigate } from 'components/navigate/Navigate'; import { PopoverV3 } from 'components/popover/PopoverV3'; +import 'elements/layoutHeader/LayoutHeader.css'; import { CurrencySelection } from './CurrencySelection'; export const LayoutHeader = () => { diff --git a/src/elements/modalApprove/modalApprove.tsx b/src/elements/modalApprove/modalApprove.tsx deleted file mode 100644 index c8a8b1786..000000000 --- a/src/elements/modalApprove/modalApprove.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Modal } from 'components/modal/Modal'; -import { ReactComponent as IconLock } from 'assets/icons/lock.svg'; -import { - addNotification, - NotificationType, -} from 'store/notification/notification'; -import { - ApprovalContract, - setNetworkContractApproval, -} from 'services/web3/approval'; -import { useDispatch } from 'react-redux'; -import { TokenMinimal } from 'services/observables/tokens'; -import { web3 } from 'services/web3'; -import { wait } from 'utils/pureFunctions'; -import { ErrorCode } from 'services/web3/types'; -import { Button, ButtonSize } from 'components/button/Button'; -import { sendConversionEvent } from 'services/api/googleTagManager/conversion'; -import { Events } from 'services/api/googleTagManager'; - -interface ModalApproveProps { - setIsOpen: Function; - isOpen: boolean; - amount: string; - fromToken?: TokenMinimal; - handleApproved: Function; - waitForApproval?: boolean; - contract: ApprovalContract; -} - -export const ModalApprove = ({ - setIsOpen, - isOpen, - amount, - fromToken, - handleApproved, - waitForApproval, - contract, -}: ModalApproveProps) => { - const dispatch = useDispatch(); - - if (!fromToken) return null; - - // Wait for user to choose approval and proceed with approval based on user selection - // Prop amount is UNDEFINED when UNLIMITED - const approve = async (amount?: string) => { - try { - setIsOpen(false); - const isUnlimited = amount === undefined; - sendConversionEvent(Events.approved, undefined, isUnlimited); - const txHash = await setNetworkContractApproval( - fromToken, - contract, - amount - ); - dispatch( - addNotification({ - type: NotificationType.pending, - title: 'Pending Confirmation', - msg: `Approve ${fromToken.symbol} is pending confirmation`, - updatedInfo: { - successTitle: 'Transaction Confirmed', - successMsg: `${amount || 'Unlimited'} approval set for ${ - fromToken.symbol - }`, - errorTitle: 'Transaction Failed', - errorMsg: `${fromToken.symbol} approval had failed. Please try again or contact support.`, - }, - txHash, - }) - ); - if (waitForApproval) { - let tx = null; - while (tx === null) - try { - await wait(2000); - tx = await web3.provider.getTransactionReceipt(txHash); - } catch (error) {} - } - handleApproved(); - } catch (e: any) { - setIsOpen(false); - if (e.code === ErrorCode.DeniedTx) - dispatch( - addNotification({ - type: NotificationType.error, - title: 'Transaction Rejected', - msg: 'You rejected the transaction. If this was by mistake, please try again.', - }) - ); - else - addNotification({ - type: NotificationType.error, - title: 'Transaction Failed', - msg: `${fromToken.symbol} approval had failed. Please try again or contact support.`, - }); - } - }; - - return ( - -
-
-
- -
-

Approve {fromToken.symbol}

-

- Before you can proceed, you need to approve {fromToken.symbol}{' '} - spending. -

- -

- Want to approve before each transaction? -

- -
-
-
- ); -}; diff --git a/src/elements/swapLimit/SwapLimit.tsx b/src/elements/swapLimit/SwapLimit.tsx index c3fcfcd3b..e9b865778 100644 --- a/src/elements/swapLimit/SwapLimit.tsx +++ b/src/elements/swapLimit/SwapLimit.tsx @@ -3,31 +3,17 @@ import dayjs from 'utils/dayjs'; import BigNumber from 'bignumber.js'; import { InputField } from 'components/inputField/InputField'; import { TokenInputField } from 'components/tokenInputField/TokenInputField'; -import { ModalDuration } from 'elements/modalDuration/modalDuration'; import { TokenMinimal, updateUserBalances } from 'services/observables/tokens'; import { ReactComponent as IconSync } from 'assets/icons/sync.svg'; import { classNameGenerator } from 'utils/pureFunctions'; import { getRate } from 'services/web3/swap/market'; import { KeeprDaoToken, swapLimit } from 'services/api/keeperDao'; -import { - addNotification, - NotificationType, -} from 'store/notification/notification'; import { useDispatch } from 'react-redux'; import { ethToken, wethToken } from 'services/web3/config'; import { useAppSelector } from 'store'; -import { ModalApprove } from 'elements/modalApprove/modalApprove'; -import { - ApprovalContract, - getNetworkContractApproval, -} from 'services/web3/approval'; -import { prettifyNumber } from 'utils/helperFunctions'; -import { - sendConversionEvent, - setCurrentConversion, -} from 'services/api/googleTagManager/conversion'; +import { ApprovalContract } from 'services/web3/approval'; +import { formatDuration, prettifyNumber } from 'utils/helperFunctions'; import { calculatePercentageChange } from 'utils/formulas'; -import { ModalDepositETH } from 'elements/modalDepositETH/modalDepositETH'; import { Button, ButtonPercentages, @@ -35,8 +21,24 @@ import { } from 'components/button/Button'; import useAsyncEffect from 'use-async-effect'; import { useWalletConnect } from 'elements/walletConnect/useWalletConnect'; -import { Events, getLimitMarket } from 'services/api/googleTagManager'; +import { useApproval } from 'hooks/useApproval'; +import { + depositETHNotification, + rejectNotification, + swapLimitFailedNotification, + swapLimitNotification, +} from 'services/notifications/notifications'; +import { depositWeth } from 'services/web3/swap/limit'; +import { ErrorCode } from 'services/web3/types'; +import { + setCurrentConversion, + sendConversionEvent, +} from 'services/api/googleTagManager/conversion'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; +import { ReactComponent as IconChevronDown } from 'assets/icons/chevronDown.svg'; import { TokenCurrency } from 'store/user/user'; +import { Events } from 'services/api/googleTagManager'; enum Field { from, @@ -62,6 +64,7 @@ export const SwapLimit = ({ refreshLimit, }: SwapLimitProps) => { const dispatch = useDispatch(); + const { pushModal } = useModal(); const account = useAppSelector((state) => state.user.account); const [fromAmount, setFromAmount] = useState(''); const [toAmount, setToAmount] = useState(''); @@ -71,8 +74,6 @@ export const SwapLimit = ({ const [marketRate, setMarketRate] = useState(-1); const [percentage, setPercentage] = useState(''); const [selPercentage, setSelPercentage] = useState(1); - const [showApproveModal, setShowApproveModal] = useState(false); - const [showEthModal, setShowEthModal] = useState(false); const [fromError, setFromError] = useState(''); const [rateWarning, setRateWarning] = useState({ type: '', msg: '' }); const [isLoadingRate, setIsLoadingRate] = useState(false); @@ -218,29 +219,6 @@ export const SwapLimit = ({ [toToken?.address, fromToken?.address] ); - //Check if approval is required - const checkApproval = async (token: TokenMinimal) => { - try { - const isApprovalReq = await getNetworkContractApproval( - token, - ApprovalContract.ExchangeProxy, - fromAmount - ); - if (isApprovalReq) { - sendConversionEvent(Events.approvePop); - setShowApproveModal(true); - } else await handleSwap(true, token.address === wethToken); - } catch (e: any) { - dispatch( - addNotification({ - type: NotificationType.error, - title: 'Transaction Failed', - msg: `${token.symbol} approval had failed. Please try again or contact support.`, - }) - ); - } - }; - const updateETHandWETH = async () => { if (!(toToken && account)) return; @@ -250,11 +228,7 @@ export const SwapLimit = ({ await updateUserBalances(); }; - const handleSwap = async ( - approved: boolean = false, - weth: boolean = false, - showETHtoWETHModal: boolean = false - ) => { + const handleSwap = async () => { if (!account) { handleWalletButtonClick(); return; @@ -262,23 +236,71 @@ export const SwapLimit = ({ if (!(fromToken && toToken && fromAmount && toAmount)) return; - if (showETHtoWETHModal) return setShowEthModal(true); - - if (!approved) return checkApproval(fromToken); + const tokenPair = fromToken.symbol + '/' + toToken?.symbol; + setCurrentConversion( + 'Limit', + tokenPair, + fromToken.symbol, + toToken?.symbol, + fromAmount, + fromAmountUsd, + toAmount, + toAmountUsd, + isCurrency, + rate, + percentage, + duration.asSeconds().toString() + ); + sendConversionEvent(Events.click); - const notification = await swapLimit( - weth ? { ...fromToken, address: wethToken } : fromToken, + await swapLimit( + fromToken.address === ethToken + ? { ...fromToken, address: wethToken } + : fromToken, toToken, fromAmount, toAmount, account, duration, - checkApproval + () => sendConversionEvent(Events.wallet_req), + () => { + swapLimitNotification( + dispatch, + fromToken, + toToken, + fromAmount, + toAmount + ); + if (fromToken.address === ethToken) updateETHandWETH(); + refreshLimit(); + }, + () => rejectNotification(dispatch), + () => + swapLimitFailedNotification( + dispatch, + fromToken, + toToken, + fromAmount, + toAmount + ) ); + }; - if (notification) dispatch(addNotification(notification)); - if (fromToken.address === ethToken) updateETHandWETH(); - refreshLimit(); + const deposiEthWeth = async () => { + try { + const txHash = await depositWeth(fromAmount); + depositETHNotification(dispatch, fromAmount, txHash); + startWETHApprove(); + } catch (e: any) { + if (e.code === ErrorCode.DeniedTx) + sendConversionEvent( + Events.fail, + undefined, + undefined, + 'User rejected transaction' + ); + else sendConversionEvent(Events.fail, undefined, undefined, e.message); + } }; const isSwapDisabled = () => { @@ -339,25 +361,22 @@ export const SwapLimit = ({ return 'Trade'; }; - const handleSwapClick = () => { - const tokenPair = fromToken.symbol + '/' + toToken?.symbol; - setCurrentConversion( - getLimitMarket(true), - tokenPair, - fromToken.symbol, - toToken?.symbol, - fromAmount, - fromAmountUsd, - toAmount, - toAmountUsd, - isCurrency, - rate, - percentage, - duration.asSeconds().toString() - ); - sendConversionEvent(Events.click); - handleSwap(false, false, fromToken.address === ethToken); - }; + const startApprove = useApproval( + [{ amount: fromAmount, token: fromToken }], + handleSwap, + ApprovalContract.ExchangeProxy + ); + + const startWETHApprove = useApproval( + [ + { + amount: fromAmount, + token: { ...fromToken, symbol: 'WETH', address: wethToken }, + }, + ], + handleSwap, + ApprovalContract.ExchangeProxy + ); return (
@@ -509,35 +528,33 @@ export const SwapLimit = ({
Expires in - +
)}
- - handleSwap(true, fromToken.address === ethToken) - } - contract={ApprovalContract.ExchangeProxy} - /> - handleSwap(true)} - /> - -
-
- + +
+
+ +
+
+
+
@@ -105,28 +100,28 @@ export const ModalCreatePool = () => { />
+
-
-
Fee
-
- -
+
+
Fee
+
+
-
- - + +
+ ); }; diff --git a/src/modals/DepositDisabledModal.tsx b/src/modals/DepositDisabledModal.tsx new file mode 100644 index 000000000..0169c3097 --- /dev/null +++ b/src/modals/DepositDisabledModal.tsx @@ -0,0 +1,33 @@ +import { useModal } from 'hooks/useModal'; +import { Modal, ModalNames } from 'modals'; +import { useAppSelector } from 'store'; +import { getIsModalOpen, getModalData } from 'store/modals/modals'; +import { DepositV3Props } from './DepositV3Modal'; + +export const DepositDisabledModal = () => { + const { popModal } = useModal(); + const props = useAppSelector((state) => + getModalData(state, ModalNames.DepositV3) + ); + + const isOpen = useAppSelector( + (state) => + (getIsModalOpen(state, ModalNames.DepositV3) && + !props?.pool.depositingEnabled) || + getIsModalOpen(state, ModalNames.DepositDisabled) + ); + + const onClose = () => { + popModal(); + }; + + return ( + <> + +
+
Deposits are temporarily paused.
+
+
+ + ); +}; diff --git a/src/elements/modalDepositETH/modalDepositETH.tsx b/src/modals/DepositETHModal.tsx similarity index 60% rename from src/elements/modalDepositETH/modalDepositETH.tsx rename to src/modals/DepositETHModal.tsx index 18a2dcf7d..b94a46e4d 100644 --- a/src/elements/modalDepositETH/modalDepositETH.tsx +++ b/src/modals/DepositETHModal.tsx @@ -1,25 +1,34 @@ -import { Modal } from 'components/modal/Modal'; import { ReactComponent as IconDeposit } from 'assets/icons/deposit.svg'; import { Button, ButtonSize } from 'components/button/Button'; +import { useModal } from 'hooks/useModal'; +import { Modal, ModalNames } from 'modals'; +import { useAppSelector } from 'store'; +import { getModalData, getIsModalOpen } from 'store/modals/modals'; -interface ModalDepositETHProps { +interface DepositETHProp { amount: string; - isOpen: boolean; - setIsOpen: Function; onConfirm: Function; } -export const ModalDepositETH = ({ - amount, - isOpen, - setIsOpen, - onConfirm, -}: ModalDepositETHProps) => { + +export const DepositETHModal = () => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.DepositETH) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.DepositETH) + ); + + const onClose = () => { + popModal(); + }; + return (
} isOpen={isOpen} - setIsOpen={setIsOpen} - onClose={() => setIsOpen(false)} + setIsOpen={onClose} >
@@ -29,7 +38,7 @@ export const ModalDepositETH = ({
You Will Receive
- {amount} WETH + {props?.amount} WETH
WETH is a token that represents ETH 1:1 and conforms to the ERC20 @@ -38,8 +47,8 @@ export const ModalDepositETH = ({ - {ApproveModal}
{!isBNT && } - +
); }; diff --git a/src/elements/modalDuration/modalDuration.tsx b/src/modals/DurationModal.tsx similarity index 71% rename from src/elements/modalDuration/modalDuration.tsx rename to src/modals/DurationModal.tsx index 5cf8edcf4..3e7fef4e6 100644 --- a/src/elements/modalDuration/modalDuration.tsx +++ b/src/modals/DurationModal.tsx @@ -1,31 +1,41 @@ import { useState } from 'react'; import dayjs from 'utils/dayjs'; import { Dropdown } from 'components/dropdown/Dropdown'; -import { Modal } from 'components/modal/Modal'; import { Duration } from 'dayjs/plugin/duration'; -import { ReactComponent as IconChevronDown } from 'assets/icons/chevronDown.svg'; import { ReactComponent as IconClock } from 'assets/icons/clock-solid.svg'; -import { formatDuration } from 'utils/helperFunctions'; import { Button, ButtonSize } from 'components/button/Button'; - -interface ModalDurationProps { - duration: Duration; - setDuration: Function; -} +import { Modal, ModalNames } from 'modals'; +import { useAppSelector } from 'store'; +import { getModalData, getIsModalOpen } from 'store/modals/modals'; +import { useModal } from 'hooks/useModal'; interface DurationItem { id: string; title: number; } -export const ModalDuration = ({ - duration, - setDuration, -}: ModalDurationProps) => { - const [isOpen, setIsOpen] = useState(false); - const [days, setDays] = useState(duration.days()); - const [hours, setHours] = useState(duration.hours()); - const [minutes, setMinutes] = useState(duration.minutes()); +interface DurationProp { + duration: Duration; + setDuration: Function; +} + +export const DurationModal = () => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.Duration) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.Duration) + ); + + const [days, setDays] = useState(props?.duration.days()); + const [hours, setHours] = useState(props?.duration.hours()); + const [minutes, setMinutes] = useState(props?.duration.minutes()); + + const onClose = () => { + popModal(); + }; const daysItems: DurationItem[] = Array.from( { length: 31 }, @@ -53,14 +63,7 @@ export const ModalDuration = ({ return ( <> - - +
Custom Time
@@ -74,7 +77,7 @@ export const ModalDuration = ({ setDays(x.title)} - title={days?.toString()} + title={days ? days.toString() : ''} items={daysItems} />
@@ -85,7 +88,7 @@ export const ModalDuration = ({ setHours(x.title)} - title={hours?.toString()} + title={hours ? hours.toString() : ''} items={hoursItems} />
@@ -96,7 +99,7 @@ export const ModalDuration = ({ setMinutes(x.title)} - title={minutes?.toString()} + title={minutes ? minutes.toString() : ''} items={minutesItems} openUp /> @@ -105,8 +108,8 @@ export const ModalDuration = ({
+ + ) : ( +
+
+ Earn BNT rewards on your bn{holding.pool.reserveToken.symbol}, there + are no cooldowns or withdrawal fees for adding or removing bnETH to + reward programs +
+ +
+ Available bn{holding.pool.reserveToken.symbol} +
+
+
+ {prettifyNumber( + new BigNumber(holding.tokenBalance).times( + holding.pool.reserveToken.usdPrice + ), + true + )} +
+
+ {prettifyNumber(holding.poolTokenBalance)} bn + {holding.pool.reserveToken.symbol} +
+
+ {holding.pool.latestProgram?.isActive && ( + + )} +
+
+ {programsSorted.length > 0 && ( +
+ )} + {programsSorted.length > 0 && ( + <> +
+ bn{holding.pool.reserveToken.symbol} in BNT Rewards +
+ +
+ {programsSorted.map((program) => ( +
+
+
+
+ {prettifyNumber( + new BigNumber( + shrinkToken( + program.tokenAmountWei, + holding.pool.decimals + ) + ).times(holding.pool.reserveToken.usdPrice), + true + )} +
+
+ {prettifyNumber( + shrinkToken( + program.poolTokenAmountWei, + holding.pool.decimals + ) + )}{' '} + bn{holding.pool.reserveToken.symbol} +
+
+ {program.isActive ? ( +
+
+ {holding.pool.apr7d.standardRewards.toFixed(2)}% APR +
+
+ + {dayjs((program.startTime ?? 0) * 1000).format( + 'MMM D, YYYY' + )}{' '} + -{' '} + {dayjs((program.endTime ?? 0) * 1000).format( + 'MMM D, YYYY' + )} +
+
+ ) : ( +
Inactive
+ )} + +
+
+ ))} +
+ + )} +
+ )} +
+ ); +}; diff --git a/src/modals/V3UnstakeModal.tsx b/src/modals/V3UnstakeModal.tsx new file mode 100644 index 000000000..3643a8c37 --- /dev/null +++ b/src/modals/V3UnstakeModal.tsx @@ -0,0 +1,193 @@ +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useAppSelector } from 'store'; +import { Holding } from 'store/portfolio/v3Portfolio.types'; +import { prettifyNumber, toBigNumber } from 'utils/helperFunctions'; +import { shrinkToken } from 'utils/formulas'; +import { Button, ButtonSize, ButtonVariant } from 'components/button/Button'; +import { ReactComponent as IconGift } from 'assets/icons/gift.svg'; +import { ContractsApi } from 'services/web3/v3/contractsApi'; +import { + confirmLeaveNotification, + genericFailedNotification, + rejectNotification, +} from 'services/notifications/notifications'; +import { updatePortfolioData } from 'services/web3/v3/portfolio/helpers'; +import { ErrorCode } from 'services/web3/types'; +import { Modal, ModalNames } from 'modals'; +import { getIsModalOpen, getModalData } from 'store/modals/modals'; +import { useModal } from 'hooks/useModal'; + +interface V3UnstakeProps { + holding: Holding; +} + +export const V3UnstakeModal = () => { + const account = useAppSelector((state) => state.user.account); + const { popModal } = useModal(); + const [txBusy, setTxBusy] = useState(false); + const dispatch = useDispatch(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.V3Unstake) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.V3Unstake) + ); + + const onClose = () => { + popModal(); + }; + + if (!props) return null; + + const { holding } = props; + + const programs = holding.programs.filter((p) => + toBigNumber(p.tokenAmountWei).gt(0) + ); + + const programsMerged = programs.map((p) => ({ + ...p, + ...holding.pool.programs.find((pp) => pp.id === p.id), + })); + + const activePrograms = programsMerged.filter((p) => p.isActive); + const inactivePrograms = programsMerged.filter((p) => !p.isActive); + + const handleLeaveClick = async ( + id: string, + poolTokenAmountWei: string, + tokenAmountWei: string + ) => { + if (!account) { + console.error('handleLeaveClick because arguments are not defined'); + return; + } + try { + setTxBusy(true); + const tx = await ContractsApi.StandardRewards.write.leave( + id, + poolTokenAmountWei + ); + confirmLeaveNotification( + dispatch, + tx.hash, + shrinkToken(tokenAmountWei, holding.pool.decimals), + holding.pool.reserveToken.symbol + ); + onClose(); + setTxBusy(false); + await tx.wait(); + await updatePortfolioData(dispatch); + } catch (e: any) { + console.error('handleLeaveClick', e); + onClose(); + setTxBusy(false); + if (e.code === ErrorCode.DeniedTx) { + rejectNotification(dispatch); + } else { + genericFailedNotification(dispatch, 'Leave rewards failed'); + } + } + }; + + return ( + <> + +
+
+ +
+

Reward program you have joined

+ {activePrograms.map((program) => ( +
+
+
+ {prettifyNumber( + shrinkToken(program.tokenAmountWei, holding.pool.decimals) + )}{' '} + {holding.pool.reserveToken.symbol} +
+
Program ID: {program.id}
+
APR: {holding.pool.apr7d.standardRewards.toFixed(2)}%
+
+
+ +
+
+ ))} + {inactivePrograms.length > 0 && ( +
+
+ You have joined an inactive program! +
+
+ First leave, then join an active program to earn more bonuses. +
+ {inactivePrograms.map((program) => ( +
+
+
+ {prettifyNumber( + shrinkToken( + program.tokenAmountWei, + holding.pool.decimals + ) + )}{' '} + {holding.pool.reserveToken.symbol} +
+
Program ID: {program.id}
+
APR: 0%
+
+ +
+ +
+
+ ))} +
+ )} +
+
+ + ); +}; diff --git a/src/modals/V3WithdrawCancelModal.tsx b/src/modals/V3WithdrawCancelModal.tsx new file mode 100644 index 000000000..9c4cd4d68 --- /dev/null +++ b/src/modals/V3WithdrawCancelModal.tsx @@ -0,0 +1,116 @@ +import { WithdrawalRequest } from 'store/portfolio/v3Portfolio.types'; +import { memo, useCallback, useMemo, useState } from 'react'; +import { Modal, ModalNames } from 'modals'; +import { Button, ButtonSize } from 'components/button/Button'; +import { TokenBalanceLarge } from 'components/tokenBalance/TokenBalanceLarge'; +import { toBigNumber } from 'utils/helperFunctions'; +import { + sendWithdrawEvent, + setCurrentWithdraw, + WithdrawEvent, +} from 'services/api/googleTagManager/withdraw'; +import BigNumber from 'bignumber.js'; +import { + getBlockchain, + getBlockchainNetwork, + getCurrency, +} from 'services/api/googleTagManager'; +import { useAppSelector } from 'store'; +import { getIsModalOpen, getModalData } from 'store/modals/modals'; +import { useModal } from 'hooks/useModal'; + +interface V3WithdrawCancelProps { + withdrawRequest: WithdrawalRequest; + cancelWithdrawal: () => Promise; +} + +export const V3WithdrawCancelModal = memo(() => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.V3WithdrawCancel) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.V3WithdrawCancel) + ); + + const onClose = useCallback(() => { + popModal(); + }, [popModal]); + + const [txBusy, setTxBusy] = useState(false); + const cancelWithdrawal = props?.cancelWithdrawal; + const withdrawRequest = props?.withdrawRequest; + const pool = withdrawRequest?.pool; + const token = pool?.reserveToken; + + const handleCTAClick = useCallback(async () => { + if (!pool || !withdrawRequest || !cancelWithdrawal) return () => {}; + + setCurrentWithdraw({ + withdraw_pool: pool.name, + withdraw_blockchain: getBlockchain(), + withdraw_blockchain_network: getBlockchainNetwork(), + withdraw_token: pool.name, + withdraw_token_amount: withdrawRequest.reserveTokenAmount, + withdraw_token_amount_usd: new BigNumber( + withdrawRequest.reserveTokenAmount + ) + .times(withdrawRequest.pool.reserveToken.usdPrice) + .toString(), + withdraw_display_currency: getCurrency(), + }); + sendWithdrawEvent(WithdrawEvent.WithdrawCancelApproveClick); + setTxBusy(true); + await cancelWithdrawal(); + onClose(); + setTxBusy(false); + }, [onClose, pool, cancelWithdrawal, withdrawRequest]); + + const compoundingApr = useMemo( + () => + pool + ? toBigNumber(pool.apr7d.autoCompounding) + .plus(pool.apr7d.tradingFees) + .toFixed(2) + : '', + [pool] + ); + + if (!token || !withdrawRequest) return null; + + return ( + +
+ + +
+
+ Compounding rewards{' '} + {token.symbol} +
+
{compoundingApr} %
+
+ + +
+
+ ); +}); diff --git a/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/V3WithdrawConfirmModal.tsx b/src/modals/V3WithdrawConfirmModal.tsx similarity index 80% rename from src/elements/earn/portfolio/v3/pendingWithdraw/confirm/V3WithdrawConfirmModal.tsx rename to src/modals/V3WithdrawConfirmModal.tsx index 75f745cb2..9ef4112ed 100644 --- a/src/elements/earn/portfolio/v3/pendingWithdraw/confirm/V3WithdrawConfirmModal.tsx +++ b/src/modals/V3WithdrawConfirmModal.tsx @@ -9,47 +9,56 @@ import { ReactComponent as IconInfo } from 'assets/icons/info.svg'; import { shrinkToken } from 'utils/formulas'; import { bntToken } from 'services/web3/config'; import { Switch } from 'components/switch/Switch'; -import ModalFullscreenV3 from 'components/modalFullscreen/modalFullscreenV3'; +import { ModalFullscreen, ModalNames } from 'modals'; +import { useAppSelector } from 'store'; +import { getIsModalOpen, getModalData } from 'store/modals/modals'; +import { useModal } from 'hooks/useModal'; -interface Props { - isModalOpen: boolean; - setIsModalOpen: (isOpen: boolean) => void; +interface V3WithdrawConfirmProps { withdrawRequest: WithdrawalRequest; openCancelModal: (req: WithdrawalRequest) => void; } -export const V3WithdrawConfirmModal = memo( - ({ - isModalOpen, - setIsModalOpen, - withdrawRequest, - openCancelModal, - }: Props) => { - return ( - - - - ); - } -); +export const V3WithdrawConfirmModal = memo(() => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.V3WithdrawConfirm) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.V3WithdrawConfirm) + ); + + if (!props) return null; + + return ( + + + + ); +}); export const V3WithdrawConfirmContent = ({ isModalOpen, setIsModalOpen, withdrawRequest, openCancelModal, -}: Props) => { +}: { + isModalOpen: boolean; + setIsModalOpen: (isOpen: boolean) => void; + withdrawRequest: WithdrawalRequest; + openCancelModal: (req: WithdrawalRequest) => void; +}) => { const { - ModalApprove, token, loadingAmounts, withdrawAmounts, @@ -82,8 +91,6 @@ export const V3WithdrawConfirmContent = ({ return (
- {ModalApprove} - void; +interface V3WithdrawProps { holding: Holding; } +const V3WithdrawModal = () => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.V3Withdraw) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.V3Withdraw) + ); + const holding = props?.holding; -const V3WithdrawModal = ({ isOpen, setIsOpen, holding }: Props) => { const { step, onClose, @@ -41,10 +51,10 @@ const V3WithdrawModal = ({ isOpen, setIsOpen, holding }: Props) => { lockDurationInDays, requestId, setRequestId, - } = useV3WithdrawModal({ setIsOpen }); + } = useV3WithdrawModal({ setIsOpen: popModal }); useEffect(() => { - if (isOpen) { + if (isOpen && holding) { setCurrentWithdraw({ withdraw_pool: holding.pool.name, withdraw_blockchain: getBlockchain(), @@ -56,10 +66,12 @@ const V3WithdrawModal = ({ isOpen, setIsOpen, holding }: Props) => { sendWithdrawEvent(WithdrawEvent.WithdrawPoolClick); sendWithdrawEvent(WithdrawEvent.WithdrawAmountView); } - }, [isOpen, isCurrency, holding.pool.name]); + }, [isOpen, isCurrency, holding]); + + if (!holding) return null; return ( - { requestId={requestId} /> )} - + ); }; diff --git a/src/elements/modalVbnt/ModalVbnt.tsx b/src/modals/VbntModal.tsx similarity index 81% rename from src/elements/modalVbnt/ModalVbnt.tsx rename to src/modals/VbntModal.tsx index 0683b542b..139c9b632 100644 --- a/src/elements/modalVbnt/ModalVbnt.tsx +++ b/src/modals/VbntModal.tsx @@ -1,4 +1,3 @@ -import { Modal } from 'components/modal/Modal'; import { useMemo, useState } from 'react'; import { Token, updateUserBalances } from 'services/observables/tokens'; import { wait } from 'utils/pureFunctions'; @@ -15,7 +14,7 @@ import { unstakeFailedNotification, unstakeNotification, } from 'services/notifications/notifications'; -import { useApproveModal } from 'hooks/useApproveModal'; +import { useApproval } from 'hooks/useApproval'; import { TokenInputPercentage } from 'components/tokenInputPercentage/TokenInputPercentage'; import { ApprovalContract } from 'services/web3/approval'; import { useAppSelector } from 'store'; @@ -26,26 +25,29 @@ import { sendGovEvent, } from 'services/api/googleTagManager/gov'; import { getFiat } from 'services/api/googleTagManager'; +import { useModal } from 'hooks/useModal'; +import { getIsModalOpen, getModalData } from 'store/modals/modals'; import { CurrencySelection } from 'elements/layoutHeader/CurrencySelection'; import { TokenCurrency } from 'store/user/user'; +import { Modal, ModalNames } from 'modals'; -interface ModalVbntProps { - setIsOpen: Function; - isOpen: boolean; +interface VbntModalProps { token: Token; stake: boolean; stakeBalance?: string; onCompleted?: Function; } -export const ModalVbnt = ({ - setIsOpen, - isOpen, - token, - stake, - stakeBalance, - onCompleted, -}: ModalVbntProps) => { +export const VbntModal = () => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.VBnt) + ); + + const props = useAppSelector((state) => + getModalData(state, ModalNames.VBnt) + ); + const account = useAppSelector((state) => state.user.account); const tokenCurrency = useAppSelector((state) => state.user.tokenCurrency); const isCurrency = tokenCurrency === TokenCurrency.Currency; @@ -56,11 +58,11 @@ export const ModalVbnt = ({ const stakeDisabled = !account || !amount || Number(amount) === 0; - const fieldBlance = stake - ? token.balance - ? token.balance + const fieldBlance = props?.stake + ? props?.token.balance + ? props?.token.balance : undefined - : stakeBalance; + : props?.stakeBalance; const govProperties: GovProperties = { stake_input_type: getFiat(isCurrency), @@ -76,10 +78,11 @@ export const ModalVbnt = ({ percentages.findIndex((x) => percentage.toFixed(10) === x.toFixed(10)) ); } - }, [amount, token, percentages, fieldBlance]); + }, [amount, props?.token, percentages, fieldBlance]); const handleStakeUnstake = async () => { - if (stakeDisabled || !account) return; + if (stakeDisabled || !account || !props) return; + const { stake, token } = props; sendGovEvent(GovEvent.Click, govProperties, stake); sendGovEvent(GovEvent.WalletRequest, govProperties, stake); @@ -127,20 +130,24 @@ export const ModalVbnt = ({ ); }; - const [checkApprove, ModalApprove] = useApproveModal( - [{ amount: amount, token: token }], + const startApprove = useApproval( + props ? [{ amount: amount, token: props.token }] : [], handleStakeUnstake, ApprovalContract.Governance, - () => sendGovEvent(GovEvent.UnlimitedPopup, govProperties, stake), + () => sendGovEvent(GovEvent.UnlimitedPopup, govProperties, props?.stake), (isUnlimited: boolean) => sendGovEvent( GovEvent.UnlimitedPopupSelect, govProperties, - stake, + props?.stake, isUnlimited ) ); + if (!props) return null; + + const { stake, token, onCompleted } = props; + const refreshBalances = async () => { await wait(8000); await updateUserBalances(); @@ -152,7 +159,7 @@ export const ModalVbnt = ({ } - setIsOpen={setIsOpen} + setIsOpen={popModal} isOpen={isOpen} separator large @@ -170,8 +177,8 @@ export const ModalVbnt = ({
- {ModalApprove} ); }; diff --git a/src/elements/walletConnect/WalletConnectModal.tsx b/src/modals/WalletConnectModal.tsx similarity index 58% rename from src/elements/walletConnect/WalletConnectModal.tsx rename to src/modals/WalletConnectModal.tsx index a7c019f0c..62346ef5a 100644 --- a/src/elements/walletConnect/WalletConnectModal.tsx +++ b/src/modals/WalletConnectModal.tsx @@ -1,8 +1,8 @@ -import { Modal } from 'components/modal/Modal'; -import { UseWalletConnect } from './useWalletConnect'; -import { WalletConnectModalList } from './WalletConnectModalList'; -import { WalletConnectModalPending } from './WalletConnectModalPending'; -import { WalletConnectModalError } from './WalletConnectModalError'; +import { Modal } from 'modals'; +import { UseWalletConnect } from '../elements/walletConnect/useWalletConnect'; +import { WalletConnectModalList } from '../elements/walletConnect/WalletConnectModalList'; +import { WalletConnectModalPending } from '../elements/walletConnect/WalletConnectModalPending'; +import { WalletConnectModalError } from '../elements/walletConnect/WalletConnectModalError'; export const WalletConnectModal = (props: UseWalletConnect) => { const { isPending, isError } = props; diff --git a/src/elements/earn/portfolio/withdrawLiquidity/WithdrawLiquidityWidget.tsx b/src/modals/WithdrawLiquidityModal.tsx similarity index 73% rename from src/elements/earn/portfolio/withdrawLiquidity/WithdrawLiquidityWidget.tsx rename to src/modals/WithdrawLiquidityModal.tsx index 04f47ca7d..6dfa60303 100644 --- a/src/elements/earn/portfolio/withdrawLiquidity/WithdrawLiquidityWidget.tsx +++ b/src/modals/WithdrawLiquidityModal.tsx @@ -1,18 +1,17 @@ import { EmergencyInfo } from 'components/EmergencyInfo'; -import { ModalV3 } from 'components/modal/ModalV3'; import { PopoverV3 } from 'components/popover/PopoverV3'; +import { useModal } from 'hooks/useModal'; +import { Modal, ModalNames } from 'modals'; import { useMemo } from 'react'; import { useAppSelector } from 'store'; +import { getIsModalOpen } from 'store/modals/modals'; -interface Props { - isModalOpen: boolean; - setIsModalOpen: (isOpen: boolean) => void; -} +export const WithdrawLiquidityModal = () => { + const { popModal } = useModal(); + const isOpen = useAppSelector((state) => + getIsModalOpen(state, ModalNames.WithdrawLiquidity) + ); -export const WithdrawLiquidityWidget = ({ - isModalOpen, - setIsModalOpen, -}: Props) => { const { lockDuration } = useAppSelector( (state) => state.v3Portfolio.withdrawalSettings ); @@ -23,7 +22,7 @@ export const WithdrawLiquidityWidget = ({ ); return ( - setIsModalOpen(false)} isOpen={isModalOpen} large> +
Migrate to Bancor V3 to withdraw.
@@ -42,6 +41,6 @@ export const WithdrawLiquidityWidget = ({ )} />
- + ); }; diff --git a/src/modals/index.tsx b/src/modals/index.tsx new file mode 100644 index 000000000..2a129d299 --- /dev/null +++ b/src/modals/index.tsx @@ -0,0 +1,226 @@ +import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, ReactNode } from 'react'; +import { ReactComponent as IconTimes } from 'assets/icons/times.svg'; +import { ReactComponent as IconChevron } from 'assets/icons/chevronRight.svg'; +import { ReactComponent as IconBancor } from 'assets/icons/bancor.svg'; +import { CreatePoolModal } from './CreatePoolModal'; +import { DepositDisabledModal } from './DepositDisabledModal'; +import { DurationModal } from './DurationModal'; +import { DepositETHModal } from './DepositETHModal'; +import { SelectPoolModal } from './SelectPoolModal'; +import { UpgradeBntModal } from './UpgradeBntModal'; +import { UpgradeTknModal } from './UpgradeTknModal'; +import { V3BonusesModal } from './V3BonusesModal'; +import { V3ExternalHoldingsModal } from './V3ExternalHoldingsModal'; +import { V3UnstakeModal } from './V3UnstakeModal'; +import { V3WithdrawCancelModal } from './V3WithdrawCancelModal'; +import { V3WithdrawConfirmModal } from './V3WithdrawConfirmModal'; +import V3WithdrawModal from './V3WithdrawModal'; +import { VbntModal } from './VbntModal'; +import { V3ManageProgramsModal } from './V3ManageProgramsModal'; +import { DepositV3Modal } from './DepositV3Modal'; +import { SearchableTokenListModal } from './SearchableTokenListModal'; +import { WithdrawLiquidityModal } from './WithdrawLiquidityModal'; +import { ApproveModal } from './ApproveModal'; + +export enum ModalNames { + ApproveModal, + CreatePool, + DepositDisabled, + DepositETH, + DepositV3, + Duration, + SearchableTokenList, + SelectPool, + UpgradeBnt, + UpgradeTkn, + V3Bonuses, + V3ExternalHoldings, + V3ManagePrograms, + V3Unstake, + V3WithdrawCancel, + V3WithdrawConfirm, + V3Withdraw, + VBnt, + WithdrawLiquidity, +} + +export const Modals = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const Modal = ({ + title, + children, + separator, + titleElement, + setIsOpen, + isOpen, + showBackButton, + onBackClick, + onClose, + large, +}: { + title?: string; + children: JSX.Element; + separator?: boolean; + titleElement?: ReactNode; + setIsOpen: (isOpen: boolean) => void; + isOpen: boolean; + onBackClick?: Function; + showBackButton?: boolean; + onClose?: Function; + large?: boolean; +}) => { + return ( + <> + + (onClose ? onClose() : setIsOpen(false))} + > +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+ +
+ {showBackButton && ( + + )} + {title && title} +
+
+ {titleElement && titleElement} + +
+
+
+ {separator &&
} + {children} +
+
+
+
+
+
+ + ); +}; + +export const ModalFullscreen = ({ + isOpen, + setIsOpen, + children, + title, + titleElement, + className, +}: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + children: ReactNode; + title: string; + titleElement?: ReactNode; + className?: string; +}) => { + return ( + <> + + setIsOpen(false)} + > +
+
+

+ + {title} +

+
+ {titleElement && titleElement} + +
+
+ +
+ {children} +
+
+
+
+ + ); +}; diff --git a/src/pages/Vote.tsx b/src/pages/Vote.tsx index 5eb10515d..e0bd76204 100644 --- a/src/pages/Vote.tsx +++ b/src/pages/Vote.tsx @@ -1,7 +1,6 @@ import { useWeb3React } from '@web3-react/core'; import { ReactComponent as IconLink } from 'assets/icons/link.svg'; import { CountdownTimer } from 'components/countdownTimer/CountdownTimer'; -import { ModalVbnt } from 'elements/modalVbnt/ModalVbnt'; import { useCallback, useState } from 'react'; import { useEffect } from 'react'; import { useAppSelector } from 'store'; @@ -19,6 +18,8 @@ import { useWalletConnect } from 'elements/walletConnect/useWalletConnect'; import { useDispatch } from 'react-redux'; import { setStakedAmount, setUnstakeTimer } from 'store/gov/gov'; import { Navigate } from 'components/navigate/Navigate'; +import { useModal } from 'hooks/useModal'; +import { ModalNames } from 'modals'; interface VoteCardProps { title: string; @@ -67,6 +68,7 @@ const VoteCard = ({ export const Vote = () => { const { chainId } = useWeb3React(); const dispatch = useDispatch(); + const { pushModal } = useModal(); const account = useAppSelector((state) => state.user.account); const tokens = useAppSelector((state) => state.bancor.tokensV2); const stakeAmount = useAppSelector( @@ -77,8 +79,6 @@ export const Vote = () => { ); const [govToken, setGovToken] = useState(); const [isUnlocked, setIsUnlocked] = useState(false); - const [stakeModal, setStakeModal] = useState(false); - const [isStake, setIsStake] = useState(false); const { handleWalletButtonClick } = useWalletConnect(); useEffect(() => { @@ -129,8 +129,16 @@ export const Vote = () => { return; } - setIsStake(true); - setStakeModal(true); + if (govToken) + pushModal({ + modalName: ModalNames.VBnt, + data: { + token: govToken, + stake: true, + stakeBalance: stakeAmount, + onCompleted: refresh, + }, + }); }} footer={
@@ -215,8 +223,16 @@ export const Vote = () => { !isUnlocked } onClick={() => { - setIsStake(false); - setStakeModal(true); + if (govToken) + pushModal({ + modalName: ModalNames.VBnt, + data: { + token: govToken, + stake: false, + stakeBalance: stakeAmount, + onCompleted: refresh, + }, + }); }} > Unstake Tokens @@ -253,16 +269,6 @@ export const Vote = () => {
- {govToken && ( - - )} ); diff --git a/src/services/api/keeperDao.ts b/src/services/api/keeperDao.ts index 583648044..ff42d734a 100644 --- a/src/services/api/keeperDao.ts +++ b/src/services/api/keeperDao.ts @@ -8,15 +8,14 @@ import { import { take } from 'rxjs/operators'; import { Token, TokenMinimal, tokensV2$ } from 'services/observables/tokens'; import { writeWeb3 } from 'services/web3'; -import { ethToken, wethToken } from 'services/web3/config'; -import { createOrder, depositWeth } from 'services/web3/swap/limit'; +import { createOrder } from 'services/web3/swap/limit'; import { prettifyNumber } from 'utils/helperFunctions'; -import { sendConversionEvent } from './googleTagManager/conversion'; import { utils } from 'ethers'; import { ErrorCode } from 'services/web3/types'; import { shrinkToken } from 'utils/formulas'; import { ExchangeProxy__factory } from 'services/web3/abis/types'; import { exchangeProxy$ } from 'services/observables/contracts'; +import { sendConversionEvent } from './googleTagManager/conversion'; import { Events } from './googleTagManager'; const baseUrl: string = 'https://hidingbook.keeperdao.com/api/v1'; @@ -49,77 +48,21 @@ export const swapLimit = async ( to: string, user: string, duration: plugin.Duration, - checkApproval: Function -): Promise => { - const fromIsEth = ethToken === fromToken.address; + onHash: () => void, + onCompleted: () => void, + rejected: () => void, + failed: (error: string) => void +) => { try { - if (fromIsEth) { - try { - const txHash = await depositWeth(from); - checkApproval({ ...fromToken, symbol: 'WETH', address: wethToken }); - return { - type: NotificationType.pending, - title: 'Pending Confirmation', - msg: `Depositing ${from} ETH to WETH is pending confirmation`, - txHash, - updatedInfo: { - successTitle: 'Success!', - successMsg: `Your deposit ${from} ETH to WETH is confirmed`, - errorTitle: 'Transaction Failed', - errorMsg: `Depositing ${from} ETH to WETH has failed. Please try again or contact support`, - }, - }; - } catch (e: any) { - if (e.code === ErrorCode.DeniedTx) { - return { - type: NotificationType.error, - title: 'Transaction Rejected', - msg: 'You rejected the transaction. To complete the order you need to click approve.', - }; - } - - sendConversionEvent(Events.fail, undefined, undefined, e.message); - - return { - type: NotificationType.error, - title: 'Transaction Failed', - msg: `Depositing ${from} ETH to WETH has failed. Please try again or contact support`, - }; - } - } else { - sendConversionEvent(Events.wallet_req); - await createOrder( - fromToken, - toToken, - from, - to, - user, - duration.asSeconds() - ); - sendConversionEvent(Events.success); - - return { - type: NotificationType.success, - title: 'Success!', - msg: `Your limit order to trade ${from} ${fromToken.symbol} for ${to} ${toToken.symbol} was created`, - }; - } + onHash(); + sendConversionEvent(Events.wallet_req); + await createOrder(fromToken, toToken, from, to, user, duration.asSeconds()); + sendConversionEvent(Events.success); + onCompleted(); } catch (e: any) { - if (e.code === ErrorCode.DeniedTx) { - return { - type: NotificationType.error, - title: 'Transaction Rejected', - msg: 'You rejected the transaction. If this was by mistake, please try again.', - }; - } - sendConversionEvent(Events.fail, undefined, undefined, e.message); - - return { - type: NotificationType.error, - title: 'Transaction Failed', - msg: `Limit order to trade ${from} ${fromToken.symbol} for ${to} ${toToken.symbol} could not be created. Please try again or contact support`, - }; + if (e.code === ErrorCode.DeniedTx) rejected(); + else failed(e.message); } }; diff --git a/src/services/notifications/notifications.ts b/src/services/notifications/notifications.ts index 2e6e15ea6..db656c889 100644 --- a/src/services/notifications/notifications.ts +++ b/src/services/notifications/notifications.ts @@ -133,6 +133,59 @@ export const swapFailedNotification = ( dispatch ); +export const swapLimitNotification = ( + dispatch: any, + fromToken: TokenMinimal, + toToken: TokenMinimal, + from: string, + to: string +) => + showNotification( + { + type: NotificationType.success, + title: 'Success!', + msg: `Your limit order to trade ${from} ${fromToken.symbol} for ${to} ${toToken.symbol} was created`, + }, + dispatch + ); + +export const swapLimitFailedNotification = ( + dispatch: any, + fromToken: TokenMinimal, + toToken: TokenMinimal, + from: string, + to: string +) => + showNotification( + { + type: NotificationType.error, + title: 'Transaction Failed', + msg: `Limit order to trade ${from} ${fromToken.symbol} for ${to} ${toToken.symbol} could not be created. Please try again or contact support`, + }, + dispatch + ); + +export const depositETHNotification = ( + dispatch: any, + from: string, + txHash: string +) => + showNotification( + { + type: NotificationType.pending, + title: 'Pending Confirmation', + msg: `Depositing ${from} ETH to WETH is pending confirmation`, + txHash, + updatedInfo: { + successTitle: 'Success!', + successMsg: `Your deposit ${from} ETH to WETH is confirmed`, + errorTitle: 'Transaction Failed', + errorMsg: `Depositing ${from} ETH to WETH has failed. Please try again or contact support`, + }, + }, + dispatch + ); + export const poolExistNotification = (dispatch: any) => showNotification( { diff --git a/src/store/index.ts b/src/store/index.ts index befcdda7a..63f6e505f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import { notification } from 'store/notification/notification'; import { liquidity } from './liquidity/liquidity'; import { v3Portfolio } from 'store/portfolio/v3Portfolio'; import { gov } from './gov/gov'; +import { modals } from './modals/modals'; export const store = configureStore({ middleware: (getDefaultMiddleware) => @@ -27,6 +28,7 @@ export const store = configureStore({ liquidity, v3Portfolio, gov, + modals, }, }); diff --git a/src/store/modals/modals.ts b/src/store/modals/modals.ts new file mode 100644 index 000000000..a1bef35b2 --- /dev/null +++ b/src/store/modals/modals.ts @@ -0,0 +1,53 @@ +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { ModalNames } from 'modals'; +import { RootState } from 'store'; + +export type ModalObj = { modalName: ModalNames; data?: any }; + +export interface ModalsState { + openModals: ModalObj[]; +} + +export const initialState: ModalsState = { + openModals: [], +}; + +const modalsSlice = createSlice({ + name: 'modals', + initialState, + reducers: { + pushModal: (state, action) => { + state.openModals.push({ + modalName: action.payload.modalName, + data: action.payload.data, + }); + }, + popModal: (state) => { + state.openModals.pop(); + }, + }, +}); + +export const getIsModalOpen = createSelector( + [ + (state: RootState) => state.modals.openModals, + (_: any, modal: ModalNames) => modal, + ], + (openModals: ModalObj[], modalName: ModalNames) => { + return openModals.some((x) => x.modalName === modalName); + } +); + +export const getModalData = createSelector( + [ + (state: RootState) => state.modals.openModals, + (_: any, modal: ModalNames) => modal, + ], + (openModals: ModalObj[], modalName: ModalNames) => { + return openModals.find((x) => x.modalName === modalName)?.data; + } +); + +export const { pushModal, popModal } = modalsSlice.actions; + +export const modals = modalsSlice.reducer; diff --git a/src/store/portfolio/v3Portfolio.ts b/src/store/portfolio/v3Portfolio.ts index e5d4ce178..aa823c0c5 100644 --- a/src/store/portfolio/v3Portfolio.ts +++ b/src/store/portfolio/v3Portfolio.ts @@ -24,7 +24,6 @@ export const initialState: V3PortfolioState = { withdrawalRequestsRaw: [], withdrawalSettings: { lockDuration: 0, withdrawalFee: 0 }, isLoadingWithdrawalRequests: true, - bonusesModal: false, standardRewards: [], isLoadingStandardRewards: true, }; @@ -53,9 +52,6 @@ const v3PortfolioSlice = createSlice({ ) => { state.withdrawalSettings = action.payload; }, - openBonusesModal: (state, action: PayloadAction) => { - state.bonusesModal = action.payload; - }, setStandardRewards: ( state, action: PayloadAction @@ -68,7 +64,6 @@ const v3PortfolioSlice = createSlice({ export const { setHoldingsRaw, - openBonusesModal, setWithdrawalRequestsRaw, setWithdrawalSettings, setStandardRewards, diff --git a/src/store/portfolio/v3Portfolio.types.ts b/src/store/portfolio/v3Portfolio.types.ts index 5788b932e..09ccb519a 100644 --- a/src/store/portfolio/v3Portfolio.types.ts +++ b/src/store/portfolio/v3Portfolio.types.ts @@ -9,7 +9,6 @@ export interface V3PortfolioState { withdrawalRequestsRaw: WithdrawalRequestRaw[]; withdrawalSettings: WithdrawalSettings; isLoadingWithdrawalRequests: boolean; - bonusesModal: boolean; standardRewards: RewardsProgramStake[]; isLoadingStandardRewards: boolean; }