diff --git a/.env.dev b/.env.dev index 66e2f1e70f..a47496cd5a 100644 --- a/.env.dev +++ b/.env.dev @@ -2,7 +2,7 @@ REACT_APP_PORTIS_ID=78552035-b1c5-4927-8d2f-4eeea42a795c REACT_APP_FORTMATIC_KEY=pk_live_EF523DF5F8AC3058 REACT_APP_MAINNET_ENV=staging -REACT_APP_AGGREGATOR_API=https://aggregator-api.dev.kyberengineering.io +REACT_APP_AGGREGATOR_API=https://meta-aggregator.dev.kyberengineering.io REACT_APP_AGGREGATOR_STATS_API=https://aggregator-stats.kyberswap.com REACT_APP_SENTRY_DNS=https://d94ee2d3c22043bdaec966758680b5a8@sentry.ops.kyberengineering.io/4 diff --git a/etc/nginx.conf b/etc/nginx.conf index a2ff8d7279..ada546fa93 100644 --- a/etc/nginx.conf +++ b/etc/nginx.conf @@ -19,6 +19,9 @@ server { index index.html index.htm; location / { + if ( $uri = '/index.html' ) { + add_header Cache-Control no-store always; + } try_files $uri $uri/ /index.html; } diff --git a/package.json b/package.json index 1ea180a02e..49d796b01b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@reach/dialog": "^0.17.0", "@reach/portal": "^0.17.0", "@reduxjs/toolkit": "^1.7.1", + "@trivago/prettier-plugin-sort-imports": "^3.3.0", "@types/aos": "^3.0.4", "@types/big.js": "^6.0.0", "@types/d3": "^7.1.0", @@ -38,6 +39,7 @@ "@types/rebass": "^4.0.5", "@types/recharts": "^1.8.23", "@types/styled-components": "^5.1.25", + "@types/ua-parser-js": "^0.7.36", "@types/wcag-contrast": "^3.0.0", "@typescript-eslint/eslint-plugin": "^4", "@typescript-eslint/parser": "^4", @@ -57,6 +59,7 @@ "copy-to-clipboard": "^3.2.0", "eslint": "^7.11.0", "eslint-config-prettier": "^8.5.0", + "eslint-plugin-better-styled-components": "^1.1.2", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -114,35 +117,24 @@ "analyze": "source-map-explorer 'build/static/js/*.js'", "lint": "yarn eslint ." }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, + "browserslist": [ + ">0.2%", + "not dead", + "not op_mini all" + ], "license": "GPL-3.0-or-later", "dependencies": { "@apollo/client": "^3.3.11", "@kyberswap/ks-sdk-classic": "^0.0.13", "@kyberswap/ks-sdk-core": "^0.0.13", "@kyberswap/ks-sdk-elastic": "^0.0.43", - "@lingui/detect-locale": "^3.10.4", "@lingui/react": "^3.14.0", "@sentry/react": "^6.16.1", - "@trivago/prettier-plugin-sort-imports": "^3.3.0", "@typeform/embed-react": "^1.2.4", - "@types/ua-parser-js": "^0.7.36", "aos": "^2.3.4", "d3": "^7.3.0", "dayjs": "^1.10.7", "env-cmd": "^10.1.0", - "eslint-plugin-better-styled-components": "^1.1.2", "eslint-plugin-unused-imports": "^2.0.0", "firebase": "^9.8.2", "graphql": "^15.5.0", @@ -163,6 +155,7 @@ "swiper": "^8.0.7", "swr": "^1.0.1", "ua-parser-js": "^1.0.2", - "uuidv4": "^6.2.12" + "uuidv4": "^6.2.12", + "walktour": "^5.1.1" } } diff --git a/src/assets/banners/kyberswap-trading-campaign-ath-desktop.png b/src/assets/banners/kyberswap-trading-campaign-ath-desktop.png deleted file mode 100644 index ca5978c881..0000000000 Binary files a/src/assets/banners/kyberswap-trading-campaign-ath-desktop.png and /dev/null differ diff --git a/src/assets/banners/kyberswap-trading-campaign-ath-mobile.png b/src/assets/banners/kyberswap-trading-campaign-ath-mobile.png deleted file mode 100644 index 7c2757a33e..0000000000 Binary files a/src/assets/banners/kyberswap-trading-campaign-ath-mobile.png and /dev/null differ diff --git a/src/assets/banners/kyberswap-trading-campaign-ath-tablet.png b/src/assets/banners/kyberswap-trading-campaign-ath-tablet.png deleted file mode 100644 index 1efcc094ef..0000000000 Binary files a/src/assets/banners/kyberswap-trading-campaign-ath-tablet.png and /dev/null differ diff --git a/src/assets/banners/kyberswap-trading-campaign-polygon-desktop.png b/src/assets/banners/kyberswap-trading-campaign-polygon-desktop.png new file mode 100644 index 0000000000..f86aca2f89 Binary files /dev/null and b/src/assets/banners/kyberswap-trading-campaign-polygon-desktop.png differ diff --git a/src/assets/banners/kyberswap-trading-campaign-polygon-mobile.png b/src/assets/banners/kyberswap-trading-campaign-polygon-mobile.png new file mode 100644 index 0000000000..735c0c29ad Binary files /dev/null and b/src/assets/banners/kyberswap-trading-campaign-polygon-mobile.png differ diff --git a/src/assets/banners/kyberswap-trading-campaign-polygon-tablet.png b/src/assets/banners/kyberswap-trading-campaign-polygon-tablet.png new file mode 100644 index 0000000000..3df56df8b9 Binary files /dev/null and b/src/assets/banners/kyberswap-trading-campaign-polygon-tablet.png differ diff --git a/src/assets/banners/polygon-desktop.png b/src/assets/banners/polygon-desktop.png new file mode 100644 index 0000000000..dd3f0fe47f Binary files /dev/null and b/src/assets/banners/polygon-desktop.png differ diff --git a/src/assets/banners/polygon-mobile.png b/src/assets/banners/polygon-mobile.png new file mode 100644 index 0000000000..db93b7caed Binary files /dev/null and b/src/assets/banners/polygon-mobile.png differ diff --git a/src/assets/banners/polygon-tablet.png b/src/assets/banners/polygon-tablet.png new file mode 100644 index 0000000000..d835984d4a Binary files /dev/null and b/src/assets/banners/polygon-tablet.png differ diff --git a/src/assets/images/brave_wallet.svg b/src/assets/images/brave_wallet.svg new file mode 100644 index 0000000000..402bdbd9f4 --- /dev/null +++ b/src/assets/images/brave_wallet.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/light-brave_wallet.svg b/src/assets/images/light-brave_wallet.svg new file mode 100644 index 0000000000..402bdbd9f4 --- /dev/null +++ b/src/assets/images/light-brave_wallet.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/tutorial_swap/menu.png b/src/assets/images/tutorial_swap/menu.png new file mode 100644 index 0000000000..aa494e2055 Binary files /dev/null and b/src/assets/images/tutorial_swap/menu.png differ diff --git a/src/assets/images/tutorial_swap/step1.png b/src/assets/images/tutorial_swap/step1.png new file mode 100644 index 0000000000..b7963e6a22 Binary files /dev/null and b/src/assets/images/tutorial_swap/step1.png differ diff --git a/src/assets/images/tutorial_swap/step2.png b/src/assets/images/tutorial_swap/step2.png new file mode 100644 index 0000000000..72dc936c73 Binary files /dev/null and b/src/assets/images/tutorial_swap/step2.png differ diff --git a/src/assets/images/tutorial_swap/step4.1.png b/src/assets/images/tutorial_swap/step4.1.png new file mode 100644 index 0000000000..1ec8be0747 Binary files /dev/null and b/src/assets/images/tutorial_swap/step4.1.png differ diff --git a/src/assets/images/tutorial_swap/step4.2.png b/src/assets/images/tutorial_swap/step4.2.png new file mode 100644 index 0000000000..83ee7c180c Binary files /dev/null and b/src/assets/images/tutorial_swap/step4.2.png differ diff --git a/src/assets/images/tutorial_swap/step5.png b/src/assets/images/tutorial_swap/step5.png new file mode 100644 index 0000000000..100f353814 Binary files /dev/null and b/src/assets/images/tutorial_swap/step5.png differ diff --git a/src/assets/images/tutorial_swap/step6.png b/src/assets/images/tutorial_swap/step6.png new file mode 100644 index 0000000000..4ed64e05a4 Binary files /dev/null and b/src/assets/images/tutorial_swap/step6.png differ diff --git a/src/assets/images/tutorial_swap/step7.png b/src/assets/images/tutorial_swap/step7.png new file mode 100644 index 0000000000..ac86343568 Binary files /dev/null and b/src/assets/images/tutorial_swap/step7.png differ diff --git a/src/assets/images/tutorial_swap/step8.1.png b/src/assets/images/tutorial_swap/step8.1.png new file mode 100644 index 0000000000..8e1eaa213e Binary files /dev/null and b/src/assets/images/tutorial_swap/step8.1.png differ diff --git a/src/assets/images/tutorial_swap/step8.2.png b/src/assets/images/tutorial_swap/step8.2.png new file mode 100644 index 0000000000..fb7470a8f7 Binary files /dev/null and b/src/assets/images/tutorial_swap/step8.2.png differ diff --git a/src/assets/images/tutorial_swap/welcome.png b/src/assets/images/tutorial_swap/welcome.png new file mode 100644 index 0000000000..5abe90ce5a Binary files /dev/null and b/src/assets/images/tutorial_swap/welcome.png differ diff --git a/src/assets/svg/touch_icon.svg b/src/assets/svg/touch_icon.svg new file mode 100644 index 0000000000..742e1a823c --- /dev/null +++ b/src/assets/svg/touch_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/AccountDetails/index.tsx b/src/components/AccountDetails/index.tsx index ec85af4b1f..f135c5bcb3 100644 --- a/src/components/AccountDetails/index.tsx +++ b/src/components/AccountDetails/index.tsx @@ -20,7 +20,15 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png' import PortisIcon from '../../assets/images/portisIcon.png' import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' import { ReactComponent as Close } from '../../assets/images/x.svg' -import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors' +import { + braveInjectedConnector, + coin98InjectedConnector, + fortmatic, + injected, + portis, + walletconnect, + walletlink, +} from '../../connectors' import { AppDispatch } from '../../state' import { clearAllTransactions } from '../../state/transactions/actions' import { ExternalLink, LinkStyledButton, TYPE } from '../../theme' @@ -207,13 +215,31 @@ export default function AccountDetails({ function formatConnectorName() { const { ethereum } = window - const isMetaMask = !!(ethereum && ethereum.isMetaMask) - const name = Object.keys(SUPPORTED_WALLETS) - .filter( - k => - SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK')), - ) - .map(k => SUPPORTED_WALLETS[k].name)[0] + const isInjected = !!(ethereum && ethereum.isMetaMask) + const isCoin98 = isInjected && ethereum.isCoin98 + const isBraveWallet = isInjected && ethereum.isBraveWallet + + const walletConfig = (() => { + if (isInjected) { + if (isCoin98) { + return SUPPORTED_WALLETS['COIN98'] + } + + if (isBraveWallet) { + return SUPPORTED_WALLETS['BRAVE'] + } + + return SUPPORTED_WALLETS['METAMASK'] + } + + return Object.values(SUPPORTED_WALLETS).find(config => config.connector === connector) + })() + + const name = walletConfig?.name || '' + + if (!walletConfig) { + console.error('Cannot find the wallet connect') + } return ( @@ -223,33 +249,43 @@ export default function AccountDetails({ } function getStatusIcon() { - if (connector === injected) { - return ( - - - - ) - } else if (connector === walletconnect) { - return ( - - {'wallet - - ) - } else if (connector === walletlink) { - return ( - - {'coinbase - - ) - } else if (connector === fortmatic) { - return ( - - {'fortmatic - - ) - } else if (connector === portis) { - return ( - <> + switch (connector) { + case injected: + case coin98InjectedConnector: + case braveInjectedConnector: { + return ( + + + + ) + } + + case walletconnect: { + return ( + + {'wallet + + ) + } + + case walletlink: { + return ( + + {'coinbase + + ) + } + + case fortmatic: { + return ( + + {'fortmatic + + ) + } + + case portis: { + return ( {'portis Show Portis - - ) + ) + } + + default: { + return null + } } - return null } const clearAllTransactionsCallback = useCallback(() => { @@ -297,19 +336,15 @@ export default function AccountDetails({ {ENSName ? ( - <> -
- {getStatusIcon()} -

{ENSName}

-
- +
+ {getStatusIcon()} +

{ENSName}

+
) : ( - <> -
- {getStatusIcon()} -

{isMobile && account ? shortenAddress(account, 10) : account}

-
- +
+ {getStatusIcon()} +

{isMobile && account ? shortenAddress(account, 10) : account}

+
)}
diff --git a/src/components/Banner/index.tsx b/src/components/Banner/index.tsx index f4c95ba692..8e715163f3 100644 --- a/src/components/Banner/index.tsx +++ b/src/components/Banner/index.tsx @@ -6,9 +6,12 @@ import styled from 'styled-components' import { Autoplay, Navigation, Pagination } from 'swiper' import { Swiper, SwiperSlide } from 'swiper/react' -import KyberSwapTradingCampaignAthDesktop from 'assets/banners/kyberswap-trading-campaign-ath-desktop.png' -import KyberSwapTradingCampaignAthMobile from 'assets/banners/kyberswap-trading-campaign-ath-mobile.png' -import KyberSwapTradingCampaignAthTablet from 'assets/banners/kyberswap-trading-campaign-ath-tablet.png' +import KyberSwapTradingCampaignDesktop from 'assets/banners/kyberswap-trading-campaign-polygon-desktop.png' +import KyberSwapTradingCampaignMobile from 'assets/banners/kyberswap-trading-campaign-polygon-mobile.png' +import KyberSwapTradingCampaignTablet from 'assets/banners/kyberswap-trading-campaign-polygon-tablet.png' +import PolygonDesktop from 'assets/banners/polygon-desktop.png' +import PolygonMobile from 'assets/banners/polygon-mobile.png' +import PolygonTablet from 'assets/banners/polygon-tablet.png' import useTheme from 'hooks/useTheme' import { useWindowSize } from 'hooks/useWindowSize' import { ExternalLink } from 'theme' @@ -58,6 +61,7 @@ const Wrapper = styled.div` img { border-radius: 8px; } + @media screen and (min-width: 1100px) { max-width: 1054px; } @@ -100,17 +104,25 @@ function Banner({ const ALL_BANNERS = [ { // KyberSwap Trading Campaign ATH - id: 'kyberSwap-trading-campaign-ath', - start: new Date('2022-08-08T09:00:00.000Z'), - end: new Date('2022-08-11T23:59:59.000Z'), + id: 'kyberSwap-trading-campaign-polygon', + start: new Date('2022-08-15T11:00:00.000Z'), + end: new Date('2022-08-30T23:59:59.000Z'), img: isInModal - ? KyberSwapTradingCampaignAthMobile + ? KyberSwapTradingCampaignMobile : w > 768 - ? KyberSwapTradingCampaignAthDesktop + ? KyberSwapTradingCampaignDesktop : w > 500 - ? KyberSwapTradingCampaignAthTablet - : KyberSwapTradingCampaignAthMobile, - link: 'https://kyberswap.com/campaigns?selectedCampaignId=3&networkId=56&utm_source=partner&utm_medium=banner&utm_campaign=athtradingcontest&utm_content=onsite', + ? KyberSwapTradingCampaignTablet + : KyberSwapTradingCampaignMobile, + link: 'https://kyberswap.com/campaigns/kyberswap-trading-campaigns-with-polygon-chain-5?networkId=137&utm_source=partner&utm_medium=banner&utm_campaign=polygontradingcontest&utm_content=onsite', + }, + { + // Polygon LM + id: 'polygon-lm', + start: new Date('2022-08-17T00:00:00.000Z'), + end: new Date('2022-09-17T00:00:00.000Z'), + img: isInModal ? PolygonMobile : w > 768 ? PolygonDesktop : w > 500 ? PolygonTablet : PolygonMobile, + link: 'https://kyberswap.com/farms?tab=elastic&networkId=137', }, ] diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 380434cfba..92ff2cd211 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -220,7 +220,6 @@ export const ButtonWhite = styled(Base)` color: black; &:focus { - // eslint-disable-next-line @typescript-eslint/no-unused-vars box-shadow: 0 0 0 1pt ${darken(0.05, '#edeef2')}; } &:hover { diff --git a/src/components/Collapse.tsx b/src/components/Collapse.tsx new file mode 100644 index 0000000000..0808c9f904 --- /dev/null +++ b/src/components/Collapse.tsx @@ -0,0 +1,117 @@ +import React, { CSSProperties, ReactNode, useState } from 'react' +import { ChevronUp } from 'react-feather' +import styled from 'styled-components' + +const ItemWrapper = styled.div` + position: relative; + padding: 16px 24px; + width: 100%; + background: ${({ theme }) => theme.background}; +` + +const Header = styled.div` + width: 100%; + height: 32px; + + display: flex; + align-items: center; + justify-content: space-between; + + cursor: pointer; +` + +const ArrowWrapper = styled.div` + width: 32px; + height: 32px; + + display: flex; + justify-content: center; + align-items: center; + + color: ${({ theme }) => theme.text}; + + svg { + transition: all 150ms ease-in-out; + } + + &[data-expanded='false'] { + svg { + transform: rotate(180deg); + } + } +` + +const ContentWrapper = styled.div` + width: 100%; + + &[data-expanded='false'] { + display: none; + } +` + +type Props = { + header: string | JSX.Element + expandedOnMount?: boolean + style?: CSSProperties + activeStyle?: CSSProperties + children: ReactNode + onExpand?: () => void +} + +export const CollapseItem: React.FC = ({ header, children, expandedOnMount = false, style = {} }) => { + const [isExpanded, setExpanded] = useState(expandedOnMount) + + return ( + +
{ + setExpanded(e => !e) + }} + > + {header} + + + +
+ {children} +
+ ) +} + +export type ToggleItemType = { title: React.ReactNode; content: ReactNode | string } +// open one, close the others +export const ToggleCollapse = ({ + data, + itemActiveStyle = {}, + itemStyle = {}, +}: { + data: ToggleItemType[] + itemActiveStyle?: CSSProperties + itemStyle?: CSSProperties +}) => { + const [expandedIndex, setExpandedIndex] = useState(0) + return ( +
+ {data.map((item, index) => { + const isActive = expandedIndex === index + return ( + +
{ + setExpandedIndex(isActive ? -1 : index) + }} + > + {item.title} + + + +
+ {item.content} +
+ ) + })} +
+ ) +} + +export default ToggleCollapse diff --git a/src/components/FarmIssueAnnouncement.tsx b/src/components/FarmIssueAnnouncement.tsx new file mode 100644 index 0000000000..a7e85ffa08 --- /dev/null +++ b/src/components/FarmIssueAnnouncement.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' +import useTheme from 'hooks/useTheme' +import { ButtonText, ExternalLink } from 'theme' + +const Wrapper = styled.div` + border-radius: 20px; + padding: 16px; + font-size: 14px; + border: 1px solid ${({ theme }) => theme.warning}; + margin-bottom: 16px; + color: ${({ theme }) => theme.subText}; + + li { + line-height: 20px; + } +` + +function FarmIssueAnnouncement() { + const theme = useTheme() + const [show, setShow] = useState(true) + return ( + + + + + + + + Important Announcement + + + + setShow(prev => !prev)} style={{ color: theme.subText }}> + + + + + {show && ( + <> + +
  • + We recently discovered an issue in our Elastic farming contract where you might not be able to harvest + your rewards or withdraw your liquidity positions like you{' '} + + normally + {' '} + would +
  • +
  • + Dont worry, your funds are{' '} + + 100% safe + + . And you are still earning farming rewards{' '} +
  • +
  • + If you still wish to withdraw your liquidity positions, you can use the{' '} + + Force Withdraw + {' '} + button as an emergency option. (Note: If you do this, your farming rewards will{' '} + + not + {' '} + be automatically harvested but we can{' '} + + manually transfer + {' '} + your farming rewards to you) +
  • +
  • + You can get in touch with us by joining our{' '} + Discord channel ↗ and we will assist you + with your questions or transfer of rewards. +
  • +
  • + We will soon deploy a{' '} + + new + {' '} + Elastic farming contract, and you will be able to migrate your liquidity positions into this contract to + continue earning rewards +
  • +
    + + + We really apologize for the trouble. + + + )} +
    + ) +} + +export default FarmIssueAnnouncement diff --git a/src/components/Header/TopBanner.tsx b/src/components/Header/TopBanner.tsx index 0c15a2b130..93cb410023 100644 --- a/src/components/Header/TopBanner.tsx +++ b/src/components/Header/TopBanner.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react' +import { rgba } from 'polished' +import { useEffect, useState } from 'react' import { X } from 'react-feather' import { useLocalStorage, useMedia } from 'react-use' import { Text } from 'rebass' @@ -9,11 +10,14 @@ import { ExternalLink } from 'theme' const BannerWrapper = styled.div` width: 100%; - padding: 10px 20px; - background: #1d7a5f; + padding: 10px 12px 10px 20px; + background: ${({ theme }) => rgba(theme.warning, 0.7)}; display: flex; align-items: center; justify-content: space-between; + ${({ theme }) => theme.mediaWidth.upToSmall` + align-items: flex-start; + `} ` const StyledClose = styled(X)` @@ -28,25 +32,28 @@ const Content = styled.div` align-items: center; justify-content: center; border-radius: 4px; - padding: 8px 16px; - background: ${({ theme }) => `${theme.buttonBlack}1a`}; + gap: 8px; ${({ theme }) => theme.mediaWidth.upToSmall` - background: transparent; - padding: 2px 0; font-size: 14px; + align-items: flex-start; + flex: 1; `} ` const banner = { - localStorageKey: 'benqi-lm', + localStorageKey: 'farm-issue', start: 'Thu, 17 Mar 2022 00:00:00 GMT', - end: 'Thu, 20 Mar 2022 00:00:00 GMT', + end: 'Thu, 20 Mar 2024 00:00:00 GMT', text: ( - - Liquidity Mining with Benqi is LIVE! Find out more{' '} - - here + + Important Announcement: If you’re currently participating in our Elastic Farms on Polygon & Avalanche, please read + this{' '} + + announcement! ), @@ -70,11 +77,11 @@ function TopBanner() { {!below768 &&
    } - {!below768 && } + {banner.text} - setShowBanner(false)} /> + setShowBanner(false)} /> ) diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 0cb1201eb6..381726f35c 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,7 +1,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' import { Trans } from '@lingui/macro' import { darken } from 'polished' -import React, { useState } from 'react' +import { useState } from 'react' import { Repeat } from 'react-feather' import { Link, NavLink, useLocation } from 'react-router-dom' import { Flex } from 'rebass' @@ -14,6 +14,7 @@ import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' import DiscoverIcon from 'components/Icons/DiscoverIcon' import Menu, { NewLabel } from 'components/Menu' import Settings from 'components/Settings' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import Web3Network from 'components/Web3Network' import { PROMM_ANALYTICS_URL } from 'constants/index' import { useActiveWeb3React } from 'hooks' @@ -139,9 +140,9 @@ const AnalyticsWrapper = styled.span` ` const DiscoverWrapper = styled.span` - @media (max-width: 576px) { + ${({ theme }) => theme.mediaWidth.upToExtraSmall` display: none; - } + `}; ` const CampaignWrapper = styled.span`` @@ -249,10 +250,9 @@ const StyledNavExternalLink = styled(ExternalLink).attrs({ ` const YieldMenuWrapper = styled.div` - @media (max-width: 576px) { - display: none; - } - + ${({ theme }) => theme.mediaWidth.upToExtraSmall` + display: none; + `} position: relative; ` @@ -391,45 +391,47 @@ export default function Header() { - - - Earn - - - - Boolean(match) || pathname.startsWith('/pools')} - style={{ width: '100%' }} - > - Pools - + + + + Earn + + + + Boolean(match) || pathname.startsWith('/pools')} + style={{ width: '100%' }} + > + Pools + - - Boolean(match) || - pathname.startsWith('/add') || - pathname.startsWith('/remove') || - pathname.startsWith('/create') || - (pathname.startsWith('/find') && pathname.endsWith('find')) - } - > - My Pools - - - + + Boolean(match) || + pathname.startsWith('/add') || + pathname.startsWith('/remove') || + pathname.startsWith('/create') || + (pathname.startsWith('/find') && pathname.endsWith('find')) + } + > + My Pools + + + - - Boolean(match)}> - Farm - - + + Boolean(match)}> + Farm + + + {!under369 && ( - + Boolean(match)}> Campaigns {!under500 && ( @@ -441,7 +443,7 @@ export default function Header() { )} - + Boolean(match)} diff --git a/src/components/Icons/Withdraw.tsx b/src/components/Icons/Withdraw.tsx index 41a61c3bc3..83cb7fdd52 100644 --- a/src/components/Icons/Withdraw.tsx +++ b/src/components/Icons/Withdraw.tsx @@ -1,5 +1,3 @@ -import React from 'react' - const Withdraw = ({ width, height }: { width?: number; height?: number }) => { return ( diff --git a/src/components/Identicon/index.tsx b/src/components/Identicon/index.tsx index 78278fe0d4..af35039d0d 100644 --- a/src/components/Identicon/index.tsx +++ b/src/components/Identicon/index.tsx @@ -1,8 +1,8 @@ import Jazzicon from 'jazzicon' -import React, { useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import styled from 'styled-components' -import { useActiveWeb3React } from '../../hooks' +import { useActiveWeb3React } from 'hooks' const StyledIdenticonContainer = styled.div` height: 1rem; diff --git a/src/components/LiveChart/LineChart.tsx b/src/components/LiveChart/LineChart.tsx index 133bd7ed4c..655d721ad9 100644 --- a/src/components/LiveChart/LineChart.tsx +++ b/src/components/LiveChart/LineChart.tsx @@ -226,7 +226,6 @@ const LineChart = ({ ( - // eslint-disable-next-line react/prop-types )} cursor={} diff --git a/src/components/MenuFlyout/index.tsx b/src/components/MenuFlyout/index.tsx index 49e9a1a8a9..d1f10a434a 100644 --- a/src/components/MenuFlyout/index.tsx +++ b/src/components/MenuFlyout/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { BrowserView, MobileView, isMobile } from 'react-device-detect' +import { isMobile } from 'react-device-detect' import { Text } from 'rebass' import styled, { css } from 'styled-components' @@ -95,25 +95,22 @@ const MenuFlyout = (props: { }) => { useOnClickOutside(props.node, props.isOpen && !isMobile ? props.toggle : undefined) if (!props.isOpen) return null - return ( - <> - - + if (isMobile) + return ( + + {props.children} - - - - - - - {props.children} - - - - - + + + ) + return ( + + + {props.children} + + ) } diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index f9e22451d9..0499a10212 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -8,7 +8,7 @@ import { useGesture } from 'react-use-gesture' import styled from 'styled-components' const AnimatedDialogOverlay = animated(DialogOverlay) -// eslint-disable-next-line @typescript-eslint/no-unused-vars + const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ zindex: string | number }>` &[data-reach-dialog-overlay] { z-index: ${({ zindex }) => zindex}; @@ -25,10 +25,11 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ zindex: string | num const AnimatedDialogContent = animated(DialogContent) // destructure to not pass custom props to Dialog DOM element -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const StyledDialogContent = styled(({ minHeight, maxHeight, maxWidth, width, mobile, isOpen, ...rest }) => ( - -)).attrs({ +const StyledDialogContent = styled( + ({ borderRadius, minHeight, maxHeight, maxWidth, width, mobile, isOpen, ...rest }) => ( + + ), +).attrs({ 'aria-label': 'dialog', })` overflow-y: ${({ mobile }) => (mobile ? 'scroll' : 'hidden')}; @@ -54,18 +55,22 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, maxWidth, width, mob min-height: ${minHeight}vh; `} display: flex; - border-radius: 20px; + ${({ borderRadius }) => + borderRadius && + ` + border-radius: ${borderRadius}; + `} ${({ theme, width }) => theme.mediaWidth.upToMedium` width: ${width || '65vw'}; margin: 0; `} - ${({ theme, mobile }) => theme.mediaWidth.upToSmall` + ${({ theme, mobile, borderRadius }) => theme.mediaWidth.upToSmall` width: 85vw; ${ mobile && ` width: 100vw; - border-radius: 20px; + border-radius: ${borderRadius}; border-bottom-left-radius: 0; border-bottom-right-radius: 0; ` @@ -80,6 +85,7 @@ export interface ModalProps { minHeight?: number | false maxHeight?: number | string maxWidth?: number | string + borderRadius?: number | string width?: string zindex?: number | string enableInitialFocusInput?: boolean @@ -100,6 +106,7 @@ export default function Modal({ children, transition = true, zindex = 100, + borderRadius = '20px', }: ModalProps) { const fadeTransition = useTransition(isOpen, { config: { duration: transition ? 200 : 0 }, @@ -119,7 +126,6 @@ export default function Modal({ } }, }) - return ( <> {fadeTransition( @@ -138,6 +144,7 @@ export default function Modal({ maxHeight={maxHeight} maxWidth={maxWidth} width={width} + borderRadius={borderRadius} mobile={isMobile} className={className} > diff --git a/src/components/PoolList/index.tsx b/src/components/PoolList/index.tsx index 245c70c33f..177bae8db2 100644 --- a/src/components/PoolList/index.tsx +++ b/src/components/PoolList/index.tsx @@ -68,6 +68,7 @@ interface PoolListProps { searchValue: string isShowOnlyActiveFarmPools: boolean onlyShowStable: boolean + shouldShowLowTVLPools: boolean } const SORT_FIELD = { @@ -79,7 +80,13 @@ const SORT_FIELD = { const ITEM_PER_PAGE = 8 -const PoolList = ({ currencies, searchValue, isShowOnlyActiveFarmPools, onlyShowStable }: PoolListProps) => { +const PoolList = ({ + currencies, + searchValue, + isShowOnlyActiveFarmPools, + onlyShowStable, + shouldShowLowTVLPools, +}: PoolListProps) => { const above1000 = useMedia('(min-width: 1000px)') const [sortDirection, setSortDirection] = useState(true) @@ -259,6 +266,10 @@ const PoolList = ({ currencies, searchValue, isShowOnlyActiveFarmPools, onlyShow res = res.filter(poolData => farmAddresses.includes(poolData.id)) } + if (!shouldShowLowTVLPools) { + res = res.filter(poolData => parseFloat(poolData.reserveUSD) > 1) + } + const ca = currencies[Field.CURRENCY_A] const cb = currencies[Field.CURRENCY_B] @@ -299,14 +310,15 @@ const PoolList = ({ currencies, searchValue, isShowOnlyActiveFarmPools, onlyShow return res }, [ - chainId, - onlyShowStable, subgraphPoolsData, listComparator, - currencies, - searchValue, isShowOnlyActiveFarmPools, + shouldShowLowTVLPools, + currencies, + onlyShowStable, farms, + searchValue, + chainId, ]) const [currentPage, setCurrentPage] = useState(1) diff --git a/src/components/Popups/TransactionPopup.tsx b/src/components/Popups/TransactionPopup.tsx index c1c99d074a..0f975ec93c 100644 --- a/src/components/Popups/TransactionPopup.tsx +++ b/src/components/Popups/TransactionPopup.tsx @@ -123,6 +123,12 @@ export const SUMMARY: { pending: summary => 'Withdrawing ' + summary, failure: summary => 'Error withdrawing ' + summary, }, + + ForceWithdraw: { + success: () => 'Force Withdrawn ', + pending: () => 'Force Withdrawing ', + failure: () => 'Error Force withdrawing ', + }, } export default function TransactionPopup({ @@ -165,10 +171,10 @@ export default function TransactionPopup({ {chainId && ( - + {getEtherscanLinkText(chainId)} diff --git a/src/components/Popups/index.tsx b/src/components/Popups/index.tsx index 231fdb4c97..ffd134ee8c 100644 --- a/src/components/Popups/index.tsx +++ b/src/components/Popups/index.tsx @@ -9,7 +9,6 @@ const FixedPopupColumn = styled.div` position: fixed; top: 108px; right: 1rem; - width: 100%; z-index: ${Z_INDEXS.POPUP_NOTIFICATION}; display: flex; flex-direction: column; diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index 288a699965..3b9c75074e 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -3,19 +3,24 @@ import { useLingui } from '@lingui/react' import React, { useEffect, useRef, useState } from 'react' import { isMobile } from 'react-device-detect' import { Settings } from 'react-feather' +import { useLocation } from 'react-router-dom' import styled, { css } from 'styled-components' import ArrowRight from 'components/Icons/ArrowRight' import LanguageSelector from 'components/LanguageSelector' import MenuFlyout from 'components/MenuFlyout' import ThemeToggle from 'components/Toggle/ThemeToggle' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import { LOCALE_LABEL, SupportedLocale } from 'constants/locales' +import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' +import { AppPaths } from 'pages/App' +import { useTutorialSwapGuide } from 'state/tutorial/hooks' import { useDarkModeManager, useUserLocale } from 'state/user/hooks' import { ApplicationModal } from '../../state/application/actions' import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks' -import { ButtonEmpty } from '../Button' +import { ButtonEmpty, ButtonLight } from '../Button' import { AutoColumn } from '../Column' import { RowBetween, RowFixed } from '../Row' @@ -83,6 +88,13 @@ const StyledLabel = styled.div` font-weight: 400; line-height: 20px; ` + +const ButtonViewGuide = styled(ButtonLight)` + display: flex; + align-items: center; + padding: 2px 5px; + width: 55px; +` export default function SettingsTab() { const theme = useTheme() const [darkMode, toggleSetDarkMode] = useDarkModeManager() @@ -98,10 +110,19 @@ export default function SettingsTab() { if (!open) setIsSelectingLanguage(false) }, [open]) + const { mixpanelHandler } = useMixpanel() + const setShowTutorialSwapGuide = useTutorialSwapGuide()[1] + const openTutorialSwapGuide = () => { + setShowTutorialSwapGuide({ show: true, step: 0 }) + mixpanelHandler(MIXPANEL_TYPE.TUTORIAL_CLICK_START) + toggle() + } + const location = useLocation() + const isShowTutorialBtn = location.pathname.startsWith(AppPaths.SWAP) return ( // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 - + + {isShowTutorialBtn && ( + + + + KyberSwap Guide + + + + + View + + + + )} + {!isSelectingLanguage ? ( <> diff --git a/src/components/TopTrendingSoonTokensInCurrentNetwork/TopTrendingSoonTokenItem.tsx b/src/components/TopTrendingSoonTokensInCurrentNetwork/TopTrendingSoonTokenItem.tsx index 58bea58811..3123d9bbac 100644 --- a/src/components/TopTrendingSoonTokensInCurrentNetwork/TopTrendingSoonTokenItem.tsx +++ b/src/components/TopTrendingSoonTokensInCurrentNetwork/TopTrendingSoonTokenItem.tsx @@ -48,20 +48,36 @@ const TopTrendingSoonTokenItem = ({ width="16px" minHeight="16px" height="16px" - style={{ borderRadius: '50%', cursor: 'pointer' }} + sx={{ borderRadius: '50%', cursor: 'pointer' }} onClick={onSelectToken} /> {tokenData.symbol} - {formattedNum(tokenData.price.toString(), true)} - = 0 ? theme.apr : theme.red}> + + {formattedNum(tokenData.price.toString(), true)} + + = 0 ? theme.apr : theme.red, + }} + > ( {tokenData.price_change_percentage_24h >= 1 ? formatNumberWithPrecisionRange(tokenData.price_change_percentage_24h, 0, 0) diff --git a/src/components/TradingViewChart/datafeed.tsx b/src/components/TradingViewChart/datafeed.tsx index d62061fb1d..8d29675fa7 100644 --- a/src/components/TradingViewChart/datafeed.tsx +++ b/src/components/TradingViewChart/datafeed.tsx @@ -86,7 +86,7 @@ const TOKEN_PAIRS_ADDRESS_MAPPING: { '0x7f5c764cbc14f9669b88837ca1490cca17c31607': '0x1a981daa7967c66c3356ad044979bc82e4a478b9', // ETH_USD Optimism '0xe9e7cea3dedca5984780bafc599bd69add087d56': '0x58f876857a02d6762e0101bb5c46a8c1ed44dc16', // BNB_BUSD '0x853ea32391aaa14c112c645fd20ba389ab25c5e0': '0x5d79a43e6b9d8e3ecca26f91afe34634248773c8', // USX on AVAX - '0x261c94f2d3cccae76f86f6c8f2c93785dd6ce022': '0xa7591a7891909b92104394329d133a96a049466a', // ATH on BSC + '0x261c94f2d3cccae76f86f6c8f2c93785dd6ce022': 'nodata', // ATH on BSC } const LOCALSTORAGE_CHECKED_PAIRS = 'proChartCheckedPairs' diff --git a/src/components/TransactionConfirmationModal/index.tsx b/src/components/TransactionConfirmationModal/index.tsx index dbf7059afd..137a3b728f 100644 --- a/src/components/TransactionConfirmationModal/index.tsx +++ b/src/components/TransactionConfirmationModal/index.tsx @@ -1,5 +1,5 @@ import { ChainId, Currency, Token } from '@kyberswap/ks-sdk-core' -import { Trans } from '@lingui/macro' +import { Trans, t } from '@lingui/macro' import React, { useState } from 'react' import { ArrowUpCircle } from 'react-feather' import { Text } from 'rebass' @@ -7,10 +7,11 @@ import styled from 'styled-components' import { ReactComponent as Alert } from 'assets/images/alert.svg' import Circle from 'assets/images/blue-loader.svg' -import MetaMaskLogo from 'assets/images/metamask.png' import Banner from 'components/Banner' +import { SUPPORTED_WALLETS } from 'constants/index' import { useActiveWeb3React } from 'hooks' import useTheme from 'hooks/useTheme' +import { useIsDarkMode } from 'state/user/hooks' import { ExternalLink } from 'theme' import { CloseIcon, CustomLightSpinner } from 'theme/components' import { getEtherscanLink, getEtherscanLinkText, getTokenLogoURL } from 'utils' @@ -44,6 +45,38 @@ const StyledLogo = styled.img` width: 16px; margin-left: 6px; ` + +const getBrowserWalletConfig = () => { + const { ethereum } = window + const hasInjectedWallet = !!ethereum + const { isCoin98, isBraveWallet, isMetaMask } = ethereum || {} + + if (hasInjectedWallet) { + if (isCoin98) { + const { name, iconName } = SUPPORTED_WALLETS.COIN98 + return { name, iconName } + } + + if (isBraveWallet) { + const { name, iconName } = SUPPORTED_WALLETS.BRAVE + return { name, iconName } + } + + if (isMetaMask) { + const { name, iconName } = SUPPORTED_WALLETS.METAMASK + return { name, iconName } + } + + const config = SUPPORTED_WALLETS.INJECTED + return { + name: t`your wallet`, + iconName: config.iconName, + } + } + + return undefined +} + function ConfirmationPendingContent({ onDismiss, pendingText, @@ -79,17 +112,18 @@ function ConfirmationPendingContent({ ) } -function AddTokenToMetaMask({ token, chainId }: { token: Token; chainId: ChainId }) { - async function addToMetaMask() { +function AddTokenToInjectedWallet({ token, chainId }: { token: Token; chainId: ChainId }) { + const isDarkMode = useIsDarkMode() + + const handleClick = async () => { const tokenAddress = token.address const tokenSymbol = token.symbol const tokenDecimals = token.decimals const tokenImage = getTokenLogoURL(token.address, chainId) try { - const { ethereum } = window - const isMetaMask = !!(ethereum && ethereum.isMetaMask) - if (isMetaMask) { + const hasInjectedWallet = !!window.ethereum + if (hasInjectedWallet) { await (window.ethereum as any).request({ method: 'wallet_watchAsset', params: { @@ -108,10 +142,20 @@ function AddTokenToMetaMask({ token, chainId }: { token: Token; chainId: ChainId } } + const walletConfig = getBrowserWalletConfig() + if (!walletConfig) { + return null + } + return ( - + - Add {token.symbol} to Metamask + + Add {token.symbol} to {walletConfig.name} + {' '} + ) @@ -131,6 +175,8 @@ function TransactionSubmittedContent({ showTxBanner?: boolean }) { const theme = useTheme() + const hasInjectedWallet = !!window.ethereum + return (
    @@ -149,7 +195,7 @@ function TransactionSubmittedContent({ - + Transaction Submitted @@ -160,9 +206,11 @@ function TransactionSubmittedContent({
    )} - {tokenAddToMetaMask?.address && } - - + {hasInjectedWallet && tokenAddToMetaMask?.address && ( + + )} + + Close diff --git a/src/components/Tutorial/TutorialSwap/CustomMask.tsx b/src/components/Tutorial/TutorialSwap/CustomMask.tsx new file mode 100644 index 0000000000..65c41cd016 --- /dev/null +++ b/src/components/Tutorial/TutorialSwap/CustomMask.tsx @@ -0,0 +1,107 @@ +import styled, { DefaultTheme, keyframes } from 'styled-components' +import { MaskOptions } from 'walktour' + +import { ReactComponent as TouchIcon } from 'assets/svg/touch_icon.svg' + +import { StepCustom } from './constant' + +const pointerToSetting = keyframes` + from { + transform: translateY(25px) + } + to { + transform: translateY(0px); + } +` + +const TouchIconWrapper = styled(TouchIcon)` + position: absolute; + animation-fill-mode: forwards; + animation: ${pointerToSetting} 3s; + bottom: -10px; + left: 0; + right: 0; + margin: auto; + path { + stroke: ${({ theme }) => theme.text}; + fill: ${({ theme }) => theme.text}; + } +` + +const highlightSpotLight = (theme: DefaultTheme, blurWidth: number, hasSpotlight: boolean) => keyframes` + 0% { + box-shadow: ${[ + '0px 0px 4px 2px rgba(0,0,0,0.45) inset', + hasSpotlight && `0 0 4px 1px ${theme.primary}`, + `0px 0px 0px ${blurWidth}px rgba(0,0,0,0.45)`, + ] + .filter(Boolean) + .join(', ')} + } + 100% { + box-shadow: ${[ + '0px 0px 4px 2px rgba(0,0,0,0.45) inset', + hasSpotlight && `0 0 8px 2px ${theme.primary}`, + `0px 0px 0px ${blurWidth}px rgba(0,0,0,0.45)`, + ] + .filter(Boolean) + .join(', ')} + } +` +const SpotLight = styled.div<{ blurWidth: number; hasSpotlight: boolean }>` + position: absolute; + animation: ${({ theme, blurWidth, hasSpotlight }) => highlightSpotLight(theme, blurWidth, hasSpotlight)} 2s infinite + alternate; + border-radius: 30px; + transition: 0.3s; +` +type Props = { options: MaskOptions; stepInfo: StepCustom } + +function CustomMask({ options, stepInfo }: Props) { + const { hasPointer, spotlightInteraction, selector, stopPropagationMouseDown } = stepInfo || ({} as StepCustom) + const { targetInfo, padding = 0, tourRoot, disableMaskInteraction } = options + const containerHeight = document.body.scrollHeight + const containerWidth = tourRoot.scrollWidth + const { coords, dims } = targetInfo || { coords: { x: 0, y: 0 }, dims: { width: 0, height: 0 } } + const width = dims.width + 2 * padding + const height = dims.height + 2 * padding + + const onClickSpotlight = () => { + if (!spotlightInteraction) return + const element: HTMLDivElement | null = document.querySelector(selector) + element?.click() + } + + return ( +
    stopPropagationMouseDown && e.stopPropagation()} + > + + {hasPointer && } + +
    + ) +} + +export default CustomMask diff --git a/src/components/Tutorial/TutorialSwap/CustomPopup.tsx b/src/components/Tutorial/TutorialSwap/CustomPopup.tsx new file mode 100644 index 0000000000..9d7fe0a7c4 --- /dev/null +++ b/src/components/Tutorial/TutorialSwap/CustomPopup.tsx @@ -0,0 +1,200 @@ +import { Trans, t } from '@lingui/macro' +import { CSSProperties, useMemo } from 'react' +import { X } from 'react-feather' +import { Flex, Text } from 'rebass' +import styled, { css } from 'styled-components' +import { CardinalOrientation, OrientationCoords, WalktourLogic } from 'walktour' + +import { ButtonPrimary } from 'components/Button' +import useTheme from 'hooks/useTheme' + +import { StepCustom, TOTAL_STEP } from './constant' + +const PopupWrapper = styled.div<{ center: boolean }>` + background-color: ${({ theme }) => theme.tableHeader}; + padding: 20px; + border-radius: 20px; + position: relative; + + ${({ center }) => + center && + css` + width: 500px; + position: fixed; + left: 0; + right: 0; + margin: auto; + top: 50%; + transform: translateY(-50%); + `}; +` +type Direction = 'left' | 'right' | 'bottom' | 'top' + +const ARROW_SIZE = 16 +const BORDER_TRANSPARENT = `${ARROW_SIZE}px solid transparent` + +const getStyleArrow = (arrowColor: string, dir: string) => + ({ + left: { + borderTop: BORDER_TRANSPARENT, + borderRight: arrowColor, + borderBottom: BORDER_TRANSPARENT, + }, + right: { + borderBottom: BORDER_TRANSPARENT, + borderLeft: arrowColor, + borderTop: BORDER_TRANSPARENT, + }, + top: { + borderLeft: BORDER_TRANSPARENT, + borderBottom: arrowColor, + borderRight: BORDER_TRANSPARENT, + }, + bottom: { + borderLeft: BORDER_TRANSPARENT, + borderTop: arrowColor, + borderRight: BORDER_TRANSPARENT, + }, + }[dir]) + +const Arrow = ({ + color, + tooltipPosition, + target, +}: { + tooltipPosition: OrientationCoords + color: string + target: string +}) => { + const targetElement = useMemo(() => { + return document.querySelector(target) + }, [target]) + + if (!tooltipPosition?.orientation) return null + const { orientation } = tooltipPosition + let arrowDir: Direction = 'top' + let position: CSSProperties = {} + + const paddingV = (targetElement?.clientHeight || 0) / 2 ?? 15 + const paddingH = (targetElement?.clientWidth || 0) / 2 ?? 15 + switch (orientation) { + case CardinalOrientation.SOUTH: + position = { display: 'flex', justifyContent: 'center', top: -ARROW_SIZE, left: 0, right: 0 } + arrowDir = 'top' + break + case CardinalOrientation.SOUTHEAST: + position = { right: paddingH, top: -ARROW_SIZE } + arrowDir = 'top' + break + case CardinalOrientation.SOUTHWEST: + position = { top: -ARROW_SIZE, left: paddingH } + arrowDir = 'top' + break + case CardinalOrientation.EAST: + position = { left: -ARROW_SIZE, top: 0, bottom: 0, display: 'flex', alignItems: 'center' } + arrowDir = 'left' + break + case CardinalOrientation.EASTNORTH: + position = { left: -ARROW_SIZE, top: paddingV } + arrowDir = 'left' + break + case CardinalOrientation.EASTSOUTH: + position = { left: -ARROW_SIZE, bottom: paddingV } + arrowDir = 'left' + break + case CardinalOrientation.WESTNORTH: + position = { right: -ARROW_SIZE, top: paddingV } + arrowDir = 'right' + break + case CardinalOrientation.WEST: + position = { right: -ARROW_SIZE, top: 0, bottom: 0, display: 'flex', alignItems: 'center' } + arrowDir = 'right' + break + case CardinalOrientation.WESTSOUTH: + position = { right: -ARROW_SIZE, bottom: paddingV } + arrowDir = 'right' + break + case CardinalOrientation.NORTHWEST: + position = { bottom: -ARROW_SIZE, left: paddingH } + arrowDir = 'bottom' + break + case CardinalOrientation.NORTH: + position = { display: 'flex', justifyContent: 'center', bottom: -ARROW_SIZE, left: 0, right: 0 } + arrowDir = 'bottom' + break + case CardinalOrientation.NORTHEAST: + position = { bottom: -ARROW_SIZE, right: paddingH } + arrowDir = 'bottom' + break + case CardinalOrientation.CENTER: + return null + } + + return ( +
    +
    +
    + ) +} + +export default function CustomPopup(props: WalktourLogic | undefined): JSX.Element { + const { stepContent, stepIndex, next, prev, close } = props || ({} as WalktourLogic) + const theme = useTheme() + const { + customFooterRenderer, + popupStyle, + title, + center = false, + stopPropagationMouseDown, + } = stepContent as StepCustom + const isLastStep = stepIndex - 1 === TOTAL_STEP + return ( + { + const target = e.target as HTMLButtonElement | HTMLDivElement + // not next/back button + if (stopPropagationMouseDown && !target.classList.contains('action-walktour')) { + e.stopPropagation() + } + }} + > + + + {title} + + close()} /> + + +
    {stepContent.description}
    + {customFooterRenderer ? ( + customFooterRenderer(props) + ) : ( + + {stepIndex > 0 && ( + prev()} + style={{ cursor: 'pointer', color: theme.primary, marginRight: 30, fontSize: 14 }} + > + Back + + )} + next()} style={{ width: 72, height: 36 }}> + {isLastStep ? t`Done` : t`Next`} + + + )} +
    + ) +} diff --git a/src/components/Tutorial/TutorialSwap/TutorialMobile.tsx b/src/components/Tutorial/TutorialSwap/TutorialMobile.tsx new file mode 100644 index 0000000000..64d9593c83 --- /dev/null +++ b/src/components/Tutorial/TutorialSwap/TutorialMobile.tsx @@ -0,0 +1,56 @@ +import { Trans } from '@lingui/macro' +import React from 'react' +import { X } from 'react-feather' +import { Flex, Text } from 'rebass' +import styled from 'styled-components' + +import ToggleCollapse, { ToggleItemType } from 'components/Collapse' +import { Z_INDEXS } from 'constants/styles' +import useTheme from 'hooks/useTheme' + +import { TOTAL_STEP } from './constant' + +const Wrapper = styled.div` + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: ${Z_INDEXS.MOBILE_MODAL}; + position: fixed; + background: ${({ theme }) => theme.buttonGray}; +` +export default function TutorialMobile({ + stopTutorial, + steps, + isOpen, +}: { + isOpen: boolean + stopTutorial: () => void + steps: ToggleItemType[] +}) { + const theme = useTheme() + return ( + + +
    + + + Welcome to KyberSwap! + + + + + {TOTAL_STEP} easy ways to get started with KyberSwap + +
    +
    + +
    +
    +
    + ) +} diff --git a/src/components/Tutorial/TutorialSwap/constant.ts b/src/components/Tutorial/TutorialSwap/constant.ts new file mode 100644 index 0000000000..2d7c276994 --- /dev/null +++ b/src/components/Tutorial/TutorialSwap/constant.ts @@ -0,0 +1,48 @@ +import { t } from '@lingui/macro' +import { Step } from 'walktour' + +export interface StepCustom extends Step { + stopPropagationMouseDown?: boolean // stop Propagation onMouseDown event, ex: prevent Menufly setting auto close + center?: boolean // popup at center + hasPointer?: boolean + spotlightInteraction?: boolean + pcOnly?: boolean + popupStyle?: React.CSSProperties + requiredClickSelector?: string // click other element before enter this step + selectorHint?: string // this is element to check we clicked requiredClickSelector or not. + stepNumber?: number // for tracking, display + callbackEndStep?: () => void +} + +export const TOTAL_STEP = 8 + +// please do not remove TutorialIds.xxxxxx in some where to make sure tutorial work well +export const TutorialIds = { + BUTTON_CONNECT_WALLET: 'btnConnectWallet', + BUTTON_ADDRESS_WALLET: 'web3-status-connected', + SELECT_NETWORK: 'selectNetwork', + + SWAP_FORM: 'swapForm', + SWAP_FORM_CONTENT: 'swap-page', + TRADING_SETTING_CONTENT: 'tradingSettingContent', + + BUTTON_SETTING: 'open-settings-dialog-button', + BUTTON_SETTING_SWAP_FORM: 'btnSettingSwapForm', + + EARNING_LINKS: 'earningLinks', + DISCOVER_LINK: 'discoverLink', + CAMPAIGN_LINK: 'campaignLink', + BUTTON_VIEW_GUIDE_SWAP: 'btnViewGuideSwap', +} + +export const LIST_TITLE = { + YOUR_WALLET: t`Your wallet address`, + CONNECT_WALLET: t`Connect a wallet`, + SELECT_NETWORK: t`Select your network`, + START_TRADING: t`Select tokens to swap & start trading`, + SETTING: t`Customize your settings`, + EARN: t`Earn trading fees through our Pools / Farms`, + DISCOVER: t`Discover trending soon tokens`, + CAMPAIGN: t`Participate in our campaigns`, + VIEW_GUIDE: t`View our KyberSwap Guide again`, +} diff --git a/src/components/Tutorial/TutorialSwap/index.tsx b/src/components/Tutorial/TutorialSwap/index.tsx new file mode 100644 index 0000000000..16e74dd9cf --- /dev/null +++ b/src/components/Tutorial/TutorialSwap/index.tsx @@ -0,0 +1,532 @@ +import { Trans } from '@lingui/macro' +import React, { CSSProperties, memo, useEffect, useMemo, useRef, useState } from 'react' +import { BrowserView } from 'react-device-detect' +import { ChevronUp } from 'react-feather' +import { Flex } from 'rebass' +import styled, { createGlobalStyle } from 'styled-components' +import { CardinalOrientation, Step, Walktour, WalktourLogic } from 'walktour' + +import WelcomeImage from 'assets/images/tutorial_swap/welcome.png' +import { ButtonOutlined, ButtonPrimary } from 'components/Button' +import { ToggleItemType } from 'components/Collapse' +import { TutorialType, getTutorialVideoId } from 'components/Tutorial' +import { SUPPORTED_WALLETS } from 'constants/index' +import { useActiveWeb3React } from 'hooks' +import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' +import useTheme from 'hooks/useTheme' +import { useTutorialSwapGuide } from 'state/tutorial/hooks' +import { useIsDarkMode } from 'state/user/hooks' +import { ExternalLink } from 'theme' + +import CustomMask from './CustomMask' +import CustomPopup from './CustomPopup' +import TutorialMobile from './TutorialMobile' +import { LIST_TITLE, StepCustom, TOTAL_STEP, TutorialIds } from './constant' + +const isMobile = window.innerWidth < 1200 // best resolution for this tutorial + +const Heading = styled.h5` + color: ${({ theme }) => theme.text}; + user-select: none; + margin: 5px 0px 10px 0px; + display: flex; + align-items: center; + font-size: 16px; +` + +const LayoutWrapper = styled.div` + color: ${({ theme }) => theme.subText}; + text-align: left; + font-size: 14px; +` +const Title = ({ stepNumber }: { stepNumber: number }) => { + const theme = useTheme() + return ( + + + Step: {stepNumber}/ + {TOTAL_STEP} + + + ) +} + +const Layout = ({ children, title }: { title?: string; children: React.ReactNode }) => { + return ( + + {!isMobile && title && {title}} + {children} + + ) +} + +const ArrowWrapper = styled.div` + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + color: ${({ theme }) => theme.text}; + svg { + transition: all 150ms ease-in-out; + } + &[data-expanded='false'] { + svg { + transform: rotate(180deg); + } + } +` + +const NetworkItemWrapper = styled.div` + background: ${({ theme }) => theme.buttonBlack}; + border-radius: 42px; + display: flex; + padding: 10px 15px; + gap: 10px; + cursor: pointer; +` + +const NetworkWrapper = styled.div` + background: ${({ theme }) => theme.background}; + border-radius: 20px; + padding: 15px; + gap: 10px; + display: flex; + flex-direction: column; +` + +const ImageMobile = ({ imageName, marginTop = false }: { imageName: string; marginTop?: boolean }) => + isMobile ? ( + + {imageName} + + ) : null + +const Desc = styled.p` + line-height: 20px; +` + +const HighlightText = styled.span` + color: ${({ theme }) => theme.text}; +` +function Welcome() { + return ( + + welcome to kyberswap + + + KyberSwap is a decentralized exchange (DEX) aggregator. We provide our traders with the{' '} + best token prices by analyzing rates across thousands of exchanges instantly! + + + + + KyberSwap is also an automated market maker (AMM) with industry-leading liquidity protocols and{' '} + concentrated liquidity. Liquidity providers can add liquidity to our pools &{' '} + earn fees! + + + + + We created this quick tutorial guide for you to highlight KyberSwap's main + features. + + + + Do you wish to have a look? + + + ) +} + +function Step1() { + const [isExpanded, setIsExpanded] = useState(false) + const toggleExpand = () => setIsExpanded(!isExpanded) + const isDarkMode = useIsDarkMode() + return ( + + + Choose your preferred wallet, connect it, and get started with KyberSwap! + + + + + Download Wallet + + + + + {isExpanded && ( + + {Object.values(SUPPORTED_WALLETS) + .filter(e => e.installLink) + .map(item => ( + window.open(item.installLink)}> + {item.name} + {item.name} + + ))} + + )} + + + ) +} + +const TouchAbleVideo = styled.div` + position: absolute; + top: 0; + left: 0; + cursor: pointer; + width: 100%; + height: 100%; +` + +function Step3({ videoStyle = {} }: { videoStyle: CSSProperties }) { + const { mixpanelHandler } = useMixpanel() + const [playedVideo, setPlayedVideo] = useState(false) + const ref = useRef(null) + + const playVideo = () => { + const iframe = ref.current + if (iframe) { + // play video + iframe.setAttribute('src', iframe.getAttribute('src') + '?autoplay=1') + mixpanelHandler(MIXPANEL_TYPE.TUTORIAL_VIEW_VIDEO_SWAP) + setPlayedVideo(true) + } + } + + return ( + + + + Select from over thousands of tokens and start trading. KyberSwap finds you the best prices across multiple + exchanges & combines them into one trade! + + +
    + + {/** because we need tracking we user click video, iframe youtube not fire any event for us. */} + {!playedVideo && } +
    +
    + ) +} + +// override lib css +const CustomCss = createGlobalStyle` + [id^=walktour-tooltip-container]:focus-visible { + outline: none; + }; +` +const getListSteps = (isLogin: boolean) => { + const isHighlightBtnConnectWallet = !isLogin || isMobile + return [ + { + title: ( + + Welcome to KyberSwap! + + ), + customFooterRenderer: (logic: WalktourLogic) => ( + + logic.close()}> + Maybe later + + logic.next()}> + Let’s get started + + + ), + stepNumber: 0, + description: , + pcOnly: true, + center: true, + popupStyle: { width: 500 }, + }, + { + selector: isHighlightBtnConnectWallet ? TutorialIds.BUTTON_CONNECT_WALLET : TutorialIds.BUTTON_ADDRESS_WALLET, + title: isMobile ? ( + isHighlightBtnConnectWallet ? ( + LIST_TITLE.CONNECT_WALLET + ) : ( + LIST_TITLE.YOUR_WALLET + ) + ) : ( + + ), + stepNumber: 1, + description: <Step1 />, + orientationPreferences: [CardinalOrientation.SOUTHEAST, CardinalOrientation.NORTHWEST], + }, + { + selector: TutorialIds.SELECT_NETWORK, + title: isMobile ? LIST_TITLE.SELECT_NETWORK : <Title stepNumber={2} />, + stepNumber: 2, + description: ( + <Layout title={LIST_TITLE.SELECT_NETWORK}> + <Desc> + <Trans> + Choose your preferred network. KyberSwap is a multi chain platform that supports over 12 networks! + </Trans> + </Desc> + <ImageMobile imageName="step2.png" /> + </Layout> + ), + orientationPreferences: [CardinalOrientation.SOUTHEAST, CardinalOrientation.NORTHWEST], + }, + { + selector: TutorialIds.SWAP_FORM, + title: isMobile ? LIST_TITLE.START_TRADING : <Title stepNumber={3} />, + stepNumber: 3, + description: <Step3 videoStyle={{ minHeight: Math.min(window.innerHeight / 2, 500) }} />, + popupStyle: { width: Math.min(0.8 * window.innerWidth, 700) }, + requiredClickSelector: '#' + TutorialIds.BUTTON_SETTING_SWAP_FORM, + selectorHint: '#' + TutorialIds.SWAP_FORM_CONTENT, + }, + { + selector: TutorialIds.BUTTON_SETTING_SWAP_FORM, + title: isMobile ? LIST_TITLE.SETTING : <Title stepNumber={4} />, + stepNumber: 4, + maskPadding: 10, + description: ( + <Layout title={LIST_TITLE.SETTING}> + <Desc> + <Trans>You can customize advanced settings like slippage and other display settings here.</Trans> + </Desc> + <ImageMobile imageName="step4.1.png" /> + <ImageMobile imageName="step4.2.png" marginTop /> + </Layout> + ), + hasPointer: true, + orientationPreferences: [CardinalOrientation.EAST, CardinalOrientation.NORTH], + spotlightInteraction: true, + }, + { + selector: TutorialIds.SWAP_FORM, + title: isMobile ? LIST_TITLE.SETTING : <Title stepNumber={4} />, + stepNumber: 4, + requiredClickSelector: '#' + TutorialIds.BUTTON_SETTING_SWAP_FORM, + selectorHint: '#' + TutorialIds.TRADING_SETTING_CONTENT, + description: ( + <Layout title={LIST_TITLE.SETTING}> + <Desc> + <Trans>Adjust the advanced settings for your trades like the max slippage.</Trans> + </Desc> + <Desc> + <Trans>Personalize your trading interface in the display settings</Trans> + </Desc> + </Layout> + ), + pcOnly: true, + callbackEndStep: () => document.getElementById(TutorialIds.BUTTON_SETTING_SWAP_FORM)?.click(), + orientationPreferences: [CardinalOrientation.EAST, CardinalOrientation.NORTH], + maskPadding: 10, + }, + { + selector: TutorialIds.EARNING_LINKS, + title: isMobile ? LIST_TITLE.EARN : <Title stepNumber={5} />, + stepNumber: 5, + description: ( + <Layout title={LIST_TITLE.EARN}> + <Desc> + <Trans> + Add liquidity into our Pools to earn trading fees & participate in our Farms to earn additional rewards! + </Trans> + </Desc> + <ImageMobile imageName="step5.png" /> + </Layout> + ), + orientationPreferences: [CardinalOrientation.SOUTH], + }, + { + selector: TutorialIds.CAMPAIGN_LINK, + title: isMobile ? LIST_TITLE.CAMPAIGN : <Title stepNumber={6} />, + stepNumber: 6, + description: ( + <Layout title={LIST_TITLE.CAMPAIGN}> + <Desc> + <Trans>Check out our latest trading campaigns and participate in them to earn rewards!</Trans> + </Desc> + <ImageMobile imageName="menu.png" /> + <ImageMobile imageName="step7.png" marginTop /> + </Layout> + ), + orientationPreferences: [CardinalOrientation.SOUTH], + }, + { + selector: TutorialIds.DISCOVER_LINK, + title: isMobile ? LIST_TITLE.DISCOVER : <Title stepNumber={7} />, + stepNumber: 7, + description: ( + <Layout title={LIST_TITLE.DISCOVER}> + <Desc> + <Trans> + Discover tokens before they start trending in the future! We analyze thousands of potential tokens & + filter out the best ones for you! + </Trans> + </Desc> + <ImageMobile imageName="menu.png" /> + <ImageMobile imageName="step6.png" marginTop /> + </Layout> + ), + orientationPreferences: [CardinalOrientation.SOUTH, CardinalOrientation.SOUTHEAST], + }, + { + selector: TutorialIds.BUTTON_VIEW_GUIDE_SWAP, + title: isMobile ? LIST_TITLE.VIEW_GUIDE : <Title stepNumber={8} />, + stepNumber: 8, + maskPadding: 10, + requiredClickSelector: '#' + TutorialIds.BUTTON_SETTING, + stopPropagationMouseDown: true, + description: ( + <Layout title={LIST_TITLE.VIEW_GUIDE}> + <Desc> + <Trans> + You can repeat these instructions anytime by clicking on the "View" button under Preferences. + </Trans> + </Desc> + <Desc> + <Trans> + For a more detailed user guide,{' '} + <ExternalLink href="https://docs.kyberswap.com/guides/getting-started">click here.</ExternalLink> + </Trans> + </Desc> + <ImageMobile imageName="step8.1.png" /> + <ImageMobile imageName="step8.2.png" marginTop /> + </Layout> + ), + }, + ] +} + +const TutorialKeys = { + SHOWED_SWAP_GUIDE: 'showedTutorialSwapGuide', +} + +export default memo(function TutorialSwap() { + const [{ show = false, step = 0 }, setShowTutorial] = useTutorialSwapGuide() + const stopTutorial = () => setShowTutorial({ show: false }) + const { account } = useActiveWeb3React() + const { mixpanelHandler } = useMixpanel() + + useEffect(() => { + if (!localStorage.getItem(TutorialKeys.SHOWED_SWAP_GUIDE)) { + // auto show for first time all user + setShowTutorial({ show: true, step: 0 }) + localStorage.setItem(TutorialKeys.SHOWED_SWAP_GUIDE, '1') + } + }, [setShowTutorial]) + + const steps = useMemo(() => { + const list = getListSteps(!!account) + if (isMobile) { + return list + .filter(e => !e.pcOnly) + .map(({ title, description }, i) => ({ + title: `${i + 1}. ${title}`, + content: description, + })) + } + return list.map(e => ({ + ...e, + description: e.description as unknown as string, // because this lib type check description is string but actually it accept any + selector: '#' + e.selector, + })) + }, [account]) + + const stepInfo = (steps[step] || {}) as StepCustom + + const onDismiss = (logic: WalktourLogic) => { + const { stepNumber } = stepInfo + mixpanelHandler(MIXPANEL_TYPE.TUTORIAL_CLICK_DENY, stepNumber) + stopTutorial() + logic.close() + } + + const onFinished = () => { + mixpanelHandler(MIXPANEL_TYPE.TUTORIAL_CLICK_DONE) + stopTutorial() + } + + const checkRequiredClick = (nextStep: StepCustom) => { + const { requiredClickSelector, selectorHint } = nextStep + const needClick = requiredClickSelector && !document.querySelector(selectorHint || nextStep?.selector) + // target next step has not render yet, => click other button to render it + // ex: click button setting to show setting popup, and then highlight content of setting + if (needClick) { + const button: HTMLButtonElement | null = document.querySelector(requiredClickSelector) + button?.click() + } + return needClick + } + + const processNextStep = ({ allSteps, prev, next, stepIndex }: WalktourLogic, isNext: boolean) => { + const nextIndex = isNext ? stepIndex + 1 : stepIndex - 1 + const needClickAnyElement = checkRequiredClick(allSteps[nextIndex]) + const { callbackEndStep } = stepInfo + callbackEndStep && callbackEndStep() + setTimeout( + () => { + setShowTutorial({ step: nextIndex }) + isNext ? next() : prev() + }, + needClickAnyElement ? 400 : 0, + ) + } + + const onNext = (logic: WalktourLogic) => { + const { stepIndex, close } = logic + if (stepIndex - 1 === TOTAL_STEP) { + onFinished() + close() + return + } + // next + processNextStep(logic, true) + } + + const onBack = (logic: WalktourLogic) => { + processNextStep(logic, false) + } + + if (!show) return null + if (isMobile) return <TutorialMobile isOpen={show} stopTutorial={stopTutorial} steps={steps as ToggleItemType[]} /> + return ( + <> + <Walktour + tooltipSeparation={25} + disableMaskInteraction + customTooltipRenderer={CustomPopup} + steps={steps as Step[]} + isOpen={show} + initialStepIndex={step} + customNextFunc={onNext} + customPrevFunc={onBack} + customCloseFunc={onDismiss} + renderMask={options => <CustomMask options={options} stepInfo={stepInfo} />} + /> + <CustomCss /> + </> + ) +}) diff --git a/src/components/Tutorial/index.tsx b/src/components/Tutorial/index.tsx index 981e2cdc3e..4a445db10f 100644 --- a/src/components/Tutorial/index.tsx +++ b/src/components/Tutorial/index.tsx @@ -61,13 +61,30 @@ interface Props { type: TutorialType customIcon?: ReactNode } +const mapVideoId = { + [TutorialType.ELASTIC_POOLS]: 'HCTI3pNDXIM', + [TutorialType.CLASSIC_POOLS]: 'HCTI3pNDXIM', + [TutorialType.ELASTIC_MY_POOLS]: 'gANTlasXStA', + [TutorialType.CLASSIC_MY_POOLS]: 'gANTlasXStA', + [TutorialType.ELASTIC_ADD_LIQUIDITY]: 'EyFOiR1httA', + [TutorialType.ELASTIC_REMOVE_LIQUIDITY]: 'VE58XeRVXgQ', + [TutorialType.ELASTIC_INCREASE_LIQUIDITY]: 'goMNh3hsjt4', + [TutorialType.SWAP]: '1cW_IhT4_dw', + [TutorialType.ELASTIC_FARMS]: 'eWHTX5jrib8', + [TutorialType.CLASSIC_FARMS]: 'FoQRGcf5tJc', + [TutorialType.CLASSIC_CREATE_POOL]: 'wIMzSIKXUbs', + [TutorialType.CLASSIC_ADD_LIQUIDITY]: '9Pudw0LqBQE', +} +export const getTutorialVideoId = (type: TutorialType) => { + return mapVideoId[type] || '' +} -function Tutorial(props: Props) { +function Tutorial({ customIcon, type }: Props) { const theme = useTheme() const [show, setShow] = useState(false) const title = (() => { - switch (props.type) { + switch (type) { case TutorialType.ELASTIC_POOLS: return <Trans>Navigating Pools Tutorial</Trans> case TutorialType.CLASSIC_POOLS: @@ -88,7 +105,7 @@ function Tutorial(props: Props) { })() const subTitle = (() => { - switch (props.type) { + switch (type) { case TutorialType.CLASSIC_ADD_LIQUIDITY: return ( <Trans> @@ -126,44 +143,14 @@ function Tutorial(props: Props) { } })() - const videoId = (() => { - switch (props.type) { - case TutorialType.ELASTIC_POOLS: - return 'HCTI3pNDXIM' - case TutorialType.CLASSIC_POOLS: - return 'HCTI3pNDXIM' - case TutorialType.ELASTIC_MY_POOLS: - return 'gANTlasXStA' - case TutorialType.CLASSIC_MY_POOLS: - return 'gANTlasXStA' - case TutorialType.ELASTIC_ADD_LIQUIDITY: - return 'EyFOiR1httA' - case TutorialType.ELASTIC_REMOVE_LIQUIDITY: - return 'VE58XeRVXgQ' - case TutorialType.ELASTIC_INCREASE_LIQUIDITY: - return 'goMNh3hsjt4' - case TutorialType.SWAP: - return '1cW_IhT4_dw' - - case TutorialType.ELASTIC_FARMS: - return 'eWHTX5jrib8' - case TutorialType.CLASSIC_FARMS: - return 'FoQRGcf5tJc' - case TutorialType.CLASSIC_CREATE_POOL: - return 'wIMzSIKXUbs' - case TutorialType.CLASSIC_ADD_LIQUIDITY: - return '9Pudw0LqBQE' - default: - return '' - } - })() + const videoId = getTutorialVideoId(type) return ( <> - {props.customIcon ? ( + {customIcon ? ( <div onClick={() => setShow(true)}> <MouseoverTooltip text={t`Tutorial`} placement="top" width="fit-content"> - {props.customIcon} + {customIcon} </MouseoverTooltip> </div> ) : ( diff --git a/src/components/Vesting/ProMMVesting.tsx b/src/components/Vesting/ProMMVesting.tsx index 158bfb71b9..d7df6a7534 100644 --- a/src/components/Vesting/ProMMVesting.tsx +++ b/src/components/Vesting/ProMMVesting.tsx @@ -1,7 +1,6 @@ import { CurrencyAmount, Token } from '@kyberswap/ks-sdk-core' import { Trans } from '@lingui/macro' import { BigNumber } from 'ethers' -import React from 'react' import { Text } from 'rebass' import LocalLoader from 'components/LocalLoader' diff --git a/src/components/WalletModal/InstallBraveNote.tsx b/src/components/WalletModal/InstallBraveNote.tsx new file mode 100644 index 0000000000..27a957f473 --- /dev/null +++ b/src/components/WalletModal/InstallBraveNote.tsx @@ -0,0 +1,51 @@ +import { Trans } from '@lingui/macro' +import { AlertTriangle } from 'react-feather' +import { Flex, Text } from 'rebass' + +import useTheme from 'hooks/useTheme' +import { ExternalLink } from 'theme' + +const InstallBraveNote: React.FC = () => { + const theme = useTheme() + + return ( + <Flex + sx={{ + background: theme.buttonBlack, + borderRadius: '16px', + padding: '8px 12px', + alignItems: 'center', + columnGap: '8px', + marginTop: '16px', + minHeight: '48px', + }} + > + <Flex + sx={{ + flex: '0 0 24px', + alignItems: 'center', + justifyContent: 'center', + }} + > + <AlertTriangle color={theme.subText} width="16px" height="16px" /> + </Flex> + + <Text + as="span" + sx={{ + color: theme.subText, + fontWeight: 400, + fontSize: '12px', + lineHeight: '16px', + }} + > + <Trans>Brave wallet can only be used in Brave Browser. Download it</Trans>{' '} + <ExternalLink href="https://brave.com/" style={{ color: theme.primary }}> + <Trans>here</Trans> + </ExternalLink> + </Text> + </Flex> + ) +} + +export default InstallBraveNote diff --git a/src/components/WalletModal/Option.tsx b/src/components/WalletModal/Option.tsx index d6df981301..956b196b7a 100644 --- a/src/components/WalletModal/Option.tsx +++ b/src/components/WalletModal/Option.tsx @@ -27,7 +27,7 @@ const HeaderText = styled.div` font-weight: 500; ` -const OptionCardClickable = styled.button<{ active?: boolean; clickable?: boolean }>` +const OptionCardClickable = styled.button<{ clickable?: boolean }>` width: 100%; outline: none; border: none; @@ -48,12 +48,22 @@ const OptionCardClickable = styled.button<{ active?: boolean; clickable?: boolea transition: all 0.2s; background-color: ${({ theme }) => theme.buttonBlack}; - ${({ active }) => (active ? '&' : ':hover')} { + &[data-active='true'] { background-color: ${({ theme }) => theme.bg7}; - & ${HeaderText} { + ${HeaderText} { color: ${({ theme }) => theme.darkText} !important; } } + + @media (hover: hover) { + &:hover { + background-color: ${({ theme }) => theme.bg7}; + ${HeaderText} { + color: ${({ theme }) => theme.darkText} !important; + } + } + } + opacity: ${({ disabled }) => (disabled ? '0.5' : '1')}; ${({ theme }) => theme.mediaWidth.upToSmall` @@ -106,7 +116,7 @@ export default function Option({ id={id} onClick={onClick} clickable={clickable && !active} - active={active} + data-active={active} disabled={clickable === false} > <IconWrapper size={size}> diff --git a/src/components/WalletModal/PendingView.tsx b/src/components/WalletModal/PendingView.tsx index 25e672b868..0e406c2a68 100644 --- a/src/components/WalletModal/PendingView.tsx +++ b/src/components/WalletModal/PendingView.tsx @@ -1,10 +1,8 @@ import { Trans } from '@lingui/macro' -import { AbstractConnector } from '@web3-react/abstract-connector' import { darken } from 'polished' import React from 'react' import styled from 'styled-components' -import { SUPPORTED_WALLETS } from '../../constants' import Loader from '../Loader' import { WarningBox } from './WarningBox' @@ -22,14 +20,13 @@ const StyledLoader = styled(Loader)` margin-right: 1rem; ` -const LoadingMessage = styled.div<{ error?: boolean }>` +const LoadingMessage = styled.div<{ hasError?: boolean }>` ${({ theme }) => theme.flexRowNoWrap}; align-items: center; justify-content: flex-start; - border-radius: 12px; - margin-bottom: 20px; - color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')}; - border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)}; + border-radius: 16px; + color: ${({ theme, hasError }) => (hasError ? theme.red1 : 'inherit')}; + border: 1px solid ${({ theme, hasError }) => (hasError ? theme.red1 : theme.text4)}; width: 100%; & > * { padding: 1rem; @@ -67,41 +64,31 @@ const LoadingWrapper = styled.div` ` export default function PendingView({ - connector, - error = false, - setPendingError, - tryActivation, + walletOptionKey, + hasError = false, + renderHelperText = () => null, + onClickTryAgain, }: { - connector?: AbstractConnector - error?: boolean - setPendingError: (error: boolean) => void - tryActivation: (connector: AbstractConnector) => void + walletOptionKey?: string // key of SUPPORTED_WALLETS + hasError?: boolean + renderHelperText?: () => React.ReactNode + onClickTryAgain: () => void }) { const isMetamask = window?.ethereum?.isMetaMask const isCoin98 = window?.ethereum?.isCoin98 || !!window.coin98 - const option = Object.keys(SUPPORTED_WALLETS).find(key => { - const wallet = SUPPORTED_WALLETS[key] - return wallet.connector === connector - }) - - const isInjected = option === 'INJECTED' || option === 'COIN98' + const isInjected = walletOptionKey === 'INJECTED' || walletOptionKey === 'COIN98' return ( <PendingSection> - <LoadingMessage error={error}> + <LoadingMessage hasError={hasError}> <LoadingWrapper> - {error ? ( + {hasError ? ( <ErrorGroup> <div> <Trans>Error connecting.</Trans> </div> - <ErrorButton - onClick={() => { - setPendingError(false) - connector && tryActivation(connector) - }} - > + <ErrorButton onClick={onClickTryAgain}> <Trans>Try Again</Trans> </ErrorButton> </ErrorGroup> @@ -113,7 +100,8 @@ export default function PendingView({ )} </LoadingWrapper> </LoadingMessage> - {isMetamask && isCoin98 && isInjected && <WarningBox option={option} />} + {isMetamask && isCoin98 && isInjected && <WarningBox option={walletOptionKey} />} + {renderHelperText()} </PendingSection> ) } diff --git a/src/components/WalletModal/index.tsx b/src/components/WalletModal/index.tsx index 07d1f0a852..1f0204b7ed 100644 --- a/src/components/WalletModal/index.tsx +++ b/src/components/WalletModal/index.tsx @@ -2,7 +2,7 @@ import { Trans, t } from '@lingui/macro' import { AbstractConnector } from '@web3-react/abstract-connector' import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core' import { WalletConnectConnector } from '@web3-react/walletconnect-connector' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { isMobile } from 'react-device-detect' import { ChevronLeft } from 'react-feather' import { useLocation } from 'react-router-dom' @@ -12,9 +12,10 @@ import styled from 'styled-components' import WrongNetworkModal from 'components/WrongNetworkModal' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' +import checkForBraveBrowser from 'utils/checkForBraveBrowser' import { ReactComponent as Close } from '../../assets/images/x.svg' -import { coin98InjectedConnector, fortmatic, injected, portis } from '../../connectors' +import { braveInjectedConnector, coin98InjectedConnector, fortmatic, injected, portis } from '../../connectors' import { OVERLAY_READY } from '../../connectors/Fortmatic' import { SUPPORTED_WALLETS } from '../../constants' import usePrevious from '../../hooks/usePrevious' @@ -24,6 +25,7 @@ import { useIsDarkMode } from '../../state/user/hooks' import { ExternalLink } from '../../theme' import AccountDetails from '../AccountDetails' import Modal from '../Modal' +import InstallBraveNote from './InstallBraveNote' import Option from './Option' import PendingView from './PendingView' @@ -62,7 +64,7 @@ const HeaderRow = styled.div<{ padding?: string }>` ` const ContentWrapper = styled.div<{ padding?: string }>` - padding: ${({ padding }) => padding ?? '2rem 2rem 8px 2rem'}; + padding: ${({ padding }) => padding ?? '2rem'}; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; @@ -104,7 +106,6 @@ const OptionGrid = styled.div` display: grid; grid-template-columns: 1fr 1fr; gap: 20px; - margin-bottom: 20px; ${({ theme }) => theme.mediaWidth.upToMedium` grid-template-columns: 1fr; @@ -165,6 +166,9 @@ export default function WalletModal({ const location = useLocation() const { mixpanelHandler } = useMixpanel() + // need to call this inside a Component as there's an async call in `checkForBraveBrowser` + const isBraveBrowser = checkForBraveBrowser() + // close on connection, when logged out before useEffect(() => { if (account && !previousAccount && walletModalOpen) { @@ -197,6 +201,11 @@ export default function WalletModal({ setPendingWallet(connector) // set wallet for pending view setWalletView(WALLET_VIEWS.PENDING) + if (connector === braveInjectedConnector && !isBraveBrowser) { + // we just want the loading indicator, so return here + return + } + // if the connector is walletconnect and the user has already tried to connect, manually reset the connector if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) { connector.walletConnectProvider = undefined @@ -231,102 +240,111 @@ export default function WalletModal({ // get wallets user can switch too, depending on device/browser function getOptions() { const isMetamask = window.ethereum && window.ethereum.isMetaMask - return Object.keys(SUPPORTED_WALLETS).map(key => { - const option = SUPPORTED_WALLETS[key] - // check for mobile options - if (isMobile) { - //disable portis on mobile for now - if (option.connector === portis) { - return null - } - if (!window.web3 && !window.ethereum && option.mobile) { - return ( - <Option - onClick={() => { - option.connector !== connector && !option.href && tryActivation(option.connector) - }} - id={`connect-${key}`} - key={key} - active={option.connector && option.connector === connector} - color={option.color} - link={option.href} - header={option.name} - subheader={null} - icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} - /> - ) - } - return null - } - // overwrite injected when needed - if (option.connector === injected) { - // don't show injected if there's no injected provider - if (!(window.web3 || window.ethereum?.isMetaMask)) { - if (option.name === 'MetaMask') { + return Object.keys(SUPPORTED_WALLETS) + .map(key => { + const option = SUPPORTED_WALLETS[key] + // check for mobile options + if (isMobile) { + //disable portis on mobile for now + if (option.connector === portis) { + return null + } + + if ( + (!window.web3 && !window.ethereum && option.mobile) || + // add this condition below for Brave browser. In Brave, window.ethereum is not undefined + // the above condition fails and there are no wallet options to choose + (option.mobile && isBraveBrowser) + ) { return ( <Option + onClick={() => { + option.connector !== connector && !option.href && tryActivation(option.connector) + }} id={`connect-${key}`} key={key} - color={'#E8831D'} - header={'Install Metamask'} + active={option.connector && option.connector === connector} + color={option.color} + link={option.href} + header={option.name} subheader={null} - link={'https://metamask.io/'} icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} /> ) - } else { - return null //dont want to return install twice } - } - // don't return metamask if injected provider isn't metamask - else if (option.name === 'MetaMask' && !isMetamask) { + return null } - // likewise for generic - else if (option.name === 'Injected' && isMetamask) { - return null + // overwrite injected when needed + if (option.connector === injected) { + // don't show injected if there's no injected provider + if (!(window.web3 || window.ethereum?.isMetaMask)) { + if (option.name === 'MetaMask') { + return ( + <Option + id={`connect-${key}`} + key={key} + color={'#E8831D'} + header={'Install Metamask'} + subheader={null} + link={'https://metamask.io/'} + icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} + /> + ) + } else { + return null //dont want to return install twice + } + } + // don't return metamask if injected provider isn't metamask + else if (option.name === 'MetaMask' && !isMetamask) { + return null + } + // likewise for generic + else if (option.name === 'Injected' && isMetamask) { + return null + } + } + + if (option.connector === coin98InjectedConnector) { + if (!(window.web3 || window.ethereum?.isCoin98)) { + return ( + <Option + id={`connect-${key}`} + key={key} + color={'#E8831D'} + header={'Install Coin98'} + link={'https://coin98.com/'} + icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} + /> + ) + } } - } - if (option.connector === coin98InjectedConnector) { - if (!(window.web3 || window.ethereum?.isCoin98)) { - return ( + // return rest of options + return ( + !isMobile && + !option.mobileOnly && ( <Option + clickable={isAccepted} id={`connect-${key}`} + onClick={() => { + option.connector === connector + ? setWalletView(WALLET_VIEWS.ACCOUNT) + : !option.href && tryActivation(option.connector) + }} key={key} - color={'#E8831D'} - header={'Install Coin98'} - link={'https://coin98.com/'} + active={option.connector === connector} + color={option.color} + link={option.href} + header={option.name} + subheader={null} //use option.descriptio to bring back multi-line icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} /> ) - } - } - - // return rest of options - return ( - !isMobile && - !option.mobileOnly && ( - <Option - clickable={isAccepted} - id={`connect-${key}`} - onClick={() => { - option.connector === connector - ? setWalletView(WALLET_VIEWS.ACCOUNT) - : !option.href && tryActivation(option.connector) - }} - key={key} - active={option.connector === connector} - color={option.color} - link={option.href} - header={option.name} - subheader={null} //use option.descriptio to bring back multi-line - icon={require(`../../assets/images/${isDarkMode ? '' : 'light-'}${option.iconName}`).default} - /> ) - ) - }) + }) + .filter(Boolean) } function getModalContent() { @@ -361,6 +379,12 @@ export default function WalletModal({ ) } + const shouldShowInstallBrave = !isBraveBrowser && pendingWallet === braveInjectedConnector + const walletOptionKey = Object.keys(SUPPORTED_WALLETS).find(key => { + const wallet = SUPPORTED_WALLETS[key] + return wallet.connector === connector + }) + return ( <UpperSection> <CloseIcon onClick={toggleWalletModal}> @@ -401,10 +425,18 @@ export default function WalletModal({ <ContentWrapper> {walletView === WALLET_VIEWS.PENDING ? ( <PendingView - connector={pendingWallet} - error={pendingError} - setPendingError={setPendingError} - tryActivation={tryActivation} + walletOptionKey={walletOptionKey} + hasError={pendingError} + renderHelperText={() => { + if (shouldShowInstallBrave) { + return <InstallBraveNote /> + } + return null + }} + onClickTryAgain={() => { + setPendingError(false) + connector && tryActivation(connector) + }} /> ) : ( <OptionGrid>{getOptions()}</OptionGrid> diff --git a/src/components/Web3Network/index.tsx b/src/components/Web3Network/index.tsx index 127ec18573..537649c75b 100644 --- a/src/components/Web3Network/index.tsx +++ b/src/components/Web3Network/index.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components' import { ReactComponent as DropdownSvg } from 'assets/svg/down.svg' import Card from 'components/Card' import Row from 'components/Row' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import { nativeOnChain } from 'constants/tokens' import { useActiveWeb3React } from 'hooks' import { ApplicationModal } from 'state/application/actions' @@ -85,7 +86,7 @@ function Web3Network(): JSX.Element | null { if (!chainId) return null return ( - <NetworkCard onClick={() => toggleNetworkModal()} role="button"> + <NetworkCard onClick={() => toggleNetworkModal()} role="button" id={TutorialIds.SELECT_NETWORK}> <NetworkSwitchContainer> <Row> <img diff --git a/src/components/Web3Status/index.tsx b/src/components/Web3Status/index.tsx index 65d2da915d..7e3ad8953d 100644 --- a/src/components/Web3Status/index.tsx +++ b/src/components/Web3Status/index.tsx @@ -7,18 +7,28 @@ import { Activity } from 'react-feather' import { useMedia } from 'react-use' import styled from 'styled-components' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' +import useENSName from 'hooks/useENSName' +import { useHasSocks } from 'hooks/useSocksBalance' +import { useWalletModalToggle } from 'state/application/hooks' +import { isTransactionRecent, useAllTransactions } from 'state/transactions/hooks' +import { TransactionDetails } from 'state/transactions/reducer' +import { shortenAddress } from 'utils' + import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' import FortmaticIcon from '../../assets/images/fortmaticIcon.png' import PortisIcon from '../../assets/images/portisIcon.png' import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' -import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors' +import { + braveInjectedConnector, + coin98InjectedConnector, + fortmatic, + injected, + portis, + walletconnect, + walletlink, +} from '../../connectors' import { NetworkContextName } from '../../constants' -import useENSName from '../../hooks/useENSName' -import { useHasSocks } from '../../hooks/useSocksBalance' -import { useWalletModalToggle } from '../../state/application/hooks' -import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks' -import { TransactionDetails } from '../../state/transactions/reducer' -import { shortenAddress } from '../../utils' import { ButtonLight, ButtonSecondary } from '../Button' import Identicon from '../Identicon' import Loader from '../Loader' @@ -104,36 +114,54 @@ const SOCK = ( </span> ) -// eslint-disable-next-line react/prop-types function StatusIcon({ connector }: { connector: AbstractConnector }) { - if (connector === injected) { - return <Identicon /> - } else if (connector === walletconnect) { - return ( - <IconWrapper size={16}> - <img src={WalletConnectIcon} alt={''} /> - </IconWrapper> - ) - } else if (connector === walletlink) { - return ( - <IconWrapper size={16}> - <img src={CoinbaseWalletIcon} alt={''} /> - </IconWrapper> - ) - } else if (connector === fortmatic) { - return ( - <IconWrapper size={16}> - <img src={FortmaticIcon} alt={''} /> - </IconWrapper> - ) - } else if (connector === portis) { - return ( - <IconWrapper size={16}> - <img src={PortisIcon} alt={''} /> - </IconWrapper> - ) + switch (connector) { + case injected: + case coin98InjectedConnector: + case braveInjectedConnector: { + return ( + <IconWrapper size={16}> + <Identicon /> + </IconWrapper> + ) + } + + case walletconnect: { + return ( + <IconWrapper size={16}> + <img src={WalletConnectIcon} alt={'wallet connect'} /> + </IconWrapper> + ) + } + + case walletlink: { + return ( + <IconWrapper size={16}> + <img src={CoinbaseWalletIcon} alt={'coinbase wallet'} /> + </IconWrapper> + ) + } + + case fortmatic: { + return ( + <IconWrapper size={16}> + <img src={FortmaticIcon} alt={'fortmatic wallet'} /> + </IconWrapper> + ) + } + + case portis: { + return ( + <IconWrapper size={16}> + <img src={PortisIcon} alt={'portis wallet'} /> + </IconWrapper> + ) + } + + default: { + return null + } } - return null } function Web3StatusInner() { @@ -158,7 +186,11 @@ function Web3StatusInner() { if (account) { return ( - <Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}> + <Web3StatusConnected + id={TutorialIds.BUTTON_ADDRESS_WALLET} + onClick={toggleWalletModal} + pending={hasPendingTransactions} + > {hasPendingTransactions ? ( <RowBetween> <Text> @@ -184,7 +216,7 @@ function Web3StatusInner() { ) } else { return ( - <ButtonLight onClick={toggleWalletModal} padding="10px 12px"> + <ButtonLight onClick={toggleWalletModal} padding="10px 12px" id={TutorialIds.BUTTON_CONNECT_WALLET}> <Trans>Connect Wallet</Trans> </ButtonLight> ) diff --git a/src/components/YieldPools/ListItem.tsx b/src/components/YieldPools/ListItem.tsx index b95ff3b453..4ba74edea5 100644 --- a/src/components/YieldPools/ListItem.tsx +++ b/src/components/YieldPools/ListItem.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import { BigNumber } from '@ethersproject/bignumber' import { MaxUint256 } from '@ethersproject/constants' import { ChainId, Fraction, Token, TokenAmount } from '@kyberswap/ks-sdk-core' diff --git a/src/components/YieldPools/ProMMFarmGroup.tsx b/src/components/YieldPools/ProMMFarmGroup.tsx index 9382238f60..fa4bad53d5 100644 --- a/src/components/YieldPools/ProMMFarmGroup.tsx +++ b/src/components/YieldPools/ProMMFarmGroup.tsx @@ -1,10 +1,11 @@ -import { CurrencyAmount, Fraction, Token } from '@kyberswap/ks-sdk-core' +import { CurrencyAmount, Token } from '@kyberswap/ks-sdk-core' import { Pool, Position } from '@kyberswap/ks-sdk-elastic' import { Trans, t } from '@lingui/macro' import { BigNumber } from 'ethers' import { rgba } from 'polished' import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { Clock, Edit2, Info, Minus, Plus } from 'react-feather' +import { Edit2, Minus, Plus } from 'react-feather' +import { Link } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' import styled from 'styled-components' @@ -21,19 +22,19 @@ import Deposit from 'components/Icons/Deposit' import Harvest from 'components/Icons/Harvest' import Withdraw from 'components/Icons/Withdraw' import InfoHelper from 'components/InfoHelper' -import Loader from 'components/Loader' import Modal from 'components/Modal' import { MouseoverTooltip, MouseoverTooltipDesktopOnly } from 'components/Tooltip' import { ELASTIC_BASE_FEE_UNIT, ZERO_ADDRESS } from 'constants/index' import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' import { useToken, useTokens } from 'hooks/Tokens' -import { useProAmmNFTPositionManagerContract, useProMMFarmContract } from 'hooks/useContract' +import { useProAmmNFTPositionManagerContract } from 'hooks/useContract' import useParsedQueryString from 'hooks/useParsedQueryString' import useTheme from 'hooks/useTheme' import { Dots } from 'pages/Pool/styleds' -import { useTokensPrice, useWalletModalToggle } from 'state/application/hooks' -import { useFarmAction, useProMMFarmTVL } from 'state/farms/promm/hooks' +import { useWalletModalToggle } from 'state/application/hooks' +import { useRewardTokenPrices } from 'state/farms/hooks' +import { useFailedNFTs, useFarmAction, useProMMFarmTVL } from 'state/farms/promm/hooks' import { ProMMFarm } from 'state/farms/promm/types' import { useSingleCallResult } from 'state/multicall/hooks' import { useIsTransactionPending } from 'state/transactions/hooks' @@ -100,39 +101,39 @@ const Reward = ({ token: address, amount }: { token: string; amount?: BigNumber ) } -const FeeTargetWrapper = styled.div<{ fullUnlock: boolean }>` - border-radius: 999px; - display: flex; - font-size: 12px; - background: ${({ theme, fullUnlock }) => (fullUnlock ? theme.primary : theme.subText)}; - position: relative; - color: ${({ theme }) => theme.textReverse}; - height: 20px; - align-items: center; - min-width: 120px; -` - -const FeeArchive = styled.div<{ width: number }>` - width: ${({ width }) => `${width}%`}; - height: 100%; - background: ${({ theme, width }) => (width === 100 ? theme.primary : theme.warning)}; - border-radius: 999px; -` -const FeeText = styled.div` - position: absolute; - left: 50%; - transform: translateX(-50%); -` - -const FeeTarget = ({ percent }: { percent: string }) => { - const p = Number(percent) * 100 - return ( - <FeeTargetWrapper fullUnlock={Number(percent) >= 1}> - <FeeArchive width={p}></FeeArchive> - <FeeText>{p.toFixed(2)}%</FeeText> - </FeeTargetWrapper> - ) -} +// const FeeTargetWrapper = styled.div<{ fullUnlock: boolean }>` +// border-radius: 999px; +// display: flex; +// font-size: 12px; +// background: ${({ theme, fullUnlock }) => (fullUnlock ? theme.primary : theme.subText)}; +// position: relative; +// color: ${({ theme }) => theme.textReverse}; +// height: 20px; +// align-items: center; +// min-width: 120px; +// ` + +// const FeeArchive = styled.div<{ width: number }>` +// width: ${({ width }) => `${width}%`}; +// height: 100%; +// background: ${({ theme, width }) => (width === 100 ? theme.primary : theme.warning)}; +// border-radius: 999px; +// ` +// const FeeText = styled.div` +// position: absolute; +// left: 50%; +// transform: translateX(-50%); +// ` + +// const FeeTarget = ({ percent }: { percent: string }) => { +// const p = Number(percent) * 100 +// return ( +// <FeeTargetWrapper fullUnlock={Number(percent) >= 1}> +// <FeeArchive width={p}></FeeArchive> +// <FeeText>{p.toFixed(2)}%</FeeText> +// </FeeTargetWrapper> +// ) +// } const Row = ({ isApprovedForAll, @@ -165,7 +166,7 @@ const Row = ({ const { tvl, farmAPR, poolAPY } = useProMMFarmTVL(fairlaunchAddress, farm.pid) - const prices = useTokensPrice([token0, token1], VERSION.ELASTIC) + const prices = useRewardTokenPrices([token0?.wrapped, token1?.wrapped], VERSION.ELASTIC) const pool = useMemo(() => { if (token0 && token1) @@ -186,11 +187,17 @@ const Row = ({ token1Amount: CurrencyAmount<Token> amountUsd: number rewardAmounts: BigNumber[] + token0Staked: CurrencyAmount<Token> + token1Staked: CurrencyAmount<Token> + stakedUsd: number } | null = useMemo(() => { if (pool && token0 && token1) { let token0Amount = CurrencyAmount.fromRawAmount(token0.wrapped, '0') let token1Amount = CurrencyAmount.fromRawAmount(token1.wrapped, '0') + let token0Staked = CurrencyAmount.fromRawAmount(token0.wrapped, '0') + let token1Staked = CurrencyAmount.fromRawAmount(token1.wrapped, '0') + const rewardAmounts = farm.rewardTokens.map(_item => BigNumber.from('0')) farm.userDepositedNFTs.forEach(item => { @@ -210,14 +217,37 @@ const Row = ({ const amount0Usd = prices[0] * parseFloat(token0Amount.toExact()) const amount1Usd = prices[1] * parseFloat(token1Amount.toExact()) - return { token1Amount, amountUsd: amount0Usd + amount1Usd, token0Amount, rewardAmounts } + farm.userDepositedNFTs.forEach(item => { + const pos = new Position({ + pool, + liquidity: item.stakedLiquidity.toString(), + tickLower: item.tickLower, + tickUpper: item.tickUpper, + }) + + token0Staked = token0Staked.add(pos.amount0) + token1Staked = token1Staked.add(pos.amount1) + }) + + const amount0StakedUsd = prices[0] * parseFloat(token0Staked.toExact()) + const amount1StakedUsd = prices[1] * parseFloat(token1Staked.toExact()) + + return { + token1Amount, + amountUsd: amount0Usd + amount1Usd, + token0Amount, + rewardAmounts, + token0Staked, + token1Staked, + stakedUsd: amount0StakedUsd + amount1StakedUsd, + } } return null }, [pool, token0, token1, prices, farm]) const canHarvest = farm.userDepositedNFTs.some(pos => !!pos.rewardPendings.length) const canUnstake = farm.userDepositedNFTs.some(pos => pos.stakedLiquidity.gt(0)) - const canStake = farm.startTime <= currentTimestamp + const farmNotStart = farm.startTime <= currentTimestamp useEffect(() => { if (position) @@ -229,42 +259,46 @@ const Row = ({ }) }, [position, farm.pid, onUpdateDepositedInfo]) - const contract = useProMMFarmContract(fairlaunchAddress) - - const [targetPercent, setTargetPercent] = useState('') - const [loading, setLoading] = useState(false) - - useEffect(() => { - const getFeeTargetInfos = async () => { - if (!contract) return - setLoading(true) - const res = await Promise.all( - farm.userDepositedNFTs.map(async pos => { - const res = await contract.getRewardCalculationData(pos.tokenId, farm.pid) - return new Fraction(res.vestingVolume.toString(), BigNumber.from(1e12).toString()) - }), - ) - - const totalLiquidity = farm.userDepositedNFTs.reduce( - (acc, cur) => acc.add(cur.stakedLiquidity), - BigNumber.from(0), - ) - const targetLiqid = farm.userDepositedNFTs.reduce( - (acc, cur, index) => acc.add(res[index].multiply(cur.stakedLiquidity.toString())), - new Fraction(0, 1), - ) - - if (totalLiquidity.gt(0)) { - const targetPercent = targetLiqid.divide(totalLiquidity.toString()) - setTargetPercent(targetPercent.toFixed(2)) - } - setLoading(false) - } - getFeeTargetInfos() - }, [contract, farm]) + // TODO: this is temporary hide target volume, an ad-hoc request from Product team. will enable soon if we have this kind of farm + + // const contract = useProMMFarmContract(fairlaunchAddress) + + // const [targetPercent, setTargetPercent] = useState('') + // const [loading, setLoading] = useState(false) + + // useEffect(() => { + // const getFeeTargetInfos = async () => { + // if (!contract) return + // setLoading(true) + // const res = await Promise.all( + // farm.userDepositedNFTs.map(async pos => { + // const res = await contract.getRewardCalculationData(pos.tokenId, farm.pid) + // return new Fraction(res.vestingVolume.toString(), BigNumber.from(1e12).toString()) + // }), + // ) + + // const totalLiquidity = farm.userDepositedNFTs.reduce( + // (acc, cur) => acc.add(cur.stakedLiquidity), + // BigNumber.from(0), + // ) + // const targetLiqid = farm.userDepositedNFTs.reduce( + // (acc, cur, index) => acc.add(res[index].multiply(cur.stakedLiquidity.toString())), + // new Fraction(0, 1), + // ) + + // if (totalLiquidity.gt(0)) { + // const targetPercent = targetLiqid.divide(totalLiquidity.toString()) + // setTargetPercent(targetPercent.toFixed(2)) + // } + // setLoading(false) + // } + // getFeeTargetInfos() + // }, [contract, farm]) const [showTargetVolInfo, setShowTargetVolInfo] = useState(false) + const amountCanStaked = (position?.amountUsd || 0) - (position?.stakedUsd || 0) + if (!above1000) return ( <> @@ -300,7 +334,7 @@ const Row = ({ {token0?.symbol} - {token1?.symbol} </Text> - {farm.startTime > currentTimestamp && ( + {/* farm.startTime > currentTimestamp && ( <MouseoverTooltip text={'Starting In ' + getFormattedTimeFromSecond(farm.startTime - currentTimestamp)} width="fit-content" @@ -308,7 +342,7 @@ const Row = ({ > <Clock size={14} style={{ marginLeft: '6px' }} /> </MouseoverTooltip> - )} + ) */} </Flex> <Flex @@ -328,6 +362,7 @@ const Row = ({ </Flex> </Flex> + {/* <InfoRow> <Text color={theme.subText} display="flex" sx={{ gap: '4px' }} onClick={() => setShowTargetVolInfo(true)}> <Trans>Target volume</Trans> @@ -335,6 +370,7 @@ const Row = ({ </Text> {farm.feeTarget.gt(0) ? loading ? <Loader /> : <FeeTarget percent={targetPercent} /> : '--'} </InfoRow> + */} <InfoRow> <Text color={theme.subText}> @@ -370,11 +406,49 @@ const Row = ({ <Text>{getFormattedTimeFromSecond(farm.vestingDuration, true)}</Text> </InfoRow> + <InfoRow> + <Text color={theme.subText}> + <Trans>Ending In</Trans> + <InfoHelper text={t`Once a farm has ended, you will continue to receive returns through LP Fees`} /> + </Text> + + <Flex flexDirection="column" alignItems="flex-end" justifyContent="center" sx={{ gap: '8px' }}> + {farm.startTime > currentTimestamp ? ( + <> + <Text color={theme.subText} fontSize="12px"> + <Trans>New phase will start in</Trans> + </Text> + {getFormattedTimeFromSecond(farm.startTime - currentTimestamp)} + </> + ) : farm.endTime > currentTimestamp ? ( + <> + <Text color={theme.subText} fontSize="12px"> + <Trans>Current phase will end in</Trans> + </Text> + {getFormattedTimeFromSecond(farm.endTime - currentTimestamp)} + </> + ) : ( + <Trans>ENDED</Trans> + )} + </Flex> + </InfoRow> + <InfoRow> <Text color={theme.subText}> <Trans>My Deposit</Trans> </Text> - <Text>{!!position?.amountUsd ? formatDollarAmount(position.amountUsd) : '--'}</Text> + + <Flex justifyContent="flex-end" color={!!amountCanStaked ? theme.warning : theme.text}> + {!!position?.amountUsd ? formatDollarAmount(position.amountUsd) : '--'} + {!!amountCanStaked && ( + <InfoHelper + color={theme.warning} + text={t`You still have ${formatDollarAmount( + amountCanStaked, + )} liquidity to stake to earn more rewards`} + /> + )} + </Flex> </InfoRow> <InfoRow> @@ -405,7 +479,7 @@ const Row = ({ <Flex sx={{ gap: '16px' }} marginTop="1.25rem"> <ButtonPrimary - disabled={!isApprovedForAll || tab === 'ended'} + disabled={!isApprovedForAll || tab === 'ended' || !farmNotStart} style={{ height: '36px', flex: 1 }} onClick={() => onOpenModal('stake', farm.pid)} > @@ -433,19 +507,16 @@ const Row = ({ <div> <Flex alignItems="center"> <DoubleCurrencyLogo currency0={token0} currency1={token1} /> - <Text fontSize={14}> - {token0?.symbol} - {token1?.symbol} - </Text> - - {farm.startTime > currentTimestamp && ( - <MouseoverTooltip - text={'Starting In ' + getFormattedTimeFromSecond(farm.startTime - currentTimestamp)} - width="fit-content" - placement="top" - > - <Clock size={14} style={{ marginLeft: '6px' }} /> - </MouseoverTooltip> - )} + <Link + to={`/pools?search=${farm.poolAddress}&tab=elastic`} + style={{ + textDecoration: 'none', + }} + > + <Text fontSize={14} fontWeight={500}> + {token0?.symbol} - {token1?.symbol} + </Text> + </Link> </Flex> <Flex @@ -465,32 +536,70 @@ const Row = ({ </Flex> </Flex> </div> - + {/* {farm.feeTarget.gt(0) ? loading ? <Loader /> : <FeeTarget percent={targetPercent} /> : '--'} - - <Text>{formatDollarAmount(tvl)}</Text> + */} + <Text textAlign="right">{formatDollarAmount(tvl)}</Text> <Text textAlign="end" color={theme.apr}> {(farmAPR + poolAPY).toFixed(2)}% <InfoHelper text={`${poolAPY.toFixed(2)}% Fee + ${farmAPR.toFixed(2)}% Rewards`} /> </Text> + {/*<Text textAlign="end">{getFormattedTimeFromSecond(farm.vestingDuration, true)}</Text>*/} + <Flex flexDirection="column" alignItems="flex-end" justifyContent="center" sx={{ gap: '8px' }}> + {farm.startTime > currentTimestamp ? ( + <> + <Text color={theme.subText} fontSize="12px"> + <Trans>New phase will start in</Trans> + </Text> + {getFormattedTimeFromSecond(farm.startTime - currentTimestamp)} + </> + ) : farm.endTime > currentTimestamp ? ( + <> + <Text color={theme.subText} fontSize="12px"> + <Trans>Current phase will end in</Trans> + </Text> + {getFormattedTimeFromSecond(farm.endTime - currentTimestamp)} + </> + ) : ( + <Trans>ENDED</Trans> + )} + </Flex> - <Text textAlign="end">{getFormattedTimeFromSecond(farm.vestingDuration, true)}</Text> + <Flex justifyContent="flex-end" color={!!amountCanStaked ? theme.warning : theme.text}> + {!!position?.amountUsd ? formatDollarAmount(position.amountUsd) : '--'} + {!!amountCanStaked && ( + <InfoHelper + color={theme.warning} + text={t`You still have ${formatDollarAmount(amountCanStaked)} liquidity to stake to earn more rewards`} + /> + )} + </Flex> - <Text textAlign="right">{!!position?.amountUsd ? formatDollarAmount(position.amountUsd) : '--'}</Text> <Flex flexDirection="column" alignItems="flex-end" sx={{ gap: '8px' }}> {farm.rewardTokens.map((token, idx) => ( <Reward key={token} token={token} amount={position?.rewardAmounts[idx]} /> ))} </Flex> <Flex justifyContent="flex-end" sx={{ gap: '4px' }}> - <ActionButton - onClick={() => onOpenModal('stake', farm.pid)} - disabled={!isApprovedForAll || tab === 'ended' || !canStake} - > - <MouseoverTooltip text={!canStake ? t`Farm has not started` : t`Stake`} placement="top" width="fit-content"> - <Plus color={isApprovedForAll && tab !== 'ended' ? theme.primary : theme.subText} size={16} /> + {!isApprovedForAll || tab === 'ended' || !farmNotStart ? ( + <MouseoverTooltip text={!farmNotStart ? t`Farm has not started` : ''} placement="top" width="fit-content"> + <ActionButton + style={{ + cursor: 'not-allowed', + backgroundColor: theme.buttonGray, + opacity: 0.4, + }} + > + <Plus color={theme.subText} size={16} style={{ minWidth: '16px' }} /> + </ActionButton> </MouseoverTooltip> - </ActionButton> + ) : ( + <ActionButton onClick={() => onOpenModal('stake', farm.pid)}> + <MouseoverTooltip text={t`Stake`} placement="top" width="fit-content"> + <Plus color={theme.primary} size={16} /> + </MouseoverTooltip> + </ActionButton> + )} <ActionButton disabled={!canUnstake} @@ -519,7 +628,10 @@ function ProMMFarmGroup({ farms, }: { address: string - onOpenModal: (modalType: 'harvest' | 'deposit' | 'withdraw' | 'stake' | 'unstake', pid?: number) => void + onOpenModal: ( + modalType: 'forcedWithdraw' | 'harvest' | 'deposit' | 'withdraw' | 'stake' | 'unstake', + pid?: number, + ) => void farms: ProMMFarm[] }) { const theme = useTheme() @@ -542,7 +654,8 @@ function ProMMFarmGroup({ const rwTokenMap = useTokens(rewardAddresses) const rwTokens = useMemo(() => Object.values(rwTokenMap), [rwTokenMap]) - const prices = useTokensPrice(rwTokens, VERSION.ELASTIC) + const prices = useRewardTokenPrices(rwTokens, VERSION.ELASTIC) + const priceMap: { [key: string]: number } = useMemo( () => prices?.reduce( @@ -606,6 +719,10 @@ function ProMMFarmGroup({ return result }, {}) + const failedNFTs = useFailedNFTs() + const userNFTs = farms.map(farm => farm.userDepositedNFTs.map(item => item.tokenId.toString())).flat() + const hasAffectedByFarmIssue = userNFTs.some(id => failedNFTs.includes(id)) + const toggleWalletModal = useWalletModalToggle() const posManager = useProAmmNFTPositionManagerContract() @@ -658,6 +775,19 @@ function ProMMFarmGroup({ return ( <FarmContent> <FarmRow> + {hasAffectedByFarmIssue && !above768 && ( + <BtnPrimary + style={{ color: theme.red, border: `1px solid ${theme.red}`, background: theme.red + '33' }} + padding={'12px'} + onClick={() => onOpenModal('forcedWithdraw')} + > + <Withdraw width={20} height={20} /> + <Text fontSize="14px" marginLeft="4px"> + <Trans>Force Withdraw</Trans> + </Text> + </BtnPrimary> + )} + <Flex sx={{ gap: '20px' }} alignItems="center" @@ -668,7 +798,7 @@ function ProMMFarmGroup({ <Text fontSize="12px" color={theme.subText}> <Trans>Deposited Liquidity</Trans> <InfoHelper - text={t`Dollar value of NFT tokens you've deposited. NFT tokens represent your liquidity position`} + text={t`Total value of the liquidity positions you've deposited. NFT tokens represent your liquidity positions`} ></InfoHelper> </Text> @@ -734,7 +864,7 @@ function ProMMFarmGroup({ <MouseoverTooltipDesktopOnly text={t`Withdraw your liquidity (the NFT tokens that represent your liquidity position)`} > - <ButtonOutlined padding="8px 12px" onClick={() => onOpenModal('withdraw')}> + <ButtonOutlined padding={above768 ? '8px 12px' : '8px'} onClick={() => onOpenModal('withdraw')}> <Withdraw width={20} height={20} /> {above768 && ( <Text fontSize="14px" marginLeft="4px"> @@ -744,7 +874,7 @@ function ProMMFarmGroup({ </ButtonOutlined> </MouseoverTooltipDesktopOnly> ) : ( - <BtnPrimary disabled width="fit-content" padding="8px 12px"> + <BtnPrimary disabled width="fit-content" padding={above768 ? '8px 12px' : '8px'}> <Withdraw width={20} height={20} /> {above768 && ( <Text fontSize="14px" marginLeft="4px"> @@ -753,6 +883,20 @@ function ProMMFarmGroup({ )} </BtnPrimary> )} + + {hasAffectedByFarmIssue && above768 && ( + <BtnPrimary + style={{ color: theme.red, border: `1px solid ${theme.red}`, background: theme.red + '33' }} + width="fit-content" + padding={above768 ? '8px 12px' : '8px'} + onClick={() => onOpenModal('forcedWithdraw')} + > + <Withdraw width={20} height={20} /> + <Text fontSize="14px" marginLeft="4px"> + <Trans>Force Withdraw</Trans> + </Text> + </BtnPrimary> + )} </Flex> ) ) : ( diff --git a/src/components/YieldPools/ProMMFarmModals/DepositModal.tsx b/src/components/YieldPools/ProMMFarmModals/DepositModal.tsx index bc42f84f6e..ee16c588c0 100644 --- a/src/components/YieldPools/ProMMFarmModals/DepositModal.tsx +++ b/src/components/YieldPools/ProMMFarmModals/DepositModal.tsx @@ -151,7 +151,7 @@ function ProMMDepositNFTModal({ selectedFarmAddress: string }) { const qs = useParsedQueryString() - const tab = qs.tab || 'active' + const tab = qs.type || 'active' const { account } = useActiveWeb3React() const theme = useTheme() @@ -162,6 +162,7 @@ function ProMMDepositNFTModal({ const poolAddresses = selectedFarm ?.filter(farm => (tab === 'active' ? farm.endTime > +new Date() / 1000 : farm.endTime < +new Date() / 1000)) .map(farm => farm.poolAddress.toLowerCase()) + const [selectedNFTs, setSeletedNFTs] = useState<string[]>([]) const tokens = useTokens(selectedFarm.map(farm => [farm.token0, farm.token1]).reduce((arr, cur) => [...arr, ...cur])) diff --git a/src/components/YieldPools/ProMMFarmModals/WithdrawModal.tsx b/src/components/YieldPools/ProMMFarmModals/WithdrawModal.tsx index 1786a85ce7..22a0324851 100644 --- a/src/components/YieldPools/ProMMFarmModals/WithdrawModal.tsx +++ b/src/components/YieldPools/ProMMFarmModals/WithdrawModal.tsx @@ -21,7 +21,7 @@ import useParsedQueryString from 'hooks/useParsedQueryString' import { usePool } from 'hooks/usePools' import useTheme from 'hooks/useTheme' import { useTokensPrice } from 'state/application/hooks' -import { useFarmAction, usePostionFilter, useProMMFarms } from 'state/farms/promm/hooks' +import { useFailedNFTs, useFarmAction, usePostionFilter, useProMMFarms } from 'state/farms/promm/hooks' import { UserPositionFarm } from 'state/farms/promm/types' import { formatDollarAmount } from 'utils/numbers' import { unwrappedToken } from 'utils/wrappedCurrency' @@ -42,10 +42,12 @@ const PositionRow = ({ position, onChange, selected, + forced, }: { selected: boolean position: UserPositionFarm onChange: (value: boolean) => void + forced: boolean }) => { const { token0: token0Address, token1: token1Address, fee: feeAmount, liquidity, tickLower, tickUpper } = position @@ -79,9 +81,24 @@ const PositionRow = ({ const above768 = useMedia('(min-width: 768px)') + const disableCheckbox = ( + <Flex + width={'17.5px'} + height="17.5px" + backgroundColor={theme.disableText} + sx={{ borderRadius: '2px' }} + alignItems="center" + justifyContent="center" + > + <X size={14} color="#333" /> + </Flex> + ) + return ( <TableRow> - {!position.stakedLiquidity.gt(BigNumber.from(0)) ? ( + {forced ? ( + <Checkbox type="checkbox" disabled checked /> + ) : !position.stakedLiquidity.gt(BigNumber.from(0)) ? ( <Checkbox type="checkbox" onChange={e => { @@ -91,16 +108,7 @@ const PositionRow = ({ /> ) : ( <MouseoverTooltip text="You will need to unstake this position first before you can withdraw it"> - <Flex - width={'17.5px'} - height="17.5px" - backgroundColor={theme.disableText} - sx={{ borderRadius: '2px' }} - alignItems="center" - justifyContent="center" - > - <X size={14} color="#333" /> - </Flex> + {disableCheckbox} </MouseoverTooltip> )} {above768 ? ( @@ -156,12 +164,20 @@ const PositionRow = ({ ) } -function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => void; selectedFarmAddress: string }) { +function WithdrawModal({ + selectedFarmAddress, + onDismiss, + forced = false, +}: { + onDismiss: () => void + selectedFarmAddress: string + forced?: boolean +}) { const theme = useTheme() const above768 = useMedia('(min-width: 768px)') const qs = useParsedQueryString() - const tab = qs.tab || 'active' + const tab = qs.type || 'active' const checkboxGroupRef = useRef<any>() const { data: farms } = useProMMFarms() @@ -171,6 +187,8 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo ?.filter(farm => (tab === 'active' ? farm.endTime > +new Date() / 1000 : farm.endTime < +new Date() / 1000)) .map(farm => farm.poolAddress.toLowerCase()) + const failedNFTs = useFailedNFTs() + const userDepositedNFTs = useMemo(() => { const uniqueNfts: { [id: string]: UserPositionFarm } = {} const res = (selectedFarm || []).reduce((allNFTs, farm) => { @@ -199,10 +217,15 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo const [selectedNFTs, setSeletedNFTs] = useState<string[]>([]) - const { withdraw } = useFarmAction(selectedFarmAddress) + const { withdraw, emergencyWithdraw } = useFarmAction(selectedFarmAddress) useEffect(() => { if (!checkboxGroupRef.current) return + if (forced) { + checkboxGroupRef.current.checked = true + checkboxGroupRef.current.indeterminate = false + return + } if (selectedNFTs.length === 0) { checkboxGroupRef.current.checked = false checkboxGroupRef.current.indeterminate = false @@ -213,7 +236,7 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo checkboxGroupRef.current.checked = true checkboxGroupRef.current.indeterminate = false } - }, [selectedNFTs.length, withDrawableNFTs]) + }, [selectedNFTs.length, withDrawableNFTs, forced]) const [showMenu, setShowMenu] = useState(false) @@ -224,6 +247,12 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo if (!selectedFarmAddress) return null const handleWithdraw = async () => { + if (forced) { + await emergencyWithdraw(failedNFTs.map(BigNumber.from)) + onDismiss() + return + } + const txHash = await withdraw(selectedNFTs.map(item => BigNumber.from(item))) if (txHash) { const finishedPoses = eligiblePositions.filter(pos => selectedNFTs.includes(pos.tokenId.toString())) @@ -268,12 +297,10 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo <Modal isOpen={!!selectedFarm} onDismiss={onDismiss} width="80vw" maxHeight={80} maxWidth="808px"> <ModalContentWrapper> <Flex alignItems="center" justifyContent="space-between"> - <Title> - <Trans>Withdraw your liquidity</Trans> - + {forced ? <Trans>Force Withdraw</Trans> : <Trans>Withdraw your liquidity</Trans>} - {above768 && filterComponent} + {above768 && !forced && filterComponent} @@ -281,16 +308,21 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo - - You will need to unstake your liquidity positions (NFT tokens) first before withdrawing it back to your - wallet - + {forced ? ( + Below is a list of your affected liquidity positions + ) : ( + + You will need to unstake your liquidity positions (NFT tokens) first before withdrawing it back to your + wallet + + )} - {!above768 && filterComponent} + {!above768 && !forced && filterComponent} { @@ -316,19 +348,28 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo
    - {(eligiblePositions as UserPositionFarm[]).map(pos => ( - { - if (selected) setSeletedNFTs(prev => [...prev, pos.tokenId.toString()]) - else { - setSeletedNFTs(prev => prev.filter(item => item !== pos.tokenId.toString())) - } - }} - /> - ))} + {(eligiblePositions as UserPositionFarm[]) + .filter(pos => { + if (forced) { + return failedNFTs.includes(pos.tokenId.toString()) + } + + return true + }) + .map(pos => ( + { + if (selected) setSeletedNFTs(prev => [...prev, pos.tokenId.toString()]) + else { + setSeletedNFTs(prev => prev.filter(item => item !== pos.tokenId.toString())) + } + }} + /> + ))}
    @@ -337,9 +378,10 @@ function WithdrawModal({ selectedFarmAddress, onDismiss }: { onDismiss: () => vo padding="10px 24px" width="fit-content" onClick={handleWithdraw} - disabled={!selectedNFTs.length} + disabled={forced ? false : !selectedNFTs.length} + style={forced ? { background: theme.red, color: theme.textReverse } : undefined} > - Withdraw Selected + {forced ? Forced Withdraw : Withdraw Selected}
    diff --git a/src/components/YieldPools/ProMMFarmModals/styled.tsx b/src/components/YieldPools/ProMMFarmModals/styled.tsx index 1cf6ffb571..cfccff0b12 100644 --- a/src/components/YieldPools/ProMMFarmModals/styled.tsx +++ b/src/components/YieldPools/ProMMFarmModals/styled.tsx @@ -72,6 +72,10 @@ export const Checkbox = styled.input` top: 5.5px; left: 3px; } + + :disabled { + background-color: ${({ theme }) => theme.disableText}; + } ` export const Select = styled.div` diff --git a/src/components/YieldPools/ProMMFarms.tsx b/src/components/YieldPools/ProMMFarms.tsx index 12fe69577a..04e7ba2a62 100644 --- a/src/components/YieldPools/ProMMFarms.tsx +++ b/src/components/YieldPools/ProMMFarms.tsx @@ -1,12 +1,12 @@ import { Trans, t } from '@lingui/macro' import { stringify } from 'querystring' -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Info, Search } from 'react-feather' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Search } from 'react-feather' import { useHistory, useLocation } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' -import HoverDropdown from 'components/HoverDropdown' +import FarmIssueAnnouncement from 'components/FarmIssueAnnouncement' import InfoHelper from 'components/InfoHelper' import LocalLoader from 'components/LocalLoader' import Toggle from 'components/Toggle' @@ -15,9 +15,9 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside' import useParsedQueryString from 'hooks/useParsedQueryString' import useTheme from 'hooks/useTheme' import { useBlockNumber } from 'state/application/hooks' -import { useGetProMMFarms, useProMMFarms } from 'state/farms/promm/hooks' +import { useFailedNFTs, useGetProMMFarms, useProMMFarms } from 'state/farms/promm/hooks' import { ProMMFarm } from 'state/farms/promm/types' -import { ExternalLink, StyledInternalLink } from 'theme' +import { StyledInternalLink } from 'theme' import ProMMFarmGroup from './ProMMFarmGroup' import { DepositModal, StakeUnstakeModal } from './ProMMFarmModals' @@ -34,7 +34,7 @@ import { StakedOnlyToggleWrapper, } from './styleds' -type ModalType = 'deposit' | 'withdraw' | 'stake' | 'unstake' | 'harvest' +type ModalType = 'deposit' | 'withdraw' | 'stake' | 'unstake' | 'harvest' | 'forcedWithdraw' function ProMMFarms({ active }: { active: boolean }) { const theme = useTheme() @@ -48,6 +48,8 @@ function ProMMFarms({ active }: { active: boolean }) { const blockNumber = useBlockNumber() + const failedNFTs = useFailedNFTs() + useEffect(() => { getProMMFarms() }, [getProMMFarms, blockNumber]) @@ -82,7 +84,11 @@ function ProMMFarms({ active }: { active: boolean }) { const filterSearchText = search ? farm.token0.toLowerCase().includes(search) || farm.token1.toLowerCase().includes(search) || - farm.poolAddress.toLowerCase() === search + farm.poolAddress.toLowerCase() === search || + farm?.token0Info?.symbol?.toLowerCase().includes(search) || + farm?.token1Info?.symbol?.toLowerCase().includes(search) || + farm?.token0Info?.name?.toLowerCase().includes(search) || + farm?.token1Info?.name?.toLowerCase().includes(search) : true let filterStaked = true @@ -129,6 +135,10 @@ function ProMMFarms({ active }: { active: boolean }) { )} + {selectedFarm && selectedModal === 'forcedWithdraw' && ( + + )} + {selectedFarm && selectedModal === 'harvest' && ( )} @@ -157,7 +167,7 @@ function ProMMFarms({ active }: { active: boolean }) { {qs.type === 'ended' && qs.tab !== VERSION.CLASSIC && ( - + Your rewards may be automatically harvested a few days after the farm ends. Please check the{' '} Vesting tab to see your rewards @@ -165,6 +175,23 @@ function ProMMFarms({ active }: { active: boolean }) { )} + {(!qs.type || qs.type === 'active') && qs.tab !== VERSION.CLASSIC && ( + <> + + Farms will run in multiple phases + + + + Once the current phase ends, you can harvest your rewards from the farm in the{' '} + Ended tab. To continue earning rewards in + the new phase, you must restake your NFT position into the active farm + + + + {!!failedNFTs.length && } + + )} + {above1000 && ( @@ -173,7 +200,7 @@ function ProMMFarms({ active }: { active: boolean }) { - + {/* + */} - + Staked TVL @@ -221,11 +249,11 @@ function ProMMFarms({ active }: { active: boolean }) { /> - + - Vesting + Ending In - + @@ -273,7 +301,7 @@ function ProMMFarms({ active }: { active: boolean }) { { + onOpenModal={(modalType: ModalType, pid?: number, forced?: boolean) => { setSeletedModal(modalType) setSeletedFarm(fairLaunchAddress) setSeletedPoolId(pid ?? null) diff --git a/src/components/YieldPools/styleds.tsx b/src/components/YieldPools/styleds.tsx index 71076326e9..f1067eab01 100644 --- a/src/components/YieldPools/styleds.tsx +++ b/src/components/YieldPools/styleds.tsx @@ -155,7 +155,6 @@ export const StakedOnlyToggleWrapper = styled.div` ` export const StakedOnlyToggleText = styled.div` - margin-left: 10px; font-size: 14px; font-weight: 500; color: ${({ theme }) => theme.subText}; @@ -366,9 +365,14 @@ export const TableHeader = styled.div<{ fade?: boolean; oddRow?: boolean }>` ` export const ProMMFarmTableHeader = styled(TableHeader)` - grid-template-columns: 150px 120px 1fr 0.75fr 0.75fr 0.75fr 1fr 1fr; - grid-template-areas: 'token_pairs pool_fee staked_tvl ending_in apr vesting my_deposit reward action'; + grid-template-columns: 200px 0.5fr 0.75fr 1fr 0.5fr 0.75fr 0.75fr; + grid-template-areas: 'token_pairs staked_tvl apr ending_in my_deposit reward action'; grid-gap: 2rem; + + ${({ theme }) => theme.mediaWidth.upToLarge` + grid-template-columns: 170px 0.5fr 0.75fr 1fr 0.5fr 0.75fr 0.75fr; + grid-gap: 1rem; + `}; ` export const ProMMFarmTableRow = styled(ProMMFarmTableHeader)` @@ -574,8 +578,7 @@ export const ProMMFarmTableRowMobile = styled.div` export const InfoRow = styled.div` display: flex; justify-content: space-between; - align-items: center; - margin-top: 8px; + margin-top: 12px; font-size: 12px; ` diff --git a/src/components/swapv2/SwapSettingsPanel/index.tsx b/src/components/swapv2/SwapSettingsPanel/index.tsx index 94412319b1..23505a122a 100644 --- a/src/components/swapv2/SwapSettingsPanel/index.tsx +++ b/src/components/swapv2/SwapSettingsPanel/index.tsx @@ -11,6 +11,7 @@ import QuestionHelper from 'components/QuestionHelper' import { RowBetween, RowFixed } from 'components/Row' import Toggle from 'components/Toggle' import useTopTrendingSoonTokensInCurrentNetwork from 'components/TopTrendingSoonTokensInCurrentNetwork/useTopTrendingSoonTokensInCurrentNetwork' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { ApplicationModal } from 'state/application/actions' @@ -38,7 +39,6 @@ type Props = { onClickGasPriceTracker: () => void onClickLiquiditySources: () => void } - const BackIconWrapper = styled(ArrowLeft)` height: 20px; width: 20px; @@ -74,6 +74,7 @@ const SettingsPanel: React.FC = ({ className, onBack, onClickLiquiditySou const toggleMobileTradeRoutes = useToggleModal(ApplicationModal.MOBILE_TRADE_ROUTES) const toggleTradeRoutes = useToggleTradeRoutes() const toggleTokenInfo = useToggleTokenInfo() + const isShowTrendingSoonTokens = useShowTopTrendingSoonTokens() const toggleTopTrendingTokens = useToggleTopTrendingTokens() @@ -105,7 +106,7 @@ const SettingsPanel: React.FC = ({ className, onBack, onClickLiquiditySou } return ( - + = ({ className, onBack, onClickLiquiditySou - diff --git a/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx b/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx index 677a595968..958b32f2e8 100644 --- a/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx +++ b/src/components/swapv2/TokenInfoV2/SingleTokenInfo.tsx @@ -6,6 +6,7 @@ import { BarChart2, DollarSign, Repeat } from 'react-feather' import { Flex, Text } from 'rebass' import styled from 'styled-components' +import { CollapseItem } from 'components/Collapse' import CurrencyLogo from 'components/CurrencyLogo' import Loader from 'components/Loader' import useTheme from 'hooks/useTheme' @@ -13,10 +14,12 @@ import { TokenInfo } from 'hooks/useTokenInfo' import { formattedNum } from 'utils' import { formatDollarAmount } from 'utils/numbers' -import BlockWrapper from './BlockWrapper' - const NOT_AVAILABLE = '--' +const CollapseItemWrapper = styled(CollapseItem)` + border-radius: 20px; +` + const InfoRow = styled.div` display: flex; flex-direction: column; @@ -123,7 +126,7 @@ export function HowToSwap({ const toName = formatString(toCurrencyInfo.name || name2) return ( - @@ -138,7 +141,7 @@ export function HowToSwap({ rates, and earn more with your {symbol1} token without needing to check rates across multiple platforms. - + ) } @@ -251,7 +254,7 @@ const SingleTokenInfo = ({ }, ] return ( - @@ -267,7 +270,7 @@ const SingleTokenInfo = ({ className="desc" ref={ref} dangerouslySetInnerHTML={{ - __html: description.replaceAll('\r\n\r\n', '

    '), + __html: description.replace(/\r\n\r\n/g, '

    '), }} /> @@ -280,7 +283,7 @@ const SingleTokenInfo = ({ ))} -
    + ) } diff --git a/src/connectors/index.ts b/src/connectors/index.ts index 7b21868e71..2012c991b8 100644 --- a/src/connectors/index.ts +++ b/src/connectors/index.ts @@ -58,6 +58,8 @@ export const injected = new InjectedConnector(injectedConnectorParam) export const coin98InjectedConnector = new InjectedConnector(injectedConnectorParam) +export const braveInjectedConnector = new InjectedConnector(injectedConnectorParam) + const WALLET_CONNECT_SUPPORTED_CHAIN_IDS: ChainId[] = [ ChainId.MAINNET, ChainId.ROPSTEN, diff --git a/src/constants/abis/aggregation-executor.json b/src/constants/abis/aggregation-executor.json deleted file mode 100644 index 3d9234c934..0000000000 --- a/src/constants/abis/aggregation-executor.json +++ /dev/null @@ -1,70 +0,0 @@ -[ - { - "inputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint16", - "name": "dexOption", - "type": "uint16" - } - ], - "internalType": "struct IMultihopRouter.Swap[][]", - "name": "swapSequences", - "type": "tuple[][]" - }, - { - "internalType": "address", - "name": "tokenIn", - "type": "address" - }, - { - "internalType": "address", - "name": "tokenOut", - "type": "address" - }, - { - "internalType": "uint256", - "name": "minTotalAmountOut", - "type": "uint256" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "deadline", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "destTokenFeeData", - "type": "bytes" - } - ], - "internalType": "struct AggregationExecutor.SwapExecutorDescription", - "name": "swapExecutorDescription", - "type": "tuple" - } - ], - "name": "nameDoesntMatter", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - } -] diff --git a/src/constants/abis/dmm-router-v2.json b/src/constants/abis/dmm-router-v2.json deleted file mode 100644 index 7b0fa71607..0000000000 --- a/src/constants/abis/dmm-router-v2.json +++ /dev/null @@ -1,263 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_WETH", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "string", - "name": "reason", - "type": "string" - } - ], - "name": "Error", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "pair", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amountOut", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "output", - "type": "address" - } - ], - "name": "Exchange", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "indexed": false, - "internalType": "contract IERC20", - "name": "srcToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "contract IERC20", - "name": "dstToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "dstReceiver", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "spentAmount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "returnAmount", - "type": "uint256" - } - ], - "name": "Swapped", - "type": "event" - }, - { - "inputs": [], - "name": "WETH", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "rescueFunds", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IAggregationExecutor", - "name": "caller", - "type": "address" - }, - { - "components": [ - { - "internalType": "contract IERC20", - "name": "srcToken", - "type": "address" - }, - { - "internalType": "contract IERC20", - "name": "dstToken", - "type": "address" - }, - { - "internalType": "address[]", - "name": "srcReceivers", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "srcAmounts", - "type": "uint256[]" - }, - { - "internalType": "address", - "name": "dstReceiver", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minReturnAmount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "flags", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "permit", - "type": "bytes" - } - ], - "internalType": "struct AggregationRouter.SwapDescription", - "name": "desc", - "type": "tuple" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "clientData", - "type": "bytes" - } - ], - "name": "swap", - "outputs": [ - { - "internalType": "uint256", - "name": "returnAmount", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } -] - diff --git a/src/constants/index.ts b/src/constants/index.ts index a966060a1c..05d3e388fe 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,7 +4,14 @@ import { AbstractConnector } from '@web3-react/abstract-connector' import JSBI from 'jsbi' import { v4 as uuid } from 'uuid' -import { coin98InjectedConnector, injected, ledger, walletconnect, walletlink } from '../connectors' +import { + braveInjectedConnector, + coin98InjectedConnector, + injected, + ledger, + walletconnect, + walletlink, +} from '../connectors' import { NETWORKS_INFO, SUPPORTED_NETWORKS } from './networks' export const EMPTY_OBJECT: any = {} @@ -657,6 +664,7 @@ export interface WalletInfo { primary?: true mobile?: true mobileOnly?: true + installLink?: string } export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { @@ -676,6 +684,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { description: 'Easy-to-use browser extension.', href: null, color: '#E8831D', + installLink: 'https://metamask.io/download/', }, COIN98: { connector: coin98InjectedConnector, @@ -684,6 +693,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { description: 'The Leading Multi-chain Wallet & DeFi Gateway', href: null, color: 'e6c959', + installLink: 'https://wallet.coin98.com/', }, WALLET_CONNECT: { connector: walletconnect, @@ -693,6 +703,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { href: null, color: '#4196FC', mobile: true, + installLink: 'https://walletconnect.com/', }, WALLET_LINK: { connector: walletlink, @@ -701,6 +712,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { description: 'Use Coinbase Wallet app on mobile device', href: null, color: '#315CF5', + installLink: 'https://www.coinbase.com/wallet', }, COINBASE_LINK: { name: 'Open in Coinbase Wallet', @@ -737,6 +749,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { description: 'Ledger Device', href: null, color: '#315CF5', + installLink: 'https://www.ledger.com/ledger-live/download', }, // TREZOR: { // connector: trezor, @@ -746,6 +759,16 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { // href: null, // color: '#315CF5' // } + BRAVE: { + connector: braveInjectedConnector, + name: 'Brave Wallet', + iconName: 'brave_wallet.svg', + description: 'Native wallet of Brave browser', + href: null, + color: '#cc1d83', + mobile: true, + installLink: 'https://brave.com/download', + }, } export const BLACKLIST_WALLETS: string[] = [ diff --git a/src/constants/networks/arbitrum-testnet.ts b/src/constants/networks/arbitrum-testnet.ts index a5a1156793..c22facf874 100644 --- a/src/constants/networks/arbitrum-testnet.ts +++ b/src/constants/networks/arbitrum-testnet.ts @@ -39,8 +39,6 @@ const arbitrumTestnetInfo: NetworkInfo = { }, oldStatic: NOT_SUPPORT, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/arbitrum.ts b/src/constants/networks/arbitrum.ts index 639131e71a..04f71c6099 100644 --- a/src/constants/networks/arbitrum.ts +++ b/src/constants/networks/arbitrum.ts @@ -45,8 +45,6 @@ const arbitrumInfo: NetworkInfo = { factory: '0x51E8D106C646cA58Caf32A47812e95887C071a62', }, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/aurora.ts b/src/constants/networks/aurora.ts index 6e07944800..f8acc39890 100644 --- a/src/constants/networks/aurora.ts +++ b/src/constants/networks/aurora.ts @@ -29,7 +29,7 @@ const auroraInfo: NetworkInfo = { logo: EthereumLogo, decimal: 18, }, - rpcUrl: 'https://mainnet.aurora.dev/GvfzNcGULXzWqaVahC8WPTdqEuSmwNCu3Nu3rtcVv9MD', + rpcUrl: 'https://aurora.kyberengineering.io', routerUri: `${process.env.REACT_APP_AGGREGATOR_API}/aurora/route/encode`, classic: { static: { @@ -43,8 +43,6 @@ const auroraInfo: NetworkInfo = { factory: '0x39a8809fBbF22cCaeAc450EaF559C076843eB910', }, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/avax-testnet.ts b/src/constants/networks/avax-testnet.ts index 097e2b0b44..aa0e83691e 100644 --- a/src/constants/networks/avax-testnet.ts +++ b/src/constants/networks/avax-testnet.ts @@ -42,8 +42,6 @@ const avaxTestnetInfo: NetworkInfo = { router: '0x19395624C030A11f58e820C3AeFb1f5960d9742a', factory: '0x7900309d0b1c8D3d665Ae40e712E8ba4FC4F5453', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: ['0xC3E2aED41ECdFB1ad41ED20D45377Da98D5489dD'], fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/avax.ts b/src/constants/networks/avax.ts index 4e3648329e..d0af416978 100644 --- a/src/constants/networks/avax.ts +++ b/src/constants/networks/avax.ts @@ -40,8 +40,6 @@ const avaxInfo: NetworkInfo = { router: '0x8Efa5A9AD6D594Cf76830267077B78cE0Bc5A5F8', factory: '0x10908C875D865C66f271F5d3949848971c9595C9', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: '0x610A05127d51dd42031A39c25aF951a8e77cDDf7', fairlaunch: [ '0xD169410524Ab1c3C51F56a856a2157B88d4D4FF5', diff --git a/src/constants/networks/bnb-testnet.ts b/src/constants/networks/bnb-testnet.ts index 0240226db2..3bc7a7cd64 100644 --- a/src/constants/networks/bnb-testnet.ts +++ b/src/constants/networks/bnb-testnet.ts @@ -43,8 +43,6 @@ const bnbTestnetInfo: NetworkInfo = { router: '0x19395624C030A11f58e820C3AeFb1f5960d9742a', factory: '0x7900309d0b1c8D3d665Ae40e712E8ba4FC4F5453', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: [ '0xf0fb5bD9EB287A902Bd45b57AE4CF5F9DcEBe550', diff --git a/src/constants/networks/bnb.ts b/src/constants/networks/bnb.ts index dd469f6ea2..456ede1ade 100644 --- a/src/constants/networks/bnb.ts +++ b/src/constants/networks/bnb.ts @@ -42,8 +42,6 @@ const bnbInfo: NetworkInfo = { router: '0x78df70615ffc8066cc0887917f2Cd72092C86409', factory: '0x878dFE971d44e9122048308301F540910Bbd934c', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: [ '0x597e3FeDBC02579232799Ecd4B7edeC4827B0435', diff --git a/src/constants/networks/bttc.ts b/src/constants/networks/bttc.ts index 1caa62d705..e2277d88a9 100644 --- a/src/constants/networks/bttc.ts +++ b/src/constants/networks/bttc.ts @@ -46,8 +46,6 @@ const bttcInfo: NetworkInfo = { router: '0xEaE47c5D99f7B31165a7f0c5f7E0D6afA25CFd55', factory: '0xD9bfE9979e9CA4b2fe84bA5d4Cf963bBcB376974', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: '0x1a91f5ADc7cB5763d35A26e98A18520CB9b67e70', fairlaunch: EMPTY_ARRAY, fairlaunchV2: [ diff --git a/src/constants/networks/cronos-testnet.ts b/src/constants/networks/cronos-testnet.ts index 7d8fb7459e..d08f65041a 100644 --- a/src/constants/networks/cronos-testnet.ts +++ b/src/constants/networks/cronos-testnet.ts @@ -52,8 +52,6 @@ const cronosTestnetInfo: NetworkInfo = { router: '0x548E585B17908D0387d16F9BFf46c4EDe7ca7746', factory: '0x9fE747AEA6173DD2c72e9D9BF4E2bCbbC0f8aD9e', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/cronos.ts b/src/constants/networks/cronos.ts index 5f4b2e8d38..bea990fb63 100644 --- a/src/constants/networks/cronos.ts +++ b/src/constants/networks/cronos.ts @@ -49,8 +49,6 @@ const cronosInfo: NetworkInfo = { router: '0xEaE47c5D99f7B31165a7f0c5f7E0D6afA25CFd55', factory: '0xD9bfE9979e9CA4b2fe84bA5d4Cf963bBcB376974', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/ethereum.ts b/src/constants/networks/ethereum.ts index f14ebdc74d..aa77073d0d 100644 --- a/src/constants/networks/ethereum.ts +++ b/src/constants/networks/ethereum.ts @@ -46,8 +46,6 @@ const ethereumInfo: NetworkInfo = { router: '0x1c87257F5e8609940Bc751a07BB085Bb7f8cDBE6', factory: '0x833e4083B7ae46CeA85695c4f7ed25CDAd8886dE', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: [ '0xc0601973451d9369252Aee01397c0270CD2Ecd60', diff --git a/src/constants/networks/fantom.ts b/src/constants/networks/fantom.ts index 25b54f5f43..8c6c082b7c 100644 --- a/src/constants/networks/fantom.ts +++ b/src/constants/networks/fantom.ts @@ -42,8 +42,6 @@ const fantomInfo: NetworkInfo = { router: '0x5d5A5a0a465129848c2549669e12cDC2f8DE039A', factory: '0x78df70615ffc8066cc0887917f2Cd72092C86409', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git "a/src/constants/networks/g\303\266rli.ts" "b/src/constants/networks/g\303\266rli.ts" index 21b3d3d318..e9fcc18e2f 100644 --- "a/src/constants/networks/g\303\266rli.ts" +++ "b/src/constants/networks/g\303\266rli.ts" @@ -39,8 +39,6 @@ const görliInfo: NetworkInfo = { }, oldStatic: NOT_SUPPORT, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/kovan.ts b/src/constants/networks/kovan.ts index 8dcbfcbb84..9c6d058b47 100644 --- a/src/constants/networks/kovan.ts +++ b/src/constants/networks/kovan.ts @@ -39,8 +39,6 @@ const kovanInfo: NetworkInfo = { }, oldStatic: NOT_SUPPORT, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/matic.ts b/src/constants/networks/matic.ts index df4c4d7746..85ed10725a 100644 --- a/src/constants/networks/matic.ts +++ b/src/constants/networks/matic.ts @@ -27,7 +27,9 @@ const maticInfo: NetworkInfo = { logo: Polygon, decimal: 18, }, - rpcUrl: 'https://polygon.kyberengineering.io/v1/mainnet/geth?appId=prod-dmm', + rpcUrl: 'https://polygon.kyberengineering.io', + // Sometime, our rpc is quite slow, keep this for testing purpose on local + // rpcUrl: 'https://polygon-rpc.com', routerUri: `${process.env.REACT_APP_AGGREGATOR_API}/polygon/route/encode`, classic: { static: { @@ -41,8 +43,6 @@ const maticInfo: NetworkInfo = { router: '0x546C79662E028B661dFB4767664d0273184E4dD1', factory: '0x5F1fe642060B5B9658C15721Ea22E982643c095c', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: '0x89929Bc485cE72D2Af7b7283B40b921e9F4f80b3', fairlaunch: [ '0xc39bD0fAE646Cb026C73943C5B50E703de2a6532', diff --git a/src/constants/networks/mumbai.ts b/src/constants/networks/mumbai.ts index 0930a88f94..50a88f5015 100644 --- a/src/constants/networks/mumbai.ts +++ b/src/constants/networks/mumbai.ts @@ -42,8 +42,6 @@ const mumbaiInfo: NetworkInfo = { router: '0xD536e64EAe5FBc62E277167e758AfEA570279956', factory: '0x7900309d0b1c8D3d665Ae40e712E8ba4FC4F5453', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: ['0x882233B197F9e50b1d41F510fD803a510470d7a6'], fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/oasis.ts b/src/constants/networks/oasis.ts index 77ba80222f..6674caefe9 100644 --- a/src/constants/networks/oasis.ts +++ b/src/constants/networks/oasis.ts @@ -32,7 +32,7 @@ const oasisInfo: NetworkInfo = { logo: OASIS, decimal: 18, }, - rpcUrl: 'https://emerald.oasis.dev', + rpcUrl: 'https://oasis.kyberengineering.io', routerUri: `${process.env.REACT_APP_AGGREGATOR_API}/oasis/route/encode`, classic: { static: { @@ -46,8 +46,6 @@ const oasisInfo: NetworkInfo = { factory: '0xD9bfE9979e9CA4b2fe84bA5d4Cf963bBcB376974', }, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/optimism.ts b/src/constants/networks/optimism.ts index 406bd26c16..ccd0dda2fb 100644 --- a/src/constants/networks/optimism.ts +++ b/src/constants/networks/optimism.ts @@ -39,8 +39,6 @@ const optimismInfo: NetworkInfo = { }, oldStatic: NOT_SUPPORT, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/rinkeby.ts b/src/constants/networks/rinkeby.ts index aa9abc0664..be7de278f6 100644 --- a/src/constants/networks/rinkeby.ts +++ b/src/constants/networks/rinkeby.ts @@ -43,8 +43,6 @@ const rinkebyInfo: NetworkInfo = { router: EMPTY, factory: EMPTY, }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/networks/ropsten.ts b/src/constants/networks/ropsten.ts index 3d53d1d92e..664451b849 100644 --- a/src/constants/networks/ropsten.ts +++ b/src/constants/networks/ropsten.ts @@ -42,8 +42,6 @@ const ropstenInfo: NetworkInfo = { router: '0x96E8B9E051c81661C36a18dF64ba45F86AC80Aae', factory: '0x0639542a5cd99bd5f4e85f58cb1f61d8fbe32de9', }, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: '0xB2eA6DaAD5334907311c63a27EdFb02535048f50', fairlaunch: ['0x0FEEa33C4dE6f37A0Fc550028FddA2401B2Ee5Ce', '0xfEf235b06AFe69589e6C7622F4C071BcCed5bb13'], fairlaunchV2: [ diff --git a/src/constants/networks/velas.ts b/src/constants/networks/velas.ts index 6158ef233b..c0ea456379 100644 --- a/src/constants/networks/velas.ts +++ b/src/constants/networks/velas.ts @@ -46,8 +46,6 @@ const velasInfo: NetworkInfo = { factory: '0xD9bfE9979e9CA4b2fe84bA5d4Cf963bBcB376974', }, dynamic: NOT_SUPPORT, - routerV2: '0x00555513Acf282B42882420E5e5bA87b44D8fA6E', - aggregationExecutor: '0x41684b361557E9282E0373CA51260D9331e518C9', claimReward: EMPTY, fairlaunch: EMPTY_ARRAY, fairlaunchV2: EMPTY_ARRAY, diff --git a/src/constants/type.ts b/src/constants/type.ts index 1265e3fe3d..69fdc3275f 100644 --- a/src/constants/type.ts +++ b/src/constants/type.ts @@ -39,8 +39,6 @@ export type NetworkInfo = { readonly router: string readonly factory: string } | null - readonly routerV2: string //todo: remove in future - readonly aggregationExecutor: string //todo: remove in future readonly claimReward: string readonly fairlaunch: string[] readonly fairlaunchV2: string[] diff --git a/src/constants/v2.ts b/src/constants/v2.ts index 092fe74869..d601c4f281 100644 --- a/src/constants/v2.ts +++ b/src/constants/v2.ts @@ -5,6 +5,9 @@ export const FARM_CONTRACTS: { readonly [chainId in ChainId]?: Array } = [ChainId.BSCTESTNET]: [], [ChainId.RINKEBY]: ['0x13c8F670d3bbd4456870a2C49Bb927F166A977Bd'], [ChainId.ROPSTEN]: [], + [ChainId.MAINNET]: ['0x5C503D4b7DE0633f031229bbAA6A5e4A31cc35d8'], + [ChainId.AVAXMAINNET]: ['0x5C503D4b7DE0633f031229bbAA6A5e4A31cc35d8'], + [ChainId.ARBITRUM]: ['0x3227AE2ab4C2ACF2eE5202311C07731d3be9637B'], [ChainId.MATIC]: ['0xC56264Fbc6060DA60F10F3AB997DCfd72601AD4A', '0x5C503D4b7DE0633f031229bbAA6A5e4A31cc35d8'], } diff --git a/src/hooks/useActiveNetwork.ts b/src/hooks/useActiveNetwork.ts index ae8071e5ee..2f8ebf6cc0 100644 --- a/src/hooks/useActiveNetwork.ts +++ b/src/hooks/useActiveNetwork.ts @@ -43,7 +43,6 @@ export function useActiveNetwork() { const locationWithoutNetworkId = useMemo(() => { // Delete networkId from qs object - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { networkId, ...qsWithoutNetworkId } = qs return { ...location, search: stringify({ ...qsWithoutNetworkId }) } diff --git a/src/hooks/useBasicChartData.ts b/src/hooks/useBasicChartData.ts index 9a3d1fc8eb..125abfcbf3 100644 --- a/src/hooks/useBasicChartData.ts +++ b/src/hooks/useBasicChartData.ts @@ -80,6 +80,19 @@ const fetchKyberDataSWR = async (url: string) => { return res.json() } +const fetchKyberDataSWRWithHeader = async (url: string) => { + const res = await fetch(url, { + headers: { + 'accept-version': 'Latest', + }, + }) + if (!res.ok) throw new Error() + if (res.status === 204) { + throw new Error('No content') + } + return res.json() +} + const fetchCoingeckoDataSWR = async (tokenAddresses: any, chainId: any, timeFrame: any): Promise => { return await Promise.all( [tokenAddresses[0], tokenAddresses[1]].map(address => @@ -110,12 +123,23 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], .map(token => (token?.isNative ? WETH[chainId || ChainId.MAINNET].address : token?.address)?.toLowerCase()), [tokens, chainId], ) + + const { + data: coingeckoData, + error: coingeckoError, + isValidating: coingeckoLoading, + } = useSWR(tokenAddresses[0] && tokenAddresses[1] && [tokenAddresses, chainId, timeFrame], fetchCoingeckoDataSWR, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateIfStale: false, + }) + const { data: kyberData, error: kyberError, isValidating: kyberLoading, } = useSWR( - tokenAddresses[0] && tokenAddresses[1] + coingeckoError && tokenAddresses[0] && tokenAddresses[1] ? `https://price-chart.dev.kyberengineering.io/api/price-chart?chainId=${chainId}&timeWindow=${timeFrame.toLowerCase()}&tokenIn=${ tokenAddresses[0] }&tokenOut=${tokenAddresses[1]}` @@ -127,6 +151,7 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], revalidateIfStale: false, }, ) + const isKyberDataNotValid = useMemo(() => { if (kyberError || kyberData === null) return true if (kyberData && kyberData.length === 0) return true @@ -139,16 +164,6 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], return false }, [kyberError, kyberData]) - const { - data: coingeckoData, - error: coingeckoError, - isValidating: coingeckoLoading, - } = useSWR(isKyberDataNotValid ? [tokenAddresses, chainId, timeFrame] : null, fetchCoingeckoDataSWR, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateIfStale: false, - }) - const chartData = useMemo(() => { if (!isKyberDataNotValid && kyberData && kyberData.length > 0) { return kyberData @@ -174,7 +189,7 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], !isKyberDataNotValid && kyberData && chainId ? liveDataApi[chainId] + `?ids=${tokenAddresses[0]},${tokenAddresses[1]}` : null, - fetchKyberDataSWR, + fetchKyberDataSWRWithHeader, { refreshInterval: 60000, shouldRetryOnError: false, @@ -182,6 +197,7 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], revalidateIfStale: false, }, ) + const { data: liveCoingeckoData } = useSWR( isKyberDataNotValid && coingeckoData ? [tokenAddresses, chainId, 'live'] : null, fetchCoingeckoDataSWR, @@ -192,6 +208,7 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], revalidateIfStale: false, }, ) + const latestData = useMemo(() => { if (isKyberDataNotValid) { if (liveCoingeckoData) { @@ -214,6 +231,7 @@ export default function useBasicChartData(tokens: (Token | null | undefined)[], } return null }, [liveKyberData, liveCoingeckoData, isKyberDataNotValid, tokenAddresses]) + return { data: useMemo(() => (latestData ? [...chartData, latestData] : chartData), [latestData, chartData]), error: error, diff --git a/src/hooks/useMixpanel.ts b/src/hooks/useMixpanel.ts index 3fadfb3108..42df5961ad 100644 --- a/src/hooks/useMixpanel.ts +++ b/src/hooks/useMixpanel.ts @@ -114,6 +114,13 @@ export enum MIXPANEL_TYPE { TRANSAK_DOWNLOAD_WALLET_CLICKED, TRANSAK_SWAP_NOW_CLICKED, SWAP_BUY_CRYPTO_CLICKED, + + // for tutorial swap + TUTORIAL_CLICK_START, + TUTORIAL_CLICK_DONE, + TUTORIAL_CLICK_DENY, + TUTORIAL_VIEW_VIDEO_SWAP, + // type and swap TAS_TYPING_KEYWORD, TAS_SELECT_PAIR, @@ -131,6 +138,10 @@ export const NEED_CHECK_SUBGRAPH_TRANSACTION_TYPES = [ 'Elastic Create pool', ] +const redactAddress = (address: string) => { + return address ? address.slice(0, 7) + '...' + address.slice(-5) : undefined +} + export default function useMixpanel(trade?: Aggregator | undefined, currencies?: { [field in Field]?: Currency }) { const { chainId, account } = useWeb3React() const { saveGas } = useSwapState() @@ -163,7 +174,7 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.WALLET_CONNECTED: - mixpanel.register({ wallet_address: account, platform: isMobile ? 'Mobile' : 'Web', network }) + mixpanel.register({ wallet_address: redactAddress(account), platform: isMobile ? 'Mobile' : 'Web', network }) mixpanel.track('Wallet Connected') break case MIXPANEL_TYPE.SWAP_INITIATED: { @@ -192,7 +203,7 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: parseFloat(formatUnits(gas_price, 18)) * parseFloat(ethPrice.currentPrice) ).toFixed(4), - tx_hash: tx_hash, + tx_hash: redactAddress(tx_hash), max_return_or_low_gas: arbitrary.saveGas ? 'Lowest Gas' : 'Maximum Return', trade_qty: arbitrary.inputAmount, slippage_setting: arbitrary.slippageSetting, @@ -306,11 +317,11 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.ADD_LIQUIDITY_COMPLETED: { - mixpanel.track('Add Liquidity Completed', payload) + mixpanel.track('Add Liquidity Completed', { ...payload, tx_hash: redactAddress(payload.tx_hash) }) break } case MIXPANEL_TYPE.REMOVE_LIQUIDITY_COMPLETED: { - mixpanel.track('Remove Liquidity Completed', payload) + mixpanel.track('Remove Liquidity Completed', { ...payload, tx_hash: redactAddress(payload.tx_hash) }) break } case MIXPANEL_TYPE.REMOVE_LIQUIDITY_INITIATED: { @@ -484,7 +495,10 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.ELASTIC_CREATE_POOL_COMPLETED: { - mixpanel.track('Elastic Pools - Create New Pool Completed', payload) + mixpanel.track('Elastic Pools - Create New Pool Completed', { + ...payload, + tx_hash: redactAddress(payload.tx_hash), + }) break } case MIXPANEL_TYPE.ELASTIC_MYPOOLS_ELASTIC_POOLS_CLICKED: { @@ -504,7 +518,10 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.ELASTIC_ADD_LIQUIDITY_COMPLETED: { - mixpanel.track('Elastic Pools - Add Liquidity Completed', payload) + mixpanel.track('Elastic Pools - Add Liquidity Completed', { + ...payload, + tx_hash: redactAddress(payload.tx_hash), + }) break } case MIXPANEL_TYPE.ELASTIC_REMOVE_LIQUIDITY_INITIATED: { @@ -512,7 +529,10 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.ELASTIC_REMOVE_LIQUIDITY_COMPLETED: { - mixpanel.track('Elastic Pools - My Pools - Remove Liquidity Completed', payload) + mixpanel.track('Elastic Pools - My Pools - Remove Liquidity Completed', { + ...payload, + tx_hash: redactAddress(payload.tx_hash), + }) break } case MIXPANEL_TYPE.ELASTIC_INCREASE_LIQUIDITY_INITIATED: { @@ -520,7 +540,10 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: break } case MIXPANEL_TYPE.ELASTIC_INCREASE_LIQUIDITY_COMPLETED: { - mixpanel.track('Elastic Pools - My Pools - Increase Liquidity Completed', payload) + mixpanel.track('Elastic Pools - My Pools - Increase Liquidity Completed', { + ...payload, + tx_hash: redactAddress(payload.tx_hash), + }) break } case MIXPANEL_TYPE.ELASTIC_COLLECT_FEES_INITIATED: { @@ -605,6 +628,22 @@ export default function useMixpanel(trade?: Aggregator | undefined, currencies?: mixpanel.track('Buy Crypto - Click on Buy Crypto on KyberSwap') break } + case MIXPANEL_TYPE.TUTORIAL_CLICK_START: { + mixpanel.track('On-Screen Guide - User click on "View" in Setting to view guide') + break + } + case MIXPANEL_TYPE.TUTORIAL_CLICK_DENY: { + mixpanel.track('On-Screen Guide - User click on "Dismiss" button', { step: payload }) + break + } + case MIXPANEL_TYPE.TUTORIAL_CLICK_DONE: { + mixpanel.track('On-Screen Guide - User click on "Done" button at Step 8') + break + } + case MIXPANEL_TYPE.TUTORIAL_VIEW_VIDEO_SWAP: { + mixpanel.track('On-Screen Guide - User click on Step 3 Embedded video') + break + } // type and swap case MIXPANEL_TYPE.TAS_TYPING_KEYWORD: { @@ -838,10 +877,11 @@ export const useGlobalMixpanelEvents = () => { useEffect(() => { if (account && isAddress(account)) { + const redactedAccount = redactAddress(account) mixpanel.init(process.env.REACT_APP_MIXPANEL_PROJECT_TOKEN || '', { debug: process.env.REACT_APP_MAINNET_ENV === 'staging', }) - mixpanel.identify(account) + mixpanel.identify(redactedAccount) const getQueryParam = (url: string, param: string) => { // eslint-disable-next-line @@ -876,7 +916,7 @@ export const useGlobalMixpanelEvents = () => { mixpanel.people.set_once(first_params) mixpanel.register_once(params) - mixpanelHandler(MIXPANEL_TYPE.WALLET_CONNECTED, { account }) + mixpanelHandler(MIXPANEL_TYPE.WALLET_CONNECTED) } return () => { if (mixpanel.hasOwnProperty('persistence')) { diff --git a/src/pages/About/AboutKyberSwap.tsx b/src/pages/About/AboutKyberSwap.tsx index dd412f6848..64b69acd00 100644 --- a/src/pages/About/AboutKyberSwap.tsx +++ b/src/pages/About/AboutKyberSwap.tsx @@ -57,6 +57,7 @@ import { useDarkModeManager } from 'state/user/hooks' import { ExternalLink, StyledInternalLink } from 'theme' import { formatBigLiquidity } from 'utils/formatBalance' +import KyberSwapGeneralIntro from './KyberSwapGeneralIntro' import { AboutKNC, AboutPage, @@ -443,13 +444,6 @@ function AboutKyberSwap() { - - - KyberSwap is DeFi’s premier automated market maker, providing the best token prices for traders across - multiple exchanges, and maximizing earnings for liquidity providers, in one decentralized platform. - - - @@ -465,36 +459,7 @@ function AboutKyberSwap() { - - mixpanelHandler(MIXPANEL_TYPE.ABOUT_SWAP_CLICKED)} - as={Link} - to="/swap?highlightBox=true" - > - - - Swap Now - - - mixpanelHandler(MIXPANEL_TYPE.ABOUT_START_EARNING_CLICKED)} - style={{ flex: 1 }} - > - - - Start Earning - - - + diff --git a/src/pages/About/KyberSwapGeneralIntro.tsx b/src/pages/About/KyberSwapGeneralIntro.tsx new file mode 100644 index 0000000000..d4775480da --- /dev/null +++ b/src/pages/About/KyberSwapGeneralIntro.tsx @@ -0,0 +1,154 @@ +import { Trans } from '@lingui/macro' +import React from 'react' +import { Repeat } from 'react-feather' +import { Link } from 'react-router-dom' +import { useMedia } from 'react-use' +import { Box, Flex, Text } from 'rebass' + +import { ButtonLight, ButtonPrimary } from 'components/Button' +import { MoneyBagOutline } from 'components/Icons' +import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' +import useTheme from 'hooks/useTheme' + +const KyberSwapGeneralIntro = () => { + const above768 = useMedia('(min-width: 768px)') + const theme = useTheme() + const { mixpanelHandler } = useMixpanel() + + const renderKyberSwapIntroDEX = () => { + return ( + + + KyberSwap is a decentralized exchange (DEX) aggregator. We provide our traders with the{' '} + best token prices by analyzing rates across thousands of exchanges instantly! + + + ) + } + + const renderKyberSwapIntroAMM = () => { + return ( + + + KyberSwap is also an automated market maker (AMM) with industry-leading liquidity protocols and{' '} + concentrated liquidity. Liquidity providers can add liquidity to our pools & earn fees! + + + ) + } + + const renderSwapNowButton = () => { + return ( + mixpanelHandler(MIXPANEL_TYPE.ABOUT_SWAP_CLICKED)} + as={Link} + to="/swap?highlightBox=true" + style={{ + width: '216px', + padding: '10px 12px', + borderRadius: '32px', + }} + > + + + Swap Now + + + ) + } + + const renderStartEarningButton = () => { + return ( + mixpanelHandler(MIXPANEL_TYPE.ABOUT_START_EARNING_CLICKED)} + style={{ + width: '216px', + }} + > + + + Start Earning + + + ) + } + + if (above768) { + return ( + + {renderKyberSwapIntroDEX()} + {renderKyberSwapIntroAMM()} + {renderSwapNowButton()} + {renderStartEarningButton()} + + ) + } + + return ( + + + {renderKyberSwapIntroDEX()} + {renderSwapNowButton()} + + + + {renderKyberSwapIntroAMM()} + {renderStartEarningButton()} + + + ) +} + +export default KyberSwapGeneralIntro diff --git a/src/pages/About/styleds.tsx b/src/pages/About/styleds.tsx index 85d3e09f2f..c6216fc2b1 100644 --- a/src/pages/About/styleds.tsx +++ b/src/pages/About/styleds.tsx @@ -70,7 +70,7 @@ export const BtnPrimary = styled(ButtonPrimary)` export const OverflowStatisticWrapper = styled.div` margin-top: 160px; - ${({ theme }) => theme.mediaWidth.upToMedium` + ${({ theme }) => theme.mediaWidth.upToMedium` margin-left: 0; margin-right: 0; `} diff --git a/src/pages/BuyCrypto/index.tsx b/src/pages/BuyCrypto/index.tsx index f9fc80d9bc..1fbf3841c7 100644 --- a/src/pages/BuyCrypto/index.tsx +++ b/src/pages/BuyCrypto/index.tsx @@ -14,6 +14,7 @@ import gPay from 'assets/buy-crypto/google-pay.svg' import introImg from 'assets/buy-crypto/intro.png' import masterCard from 'assets/buy-crypto/master-card.svg' import visa from 'assets/buy-crypto/visa.svg' +import { ReactComponent as Brave } from 'assets/images/brave_wallet.svg' import c98 from 'assets/images/coin98.svg' import { ReactComponent as Ledger } from 'assets/images/ledger.svg' import metamask from 'assets/images/metamask.svg' @@ -27,6 +28,7 @@ import CopyHelper from 'components/Copy' import Cart from 'components/Icons/Cart' import Deposit from 'components/Icons/Deposit' import Modal from 'components/Modal' +import { SUPPORTED_WALLETS } from 'constants/index' import { useActiveWeb3React } from 'hooks' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' @@ -260,33 +262,22 @@ function BuyCrypto() { - - - MetaMask - - - - Coin98 - - - - - WalletConnect - - - - - Coinbase Wallet - - - - - Ledger - + {Object.values(SUPPORTED_WALLETS) + .filter(e => e.installLink) + .map(item => ( + + + {item.name} + + ))} @@ -394,12 +385,13 @@ function BuyCrypto() { - + + diff --git a/src/pages/Campaign/CampaignButtonWithOptions.tsx b/src/pages/Campaign/CampaignButtonWithOptions.tsx index 0f246bb5c5..b4ac2d1d1e 100644 --- a/src/pages/Campaign/CampaignButtonWithOptions.tsx +++ b/src/pages/Campaign/CampaignButtonWithOptions.tsx @@ -159,7 +159,12 @@ export default function CampaignButtonWithOptions({ onClick={async () => { if (type === 'enter_now') { mixpanelHandler(MIXPANEL_TYPE.CAMPAIGN_ENTER_NOW_CLICKED, { campaign_name: campaign?.name }) - window.open(campaign?.enterNowUrl + '?networkId=' + chainId) + let url = campaign?.enterNowUrl + '?networkId=' + chainId + if (campaign?.eligibleTokens?.length) { + const outputCurrency = campaign?.eligibleTokens[0].address + url += '&outputCurrency=' + outputCurrency + } + window.open(url) } else { mixpanelHandler(MIXPANEL_TYPE.CAMPAIGN_CLAIM_REWARDS_CLICKED, { campaign_name: campaign?.name }) await changeNetwork(chainId, () => claimRewards(chainId)) @@ -167,7 +172,7 @@ export default function CampaignButtonWithOptions({ }} > Network - + {type === 'enter_now' ? t`Swap on ${NETWORKS_INFO[chainId].name}` : t`Claim on ${NETWORKS_INFO[chainId].name}`} @@ -189,6 +194,7 @@ const StyledCampaignButtonWithOptions = styled(ButtonPrimary)` height: 44px; font-weight: 500; color: ${({ theme }) => theme.textReverse}; + border: none; ${({ theme }) => theme.mediaWidth.upToSmall` ${css` diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index 3bd1a542c7..dcf19ed216 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -118,7 +118,10 @@ export const FilterRow = styled(Flex)` ${({ theme }) => theme.mediaWidth.upToSmall` align-items: flex-start; flex-direction: column-reverse; - >div { + gap: 0; + + > div { + margin-top: 12px; width: 100% justify-content: space-between &:nth-child(1){ diff --git a/src/pages/Pools/InstructionAndGlobalData.tsx b/src/pages/Pools/InstructionAndGlobalData.tsx index e4623b0e6c..da358d0e5c 100644 --- a/src/pages/Pools/InstructionAndGlobalData.tsx +++ b/src/pages/Pools/InstructionAndGlobalData.tsx @@ -54,6 +54,10 @@ const DetailWrapper = styled.div<{ isOpen?: boolean }>` const DetailWrapperClassic = styled(DetailWrapper)` grid-template-columns: 1fr 1fr 1fr; + + ${({ theme }) => theme.mediaWidth.upToMedium` + grid-template-columns: 1fr; + `} ` const DetailItem = styled.div` border-radius: 20px; diff --git a/src/pages/Pools/index.tsx b/src/pages/Pools/index.tsx index 87b1b96839..6663f79a7d 100644 --- a/src/pages/Pools/index.tsx +++ b/src/pages/Pools/index.tsx @@ -1,6 +1,6 @@ import { Currency } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' -import React, { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { Link, RouteComponentProps } from 'react-router-dom' import { useMedia } from 'react-use' import { Flex, Text } from 'rebass' @@ -14,6 +14,7 @@ import PoolsCurrencyInputPanel from 'components/PoolsCurrencyInputPanel' import Search from 'components/Search' import { SwitchLocaleLink } from 'components/SwitchLocaleLink' import Toggle from 'components/Toggle' +import { MouseoverTooltip } from 'components/Tooltip' import Tutorial, { TutorialType } from 'components/Tutorial' import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' @@ -61,6 +62,22 @@ const ButtonLightWithHighlight = styled(ButtonLight)` } ` +const TextWithTooltip = styled(Text)` + position: relative; + cursor: pointer; + + ::after { + content: ''; + position: absolute; + bottom: -2px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 2px); + height: 0; + border-bottom: ${({ theme }) => `1px dashed ${theme.subText}`}; + } +` + const Pools = ({ match: { params: { currencyIdA, currencyIdB }, @@ -74,6 +91,7 @@ const Pools = ({ const above1260 = useMedia('(min-width: 1260px)') const below1124 = useMedia('(max-width: 1124px)') const [isShowOnlyActiveFarmPools, setIsShowOnlyActiveFarmPools] = useState(false) + const [shouldShowLowTVLPools, setShowLowTVLPools] = useState(false) const qs = useParsedQueryString() const searchValueInQs: string = (qs.search as string) ?? '' const debouncedSearchValue = useDebounce(searchValueInQs.trim().toLowerCase(), 200) @@ -214,7 +232,19 @@ const Pools = ({ - + + + + Low TVL Pools + + + + + setShowLowTVLPools(prev => !prev)} /> + + + + Farming Pools @@ -400,10 +430,13 @@ const Pools = ({ )} - + + Farming Pools @@ -414,6 +447,18 @@ const Pools = ({ toggle={() => setIsShowOnlyActiveFarmPools(prev => !prev)} /> + + + + + + Low TVL Pools + + + + + setShowLowTVLPools(prev => !prev)} /> + )} @@ -424,6 +469,7 @@ const Pools = ({ searchValue={debouncedSearchValue} isShowOnlyActiveFarmPools={isShowOnlyActiveFarmPools} onlyShowStable={onlyShowStable} + shouldShowLowTVLPools={shouldShowLowTVLPools} /> ) : ( )} diff --git a/src/pages/ProAmmPools/CardItem.tsx b/src/pages/ProAmmPools/CardItem.tsx index 907594d39c..11a3761635 100644 --- a/src/pages/ProAmmPools/CardItem.tsx +++ b/src/pages/ProAmmPools/CardItem.tsx @@ -1,7 +1,7 @@ import { ChainId, Token, WETH } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' import { rgba } from 'polished' -import React, { useState } from 'react' +import { useState } from 'react' import { ChevronUp, Share2 } from 'react-feather' import { Link } from 'react-router-dom' import { Flex, Text } from 'rebass' @@ -18,12 +18,13 @@ import { ELASTIC_BASE_FEE_UNIT, PROMM_ANALYTICS_URL } from 'constants/index' import { nativeOnChain } from 'constants/tokens' import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' +import { useAllTokens } from 'hooks/Tokens' import useTheme from 'hooks/useTheme' import { IconWrapper } from 'pages/Pools/styleds' import { useProMMFarms } from 'state/farms/promm/hooks' import { ProMMPoolData } from 'state/prommPools/hooks' import { ExternalLink } from 'theme' -import { shortenAddress } from 'utils' +import { isAddressString, shortenAddress } from 'utils' import { formatDollarAmount } from 'utils/numbers' interface ListItemProps { @@ -70,9 +71,14 @@ export default function ProAmmPoolCardItem({ pair, onShared, userPositions, idx const theme = useTheme() const [isOpen, setIsOpen] = useState(true) + const allTokens = useAllTokens() const { data: farms } = useProMMFarms() - const token0 = new Token(chainId as ChainId, pair[0].token0.address, pair[0].token0.decimals, pair[0].token0.symbol) - const token1 = new Token(chainId as ChainId, pair[0].token1.address, pair[0].token1.decimals, pair[0].token1.symbol) + const token0 = + allTokens[isAddressString(pair[0].token0.address)] || + new Token(chainId as ChainId, pair[0].token0.address, pair[0].token0.decimals, pair[0].token0.symbol) + const token1 = + allTokens[isAddressString(pair[0].token1.address)] || + new Token(chainId as ChainId, pair[0].token1.address, pair[0].token1.decimals, pair[0].token1.symbol) const token0Address = token0.address.toLowerCase() === WETH[chainId as ChainId].address.toLowerCase() diff --git a/src/pages/ProAmmPools/ListItem.tsx b/src/pages/ProAmmPools/ListItem.tsx index 6572d7d4f5..67829375a2 100644 --- a/src/pages/ProAmmPools/ListItem.tsx +++ b/src/pages/ProAmmPools/ListItem.tsx @@ -1,7 +1,7 @@ import { ChainId, Token, WETH } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' import { rgba } from 'polished' -import React, { useState } from 'react' +import { useState } from 'react' import { BarChart2, ChevronUp, Plus, Share2 } from 'react-feather' import { Link } from 'react-router-dom' import { Flex, Text } from 'rebass' @@ -17,13 +17,14 @@ import { ELASTIC_BASE_FEE_UNIT, PROMM_ANALYTICS_URL } from 'constants/index' import { nativeOnChain } from 'constants/tokens' import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' +import { useAllTokens } from 'hooks/Tokens' import useMixpanel, { MIXPANEL_TYPE } from 'hooks/useMixpanel' import useTheme from 'hooks/useTheme' import { ButtonIcon } from 'pages/Pools/styleds' import { useProMMFarms } from 'state/farms/promm/hooks' import { ProMMPoolData } from 'state/prommPools/hooks' import { ExternalLink } from 'theme' -import { shortenAddress } from 'utils' +import { isAddressString, shortenAddress } from 'utils' import { formatDollarAmount } from 'utils/numbers' import { ReactComponent as ViewPositionIcon } from '../../assets/svg/view_positions.svg' @@ -93,8 +94,14 @@ export default function ProAmmPoolListItem({ pair, idx, onShared, userPositions, const theme = useTheme() const [isOpen, setIsOpen] = useState(pair.length > 1 ? idx === 0 : false) - const token0 = new Token(chainId as ChainId, pair[0].token0.address, pair[0].token0.decimals, pair[0].token0.symbol) - const token1 = new Token(chainId as ChainId, pair[0].token1.address, pair[0].token1.decimals, pair[0].token1.symbol) + const allTokens = useAllTokens() + + const token0 = + allTokens[isAddressString(pair[0].token0.address)] || + new Token(chainId as ChainId, pair[0].token0.address, pair[0].token0.decimals, pair[0].token0.symbol) + const token1 = + allTokens[isAddressString(pair[0].token1.address)] || + new Token(chainId as ChainId, pair[0].token1.address, pair[0].token1.decimals, pair[0].token1.symbol) const { data: farms } = useProMMFarms() @@ -115,7 +122,7 @@ export default function ProAmmPoolListItem({ pair, idx, onShared, userPositions, const token0Symbol = pool.token0.address === WETH[chainId as ChainId].address.toLowerCase() ? nativeOnChain(chainId as ChainId).symbol - : pool.token0.symbol + : token0.symbol const token1Address = pool.token1.address === WETH[chainId as ChainId].address.toLowerCase() @@ -124,7 +131,7 @@ export default function ProAmmPoolListItem({ pair, idx, onShared, userPositions, const token1Symbol = pool.token1.address === WETH[chainId as ChainId].address.toLowerCase() ? nativeOnChain(chainId as ChainId).symbol - : pool.token1.symbol + : token1.symbol const isFarmingPool = Object.values(farms) .flat() diff --git a/src/pages/ProAmmPools/index.tsx b/src/pages/ProAmmPools/index.tsx index 40668ff2fd..393e563635 100644 --- a/src/pages/ProAmmPools/index.tsx +++ b/src/pages/ProAmmPools/index.tsx @@ -29,6 +29,7 @@ type PoolListProps = { searchValue: string isShowOnlyActiveFarmPools: boolean onlyShowStable: boolean + shouldShowLowTVLPools: boolean } const PageWrapper = styled.div` @@ -85,6 +86,7 @@ export default function ProAmmPoolList({ searchValue, isShowOnlyActiveFarmPools, onlyShowStable, + shouldShowLowTVLPools, }: PoolListProps) { const above1000 = useMedia('(min-width: 1000px)') @@ -146,6 +148,10 @@ export default function ProAmmPoolList({ filteredPools = filteredPools.filter(pool => activePoolFarmAddress.includes(pool.address.toLowerCase())) } + if (!shouldShowLowTVLPools) { + filteredPools = filteredPools.filter(pool => pool.tvlUSD > 1) + } + if (caId && cbId && caId === cbId) filteredPools = [] else { if (caId) @@ -173,7 +179,18 @@ export default function ProAmmPoolList({ }, initPairs) return Object.values(poolsGroupByPair).sort((a, b) => listComparator(a[0], b[0])) - }, [poolDatas, searchValue, caId, cbId, listComparator, farms, isShowOnlyActiveFarmPools, chainId, onlyShowStable]) + }, [ + poolDatas, + isShowOnlyActiveFarmPools, + shouldShowLowTVLPools, + caId, + cbId, + onlyShowStable, + searchValue, + farms, + chainId, + listComparator, + ]) const renderHeader = () => { return above1000 ? ( diff --git a/src/pages/SwapV2/index.tsx b/src/pages/SwapV2/index.tsx index b620ef7009..4e21afecd4 100644 --- a/src/pages/SwapV2/index.tsx +++ b/src/pages/SwapV2/index.tsx @@ -1,7 +1,6 @@ import { ChainId, Currency, CurrencyAmount, NativeCurrency, Token } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' import JSBI from 'jsbi' -import { debounce } from 'lodash' import { stringify } from 'qs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { BrowserView } from 'react-device-detect' @@ -33,6 +32,8 @@ import { MouseoverTooltip } from 'components/Tooltip' import TopTrendingSoonTokensInCurrentNetwork from 'components/TopTrendingSoonTokensInCurrentNetwork' import TrendingSoonTokenBanner from 'components/TrendingSoonTokenBanner' import Tutorial, { TutorialType } from 'components/Tutorial' +import TutorialSwap from 'components/Tutorial/TutorialSwap' +import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' import AdvancedSwapDetailsDropdown from 'components/swapv2/AdvancedSwapDetailsDropdown' import ConfirmSwapModal from 'components/swapv2/ConfirmSwapModal' import GasPriceTrackerPanel from 'components/swapv2/GasPriceTrackerPanel' @@ -87,6 +88,7 @@ import { useToggleTransactionSettingsMenu, useWalletModalToggle } from 'state/ap import { Field } from 'state/swap/actions' import { useDefaultsFromURLSearch, useSwapActionHandlers, useSwapState } from 'state/swap/hooks' import { useDerivedSwapInfoV2 } from 'state/swap/useAggregator' +import { useTutorialSwapGuide } from 'state/tutorial/hooks' import { useExpertModeManager, useShowLiveChart, @@ -122,7 +124,7 @@ enum TAB { // LIMIT = 'limit' } -const SwapFormWrapper = styled.div` +const SwapFormWrapper = styled.div<{ isShowTutorial: boolean }>` width: 100%; max-width: 425px; @@ -130,7 +132,11 @@ const SwapFormWrapper = styled.div` max-width: 404px; } @media only screen and (min-width: 1100px) { - position: sticky; + position: ${({ isShowTutorial }) => (isShowTutorial ? 'unset' : 'sticky')}; + /** + When tutorial appear, there is no need sticky form. + Besides, it is also easy for us control position of tutorial popup when scroll page. + */ top: 16px; } ` @@ -183,6 +189,7 @@ export default function Swap({ history }: RouteComponentProps) { const isShowTradeRoutes = useShowTradeRoutes() const isShowTokenInfoSetting = useShowTokenInfo() const qs = useParsedQueryString() + const [{ show: isShowTutorial = false }] = useTutorialSwapGuide() const refSuggestPair = useRef(null) const [showingPairSuggestionImport, setShowingPairSuggestionImport] = useState(false) // show modal import when click pair suggestion @@ -241,6 +248,7 @@ export default function Swap({ history }: RouteComponentProps) { tradeComparer, onRefresh, loading: loadingAPI, + isPairNotfound, } = useDerivedSwapInfoV2() const currencyIn = currencies[Field.INPUT] @@ -609,54 +617,46 @@ export default function Swap({ history }: RouteComponentProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [tokenImports]) + const initialTotalTokenDefault = useRef(null) + + useEffect(() => { + checkAutoSelectTokenFromUrl() + initialTotalTokenDefault.current = Object.keys(defaultTokens).length // it will be equal with tokenImports.length + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const isLoadedTokenDefault = account + ? Object.keys(defaultTokens).length > 0 + : initialTotalTokenDefault.current !== null && Object.keys(defaultTokens).length > initialTotalTokenDefault.current // + useEffect(() => { /** * defaultTokens change only when: * - the first time get data * - change network * - import/remove token */ - if (refIsCheckNetworkAutoSelect.current && !refIsImportUserToken.current && Object.keys(defaultTokens).length) { + if (refIsCheckNetworkAutoSelect.current && !refIsImportUserToken.current && isLoadedTokenDefault) { findTokenPairFromUrl() } refIsImportUserToken.current = false // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultTokens, refIsCheckNetworkAutoSelect.current]) - useEffect(() => { - checkAutoSelectTokenFromUrl() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - useEffect(() => { if (isSelectCurencyMannual) syncUrl(currencyIn, currencyOut) // when we select token manual }, [currencyIn, currencyOut, isSelectCurencyMannual, syncUrl]) - const refLoadedCurrency = useRef<{ - currencyIn: Currency | null | undefined - currencyOut: Currency | null | undefined - }>({ currencyIn: null, currencyOut: null }) - + // swap?inputCurrency=xxx&outputCurrency=yyy. xxx yyy not exist in chain => remove params => select default pair useEffect(() => { - refLoadedCurrency.current = { currencyIn: loadedInputCurrency, currencyOut: loadedOutputCurrency } - }, [loadedInputCurrency, loadedOutputCurrency]) - - const checkParamWrong = useCallback(() => { - const { currencyIn, currencyOut } = refLoadedCurrency.current - if (!currencyIn || !currencyOut) { + if (isPairNotfound) { const newQuery = { ...qs } - if (!currencyIn) delete newQuery.inputCurrency - if (!currencyOut) delete newQuery.outputCurrency + delete newQuery.inputCurrency + delete newQuery.outputCurrency history.replace({ search: stringify(newQuery), }) } - }, [qs, history]) - - // swap?inputCurrency=xxx&outputCurrency=yyy. xxx yyy not exist in chain => remove params => select default pair - const checkParamWrongDebounce = useMemo(() => debounce(checkParamWrong, 300), [checkParamWrong]) - useEffect(() => { - checkParamWrongDebounce() - }, [chainId, checkParamWrongDebounce]) + }, [isPairNotfound, history, qs]) useEffect(() => { if (isExpertMode) { @@ -674,8 +674,7 @@ export default function Swap({ history }: RouteComponentProps) { })}` : '' }` - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currencyIn, currencyOut, chainId, currencyId, window.location.origin]) + }, [currencyIn, currencyOut, chainId]) const { isInWhiteList: isPairInWhiteList, canonicalUrl } = checkPairInWhiteList( chainId, @@ -686,7 +685,7 @@ export default function Swap({ history }: RouteComponentProps) { const shouldRenderTokenInfo = isShowTokenInfoSetting && currencyIn && currencyOut && isPairInWhiteList const isShowModalImportToken = - importTokensNotInDefault.length > 0 && (!dismissTokenWarning || showingPairSuggestionImport) + isLoadedTokenDefault && importTokensNotInDefault.length > 0 && (!dismissTokenWarning || showingPairSuggestionImport) return ( <> @@ -695,7 +694,7 @@ export default function Swap({ history }: RouteComponentProps) { * => add canonical link that specify which is main page, => /swap/bnb/knc-to-usdt */} - + - + @@ -746,7 +745,9 @@ export default function Swap({ history }: RouteComponentProps) { placement="top" width="fit-content" > - + + + {/* */} @@ -761,10 +762,10 @@ export default function Swap({ history }: RouteComponentProps) { /> - + {activeTab === TAB.SWAP && ( <> - + Promise + } +} interface Window { ethereum?: { isMetaMask?: boolean isCoin98?: boolean + isBraveWallet?: any on?: (...args: any[]) => void removeListener?: (...args: any[]) => void request: (params: { method: string; params?: any }) => Promise diff --git a/src/state/campaigns/actions.ts b/src/state/campaigns/actions.ts index d716302688..9c625903fd 100644 --- a/src/state/campaigns/actions.ts +++ b/src/state/campaigns/actions.ts @@ -81,6 +81,7 @@ export interface CampaignData { enterNowUrl: string rewardDistribution: RewardDistribution[] campaignState: CampaignState + eligibleTokens: SerializedToken[] chainIds: string rewardChainIds: string tradingVolumeRequired: number diff --git a/src/state/campaigns/updater.tsx b/src/state/campaigns/updater.tsx index bb4992069f..97c415f1f4 100644 --- a/src/state/campaigns/updater.tsx +++ b/src/state/campaigns/updater.tsx @@ -141,6 +141,18 @@ export default function CampaignsUpdater(): null { chainIds: campaign.chainIds, rewardChainIds: campaign.rewardChainIds, tradingVolumeRequired: campaign.tradingVolumeRequired, + eligibleTokens: campaign.eligibleTokens.map( + ({ chainId, name, symbol, address, logoURI, decimals }: any): SerializedToken => { + return { + chainId, + name, + symbol, + address, + logoURI, + decimals, + } + }, + ), } }) return formattedCampaigns diff --git a/src/state/farms/hooks.ts b/src/state/farms/hooks.ts index 5165ffd8db..d12ecf180e 100644 --- a/src/state/farms/hooks.ts +++ b/src/state/farms/hooks.ts @@ -21,6 +21,7 @@ import { } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import { nativeOnChain } from 'constants/tokens' +import { VERSION } from 'constants/v2' import { useActiveWeb3React } from 'hooks' import { useAllTokens } from 'hooks/Tokens' import { useFairLaunchContracts } from 'hooks/useContract' @@ -75,11 +76,14 @@ export const useRewardTokens = () => { }, [rewardTokensMulticallResult, rewardTokensV2MulticallResult, defaultRewards]) } -export const useRewardTokenPrices = (tokens: (Token | undefined)[]) => { - const tokenPrices = useTokensPrice(tokens) +export const useRewardTokenPrices = (tokens: (Token | undefined | null)[], version?: VERSION) => { + const tokenPrices = useTokensPrice(tokens, version) const marketPrices = useTokensMarketPrice(tokens) - return tokenPrices.map((price, index) => marketPrices[index] || price || 0) + return useMemo( + () => tokenPrices.map((price, index) => marketPrices[index] || price || 0), + [tokenPrices, marketPrices], + ) } export const useFarmsData = (isIncludeOutsideFarms = true) => { diff --git a/src/state/farms/promm/actions.ts b/src/state/farms/promm/actions.ts index 598e2c5ebd..d00ad4747a 100644 --- a/src/state/farms/promm/actions.ts +++ b/src/state/farms/promm/actions.ts @@ -8,3 +8,5 @@ export const setShowConfirm = createAction('prommVesting/setShowConfirm export const setAttemptingTxn = createAction('prommVesting/setAttemptingTxn') export const setVestingTxHash = createAction('prommVesting/setVestingTxHash') export const setError = createAction('prommVesting/setError') +export const addFailedNFTs = createAction('elasticFarm/addFailedNFTs') +export const resetErrorNFTs = createAction('elasticFarm/resetErrorNFTs') diff --git a/src/state/farms/promm/hooks.ts b/src/state/farms/promm/hooks.ts index 97aae4f12c..5863babc24 100644 --- a/src/state/farms/promm/hooks.ts +++ b/src/state/farms/promm/hooks.ts @@ -16,19 +16,20 @@ import { CONTRACT_NOT_FOUND_MSG } from 'constants/messages' import { NETWORKS_INFO } from 'constants/networks' import { FARM_CONTRACTS, VERSION } from 'constants/v2' import { providers, useActiveWeb3React } from 'hooks' -import { useTokens } from 'hooks/Tokens' +import { useAllTokens, useTokens } from 'hooks/Tokens' import { useProAmmNFTPositionManagerContract, useProMMFarmContract, useProMMFarmContracts } from 'hooks/useContract' import { usePools } from 'hooks/usePools' import usePrevious from 'hooks/usePrevious' import { AppState } from 'state' -import { useETHPrice, useTokensPrice } from 'state/application/hooks' +import { useETHPrice } from 'state/application/hooks' import { useAppDispatch } from 'state/hooks' import { usePoolBlocks } from 'state/prommPools/hooks' import { useTransactionAdder } from 'state/transactions/hooks' import { PositionDetails } from 'types/position' import { calculateGasMargin, getContractForReading, isAddressString } from 'utils' -import { setLoading, updatePrommFarms } from './actions' +import { useRewardTokenPrices } from '../hooks' +import { addFailedNFTs, setLoading, updatePrommFarms } from './actions' import { ProMMFarm, ProMMFarmResponse } from './types' export const useProMMFarms = () => { @@ -38,7 +39,16 @@ export const useProMMFarms = () => { export const useGetProMMFarms = () => { const dispatch = useAppDispatch() const { chainId, account } = useActiveWeb3React() + + // TODO: revert this address belongs to user which affected by farm issue + // const account = '0x6f0Ca88060F91b5eF839bE5a7211293Bfc27c06C' const prommFarmContracts = useProMMFarmContracts() + const tokens = useAllTokens() + + // dont need all tokens on dependency + const allTokensRef = useRef(tokens) + allTokensRef.current = tokens + const positionManager = useProAmmNFTPositionManagerContract() const prevChainId = usePrevious(chainId) @@ -111,6 +121,16 @@ export const useGetProMMFarms = () => { ), ) + const errorNFTs: string[] = [] + userInfo.forEach((info, index) => { + if (info instanceof Error && info.message.includes('Panic')) + errorNFTs.push(userNFTForPool[index].tokenId.toString()) + }) + + if (errorNFTs.length) { + dispatch(addFailedNFTs(errorNFTs)) + } + const userNFTInfo = userInfo // .filter(item => item.pid === pid) .map((item, index) => { @@ -143,6 +163,8 @@ export const useGetProMMFarms = () => { pid: pid, userDepositedNFTs: userNFTInfo, rewardLocker, + token0Info: allTokensRef.current?.[token0], + token1Info: allTokensRef.current?.[token1], } }), ) @@ -240,6 +262,22 @@ export const useFarmAction = (address: string) => { [addTransactionWithType, contract], ) + const emergencyWithdraw = useCallback( + async (nftIds: BigNumber[]) => { + if (!contract) { + throw new Error(CONTRACT_NOT_FOUND_MSG) + } + const estimateGas = await contract.estimateGas.emergencyWithdraw(nftIds) + const tx = await contract.emergencyWithdraw(nftIds, { + gasLimit: calculateGasMargin(estimateGas), + }) + addTransactionWithType(tx, { type: 'ForceWithdraw' }) + + return tx.hash + }, + [addTransactionWithType, contract], + ) + const stake = useCallback( async (pid: BigNumber, nftIds: BigNumber[], liqs: BigNumber[]) => { if (!contract) { @@ -297,7 +335,7 @@ export const useFarmAction = (address: string) => { [addTransactionWithType, contract], ) - return { deposit, withdraw, approve, stake, unstake, harvest } + return { deposit, withdraw, approve, stake, unstake, harvest, emergencyWithdraw } } export const usePostionFilter = (positions: PositionDetails[], validPools: string[]) => { @@ -442,16 +480,21 @@ export const useProMMFarmTVL = (fairlaunchAddress: string, pid: number) => { const dataClient = NETWORKS_INFO[chainId || ChainId.MAINNET].elasticClient const { block24 } = usePoolBlocks() - const { data } = useQuery(PROMM_JOINED_POSITION(fairlaunchAddress.toLowerCase(), pid, block24), { + const { data, loading } = useQuery(PROMM_JOINED_POSITION(fairlaunchAddress.toLowerCase(), pid, block24), { client: dataClient, fetchPolicy: 'cache-first', }) - const rewardAddress = useMemo(() => data?.farmingPool?.rewardTokens.map(item => item.id) || [], [data]) + const rewardAddress = useMemo( + () => data?.farmingPool?.rewardTokens.map(item => isAddressString(item.id)) || [], + [data], + ) const rwTokenMap = useTokens(rewardAddress) const rwTokens = useMemo(() => Object.values(rwTokenMap), [rwTokenMap]) - const prices = useTokensPrice(rwTokens, VERSION.ELASTIC) + + const prices = useRewardTokenPrices(rwTokens) + const priceMap: { [key: string]: number } = useMemo( () => prices?.reduce( @@ -466,7 +509,16 @@ export const useProMMFarmTVL = (fairlaunchAddress: string, pid: number) => { const ethPriceUSD = useETHPrice(VERSION.ELASTIC) - return useMemo(() => { + const [farmData, setData] = useState({ + tvl: 0, + poolAPY: 0, + farmAPR: 0, + }) + + useEffect(() => { + if (loading || !Object.values(priceMap).length || (farmData.tvl && farmData.poolAPY && farmData.farmAPR)) { + return + } let tvl = 0 data?.joinedPositions.forEach(({ position, pool }) => { const token0 = new Token(chainId as ChainId, pool.token0.id, Number(pool.token0.decimals), pool.token0.symbol) @@ -516,6 +568,12 @@ export const useProMMFarmTVL = (fairlaunchAddress: string, pid: number) => { Number(data?.farmingPool?.pool?.totalValueLockedUSD || 1) : 0 - return { tvl, farmAPR, poolAPY } - }, [chainId, data, ethPriceUSD.currentPrice, priceMap]) + setData({ tvl, farmAPR, poolAPY }) + }, [chainId, data, ethPriceUSD.currentPrice, priceMap, loading, farmData.poolAPY, farmData.tvl, farmData.farmAPR]) + + return { ...farmData } +} + +export const useFailedNFTs = () => { + return useSelector((state: AppState) => state.prommFarms.failedNFTs) } diff --git a/src/state/farms/promm/reducer.ts b/src/state/farms/promm/reducer.ts index 582df36945..a7178cc637 100644 --- a/src/state/farms/promm/reducer.ts +++ b/src/state/farms/promm/reducer.ts @@ -1,6 +1,15 @@ import { createReducer } from '@reduxjs/toolkit' -import { setAttemptingTxn, setError, setLoading, setShowConfirm, setVestingTxHash, updatePrommFarms } from './actions' +import { + addFailedNFTs, + resetErrorNFTs, + setAttemptingTxn, + setError, + setLoading, + setShowConfirm, + setVestingTxHash, + updatePrommFarms, +} from './actions' import { ProMMFarm } from './types' export interface FarmsState { @@ -10,6 +19,9 @@ export interface FarmsState { readonly attemptingTxn: boolean readonly vestingTxHash: string readonly error: string + // List nft can not withdraw because of contract issue + // https://www.notion.so/kybernetwork/Elastic-Farm-Issue-Product-Changes-High-Priority-d2c086629d1d4332a8e96adfa4295c86 + readonly failedNFTs: string[] } const initialState: FarmsState = { @@ -19,6 +31,7 @@ const initialState: FarmsState = { attemptingTxn: false, vestingTxHash: '', error: '', + failedNFTs: [], } export default createReducer(initialState, builder => @@ -49,5 +62,11 @@ export default createReducer(initialState, builder => ...state, error, } + }) + .addCase(addFailedNFTs, (state, { payload: ids }) => { + state.failedNFTs = [...new Set([...state.failedNFTs, ...ids])] + }) + .addCase(resetErrorNFTs, state => { + state.failedNFTs = [] }), ) diff --git a/src/state/farms/promm/types.ts b/src/state/farms/promm/types.ts index 0a44404566..321686d7a2 100644 --- a/src/state/farms/promm/types.ts +++ b/src/state/farms/promm/types.ts @@ -1,3 +1,4 @@ +import { Token } from '@kyberswap/ks-sdk-core' import { BigNumber } from 'ethers' import { ProMMPoolData } from 'state/prommPools/hooks' @@ -27,6 +28,8 @@ export interface ProMMFarm { currentTick: number rewardLocker: string feeTarget: BigNumber + token0Info?: Token + token1Info?: Token } export interface ProMMFarmResponse { diff --git a/src/state/farms/promm/updater.tsx b/src/state/farms/promm/updater.tsx new file mode 100644 index 0000000000..980144c930 --- /dev/null +++ b/src/state/farms/promm/updater.tsx @@ -0,0 +1,18 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' + +import { useActiveWeb3React } from 'hooks' + +import { resetErrorNFTs } from './actions' + +function Updater() { + const { account, chainId } = useActiveWeb3React() + const dispatch = useDispatch() + + useEffect(() => { + dispatch(resetErrorNFTs()) + }, [account, chainId]) + return null +} + +export default Updater diff --git a/src/state/index.ts b/src/state/index.ts index 412534be17..941e65fe74 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -16,6 +16,7 @@ import pair from './pair/reducer' import pools from './pools/reducer' import swap from './swap/reducer' import transactions from './transactions/reducer' +import tutorial from './tutorial/reducer' import user from './user/reducer' import vesting from './vesting/reducer' @@ -41,6 +42,7 @@ const store = configureStore({ vesting, // [dataApi.reducerPath]: dataApi.reducer campaigns, + tutorial, }, middleware: getDefaultMiddleware => getDefaultMiddleware({ thunk: false, immutableCheck: false, serializableCheck: false }) diff --git a/src/state/swap/reducer.ts b/src/state/swap/reducer.ts index 53cf2e209c..828c52f30e 100644 --- a/src/state/swap/reducer.ts +++ b/src/state/swap/reducer.ts @@ -1,4 +1,5 @@ import { createReducer } from '@reduxjs/toolkit' +import { parse } from 'qs' import { FeeConfig } from 'hooks/useSwapV2Callback' @@ -30,14 +31,17 @@ export interface SwapState { readonly feeConfig: FeeConfig | undefined } +const { search } = window.location +const { inputCurrency, outputCurrency } = parse(search.indexOf('?') === 0 ? search.substring(1) : search) + const initialState: SwapState = { independentField: Field.INPUT, typedValue: '', [Field.INPUT]: { - currencyId: '', + currencyId: inputCurrency?.toString() || '', }, [Field.OUTPUT]: { - currencyId: '', + currencyId: outputCurrency?.toString() || '', }, recipient: null, saveGas: false, @@ -72,14 +76,14 @@ export default createReducer(initialState, builder => ...state, typedValue: '', independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT, - [field]: { currencyId: currencyId }, + [field]: { currencyId }, [otherField]: { currencyId: state[field].currencyId }, } } else { // the normal case return { ...state, - [field]: { currencyId: currencyId }, + [field]: { currencyId }, } } }) diff --git a/src/state/swap/useAggregator.ts b/src/state/swap/useAggregator.ts index aab1c4b661..ea2337a6a6 100644 --- a/src/state/swap/useAggregator.ts +++ b/src/state/swap/useAggregator.ts @@ -28,6 +28,7 @@ export function useDerivedSwapInfoV2(): { inputError?: string onRefresh: (resetRoute: boolean, minimumLoadingTime: number) => void loading: boolean + isPairNotfound: boolean } { const { account } = useActiveWeb3React() @@ -147,6 +148,9 @@ export function useDerivedSwapInfoV2(): { inputError = t`Insufficient ${amountIn.currency.symbol} balance` } + // inputCurrency/outputCurrency null is loading, undefined is not found, see useToken for detail + const isPairNotfound = inputCurrency === undefined && outputCurrency === undefined + return useMemo( () => ({ currencies, @@ -157,7 +161,18 @@ export function useDerivedSwapInfoV2(): { inputError, onRefresh: onUpdateCallback, loading, + isPairNotfound, }), - [currencies, currencyBalances, inputError, loading, onUpdateCallback, parsedAmount, tradeComparer, v2Trade], + [ + currencies, + currencyBalances, + inputError, + loading, + onUpdateCallback, + parsedAmount, + tradeComparer, + v2Trade, + isPairNotfound, + ], ) } diff --git a/src/state/tutorial/actions.ts b/src/state/tutorial/actions.ts new file mode 100644 index 0000000000..fc32cb1a04 --- /dev/null +++ b/src/state/tutorial/actions.ts @@ -0,0 +1,3 @@ +import { createAction } from '@reduxjs/toolkit' + +export const setShowTutorial = createAction<{ show?: boolean; step?: number }>('tutorial/setShowTutorial') diff --git a/src/state/tutorial/hooks.ts b/src/state/tutorial/hooks.ts new file mode 100644 index 0000000000..fd505cd147 --- /dev/null +++ b/src/state/tutorial/hooks.ts @@ -0,0 +1,15 @@ +import { useCallback } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { AppDispatch, AppState } from 'state' + +import { setShowTutorial } from './actions' +import { DEFAULT_TUTORIAL_STATE } from './reducer' + +type TutorialParam = { show?: boolean; step?: number } +export function useTutorialSwapGuide(): [TutorialParam, (value: TutorialParam) => void] { + const dispatch = useDispatch() + const { swap } = useSelector((state: AppState) => state.tutorial) || DEFAULT_TUTORIAL_STATE + const setShow = useCallback(({ show, step }: TutorialParam) => dispatch(setShowTutorial({ show, step })), [dispatch]) + return [swap as TutorialParam, setShow] +} diff --git a/src/state/tutorial/reducer.ts b/src/state/tutorial/reducer.ts new file mode 100644 index 0000000000..bcf8611ee5 --- /dev/null +++ b/src/state/tutorial/reducer.ts @@ -0,0 +1,19 @@ +import { createReducer } from '@reduxjs/toolkit' + +import { setShowTutorial } from './actions' + +interface TutorialState { + swap: { show: boolean; step: number } // in the future we can have many tutorial per page +} + +export const DEFAULT_TUTORIAL_STATE: TutorialState = { swap: { show: false, step: 0 } } + +export default createReducer(DEFAULT_TUTORIAL_STATE, builder => + builder.addCase(setShowTutorial, (state, { payload: { show, step } }) => { + if (!state) state = DEFAULT_TUTORIAL_STATE // for old user + const swapTutorial = { ...state.swap } + if (step !== undefined) swapTutorial.step = step + if (show !== undefined) swapTutorial.show = show + state.swap = swapTutorial + }), +) diff --git a/src/state/vesting/hooks.ts b/src/state/vesting/hooks.ts index 0ed4bfabdf..12c72c0687 100644 --- a/src/state/vesting/hooks.ts +++ b/src/state/vesting/hooks.ts @@ -15,7 +15,7 @@ import { useActiveWeb3React } from 'hooks' import { useTokens } from 'hooks/Tokens' import { useMultipleContracts, useRewardLockerContracts } from 'hooks/useContract' import { AppState } from 'state' -import { useTokensPrice } from 'state/application/hooks' +import { useRewardTokenPrices } from 'state/farms/hooks' import { useGetProMMFarms, useProMMFarms } from 'state/farms/promm/hooks' import { RewardLockerVersion } from 'state/farms/types' import { useAppDispatch } from 'state/hooks' @@ -232,7 +232,7 @@ export const usePrommSchedules = () => { return Object.values(rwTokenMap) }, [rwTokenMap]) - const prices = useTokensPrice(tokens, VERSION.ELASTIC) + const prices = useRewardTokenPrices(tokens, VERSION.ELASTIC) const tokenPriceMap = useMemo(() => { return prices.reduce((priceMap, price, index) => { diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 8044880bad..2409330353 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -125,8 +125,10 @@ const StyledLink = styled.a` color: ${({ theme }) => theme.primary}; font-weight: 500; - :hover { - text-decoration: underline; + @media (hover: hover) { + :hover { + text-decoration: underline; + } } :focus { diff --git a/src/utils/checkForBraveBrowser.ts b/src/utils/checkForBraveBrowser.ts new file mode 100644 index 0000000000..b3c4bc8d42 --- /dev/null +++ b/src/utils/checkForBraveBrowser.ts @@ -0,0 +1,12 @@ +// https://stackoverflow.com/questions/36523448/how-do-i-tell-if-a-user-is-using-brave-as-their-browser +let isBraveBrowser = false +;(async () => { + isBraveBrowser = (navigator.brave && (await navigator.brave.isBrave())) || false +})() + +const checkForBraveBrowser = () => { + return isBraveBrowser +} + +// this func should be called inside a component to make sure isBraveBrowser is updated +export default checkForBraveBrowser diff --git a/src/utils/index.ts b/src/utils/index.ts index 3ae1244d02..246b14b1e4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -22,11 +22,9 @@ import { import { NETWORKS_INFO } from 'constants/networks' import store from 'state' -import AGGREGATOR_EXECUTOR_ABI from '../constants/abis/aggregation-executor.json' import CLAIM_REWARD_ABI from '../constants/abis/claim-reward.json' import ROUTER_DYNAMIC_FEE_ABI from '../constants/abis/dmm-router-dynamic-fee.json' import ROUTER_STATIC_FEE_ABI from '../constants/abis/dmm-router-static-fee.json' -import ROUTER_ABI_V2 from '../constants/abis/dmm-router-v2.json' import KS_ROUTER_STATIC_FEE_ABI from '../constants/abis/ks-router-static-fee.json' import ROUTER_PRO_AMM from '../constants/abis/v2/ProAmmRouter.json' import ZAP_ABI from '../constants/abis/zap.json' @@ -170,10 +168,6 @@ export function getProAmmRouterContract(chainId: ChainId, library: Web3Provider, return getContract(NETWORKS_INFO[chainId].elastic.routers, ROUTER_PRO_AMM.abi, library, account) } -export function getRouterV2Contract(chainId: ChainId, library: Web3Provider, account?: string): Contract { - return getContract(NETWORKS_INFO[chainId].classic.routerV2 || '', ROUTER_ABI_V2, library, account) -} - // account is optional export function getZapContract( chainId: ChainId, @@ -203,14 +197,6 @@ export function getClaimRewardContract( return getContract(NETWORKS_INFO[chainId].classic.claimReward, CLAIM_REWARD_ABI, library, account) } -export function getAggregationExecutorAddress(chainId: ChainId): string { - return NETWORKS_INFO[chainId].classic.aggregationExecutor || '' -} - -export function getAggregationExecutorContract(chainId: ChainId, library: Web3Provider, account?: string): Contract { - return getContract(getAggregationExecutorAddress(chainId), AGGREGATOR_EXECUTOR_ABI, library, account) -} - export function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string } diff --git a/yarn.lock b/yarn.lock index 5d3a944800..1c61149957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -229,7 +229,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.11.6", "@babel/generator@^7.12.1", "@babel/generator@^7.17.3", "@babel/generator@^7.17.7", "@babel/generator@^7.18.10": +"@babel/generator@^7.11.6", "@babel/generator@^7.12.1", "@babel/generator@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.10.tgz#794f328bfabdcbaf0ebf9bf91b5b57b61fa77a2a" integrity sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA== @@ -238,6 +238,15 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.17.3", "@babel/generator@^7.17.7": + version "7.18.12" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4" + integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg== + dependencies: + "@babel/types" "^7.18.10" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.15.4", "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -453,11 +462,16 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== -"@babel/parser@^7.1.0", "@babel/parser@^7.11.5", "@babel/parser@^7.12.3", "@babel/parser@^7.14.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8", "@babel/parser@^7.18.10", "@babel/parser@^7.7.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.11.5", "@babel/parser@^7.12.3", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.7.0": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== +"@babel/parser@^7.17.3", "@babel/parser@^7.17.8", "@babel/parser@^7.18.11": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1242,7 +1256,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.17.3", "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.10.tgz#37ad97d1cb00efa869b91dd5d1950f8a6cf0cb08" integrity sha512-J7ycxg0/K9XCtLyHf0cz2DqDihonJeIo+z+HEdRe9YuT8TY4A66i+Ab2/xZCEW7Ro60bPCBBfqqboHSamoV3+g== @@ -1258,6 +1272,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.17.3": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" + integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.11" + "@babel/types" "^7.18.10" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.17.0": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" @@ -2845,11 +2875,6 @@ make-plural "^6.2.2" messageformat-parser "^4.1.3" -"@lingui/detect-locale@^3.10.4": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@lingui/detect-locale/-/detect-locale-3.14.0.tgz#efbec423a8bf0e2785db320c5d015e4023ae8f8b" - integrity sha512-IELWULt9I+iyVlxGES21cXoOwTcPSIisElAmr3/KJlqvJ9zXT3s4w4Jxw9j5oHJjdxBDRkifwpnVmGd57wrmzg== - "@lingui/loader@^3.14.0": version "3.14.0" resolved "https://registry.yarnpkg.com/@lingui/loader/-/loader-3.14.0.tgz#f8f00532f1788ffcccb76ec89f742d4714bc4b0f" @@ -19192,6 +19217,11 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" +walktour@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/walktour/-/walktour-5.1.1.tgz#951b5bce2abed0ae4209dc74d4d79f252a85e813" + integrity sha512-pRjbjjBGddgiWaWryE+H6DxOeAuUgIZJ5ck0hOVrhZ4QNgZDs1okuGMXDKx3Gi4hi/N9ESwATyyB5hb7kiK7wQ== + warning@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"