diff --git a/.env.example b/.env.example deleted file mode 100644 index 28c8161cdc..0000000000 --- a/.env.example +++ /dev/null @@ -1,32 +0,0 @@ -# You can leave this empty for rinkeby or use "mainnet" -REACT_APP_NETWORK= - -# For all environments -REACT_APP_GOOGLE_ANALYTICS= -REACT_APP_INFURA_TOKEN= -REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs -REACT_APP_SENTRY_DSN= - -# For production environments -REACT_APP_BLOCKNATIVE_KEY= -REACT_APP_INTERCOM_ID= -REACT_APP_PORTIS_ID= -REACT_APP_SQUARELINK_ID= -REACT_APP_FORTMATIC_KEY= -REACT_APP_OPENSEA_API_KEY= -REACT_APP_COLLECTIBLES_SOURCE= -REACT_APP_ETHERSCAN_API_KEY= -REACT_APP_ETHGASSTATION_API_KEY= - -# Versions -REACT_APP_LATEST_SAFE_VERSION= - -# Leave it untouched, version will set using dotenv-expand -REACT_APP_APP_VERSION=$npm_package_version - -# For Apps -REACT_APP_GNOSIS_APPS_URL=https://safe-apps.staging.gnosisdev.com - -# Contracts Addresses -REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS=0x9e9Bf12b5a66c0f0A7435835e0365477E121B110 - diff --git a/README.md b/README.md index 3949e4e0e9..52ecfc14d9 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,129 @@ -# Gnosis Safe +# Harmony Safe Multisig -The most trusted platform to store digital assets on Ethereum. More info at [gnosis-safe.io](https://gnosis-safe.io/) - -This repository contains the code for the frontend code hosted at [https://gnosis-safe.io/app/] - -Besides the Ethereum Mainnet, the following networks are supported: - -- [Rinkeby Testnet](https://rinkeby.gnosis-safe.io/app/) -- [xDai](https://xdai.gnosis-safe.io/app/) -- [Energy Web Chain](https://ewc.gnosis-safe.io/app/) -- [Volta Testnet](https://volta.gnosis-safe.io/app/) - -For technical information please refer to the [Gnosis Developer Portal](https://docs.gnosis.io/safe/). - -For support requests, please open up a [bug issue](https://github.com/gnosis/safe-react/issues/new?template=bug-report.md) or reach out via [Discord](https://discordapp.com/invite/FPMRAwK). +The most trusted platform to store digital assets on Harmony ## Getting Started -These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [Deployment](#deployment) for notes on how to deploy the project on a live system. +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. ### Prerequisites -We use [yarn](https://yarnpkg.com) in our infrastructure, so we decided to go with yarn in the README. -Please install yarn globally if you haven't already. +What things you need to install the software and how to install them -### Environment variables -The app grabs environment variables from the `.env` file. Copy our template to your own local file: ``` -cp .env.example .env +yarn add truffle // recommended usage of -g flag +yarn add ganache-cli // recommended usage of -g flag +yarn add flow-type // recommended usage of -g flag ``` -To execute transactions, you'll need to create an [Infura](https://infura.io) project and set the project ID in the `.env` you've just created: -``` -REACT_APP_INFURA_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -``` -Once done, you'll need to restart the app if it's already running. +We use [yarn](https://yarnpkg.com) in our infrastacture, so we decided to go with yarn in the README ### Installing and running +A step by step series of examples that tell you have to get a development env running + Install dependencies for the project: + ``` yarn install ``` -To use the Rinkeby services: +For using the Testnet services: + ``` yarn start ``` -If you prefer using the Mainnet ones: +If you prefer using Mainnet ones: + ``` yarn start-mainnet ``` ### Building -For Rinkeby: + +For Testnet: + ``` yarn build ``` For Mainnet: + ``` yarn build-mainnet ``` ## Running the tests -To run the tests: +1. Run `transaction-history-service` + ``` -yarn test +git clone https://github.com/harmony-one/multisig-react.git +cd safe-transaction-service +git checkout develop +docker-compose build +# it comes enabled by default in docker-compose +sudo service postgresql stop +docker-compose up -d ``` -### Lint +Check that the service is running at https://localhost:8000 -ESLint will be run automatically before you commit. To run it manually: +2. Migrate Safe Contracts: ``` -yarn lint:fix +git clone https://github.com/harmony-one/multisig-contracts.git +cd safe-contracts +yarn +npx truffle migrate ``` -## Deployment +3. Migrate Token Contracts for the tests: + Inside `safe-react` directory + +``` +npx truffle migrate +``` -### Dev & staging -The code is deployed to a testing website automatically on each push via a GitHub Action. -The GitHub Action will create a new subdomain and post the link as a comment in the PR. +4. Run the tests: -When pushing to the `master` branch, the code will be automatically deployed to [staging](https://safe-team-rinkeby.staging.gnosisdev.com/). +``` +yarn test +``` -### Production -Deployment to production is done manually. Please see the [release procedure](docs/release-procedure.md) notes for details. +### Break down into end to end tests -## Configuring the app for running on different networks +Explain what these tests test and why -[Please check the network configuration documentation](./docs/networks.md) +``` +Give an example +``` -## Built With +### And coding style tests -* [React](https://reactjs.org/) - A JS library for building user interfaces -* [Material UI 4.X](https://material-ui.com/) - React components that implement Google's Material Design -* [redux, immutable, reselect, final-form](https://redux.js.org/) - React ecosystem libraries +Explain what these tests test and why -![app diagram](https://user-images.githubusercontent.com/381895/121764528-e5e2e900-cb44-11eb-8643-483d41040349.png) +``` +Give an example +``` -## Contributing +## Deployment -Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) for details on our code of conduct, and the process for submitting pull requests to us. +Add additional notes about how to deploy this on a live system + +## Configuring the app for running on different networks + +[Please check the network configuration documentation](./docs/networks.md) -## Versioning +## Contributing -We use [SemVer](https://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/gnosis/gnosis-team-safe/tags). +Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) for details on our code of conduct, and the process for submitting pull requests to us. ## License This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details + +## Acknowledgments + +- Thanks for Gnosis Team for providing the Safe contracts. diff --git a/public/favicon.ico b/public/favicon.ico index 85cacafc91..97d02f875c 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index ac3e48c24c..725affffef 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ - Gnosis Safe + Harmony Multisig Wallet -
+
diff --git a/public/resources/background.png b/public/resources/background.png deleted file mode 100644 index 4c0b9ba391..0000000000 Binary files a/public/resources/background.png and /dev/null differ diff --git a/public/resources/harmony.png b/public/resources/harmony.png new file mode 100644 index 0000000000..39084ed03f Binary files /dev/null and b/public/resources/harmony.png differ diff --git a/public/resources/icon.icns b/public/resources/icon.icns deleted file mode 100644 index 4c5e257181..0000000000 Binary files a/public/resources/icon.icns and /dev/null differ diff --git a/public/resources/icon.ico b/public/resources/icon.ico deleted file mode 100644 index 8039e7963d..0000000000 Binary files a/public/resources/icon.ico and /dev/null differ diff --git a/public/resources/safe.png b/public/resources/safe.png deleted file mode 100644 index fec10acfd3..0000000000 Binary files a/public/resources/safe.png and /dev/null differ diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 2c26a45533..7c1521f11e 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -12,12 +12,11 @@ import InfoIcon from 'src/assets/icons/info.svg' import AppLayout from 'src/components/AppLayout' import { SafeListSidebar, SafeListSidebarContext } from 'src/components/SafeListSidebar' -import CookiesBanner from 'src/components/CookiesBanner' import Notifier from 'src/components/Notifier' import Backdrop from 'src/components/layout/Backdrop' import Img from 'src/components/layout/Img' import { getNetworkId } from 'src/config' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' import { networkSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { currentSafeWithNames, safeAddressFromUrl } from 'src/logic/safe/store/selectors' @@ -62,13 +61,15 @@ const useStyles = makeStyles(notificationStyles) const App: React.FC = ({ children }) => { const classes = useStyles() const currentNetwork = useSelector(networkSelector) - const isWrongNetwork = currentNetwork !== ETHEREUM_NETWORK.UNKNOWN && currentNetwork !== desiredNetwork + const isWrongNetwork = currentNetwork !== HARMONY_NETWORK.UNKNOWN && currentNetwork !== desiredNetwork const { toggleSidebar } = useContext(SafeListSidebarContext) const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false }) const history = useHistory() - const { address: safeAddress, name: safeName, totalFiatBalance: currentSafeBalance } = useSelector( - currentSafeWithNames, - ) + const { + address: safeAddress, + name: safeName, + totalFiatBalance: currentSafeBalance, + } = useSelector(currentSafeWithNames) const addressFromUrl = useSelector(safeAddressFromUrl) const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() const currentCurrency = useSelector(currentCurrencySelector) @@ -149,7 +150,6 @@ const App: React.FC = ({ children }) => { )} - ) } diff --git a/src/components/AppLayout/Footer/index.tsx b/src/components/AppLayout/Footer/index.tsx index 598041bfbf..42f9d22fac 100644 --- a/src/components/AppLayout/Footer/index.tsx +++ b/src/components/AppLayout/Footer/index.tsx @@ -1,11 +1,7 @@ import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' import * as React from 'react' -import { useDispatch } from 'react-redux' - -import GnoButtonLink from 'src/components/layout/ButtonLink' import Link from 'src/components/layout/Link' -import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner' import { screenSm, secondary, sm } from 'src/theme/variables' const useStyles = makeStyles({ @@ -42,52 +38,21 @@ const useStyles = makeStyles({ }, } as any) -const appVersion = process.env.REACT_APP_APP_VERSION ? `v${process.env.REACT_APP_APP_VERSION} ` : 'Versions' - const Footer = (): React.ReactElement => { const date = new Date() const classes = useStyles() - const dispatch = useDispatch() - - const openCookiesHandler = () => { - dispatch(openCookieBanner({ cookieBannerOpen: true })) - } return ( ) } diff --git a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg deleted file mode 100644 index 62ed0fd93d..0000000000 --- a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - horizontal_left_small_black - - - diff --git a/src/components/AppLayout/Header/assets/logo.png b/src/components/AppLayout/Header/assets/logo.png new file mode 100644 index 0000000000..df3aeb60d3 Binary files /dev/null and b/src/components/AppLayout/Header/assets/logo.png differ diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 72f6be6413..7383e80418 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -7,8 +7,6 @@ import * as React from 'react' import { Link } from 'react-router-dom' import Provider from './Provider' -import NetworkSelector from './NetworkSelector' - import Spacer from 'src/components/Spacer' import Col from 'src/components/layout/Col' import Img from 'src/components/layout/Img' @@ -16,8 +14,7 @@ import Row from 'src/components/layout/Row' import { headerHeight, md, screenSm, sm } from 'src/theme/variables' import { useStateHandler } from 'src/logic/hooks/useStateHandler' -import SafeLogo from '../assets/gnosis-safe-multisig-logo.svg' -import { getNetworks } from 'src/config' +import HarmonyLogo from '../assets/logo.png' const styles = () => ({ root: { @@ -38,10 +35,8 @@ const styles = () => ({ zIndex: 1301, }, logo: { - flexBasis: '140px', flexShrink: '0', flexGrow: '0', - maxWidth: '55px', padding: sm, marginTop: '4px', [`@media (min-width: ${screenSm}px)`]: { @@ -65,14 +60,11 @@ const styles = () => ({ const Layout = ({ classes, providerDetails, providerInfo }) => { const { clickAway, open, toggle } = useStateHandler() - const { clickAway: clickAwayNetworks, open: openNetworks, toggle: toggleNetworks } = useStateHandler() - const networks = getNetworks() - const { isDesktop } = window return ( - Gnosis Team Safe + Harmony Multisig @@ -102,14 +94,6 @@ const Layout = ({ classes, providerDetails, providerInfo }) => { )} /> - {!isDesktop && ( - - )} ) } diff --git a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx index 83e832ee11..c339709a76 100644 --- a/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx +++ b/src/components/AppLayout/Header/components/ProviderDetails/UserDetails.tsx @@ -14,7 +14,7 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { background, connected as connectedBg, lg, md, sm, warning, xs } from 'src/theme/variables' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' import { getExplorerInfo } from 'src/config' import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing' import WalletIcon from '../../assets/wallet.svg' @@ -95,7 +95,7 @@ const StyledCard = styled(Card)` ` type Props = { connected: boolean - network: ETHEREUM_NETWORK + network: HARMONY_NETWORK onDisconnect: () => void openDashboard?: (() => void | null) | boolean provider?: string diff --git a/src/components/CookiesBanner/assets/alert-red.svg b/src/components/CookiesBanner/assets/alert-red.svg deleted file mode 100644 index 6a20c41cc3..0000000000 --- a/src/components/CookiesBanner/assets/alert-red.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/CookiesBanner/assets/intercom.png b/src/components/CookiesBanner/assets/intercom.png deleted file mode 100644 index 74eb6efac1..0000000000 Binary files a/src/components/CookiesBanner/assets/intercom.png and /dev/null differ diff --git a/src/components/CookiesBanner/index.tsx b/src/components/CookiesBanner/index.tsx deleted file mode 100644 index 3796d43db1..0000000000 --- a/src/components/CookiesBanner/index.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import Checkbox from '@material-ui/core/Checkbox' -import FormControlLabel from '@material-ui/core/FormControlLabel' -import { makeStyles } from '@material-ui/core/styles' -import React, { ReactElement, useEffect, useRef, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import Button from 'src/components/layout/Button' -import Link from 'src/components/layout/Link' -import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' -import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner' -import { cookieBannerOpen } from 'src/logic/cookies/store/selectors' -import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils' -import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables' -import { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics' -import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom' -import AlertRedIcon from './assets/alert-red.svg' -import IntercomIcon from './assets/intercom.png' -import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl' - -const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP - -const useStyles = makeStyles({ - container: { - backgroundColor: '#fff', - bottom: '0', - boxShadow: '1px 2px 10px 0 rgba(40, 54, 61, 0.18)', - boxSizing: 'border-box', - display: 'flex', - justifyContent: 'center', - left: '0', - minHeight: '200px', - padding: '30px 15px 45px', - position: 'fixed', - width: '100%', - zIndex: '999', - }, - content: { - maxWidth: '100%', - }, - text: { - color: primary, - fontFamily: mainFontFamily, - fontSize: md, - fontWeight: 'normal', - lineHeight: '1.38', - margin: '0 auto 35px', - textAlign: 'center', - maxWidth: '810px', - }, - form: { - columnGap: '20px', - display: 'grid', - gridTemplateColumns: '1fr', - paddingBottom: '50px', - rowGap: '15px', - margin: '0 auto', - [`@media (min-width: ${screenSm}px)`]: { - gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', - paddingBottom: '0', - rowGap: '5px', - }, - }, - formItem: { - alignItems: 'center', - display: 'flex', - justifyContent: 'center', - }, - link: { - textDecoration: 'underline', - '&:hover': { - textDecoration: 'none', - }, - }, - intercomAlert: { - fontWeight: 'bold', - display: 'flex', - justifyContent: 'center', - padding: '0 0 13px 0', - svg: { - marginRight: '5px', - }, - }, - intercomImage: { - position: 'fixed', - cursor: 'pointer', - height: '80px', - width: '80px', - bottom: '8px', - right: '10px', - zIndex: '1000', - boxShadow: '1px 2px 10px 0 var(rgba(40, 54, 61, 0.18))', - }, -} as any) - -interface CookiesBannerFormProps { - alertMessage: boolean -} - -const CookiesBanner = (): ReactElement => { - const classes = useStyles() - const dispatch = useRef(useDispatch()) - const intercomLoaded = isIntercomLoaded() - - const [showAnalytics, setShowAnalytics] = useState(false) - const [showIntercom, setShowIntercom] = useState(false) - const [localNecessary, setLocalNecessary] = useState(true) - const [localAnalytics, setLocalAnalytics] = useState(false) - const [localIntercom, setLocalIntercom] = useState(false) - const { getAppUrl } = useSafeAppUrl() - - const showBanner = useSelector(cookieBannerOpen) - const newAppUrl = getAppUrl() - const isSafeAppView = newAppUrl !== null - - useEffect(() => { - if (intercomLoaded && isSafeAppView) { - closeIntercom() - } - }, [isSafeAppView, intercomLoaded]) - - useEffect(() => { - async function fetchCookiesFromStorage() { - const cookiesState = await loadFromCookie(COOKIES_KEY) - if (!cookiesState) { - dispatch.current(openCookieBanner({ cookieBannerOpen: true })) - } else { - const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState - if (acceptedIntercom === undefined) { - const newState = { - acceptedNecessary, - acceptedAnalytics, - acceptedIntercom: acceptedAnalytics, - } - const expDays = acceptedAnalytics ? 365 : 7 - await saveCookie(COOKIES_KEY, newState, expDays) - setLocalIntercom(newState.acceptedIntercom) - setShowIntercom(newState.acceptedIntercom) - } else { - setLocalIntercom(acceptedIntercom) - setShowIntercom(acceptedIntercom) - } - setLocalAnalytics(acceptedAnalytics) - setLocalNecessary(acceptedNecessary) - - if (acceptedAnalytics && !isDesktop) { - loadGoogleAnalytics() - } - } - } - fetchCookiesFromStorage() - }, [showAnalytics, showIntercom]) - - const acceptCookiesHandler = async () => { - const newState = { - acceptedNecessary: true, - acceptedAnalytics: !isDesktop, - acceptedIntercom: true, - } - await saveCookie(COOKIES_KEY, newState, 365) - setShowAnalytics(!isDesktop) - setShowIntercom(true) - dispatch.current(openCookieBanner({ cookieBannerOpen: false })) - } - - const closeCookiesBannerHandler = async () => { - const newState = { - acceptedNecessary: true, - acceptedAnalytics: localAnalytics, - acceptedIntercom: localIntercom, - } - const expDays = localAnalytics ? 365 : 7 - await saveCookie(COOKIES_KEY, newState, expDays) - setShowAnalytics(localAnalytics) - setShowIntercom(localIntercom) - - if (!localAnalytics) { - removeCookies() - } - - if (!localIntercom && isIntercomLoaded()) { - closeIntercom() - } - dispatch.current(openCookieBanner({ cookieBannerOpen: false })) - } - - if (showIntercom && !isSafeAppView) { - loadIntercom() - } - - const CookiesBannerForm = (props: CookiesBannerFormProps) => { - const { alertMessage } = props - return ( -
-
- {alertMessage && ( -
- - You attempted to open the customer support chat. Please accept the customer support cookie. -
- )} -

- We use cookies to provide you with the best experience and to help improve our website and application. - Please read our{' '} - - Cookie Policy - {' '} - for more information. By clicking "Accept all", you agree to the storing of cookies on your device - to enhance site navigation, analyze site usage and provide customer support. -

-
-
- } - disabled - label="Necessary" - name="Necessary" - onChange={() => setLocalNecessary((prev) => !prev)} - value={localNecessary} - /> -
-
- } - label="Customer support" - name="Customer support" - onChange={() => setLocalIntercom((prev) => !prev)} - value={localIntercom} - /> -
-
- } - label="Analytics" - name="Analytics" - onChange={() => setLocalAnalytics((prev) => !prev)} - value={localAnalytics} - /> -
-
- -
-
- -
-
-
-
- ) - } - - return ( - <> - {!isDesktop && !showIntercom && !isSafeAppView && ( - dispatch.current(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))} - /> - )} - {!isDesktop && showBanner?.cookieBannerOpen && ( - - )} - - ) -} - -export default CookiesBanner diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx deleted file mode 100644 index deb5f4747e..0000000000 --- a/src/components/DecodeTxs/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, { ReactElement } from 'react' -import styled from 'styled-components' -import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' -import { Text, EthHashInfo, CopyToClipboardBtn, IconText, FixedIcon } from '@gnosis.pm/safe-react-components' -import get from 'lodash.get' - -import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' -import { getExplorerInfo, getNetworkInfo } from 'src/config' -import { DecodedData, DecodedDataBasicParameter, DecodedDataParameterValue } from 'src/types/transactions/decode.d' -import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' - -const FlexWrapper = styled.div<{ margin: number }>` - display: flex; - align-items: center; - - > :nth-child(2) { - margin-left: ${({ margin }) => margin}px; - } -` - -const BasicTxInfoWrapper = styled.div` - margin-bottom: 15px; - - > :nth-child(2) { - margin-bottom: 15px; - } -` - -const TxList = styled.div` - width: 100%; - max-height: 260px; - overflow-y: auto; - border-top: 2px solid ${({ theme }) => theme.colors.separator}; -` - -const TxListItem = styled.div` - display: flex; - justify-content: space-between; - - padding: 0 24px; - height: 50px; - border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; - - :hover { - cursor: pointer; - } -` -const ElementWrapper = styled.div` - margin-bottom: 15px; -` - -export const BasicTxInfo = ({ - txRecipient, - txData, - txValue, -}: { - txRecipient: string - txData: string - txValue: string -}): ReactElement => { - const { nativeCoin } = getNetworkInfo() - - return ( - - {/* TO */} - <> - - {`Send ${txValue} ${nativeCoin.symbol} to:`} - - - - <> - {/* Data */} - - Data (hex encoded): - - - {txData ? web3.utils.hexToBytes(txData).length : 0} bytes - - - - - ) -} - -export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => { - let valueElement - - if (parameter.type === 'address') { - valueElement = ( - - ) - } - - if (parameter.type.startsWith('bytes')) { - valueElement = ( - - {web3.utils.hexToBytes(parameter.value).length} bytes - - - ) - } - - if (!valueElement) { - let value = parameter.value - if (parameter.type.endsWith('[]')) { - try { - value = JSON.stringify(parameter.value) - } catch (e) {} - } - valueElement = {value} - } - - return ( - - - {parameter.name} ({parameter.type}) - - {valueElement} - - ) -} - -const SingleTx = ({ - decodedData, - onTxItemClick, -}: { - decodedData: DecodedData | null - onTxItemClick: (decodedTxDetails: DecodedData) => void -}): ReactElement | null => { - if (!decodedData) { - return null - } - - return ( - - onTxItemClick(decodedData)}> - - - - {decodedData.method} - - - - - ) -} - -const MultiSendTx = ({ - decodedData, - onTxItemClick, -}: { - decodedData: DecodedData | null - onTxItemClick: (decodedTxDetails: DecodedDataParameterValue) => void -}): ReactElement | null => { - const txs: DecodedDataParameterValue[] | undefined = get(decodedData, 'parameters[0].valueDecoded') - - if (!txs) { - return null - } - - return ( - - {txs.map((tx, index) => ( - onTxItemClick(tx)}> - - - - {tx.dataDecoded && {tx.dataDecoded.method}} - - - - ))} - - ) -} - -type Props = { - txs: Transaction[] - decodedData: DecodedData | null - onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void -} - -export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => { - return txs.length > 1 ? ( - - ) : ( - - ) -} diff --git a/src/components/SafeListSidebar/index.tsx b/src/components/SafeListSidebar/index.tsx index 9113230652..3c1cf41954 100644 --- a/src/components/SafeListSidebar/index.tsx +++ b/src/components/SafeListSidebar/index.tsx @@ -15,7 +15,6 @@ import Hairline from 'src/components/layout/Hairline' import Link from 'src/components/layout/Link' import Row from 'src/components/layout/Row' import { WELCOME_ADDRESS } from 'src/routes/routes' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { safeAddressFromUrl, defaultSafe as defaultSafeSelector } from 'src/logic/safe/store/selectors' @@ -44,7 +43,6 @@ export const SafeListSidebar = ({ children }: Props): ReactElement => { const safeAddress = useSelector(safeAddressFromUrl) const classes = useSidebarStyles() - const { trackEvent } = useAnalytics() const searchClasses = { input: classes.searchInput, @@ -54,9 +52,6 @@ export const SafeListSidebar = ({ children }: Props): ReactElement => { } const toggleSidebar = () => { - if (!isOpen) { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Safe List Sidebar' }) - } setIsOpen((prevIsOpen) => !prevIsOpen) } diff --git a/src/config/assets/token-one.png b/src/config/assets/token-one.png new file mode 100644 index 0000000000..62abd95c1f Binary files /dev/null and b/src/config/assets/token-one.png differ diff --git a/src/config/assets/token_eth.svg b/src/config/assets/token_eth.svg deleted file mode 100644 index 235beb548b..0000000000 --- a/src/config/assets/token_eth.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/src/config/assets/token_ewc.svg b/src/config/assets/token_ewc.svg deleted file mode 100644 index f52e22854d..0000000000 --- a/src/config/assets/token_ewc.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - -ewf_logo - - diff --git a/src/config/assets/token_xdai.svg b/src/config/assets/token_xdai.svg deleted file mode 100644 index 044b3395a7..0000000000 --- a/src/config/assets/token_xdai.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - -Group 6 -Created with Sketch. - - - - - - - - - - - - diff --git a/src/config/index.ts b/src/config/index.ts index 0eec893d68..734c7f66ee 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,7 +2,7 @@ import memoize from 'lodash.memoize' import networks from 'src/config/networks' import { EnvironmentSettings, - ETHEREUM_NETWORK, + HARMONY_NETWORK, FEATURES, GasPriceOracle, NetworkInfo, @@ -10,14 +10,12 @@ import { SafeFeatures, Wallets, } from 'src/config/networks/network.d' -import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants' +import { APP_ENV, NETWORK, NODE_ENV } from 'src/utils/constants' import { ensureOnce } from 'src/utils/singleton' -export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK] +export const getNetworkId = (): HARMONY_NETWORK => HARMONY_NETWORK[NETWORK] -export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()] - -export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId()) +export const getNetworkName = (): string => HARMONY_NETWORK[getNetworkId()] const getCurrentEnvironment = (): string => { switch (NODE_ENV) { @@ -44,7 +42,7 @@ const configuration = (): NetworkSpecificConfiguration => { // special case for test environment if (currentEnvironment === 'test') { - const configFile = networks.local + const configFile = networks.testnet return { ...configFile.environment.production, @@ -69,7 +67,7 @@ const configuration = (): NetworkSpecificConfiguration => { const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration) export const getNetworks = (): NetworkInfo[] => { - const { local, ...usefulNetworks } = networks + const { ...usefulNetworks } = networks return Object.values(usefulNetworks).map((networkObj) => ({ id: networkObj.network.id, label: networkObj.network.label, @@ -91,14 +89,15 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle -export const getRpcServiceUrl = (): string => - usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl - export const getSafeClientGatewayBaseUrl = (safeAddress: string) => `${getClientGatewayUrl()}/safes/${safeAddress}` export const getTxDetailsUrl = (clientGatewayTxId: string) => `${getClientGatewayUrl()}/transactions/${clientGatewayTxId}` +export const getRpcServiceUrl = (): string => { + return getConfig().rpcServiceUrl +} + export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}` export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens` @@ -125,8 +124,6 @@ export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disab export const getNetworkInfo = (): NetworkSettings => getConfig().network -export const getGoogleAnalyticsTrackingID = (): string => GOOGLE_ANALYTICS_ID - const fetchContractABI = memoize( async (url: string, contractAddress: string, apiKey?: string) => { let params: Record = { @@ -152,9 +149,6 @@ const fetchContractABI = memoize( const getNetworkExplorerApiKey = (networkExplorerName: string): string | undefined => { switch (networkExplorerName.toLowerCase()) { - case 'etherscan': { - return ETHERSCAN_API_KEY - } default: { return undefined } diff --git a/src/config/networks/energy_web_chain.ts b/src/config/networks/energy_web_chain.ts deleted file mode 100644 index f8423b6747..0000000000 --- a/src/config/networks/energy_web_chain.ts +++ /dev/null @@ -1,65 +0,0 @@ -import EwcLogo from 'src/config/assets/token_ewc.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d' - -// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0 -// once the oracle is fixed we need to remove the fixed value -const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'https://safe-client.ewc.gnosis.io/v1', - txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1', - safeUrl: 'https://ewc.gnosis-safe.io/app', - safeAppsUrl: 'https://safe-apps-ewc.staging.gnosisdev.com', - gasPriceOracle: { - url: 'https://station.energyweb.org', - gasParameter: 'standard', - }, - gasPrice: 1e6, - rpcServiceUrl: 'https://rpc.energyweb.org', - networkExplorerName: 'Energy web explorer', - networkExplorerUrl: 'https://explorer.energyweb.org', - networkExplorerApiUrl: 'https://explorer.energyweb.org/api', -} - -const mainnet: NetworkConfig = { - environment: { - dev: { - ...baseConfig, - }, - staging: { - ...baseConfig, - }, - production: { - ...baseConfig, - safeAppsUrl: 'https://apps-ewc.gnosis-safe.io', - }, - }, - network: { - id: ETHEREUM_NETWORK.ENERGY_WEB_CHAIN, - backgroundColor: '#A566FF', - textColor: '#ffffff', - label: 'EWC', - isTestNet: false, - nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'Energy web token', - symbol: 'EWT', - decimals: 18, - logoUri: EwcLogo, - }, - }, - disabledWallets: [ - WALLETS.TREZOR, - WALLETS.LEDGER, - WALLETS.COINBASE, - WALLETS.FORTMATIC, - WALLETS.OPERA, - WALLETS.OPERA_TOUCH, - WALLETS.PORTIS, - WALLETS.TORUS, - WALLETS.TRUST, - WALLETS.WALLET_LINK, - WALLETS.AUTHEREUM, - WALLETS.LATTICE, - ], -} - -export default mainnet diff --git a/src/config/networks/index.ts b/src/config/networks/index.ts index 03ef405b11..68cbf41314 100644 --- a/src/config/networks/index.ts +++ b/src/config/networks/index.ts @@ -1,15 +1,7 @@ -import local from './local' import mainnet from './mainnet' -import rinkeby from './rinkeby' -import xdai from './xdai' -import energy_web_chain from './energy_web_chain' -import volta from './volta' +import testnet from './testnet' export default { - local, mainnet, - rinkeby, - xdai, - energy_web_chain, - volta, + testnet, } diff --git a/src/config/networks/local.ts b/src/config/networks/local.ts deleted file mode 100644 index eaaed47bbf..0000000000 --- a/src/config/networks/local.ts +++ /dev/null @@ -1,42 +0,0 @@ -import EtherLogo from 'src/config/assets/token_eth.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d' - -const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'http://localhost:8001/v1', - txServiceUrl: 'http://localhost:8000/api/v1', - relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1', - safeUrl: 'http://localhost:3000/app', - safeAppsUrl: 'http://localhost:3002', - gasPriceOracle: { - url: 'https://ethgasstation.info/json/ethgasAPI.json', - gasParameter: 'average', - }, - rpcServiceUrl: 'http://localhost:4447', - networkExplorerName: 'Etherscan', - networkExplorerUrl: 'https://rinkeby.etherscan.io', - networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api', -} - -const local: NetworkConfig = { - environment: { - production: { - ...baseConfig, - }, - }, - network: { - id: ETHEREUM_NETWORK.LOCAL, - backgroundColor: '#E8673C', - textColor: '#ffffff', - label: 'LocalRPC', - isTestNet: true, - nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'Ether', - symbol: 'ETH', - decimals: 18, - logoUri: EtherLogo, - }, - }, -} - -export default local diff --git a/src/config/networks/mainnet.ts b/src/config/networks/mainnet.ts index 86566f47b4..d1320934e3 100644 --- a/src/config/networks/mainnet.ts +++ b/src/config/networks/mainnet.ts @@ -1,19 +1,18 @@ -import EtherLogo from 'src/config/assets/token_eth.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d' -import { ETHGASSTATION_API_KEY } from 'src/utils/constants' +import HarmonyLogo from 'src/config/assets/token-one.png' +import { EnvironmentSettings, HARMONY_NETWORK, NetworkConfig } from 'src/config/networks/network.d' const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'https://safe-client.mainnet.staging.gnosisdev.com/v1', - txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1', - safeUrl: 'https://gnosis-safe.io/app', - safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com', + clientGatewayUrl: 'https://safe-client.t.hmny.io/v1', + txServiceUrl: 'https://multisig.t.hmny.io/api/v1', + safeUrl: 'https://multisig.harmony.one', + safeAppsUrl: 'https://multisig.harmony.one', gasPriceOracle: { url: 'https://ethgasstation.info/json/ethgasAPI.json', gasParameter: 'average', }, - rpcServiceUrl: 'https://mainnet.infura.io:443/v3', - networkExplorerName: 'Etherscan', - networkExplorerUrl: 'https://etherscan.io', + rpcServiceUrl: 'https://api.s0.t.hmny.io', + networkExplorerName: 'Harmony Explorer', + networkExplorerUrl: 'https://explorer.harmony.one/#', networkExplorerApiUrl: 'https://api.etherscan.io/api', } @@ -24,27 +23,26 @@ const mainnet: NetworkConfig = { }, staging: { ...baseConfig, - safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com', + safeAppsUrl: 'https://multisig.harmony.one', }, production: { ...baseConfig, - clientGatewayUrl: 'https://safe-client.mainnet.gnosis.io/v1', - txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1', - safeAppsUrl: 'https://apps.gnosis-safe.io', + txServiceUrl: 'https://multisig.t.hmny.io/api/v1', + safeAppsUrl: 'https://multisig.harmony.one', }, }, network: { - id: ETHEREUM_NETWORK.MAINNET, + id: HARMONY_NETWORK.MAINNET, backgroundColor: '#E8E7E6', textColor: '#001428', label: 'Mainnet', isTestNet: false, nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'Ether', - symbol: 'ETH', + address: '0x000', + name: 'Harmony', + symbol: 'ONE', decimals: 18, - logoUri: EtherLogo, + logoUri: HarmonyLogo, }, }, } diff --git a/src/config/networks/network.d.ts b/src/config/networks/network.d.ts index bdde60c74c..ba81f31d37 100644 --- a/src/config/networks/network.d.ts +++ b/src/config/networks/network.d.ts @@ -2,19 +2,6 @@ export enum WALLETS { METAMASK = 'metamask', - WALLET_CONNECT = 'walletConnect', - TREZOR = 'trezor', - LEDGER = 'ledger', - TRUST = 'trust', - FORTMATIC = 'fortmatic', - PORTIS = 'portis', - AUTHEREUM = 'authereum', - TORUS = 'torus', - COINBASE = 'coinbase', - WALLET_LINK = 'walletLink', - OPERA = 'opera', - OPERA_TOUCH = 'operaTouch', - LATTICE = 'lattice', } export enum FEATURES { @@ -33,18 +20,10 @@ type Token = { logoUri?: string } -export enum ETHEREUM_NETWORK { +export enum HARMONY_NETWORK { + MAINNET = 1666600000, + TESTNET = 1666700000, UNKNOWN = 0, - MAINNET = 1, - MORDEN = 2, - ROPSTEN = 3, - RINKEBY = 4, - GOERLI = 5, - KOVAN = 42, - XDAI = 100, - ENERGY_WEB_CHAIN = 246, - LOCAL = 4447, - VOLTA = 73799, } export type NetworkSettings = { diff --git a/src/config/networks/rinkeby.ts b/src/config/networks/rinkeby.ts deleted file mode 100644 index 4cb16b488c..0000000000 --- a/src/config/networks/rinkeby.ts +++ /dev/null @@ -1,53 +0,0 @@ -import EtherLogo from 'src/config/assets/token_eth.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d' -import { ETHGASSTATION_API_KEY } from 'src/utils/constants' - -const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'https://safe-client.rinkeby.staging.gnosisdev.com/v1', - txServiceUrl: 'https://safe-transaction.rinkeby.staging.gnosisdev.com/api/v1', - safeUrl: 'https://rinkeby.gnosis-safe.io/app', - safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com', - gasPriceOracle: { - url: `https://ethgasstation.info/json/ethgasAPI.json?api-key=${ETHGASSTATION_API_KEY}`, - gasParameter: 'average', - }, - rpcServiceUrl: 'https://rinkeby.infura.io:443/v3', - networkExplorerName: 'Etherscan', - networkExplorerUrl: 'https://rinkeby.etherscan.io', - networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api', -} - -const rinkeby: NetworkConfig = { - environment: { - dev: { - ...baseConfig, - }, - staging: { - ...baseConfig, - safeAppsUrl: 'https://safe-apps.staging.gnosisdev.com', - }, - production: { - ...baseConfig, - clientGatewayUrl: 'https://safe-client.rinkeby.gnosis.io/v1', - txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1', - safeAppsUrl: 'https://apps.gnosis-safe.io', - }, - }, - network: { - id: ETHEREUM_NETWORK.RINKEBY, - backgroundColor: '#E8673C', - textColor: '#ffffff', - label: 'Rinkeby', - isTestNet: true, - nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'Ether', - symbol: 'ETH', - decimals: 18, - logoUri: EtherLogo, - }, - }, - disabledWallets: [WALLETS.FORTMATIC], -} - -export default rinkeby diff --git a/src/config/networks/testnet.ts b/src/config/networks/testnet.ts new file mode 100644 index 0000000000..7d137eabd3 --- /dev/null +++ b/src/config/networks/testnet.ts @@ -0,0 +1,50 @@ +import HarmonyLogo from 'src/config/assets/token-one.png' +import { EnvironmentSettings, HARMONY_NETWORK, NetworkConfig } from 'src/config/networks/network.d' + +const baseConfig: EnvironmentSettings = { + clientGatewayUrl: 'https://safe-client.b.hmny.io/v1', + txServiceUrl: 'https://multisig-staging.hmny.io/api/v1', + safeUrl: 'https://testnet.multisig.harmony.one', + safeAppsUrl: 'https://testnet.multisig.harmony.one', + gasPriceOracle: { + url: 'https://ethgasstation.info/json/ethgasAPI.json', + gasParameter: 'average', + }, + rpcServiceUrl: 'https://api.s0.b.hmny.io', + networkExplorerName: 'Harmony Explorer', + networkExplorerUrl: 'https://explorer.pops.one/#', + networkExplorerApiUrl: 'https://api-rinkeby.etherscan.io/api', +} + +const testnet: NetworkConfig = { + environment: { + dev: { + ...baseConfig, + }, + staging: { + ...baseConfig, + safeAppsUrl: 'https://testnet.multisig.harmony.one', + }, + production: { + ...baseConfig, + txServiceUrl: 'https://multisig-staging.hmny.io/api/v1', + safeAppsUrl: 'https://testnet.multisig.harmony.one', + }, + }, + network: { + id: HARMONY_NETWORK.TESTNET, + backgroundColor: '#E8673C', + textColor: '#ffffff', + label: 'Testnet', + isTestNet: true, + nativeCoin: { + address: '0x000', + name: 'Harmony', + symbol: 'ONE', + decimals: 18, + logoUri: HarmonyLogo, + }, + }, +} + +export default testnet diff --git a/src/config/networks/volta.ts b/src/config/networks/volta.ts deleted file mode 100644 index 70601edf1c..0000000000 --- a/src/config/networks/volta.ts +++ /dev/null @@ -1,62 +0,0 @@ -import EwcLogo from 'src/config/assets/token_ewc.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d' - -const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'https://safe-client.volta.gnosis.io/v1', - txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1', - safeUrl: 'https://volta.gnosis-safe.io/app', - safeAppsUrl: 'https://safe-apps-volta.staging.gnosisdev.com', - gasPriceOracle: { - url: 'https://station.energyweb.org', - gasParameter: 'standard', - }, - rpcServiceUrl: 'https://volta-rpc.energyweb.org', - networkExplorerName: 'Volta explorer', - networkExplorerUrl: 'https://volta-explorer.energyweb.org', - networkExplorerApiUrl: 'https://volta-explorer.energyweb.org/api', -} - -const mainnet: NetworkConfig = { - environment: { - dev: { - ...baseConfig, - }, - staging: { - ...baseConfig, - }, - production: { - ...baseConfig, - safeAppsUrl: 'https://apps-volta.gnosis-safe.io', - }, - }, - network: { - id: ETHEREUM_NETWORK.VOLTA, - backgroundColor: '#514989', - textColor: '#ffffff', - label: 'Volta', - isTestNet: true, - nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'Volta Token', - symbol: 'VT', - decimals: 18, - logoUri: EwcLogo, - }, - }, - disabledWallets: [ - WALLETS.TREZOR, - WALLETS.LEDGER, - WALLETS.COINBASE, - WALLETS.FORTMATIC, - WALLETS.OPERA, - WALLETS.OPERA_TOUCH, - WALLETS.PORTIS, - WALLETS.TORUS, - WALLETS.TRUST, - WALLETS.WALLET_LINK, - WALLETS.AUTHEREUM, - WALLETS.LATTICE, - ], -} - -export default mainnet diff --git a/src/config/networks/xdai.ts b/src/config/networks/xdai.ts deleted file mode 100644 index 3dee3d8e1a..0000000000 --- a/src/config/networks/xdai.ts +++ /dev/null @@ -1,59 +0,0 @@ -import xDaiLogo from 'src/config/assets/token_xdai.svg' -import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d' - -const baseConfig: EnvironmentSettings = { - clientGatewayUrl: 'https://safe-client.xdai.gnosis.io/v1', - txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1', - safeUrl: 'https://xdai.gnosis-safe.io/app', - safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com', - gasPrice: 1e9, - rpcServiceUrl: 'https://dai.poa.network/', - networkExplorerName: 'Blockscout', - networkExplorerUrl: 'https://blockscout.com/poa/xdai', - networkExplorerApiUrl: 'https://blockscout.com/poa/xdai/api', -} - -const xDai: NetworkConfig = { - environment: { - dev: { - ...baseConfig, - }, - staging: { - ...baseConfig, - }, - production: { - ...baseConfig, - safeAppsUrl: 'https://apps-xdai.gnosis-safe.io', - }, - }, - network: { - id: ETHEREUM_NETWORK.XDAI, - backgroundColor: '#48A8A6', - textColor: '#ffffff', - label: 'xDai', - isTestNet: false, - nativeCoin: { - address: '0x0000000000000000000000000000000000000000', - name: 'xDai', - symbol: 'xDai', - decimals: 18, - logoUri: xDaiLogo, - }, - }, - disabledWallets: [ - WALLETS.TREZOR, - WALLETS.LEDGER, - WALLETS.COINBASE, - WALLETS.FORTMATIC, - WALLETS.OPERA, - WALLETS.OPERA_TOUCH, - WALLETS.TORUS, - WALLETS.TRUST, - WALLETS.WALLET_LINK, - WALLETS.AUTHEREUM, - WALLETS.LATTICE, - ], - disabledFeatures: [FEATURES.DOMAIN_LOOKUP], -} - -export default xDai diff --git a/src/index.tsx b/src/index.tsx index 7a3aeb82fa..5d204adf34 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,12 @@ import { BigNumber } from 'bignumber.js' import React from 'react' import ReactDOM from 'react-dom' -import * as Sentry from '@sentry/react' -import { Integrations } from '@sentry/tracing' import Root from 'src/components/Root' import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe' import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage' import { store } from 'src/store' -import { SENTRY_DSN } from './utils/constants' import { disableMMAutoRefreshWarning } from './utils/mm_warnings' disableMMAutoRefreshWarning() @@ -20,13 +17,6 @@ store.dispatch(loadSafesFromStorage()) store.dispatch(loadDefaultSafe()) store.dispatch(loadCurrentSessionFromStorage()) -Sentry.init({ - dsn: SENTRY_DSN, - release: `safe-react@${process.env.REACT_APP_APP_VERSION}`, - integrations: [new Integrations.BrowserTracing()], - sampleRate: 0.01, -}) - const root = document.getElementById('root') if (root !== null) { diff --git a/src/logic/addressBook/model/addressBook.ts b/src/logic/addressBook/model/addressBook.ts index d6ddcaeb2a..a136a550bf 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -1,4 +1,4 @@ -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' import { getNetworkId } from 'src/config' export const ADDRESS_BOOK_DEFAULT_NAME = 'UNKNOWN' @@ -6,7 +6,7 @@ export const ADDRESS_BOOK_DEFAULT_NAME = 'UNKNOWN' export type AddressBookEntry = { address: string // the contact address name: string // human-readable name - chainId: ETHEREUM_NETWORK // see https://chainid.network + chainId: HARMONY_NETWORK // see https://chainid.network } const networkId = getNetworkId() diff --git a/src/logic/collectibles/utils/index.ts b/src/logic/collectibles/utils/index.ts index 6aae4ff236..756618f0ea 100644 --- a/src/logic/collectibles/utils/index.ts +++ b/src/logic/collectibles/utils/index.ts @@ -1,18 +1,6 @@ -import { getNetworkId } from 'src/config' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens' -import { sameAddress } from 'src/logic/wallets/ethAddresses' import { CollectibleTx } from 'src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible' -// CryptoKitties Contract Addresses by network -// This is an exception made for a popular NFT that's not ERC721 standard-compatible, -// so we can allow the user to transfer the assets by using `transferFrom` instead of -// the standard `safeTransferFrom` method. -export const CK_ADDRESS = { - [ETHEREUM_NETWORK.MAINNET]: '0x06012c8cf97bead5deae237070f9587f8e7a266d', - [ETHEREUM_NETWORK.RINKEBY]: '0x16baf0de678e52367adc69fd067e5edd1d33e3bf', -} - // safeTransferFrom(address,address,uint256) export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' @@ -22,12 +10,6 @@ export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' * @returns string */ export const getTransferMethodByContractAddress = (contractAddress: string): string => { - if (sameAddress(contractAddress, CK_ADDRESS[getNetworkId()])) { - // on mainnet `transferFrom` seems to work fine but we can assure that `transfer` will work on both networks - // so that's the reason why we're falling back to `transfer` for CryptoKitties - return 'transfer' - } - return `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}` } diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index a886ba67f0..c489abc02d 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -3,7 +3,8 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe. import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json' import Web3 from 'web3' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' +import { getNetworkId } from 'src/config/index' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' @@ -15,41 +16,68 @@ import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import SpendingLimitModule from './artifacts/AllowanceModule.json' +const MULTI_SEND_ADDRESSES = { + [HARMONY_NETWORK.MAINNET]: '0xDEff67e9A02b4Ce60ff62F3CB5FFB41d48856285', + [HARMONY_NETWORK.TESTNET]: '0xF39E79A7B8B319a2554abd3469463f6620C117Bb', +} + +const SAFE_MASTER_COPY_ADDRESSES = { + [HARMONY_NETWORK.MAINNET]: '0x3736aC8400751bf07c6A2E4db3F4f3D9D422abB2', + [HARMONY_NETWORK.TESTNET]: '0x0F2f043DBc72D3948bB7E392E6E3258dc2743376', +} + +const DEFAULT_FALLBACK_HANDLER_ADDRESSES = { + [HARMONY_NETWORK.MAINNET]: '0xC5d654bcE1220241FCe1f0F1D6b9E04f75175452', + [HARMONY_NETWORK.TESTNET]: '0x6B0d84741F7EE66B72bF262A1eDB772d01E2aFE6', +} + +const SAFE_MASTER_COPY_ADDRESS_V10ES = { + [HARMONY_NETWORK.MAINNET]: '0x3736aC8400751bf07c6A2E4db3F4f3D9D422abB2', + [HARMONY_NETWORK.TESTNET]: '0x0F2f043DBc72D3948bB7E392E6E3258dc2743376', +} + export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' -export const MULTI_SEND_ADDRESS = '0x8d29be29923b68abfdd21e541b9374737b49cdad' -export const SAFE_MASTER_COPY_ADDRESS = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' -export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44' -export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A' +export const MULTI_SEND_ADDRESS = MULTI_SEND_ADDRESSES[HARMONY_NETWORK[getNetworkId()]] +export const SAFE_MASTER_COPY_ADDRESS = SAFE_MASTER_COPY_ADDRESSES[HARMONY_NETWORK[getNetworkId()]] +export const DEFAULT_FALLBACK_HANDLER_ADDRESS = DEFAULT_FALLBACK_HANDLER_ADDRESSES[HARMONY_NETWORK[getNetworkId()]] +export const SAFE_MASTER_COPY_ADDRESS_V10 = SAFE_MASTER_COPY_ADDRESS_V10ES[HARMONY_NETWORK[getNetworkId()]] let proxyFactoryMaster: GnosisSafeProxyFactory let safeMaster: GnosisSafe +const SAFE_CONTRACTS = { + [HARMONY_NETWORK.MAINNET]: '0x3736aC8400751bf07c6A2E4db3F4f3D9D422abB2', + [HARMONY_NETWORK.TESTNET]: '0x0F2f043DBc72D3948bB7E392E6E3258dc2743376', +} + +const PROXY_CONTRACTS = { + [HARMONY_NETWORK.MAINNET]: '0x4f9b1dEf3a0f6747bF8C870a27D3DeCdf029100e', + [HARMONY_NETWORK.TESTNET]: '0xAaCf9eb6614f7C110EF9b7D832BCe2E67EEC08c1', +} /** * Creates a Contract instance of the GnosisSafe contract * @param {Web3} web3 - * @param {ETHEREUM_NETWORK} networkId + * @param {HARMONY_NETWORK} networkId */ -export const getGnosisSafeContract = (web3: Web3, networkId: ETHEREUM_NETWORK) => { - const networks = GnosisSafeSol.networks +export const getGnosisSafeContract = (web3: Web3, networkId: HARMONY_NETWORK) => { // TODO: this may not be the most scalable approach, // but up until v1.2.0 the address is the same for all the networks. // So, if we can't find the network in the Contract artifact, we fallback to MAINNET. - const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address - return (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown) as GnosisSafe + const contractAddress = SAFE_CONTRACTS[networkId] ?? SAFE_CONTRACTS[HARMONY_NETWORK.MAINNET] + return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], contractAddress) as unknown as GnosisSafe } /** * Creates a Contract instance of the GnosisSafeProxyFactory contract * @param {Web3} web3 - * @param {ETHEREUM_NETWORK} networkId + * @param {HARMONY_NETWORK} networkId */ -const getProxyFactoryContract = (web3: Web3, networkId: ETHEREUM_NETWORK): GnosisSafeProxyFactory => { - const networks = ProxyFactorySol.networks +const getProxyFactoryContract = (web3: Web3, networkId: HARMONY_NETWORK): GnosisSafeProxyFactory => { // TODO: this may not be the most scalable approach, // but up until v1.2.0 the address is the same for all the networks. // So, if we can't find the network in the Contract artifact, we fallback to MAINNET. - const contractAddress = networks[networkId]?.address ?? networks[ETHEREUM_NETWORK.MAINNET].address - return (new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown) as GnosisSafeProxyFactory + const contractAddress = PROXY_CONTRACTS[networkId] ?? PROXY_CONTRACTS[HARMONY_NETWORK.MAINNET] + return new web3.eth.Contract(ProxyFactorySol.abi as AbiItem[], contractAddress) as unknown as GnosisSafeProxyFactory } export const getMasterCopyAddressFromProxyAddress = async (proxyAddress: string): Promise => { @@ -132,7 +160,7 @@ export const estimateGasForDeployingSafe = async ( export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => { const web3 = getWeb3() - return (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe + return new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown as GnosisSafe } /** @@ -141,8 +169,8 @@ export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => { export const getSpendingLimitContract = () => { const web3 = getWeb3() - return (new web3.eth.Contract( + return new web3.eth.Contract( SpendingLimitModule.abi as AbiItem[], SPENDING_LIMIT_MODULE_ADDRESS, - ) as unknown) as AllowanceModule + ) as unknown as AllowanceModule } diff --git a/src/logic/cookies/model/cookie.ts b/src/logic/cookies/model/cookie.ts deleted file mode 100644 index a119ac0d35..0000000000 --- a/src/logic/cookies/model/cookie.ts +++ /dev/null @@ -1 +0,0 @@ -export const COOKIES_KEY = 'COOKIES' diff --git a/src/logic/cookies/store/actions/openCookieBanner.ts b/src/logic/cookies/store/actions/openCookieBanner.ts deleted file mode 100644 index e3db9cb702..0000000000 --- a/src/logic/cookies/store/actions/openCookieBanner.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -import { OpenCookieBannerPayload } from 'src/logic/cookies/store/reducer/cookies' - -export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER' - -export const openCookieBanner = createAction(OPEN_COOKIE_BANNER) diff --git a/src/logic/cookies/store/reducer/cookies.ts b/src/logic/cookies/store/reducer/cookies.ts deleted file mode 100644 index e8411153de..0000000000 --- a/src/logic/cookies/store/reducer/cookies.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Map } from 'immutable' -import { handleActions } from 'redux-actions' -import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner' -import { AppReduxState } from 'src/store' - -export const COOKIES_REDUCER_ID = 'cookies' - -export type OpenCookieBannerPayload = { cookieBannerOpen: boolean; intercomAlertDisplayed?: boolean } - -export default handleActions( - { - [OPEN_COOKIE_BANNER]: (state, action) => { - const { intercomAlertDisplayed = false, cookieBannerOpen } = action.payload - return state.set('cookieBannerOpen', { intercomAlertDisplayed, cookieBannerOpen }) - }, - }, - Map(), -) diff --git a/src/logic/cookies/store/selectors/index.ts b/src/logic/cookies/store/selectors/index.ts deleted file mode 100644 index 9104be6934..0000000000 --- a/src/logic/cookies/store/selectors/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies' - -export const cookieBannerOpen = (state) => state[COOKIES_REDUCER_ID].get('cookieBannerOpen') diff --git a/src/logic/cookies/utils/index.ts b/src/logic/cookies/utils/index.ts deleted file mode 100644 index 3d894b517c..0000000000 --- a/src/logic/cookies/utils/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Cookies from 'js-cookie' - -import { getNetworkName } from 'src/config' -import { Errors, logError } from 'src/logic/exceptions/CodedException' - -const PREFIX = `v1_${getNetworkName()}` - -export const loadFromCookie = async (key: string, withoutPrefix = false): Promise> => { - const prefix = withoutPrefix ? '' : `${PREFIX}__` - try { - const stringifiedValue = await Cookies.get(`${prefix}${key}`) - if (stringifiedValue === null || stringifiedValue === undefined) { - return undefined - } - - return JSON.parse(stringifiedValue) - } catch (err) { - logError(Errors._700, `cookie ${key} – ${err.message}`) - return undefined - } -} - -export const saveCookie = async (key: string, value: Record, expirationDays: number): Promise => { - try { - const stringifiedValue = JSON.stringify(value) - const expiration = expirationDays ? { expires: expirationDays } : undefined - await Cookies.set(`${PREFIX}__${key}`, stringifiedValue, expiration) - } catch (err) { - logError(Errors._701, `cookie ${key} – ${err.message}`) - } -} - -export const removeCookie = (key: string, path: string, domain: string): void => Cookies.remove(key, { path, domain }) diff --git a/src/logic/wallets/store/model/provider.ts b/src/logic/wallets/store/model/provider.ts index 146b2ddb70..7298b804c5 100644 --- a/src/logic/wallets/store/model/provider.ts +++ b/src/logic/wallets/store/model/provider.ts @@ -1,13 +1,13 @@ import { Record, RecordOf } from 'immutable' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' export type ProviderProps = { name: string loaded: boolean available: boolean account: string - network: ETHEREUM_NETWORK + network: HARMONY_NETWORK smartContractWallet: boolean hardwareWallet: boolean } @@ -17,7 +17,7 @@ export const makeProvider = Record({ loaded: false, available: false, account: '', - network: ETHEREUM_NETWORK.UNKNOWN, + network: HARMONY_NETWORK.UNKNOWN, smartContractWallet: false, hardwareWallet: false, }) diff --git a/src/logic/wallets/store/selectors/index.ts b/src/logic/wallets/store/selectors/index.ts index 56e41f67c8..fb8d3848d0 100644 --- a/src/logic/wallets/store/selectors/index.ts +++ b/src/logic/wallets/store/selectors/index.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' import { AppReduxState } from 'src/store' @@ -16,14 +16,11 @@ export const providerNameSelector = createSelector(providerSelector, (provider: return name ? name.toLowerCase() : undefined }) -export const networkSelector = createSelector( - providerSelector, - (provider: ProviderState): ETHEREUM_NETWORK => { - const networkId = provider.get('network') +export const networkSelector = createSelector(providerSelector, (provider: ProviderState): HARMONY_NETWORK => { + const networkId = provider.get('network') - return networkId ?? ETHEREUM_NETWORK.UNKNOWN - }, -) + return networkId ?? HARMONY_NETWORK.UNKNOWN +}) export const loadedSelector = createSelector(providerSelector, (provider: ProviderState): boolean => provider.get('loaded'), diff --git a/src/logic/wallets/utils/walletList.ts b/src/logic/wallets/utils/walletList.ts index 49f6ad6d43..efeffe6e49 100644 --- a/src/logic/wallets/utils/walletList.ts +++ b/src/logic/wallets/utils/walletList.ts @@ -1,8 +1,7 @@ import { WalletInitOptions } from 'bnc-onboard/dist/src/interfaces' -import { getNetworkId, getRpcServiceUrl, getNetworkConfigDisabledWallets } from 'src/config' +import { getNetworkId, getNetworkConfigDisabledWallets } from 'src/config' import { WALLETS } from 'src/config/networks/network.d' -import { FORTMATIC_KEY, PORTIS_ID } from 'src/utils/constants' const networkId = getNetworkId() const disabledWallets = getNetworkConfigDisabledWallets() @@ -12,56 +11,7 @@ type Wallet = WalletInitOptions & { walletName: WALLETS } -const rpcUrl = getRpcServiceUrl() -const wallets: Wallet[] = [ - { walletName: WALLETS.METAMASK, preferred: true, desktop: false }, - { - walletName: WALLETS.WALLET_CONNECT, - preferred: true, - // as stated in the documentation, `infuraKey` is not mandatory if rpc is provided - rpc: { [networkId]: rpcUrl }, - desktop: true, - bridge: 'https://safe-walletconnect.gnosis.io/', - }, - { - walletName: WALLETS.TREZOR, - appUrl: 'gnosis-safe.io', - preferred: true, - email: 'safe@gnosis.io', - desktop: true, - rpcUrl, - }, - { - walletName: WALLETS.LEDGER, - desktop: true, - preferred: true, - rpcUrl, - LedgerTransport: (window as any).TransportNodeHid, - }, - { walletName: WALLETS.TRUST, preferred: true, desktop: false }, - { - walletName: WALLETS.LATTICE, - rpcUrl, - appName: 'Gnosis Safe', - desktop: false, - }, - { - walletName: WALLETS.FORTMATIC, - apiKey: FORTMATIC_KEY, - desktop: true, - }, - { - walletName: WALLETS.PORTIS, - apiKey: PORTIS_ID, - desktop: true, - }, - { walletName: WALLETS.AUTHEREUM, desktop: false }, - { walletName: WALLETS.TORUS, desktop: true }, - { walletName: WALLETS.COINBASE, desktop: false }, - { walletName: WALLETS.WALLET_LINK, rpcUrl, desktop: false }, - { walletName: WALLETS.OPERA, desktop: false }, - { walletName: WALLETS.OPERA_TOUCH, desktop: false }, -] +const wallets: Wallet[] = [{ walletName: WALLETS.METAMASK, preferred: true, desktop: false }] export const getSupportedWallets = (): WalletInitOptions[] => { const { isDesktop } = window diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3a04fce160..1b872832d9 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -6,7 +6,6 @@ import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCO import { Loader } from '@gnosis.pm/safe-react-components' import { defaultSafe as defaultSafeSelector } from 'src/logic/safe/store/selectors' -import { useAnalytics } from 'src/utils/googleAnalytics' import { DEFAULT_SAFE_INITIAL_STATE } from 'src/logic/safe/store/reducer/safe' import { LoadingContainer } from 'src/components/LoaderContainer' @@ -28,7 +27,6 @@ const Routes = (): React.ReactElement => { }) const defaultSafe = useSelector(defaultSafeSelector) - const { trackPage } = useAnalytics() useEffect(() => { if (isInitialLoad && location.pathname !== '/') { @@ -43,12 +41,10 @@ const Routes = (): React.ReactElement => { if (matchSafeWithAction.params?.safeAction) { safePage += `/${matchSafeWithAction.params?.safeAction}` } - trackPage(safePage) } else { const page = `${location.pathname}${location.search}` - trackPage(page) } - }, [location, matchSafeWithAction, trackPage]) + }, [location, matchSafeWithAction]) return ( diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 94cd28cd11..9e452814d8 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -27,7 +27,6 @@ import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' -import { useAnalytics } from 'src/utils/googleAnalytics' import { sleep } from 'src/utils/timer' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' @@ -104,7 +103,6 @@ const Open = (): ReactElement => { const userAccount = useSelector(userAccountSelector) const dispatch = useDispatch() const location = useLocation() - const { trackEvent } = useAnalytics() useEffect(() => { // #122: Allow to migrate an old Multisig by passing the parameters to the URL. @@ -173,11 +171,6 @@ const Open = (): ReactElement => { const safeProps = await buildSafe(safeAddress) await dispatch(addOrUpdateSafe(safeProps)) - trackEvent({ - category: 'User', - action: 'Created a safe', - }) - // a default 5s wait before starting to request safe information await sleep(5000) diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 5f96809f62..143c23ee3e 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -35,7 +35,7 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal' import { safesAsList } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { grantedSelector } from 'src/routes/safe/container/selector' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' + import ImportEntriesModal from './ImportEntriesModal' import { isValidAddress } from 'src/utils/isValidAddress' @@ -81,11 +81,6 @@ const AddressBookTable = (): ReactElement => { const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [exportEntriesModalOpen, setExportEntriesModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) - const { trackEvent } = useAnalytics() - - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'AddressBook' }) - }, [trackEvent]) useEffect(() => { if (entryAddressToEditOrCreateNew) { diff --git a/src/routes/safe/components/Apps/__tests__/utils.test.ts b/src/routes/safe/components/Apps/__tests__/utils.test.ts deleted file mode 100644 index 795f53ef68..0000000000 --- a/src/routes/safe/components/Apps/__tests__/utils.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isAppManifestValid } from '../utils' -import { SafeApp } from '../types' - -describe('SafeApp manifest', () => { - it('It should return true given a manifest with mandatory values supplied', async () => { - const manifest = { - name: 'test', - description: 'a test', - error: false, - } - - const result = isAppManifestValid(manifest as SafeApp) - expect(result).toBe(true) - }) - - it('It should return false given a manifest without name', async () => { - const manifest = { - name: '', - description: 'a test', - error: false, - } - - const result = isAppManifestValid(manifest as SafeApp) - expect(result).toBe(false) - }) - - it('It should return false given a manifest without description', async () => { - const manifest = { - name: 'test', - description: '', - error: false, - } - - const result = isAppManifestValid(manifest as SafeApp) - expect(result).toBe(false) - }) - - it('It should return false given a manifest with error', async () => { - const manifest = { - name: 'test', - description: 'a test', - error: true, - } - - const result = isAppManifestValid(manifest as SafeApp) - expect(result).toBe(false) - }) -}) diff --git a/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts b/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts deleted file mode 100644 index fe3981dc44..0000000000 --- a/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios from 'axios' - -import { SAFE_APPS_LIST_URL } from 'src/utils/constants' - -export type TokenListResult = { - name: string - timestamp: string - apps: AppData[] -} - -export type AppData = { - url: string - name?: string - disabled?: boolean - description?: string - networks: number[] -} - -export const fetchSafeAppsList = async (): Promise => { - return axios.get(SAFE_APPS_LIST_URL).then(({ data }) => data) -} diff --git a/src/routes/safe/components/Apps/assets/addApp.svg b/src/routes/safe/components/Apps/assets/addApp.svg deleted file mode 100644 index 76500778d3..0000000000 --- a/src/routes/safe/components/Apps/assets/addApp.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/routes/safe/components/Apps/communicator.ts b/src/routes/safe/components/Apps/communicator.ts deleted file mode 100644 index ae1d21f043..0000000000 --- a/src/routes/safe/components/Apps/communicator.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { MutableRefObject, useEffect, useState } from 'react' -import { - getSDKVersion, - SDKMessageEvent, - MethodToResponse, - Methods, - ErrorResponse, - MessageFormatter, - METHODS, - RequestId, -} from '@gnosis.pm/safe-apps-sdk' -import { trackError, Errors } from 'src/logic/exceptions/CodedException' -import { SafeApp } from './types' - -type MessageHandler = ( - msg: SDKMessageEvent, -) => void | MethodToResponse[Methods] | ErrorResponse | Promise - -type LegacyMethods = 'getEnvInfo' -type SDKMethods = Methods | LegacyMethods - -class AppCommunicator { - private iframeRef: MutableRefObject - private handlers = new Map() - private app: SafeApp - - constructor(iframeRef: MutableRefObject, app: SafeApp) { - this.iframeRef = iframeRef - this.app = app - - window.addEventListener('message', this.handleIncomingMessage) - } - - on = (method: SDKMethods, handler: MessageHandler): void => { - this.handlers.set(method, handler) - } - - private isValidMessage = (msg: SDKMessageEvent): boolean => { - // @ts-expect-error .parent doesn't exist on some possible types - const sentFromIframe = msg.source.parent === window.parent - const knownMethod = Object.values(METHODS).includes(msg.data.method) - - return sentFromIframe && knownMethod - } - - private canHandleMessage = (msg: SDKMessageEvent): boolean => { - return Boolean(this.handlers.get(msg.data.method)) - } - - send = (data: unknown, requestId: RequestId, error = false): void => { - const sdkVersion = getSDKVersion() - const msg = error - ? MessageFormatter.makeErrorResponse(requestId, data as string, sdkVersion) - : MessageFormatter.makeResponse(requestId, data, sdkVersion) - - this.iframeRef.current?.contentWindow?.postMessage(msg, '*') - } - - handleIncomingMessage = async (msg: SDKMessageEvent): Promise => { - const validMessage = this.isValidMessage(msg) - const hasHandler = this.canHandleMessage(msg) - - if (validMessage && hasHandler) { - const handler = this.handlers.get(msg.data.method) - try { - // @ts-expect-error Handler existence is checked in this.canHandleMessage - const response = await handler(msg) - - // If response is not returned, it means the response will be send somewhere else - if (typeof response !== 'undefined') { - this.send(response, msg.data.id) - } - } catch (err) { - this.send(err.message, msg.data.id, true) - trackError(Errors._901, err.message, { - contexts: { - safeApp: this.app, - request: msg.data, - }, - }) - } - } - } - - clear = (): void => { - window.removeEventListener('message', this.handleIncomingMessage) - } -} - -const useAppCommunicator = ( - iframeRef: MutableRefObject, - app?: SafeApp, -): AppCommunicator | undefined => { - const [communicator, setCommunicator] = useState(undefined) - useEffect(() => { - let communicatorInstance - const initCommunicator = (iframeRef: MutableRefObject, app: SafeApp) => { - communicatorInstance = new AppCommunicator(iframeRef, app) - setCommunicator(communicatorInstance) - } - - if (app) { - initCommunicator(iframeRef as MutableRefObject, app) - } - - return () => { - communicatorInstance?.clear() - } - }, [app, iframeRef]) - - return communicator -} - -export { useAppCommunicator } diff --git a/src/routes/safe/components/Apps/components/AddAppForm/AppAgreement.tsx b/src/routes/safe/components/Apps/components/AddAppForm/AppAgreement.tsx deleted file mode 100644 index f4330e7091..0000000000 --- a/src/routes/safe/components/Apps/components/AddAppForm/AppAgreement.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Checkbox, Text } from '@gnosis.pm/safe-react-components' -import React from 'react' -import { useFormState } from 'react-final-form' -import styled from 'styled-components' - -import { required } from 'src/components/forms/validator' -import Field from 'src/components/forms/Field' - -const StyledCheckbox = styled(Checkbox)` - margin: 0; -` - -const AppAgreement = (): React.ReactElement => { - const { visited } = useFormState({ subscription: { visited: true } }) - - // trick to prevent having the field validated by default. Not sure why this happens in this form - const validate = !visited?.agreementAccepted ? undefined : required - - return ( - This app is not a Gnosis product and I agree to use this app at my own risk.} - name="agreementAccepted" - type="checkbox" - validate={validate} - /> - ) -} - -export default AppAgreement diff --git a/src/routes/safe/components/Apps/components/AddAppForm/AppUrl.tsx b/src/routes/safe/components/Apps/components/AddAppForm/AppUrl.tsx deleted file mode 100644 index 4e1a7e44df..0000000000 --- a/src/routes/safe/components/Apps/components/AddAppForm/AppUrl.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { TextField } from '@gnosis.pm/safe-react-components' -import createDecorator from 'final-form-calculate' -import React from 'react' -import { useField, useFormState } from 'react-final-form' -import styled from 'styled-components' - -import { SafeApp } from 'src/routes/safe/components/Apps/types' -import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils' -import { composeValidators, required } from 'src/components/forms/validator' -import Field from 'src/components/forms/Field' -import { isValidURL } from 'src/utils/url' -import { isValidEnsName } from 'src/logic/wallets/ethAddresses' -import { useDebounce } from 'src/logic/hooks/useDebounce' - -const validateUrl = (url: string): string | undefined => (isValidURL(url) ? undefined : 'Invalid URL') - -export const appUrlResolver = createDecorator({ - field: 'appUrl', - updates: { - appUrl: async (appUrl: string): Promise => { - const ensContent = !isValidURL(appUrl) && isValidEnsName(appUrl) && (await getIpfsLinkFromEns(appUrl)) - - if (ensContent) { - return ensContent - } - - return appUrl - }, - }, -}) - -type AppInfoUpdaterProps = { - onAppInfo: (appInfo: SafeApp) => void - onLoading: (isLoading: boolean) => void -} - -export const AppInfoUpdater = ({ onAppInfo, onLoading }: AppInfoUpdaterProps): null => { - const { - input: { value: appUrl }, - } = useField('appUrl', { subscription: { value: true } }) - - const debouncedValue = useDebounce(appUrl, 500) - - React.useEffect(() => { - const updateAppInfo = async () => { - try { - onLoading(true) - const appInfo = await getAppInfoFromUrl(debouncedValue) - onAppInfo({ ...appInfo }) - onLoading(false) - } catch (error) { - onLoading(false) - } - } - - if (isValidURL(debouncedValue)) { - updateAppInfo() - } - }, [debouncedValue, onAppInfo, onLoading]) - - return null -} - -const StyledAppUrlField = styled(Field)` - && { - width: 100%; - } -` - -const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => { - const { visited } = useFormState({ subscription: { visited: true } }) - - // trick to prevent having the field validated by default. Not sure why this happens in this form - const validate = !visited?.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList)) - - return ( - - ) -} - -export default AppUrl diff --git a/src/routes/safe/components/Apps/components/AddAppForm/FormButtons.tsx b/src/routes/safe/components/Apps/components/AddAppForm/FormButtons.tsx deleted file mode 100644 index 24b936352a..0000000000 --- a/src/routes/safe/components/Apps/components/AddAppForm/FormButtons.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { ReactElement, useMemo } from 'react' -import { useFormState } from 'react-final-form' - -import { Modal } from 'src/components/Modal' -import { SafeApp } from 'src/routes/safe/components/Apps/types' -import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils' - -interface Props { - appInfo: SafeApp - onCancel: () => void -} - -export const FormButtons = ({ appInfo, onCancel }: Props): ReactElement => { - const { valid, validating, visited } = useFormState({ - subscription: { valid: true, validating: true, visited: true }, - }) - - const isSubmitDisabled = useMemo(() => { - // if non visited, fields were not evaluated yet. Then, the default value is considered invalid - const fieldsVisited = visited?.agreementAccepted && visited?.appUrl - - return validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo) - }, [validating, valid, visited, appInfo]) - - return ( - - ) -} diff --git a/src/routes/safe/components/Apps/components/AddAppForm/index.tsx b/src/routes/safe/components/Apps/components/AddAppForm/index.tsx deleted file mode 100644 index 12eadd5854..0000000000 --- a/src/routes/safe/components/Apps/components/AddAppForm/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Icon, Link, Loader, Text, TextField } from '@gnosis.pm/safe-react-components' -import React, { useState, ReactElement } from 'react' -import styled from 'styled-components' - -import { SafeApp } from 'src/routes/safe/components/Apps/types' -import GnoForm from 'src/components/forms/GnoForm' -import Img from 'src/components/layout/Img' -import { Modal } from 'src/components/Modal' - -import AppAgreement from './AppAgreement' -import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl' -import { FormButtons } from './FormButtons' -import { APPS_STORAGE_KEY, getEmptySafeApp } from 'src/routes/safe/components/Apps/utils' -import { saveToStorage } from 'src/utils/storage' -import { SAFELIST_ADDRESS } from 'src/routes/routes' -import { useHistory, useRouteMatch } from 'react-router-dom' - -const FORM_ID = 'add-apps-form' - -const StyledTextFileAppName = styled(TextField)` - && { - width: 385px; - } -` - -const AppInfo = styled.div` - display: flex; - margin: 36px 0 24px 0; - - img { - margin-right: 10px; - } -` -const AppDocsInfo = styled.div` - display: flex; - margin-bottom: 24px; - flex-direction: column; - svg { - position: relative; - top: 4px; - left: 4px; - } -` - -const WrapperLoader = styled.div` - height: 55px; - width: 65px; - display: flex; - align-items: center; - justify-content: center; -` - -const StyledLoader = styled(Loader)` - margin-right: 15px; -` - -interface AddAppFormValues { - appUrl: string - agreementAccepted: boolean -} - -const INITIAL_VALUES: AddAppFormValues = { - appUrl: '', - agreementAccepted: false, -} - -const APP_INFO = getEmptySafeApp() - -interface AddAppProps { - appList: SafeApp[] - closeModal: () => void -} - -const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => { - const [appInfo, setAppInfo] = useState(APP_INFO) - const history = useHistory() - const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) - const [isLoading, setIsLoading] = useState(false) - - const handleSubmit = () => { - const newAppList = [ - { url: appInfo.url, disabled: false }, - ...appList.map(({ url, disabled }) => ({ url, disabled })), - ] - saveToStorage(APPS_STORAGE_KEY, newAppList) - const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(appInfo.url)}` - history.push(goToApp) - } - - return ( - - {() => ( - <> - - - - Safe Apps are third-party extensions. - - - - Learn more about building Safe Apps. - - - - - - {/* Fetch app from url and return a SafeApp */} - - - {isLoading ? ( - - - - ) : ( - Token image - )} - {}} - /> - - - - - - - - )} - - ) -} - -export default AddApp diff --git a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx b/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx deleted file mode 100644 index 32020d3ee2..0000000000 --- a/src/routes/safe/components/Apps/components/AppCard/index.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' - -import AppCard from './index' - -import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' - -export default { - title: 'Apps/AppCard', - component: AppCard, -} - -export const Loading = (): React.ReactElement => - -export const AddCustomApp = (): React.ReactElement => ( - -) - -export const LoadedApp = (): React.ReactElement => ( - -) diff --git a/src/routes/safe/components/Apps/components/AppCard/index.tsx b/src/routes/safe/components/Apps/components/AppCard/index.tsx deleted file mode 100644 index 234e6116d3..0000000000 --- a/src/routes/safe/components/Apps/components/AppCard/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { SyntheticEvent } from 'react' -import styled from 'styled-components' -import { fade } from '@material-ui/core/styles/colorManipulator' -import { Title, Text, Button, Card } from '@gnosis.pm/safe-react-components' - -import appsIconSvg from 'src/assets/icons/apps.svg' -import { AppIconSK, DescriptionSK, TitleSK } from './skeleton' - -const StyledAppCard = styled(Card)` - display: flex; - align-items: center; - flex-direction: column; - justify-content: space-evenly; - box-shadow: 1px 2px 10px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.18)}; - height: 232px !important; - box-sizing: border-box; - cursor: pointer; - color: ${({ theme }) => theme.colors.secondary}; - - :hover { - box-shadow: 1px 2px 16px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.35)}; - transition: box-shadow 0.3s ease-in-out; - background-color: ${({ theme }) => theme.colors.background}; - cursor: pointer; - - h5 { - color: ${({ theme }) => theme.colors.primary}; - } - } -` - -const IconImg = styled.img<{ size: 'md' | 'lg'; src: string | undefined }>` - width: ${({ size }) => (size === 'md' ? '60px' : '102px')}; - height: ${({ size }) => (size === 'md' ? '60px' : '92px')}; - margin-top: ${({ size }) => (size === 'md' ? '0' : '-16px')}; - object-fit: contain; -` - -const AppName = styled(Title)` - text-align: center; - margin: 16px 0 9px 0; -` - -const AppDescription = styled(Text)` - height: 71px; - text-align: center; -` - -export const setAppImageFallback = (error: SyntheticEvent): void => { - error.currentTarget.onerror = null - error.currentTarget.src = appsIconSvg -} - -export enum TriggerType { - Button, - Content, -} - -type Props = { - onClick?: () => void - isLoading?: boolean - className?: string - name?: string - description?: string - iconUrl?: string - iconSize?: 'md' | 'lg' - buttonText?: string -} - -const AppCard = ({ - isLoading = false, - className, - name, - description, - iconUrl, - iconSize = 'md', - buttonText, - onClick = () => undefined, -}: Props): React.ReactElement => { - if (isLoading) { - return ( - - - - - - - ) - } - - return ( - - - - {name && {name}} - - {description && {description} } - - {buttonText && ( - - )} - - ) -} - -export default AppCard diff --git a/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx b/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx deleted file mode 100644 index 92ef8718d2..0000000000 --- a/src/routes/safe/components/Apps/components/AppCard/skeleton.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import styled, { keyframes } from 'styled-components' - -const gradientSK = keyframes` - 0% { - background-position: 0% 54%; - } - 50% { - background-position: 100% 47%; - } - 100% { - background-position: 0% 54%; - } -` - -export const AppIconSK = styled.div` - height: 60px; - width: 60px; - border-radius: 30px; - margin: 0 auto; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - animation: ${gradientSK} 1.5s ease infinite; -` -export const TitleSK = styled.div` - height: 24px; - width: 160px; - margin: 24px auto; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - animation: ${gradientSK} 1.5s ease infinite; -` -export const DescriptionSK = styled.div` - height: 16px; - width: 200px; - background-color: lightgrey; - background: linear-gradient(84deg, lightgrey, transparent); - background-size: 400% 400%; - animation: ${gradientSK} 1.5s ease infinite; -` diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx deleted file mode 100644 index 471648ed54..0000000000 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { ReactElement, useState, useRef, useCallback, useEffect } from 'react' -import styled from 'styled-components' -import { FixedIcon, Loader, Title, Card } from '@gnosis.pm/safe-react-components' -import { GetBalanceParams, GetTxBySafeTxHashParams, MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk' -import { useHistory } from 'react-router-dom' -import { useSelector } from 'react-redux' -import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1' - -import { currentSafeWithNames } from 'src/logic/safe/store/selectors' -import { grantedSelector } from 'src/routes/safe/container/selector' -import { getNetworkId, getNetworkName, getTxServiceUrl } from 'src/config' -import { SAFELIST_ADDRESS } from 'src/routes/routes' -import { isSameURL } from 'src/utils/url' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -import { useAppList } from '../hooks/useAppList' -import { LoadingContainer } from 'src/components/LoaderContainer/index' -import { TIMEOUT } from 'src/utils/constants' -import { web3ReadOnly } from 'src/logic/wallets/getWeb3' - -import { ConfirmTxModal } from './ConfirmTxModal' -import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' -import { useLegalConsent } from '../hooks/useLegalConsent' -import LegalDisclaimer from './LegalDisclaimer' -import { getAppInfoFromUrl } from '../utils' -import { SafeApp } from '../types' -import { useAppCommunicator } from '../communicator' -import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances' -import { fetchSafeTransaction } from 'src/logic/safe/transactions/api/fetchSafeTransaction' - -const OwnerDisclaimer = styled.div` - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - height: 476px; -` - -const AppWrapper = styled.div` - display: flex; - flex-direction: column; - height: calc(100% + 59px); - margin: 0 -16px; -` - -const StyledCard = styled(Card)` - flex-grow: 1; - padding: 0; - border-radius: 0; -` - -const StyledIframe = styled.iframe<{ isLoading: boolean }>` - height: 100%; - width: 100%; - overflow: auto; - box-sizing: border-box; - display: ${({ isLoading }) => (isLoading ? 'none' : 'block')}; -` - -export type TransactionParams = { - safeTxGas?: number -} - -type ConfirmTransactionModalState = { - isOpen: boolean - txs: Transaction[] - requestId: RequestId - params?: TransactionParams -} - -type Props = { - appUrl: string -} - -const NETWORK_NAME = getNetworkName() -const NETWORK_ID = getNetworkId() - -const INITIAL_CONFIRM_TX_MODAL_STATE: ConfirmTransactionModalState = { - isOpen: false, - txs: [], - requestId: '', - params: undefined, -} - -const AppFrame = ({ appUrl }: Props): ReactElement => { - const granted = useSelector(grantedSelector) - const { address: safeAddress, ethBalance, name: safeName } = useSelector(currentSafeWithNames) - const { trackEvent } = useAnalytics() - const history = useHistory() - const { consentReceived, onConsentReceipt } = useLegalConsent() - const { staticAppsList } = useAppList() - - const iframeRef = useRef(null) - const [confirmTransactionModal, setConfirmTransactionModal] = useState( - INITIAL_CONFIRM_TX_MODAL_STATE, - ) - const [appIsLoading, setAppIsLoading] = useState(true) - const [safeApp, setSafeApp] = useState() - - const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`) - const timer = useRef() - const [appTimeout, setAppTimeout] = useState(false) - - useEffect(() => { - if (appIsLoading) { - timer.current = window.setTimeout(() => { - setAppTimeout(true) - }, TIMEOUT) - } else { - clearTimeout(timer.current) - setAppTimeout(false) - } - - return () => { - clearTimeout(timer.current) - } - }, [appIsLoading]) - - const openConfirmationModal = useCallback( - (txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) => - setConfirmTransactionModal({ - isOpen: true, - txs, - requestId, - params, - }), - [setConfirmTransactionModal], - ) - const closeConfirmationModal = useCallback(() => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), [ - setConfirmTransactionModal, - ]) - - const { sendMessageToIframe } = useIframeMessageHandler( - safeApp, - openConfirmationModal, - closeConfirmationModal, - iframeRef, - ) - - const onIframeLoad = useCallback(() => { - const iframe = iframeRef.current - if (!iframe || !isSameURL(iframe.src, appUrl as string)) { - return - } - - setAppIsLoading(false) - sendMessageToIframe({ - messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, - data: { - safeAddress: safeAddress as string, - network: NETWORK_NAME.toLowerCase() as LowercaseNetworks, - ethBalance: ethBalance as string, - }, - }) - }, [ethBalance, safeAddress, appUrl, sendMessageToIframe]) - - const communicator = useAppCommunicator(iframeRef, safeApp) - - useEffect(() => { - communicator?.on('getEnvInfo', () => ({ - txServiceUrl: getTxServiceUrl(), - })) - - communicator?.on('getTxBySafeTxHash', async (msg) => { - const { safeTxHash } = msg.data.params as GetTxBySafeTxHashParams - - const tx = await fetchSafeTransaction(safeTxHash) - - return tx - }) - - communicator?.on('getSafeInfo', () => ({ - safeAddress, - network: NETWORK_NAME, - chainId: NETWORK_ID, - })) - - communicator?.on('getSafeBalances', async (msg) => { - const { currency = 'usd' } = msg.data.params as GetBalanceParams - - const balances = await fetchTokenCurrenciesBalances({ safeAddress, selectedCurrency: currency }) - - return balances - }) - - communicator?.on('rpcCall', async (msg) => { - const params = msg.data.params as RPCPayload - - try { - const response = new Promise((resolve, reject) => { - if ( - web3ReadOnly.currentProvider !== null && - typeof web3ReadOnly.currentProvider !== 'string' && - 'send' in web3ReadOnly.currentProvider - ) { - web3ReadOnly.currentProvider?.send?.( - { - jsonrpc: '2.0', - method: params.call, - params: params.params, - id: '1', - }, - (err, res) => { - if (err || res?.error) { - reject(err || res?.error) - } - - resolve(res?.result) - }, - ) - } - }) - - return response - } catch (err) { - return err - } - }) - - communicator?.on('sendTransactions', (msg) => { - // @ts-expect-error explore ways to fix this - openConfirmationModal(msg.data.params.txs as Transaction[], msg.data.params.params, msg.data.id) - }) - }, [communicator, openConfirmationModal, safeAddress]) - - const onUserTxConfirm = (safeTxHash: string) => { - // Safe Apps SDK V1 Handler - sendMessageToIframe( - { messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } }, - confirmTransactionModal.requestId, - ) - - // Safe Apps SDK V2 Handler - communicator?.send({ safeTxHash }, confirmTransactionModal.requestId as string) - } - - const onTxReject = () => { - // Safe Apps SDK V1 Handler - sendMessageToIframe( - { messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} }, - confirmTransactionModal.requestId, - ) - - // Safe Apps SDK V2 Handler - communicator?.send('Transaction was rejected', confirmTransactionModal.requestId as string, true) - } - - useEffect(() => { - const loadApp = async () => { - const app = await getAppInfoFromUrl(appUrl) - - setSafeApp(app) - } - if (staticAppsList.length) { - loadApp() - } - }, [appUrl, staticAppsList]) - - //track GA - useEffect(() => { - if (safeApp) { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: safeApp.name }) - } - }, [safeApp, trackEvent]) - - if (!appUrl) { - throw Error('App url No provided or it is invalid.') - } - - if (!safeApp) { - return ( - - - - ) - } - - if (consentReceived === false) { - return - } - - if (NETWORK_NAME === 'UNKNOWN' || !granted) { - return ( - - - To use apps, you must be an owner of this Safe - - ) - } - - return ( - - - {appIsLoading && ( - - {appTimeout && ( - - The safe app is taking longer than usual to load. There might be a problem with the app provider. - - )} - - - )} - - - - - - - ) -} - -export default AppFrame diff --git a/src/routes/safe/components/Apps/components/AppsList.tsx b/src/routes/safe/components/Apps/components/AppsList.tsx deleted file mode 100644 index 0ed603b7bb..0000000000 --- a/src/routes/safe/components/Apps/components/AppsList.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React, { useState } from 'react' -import styled, { css } from 'styled-components' -import { useSelector } from 'react-redux' -import { IconText, Loader, Menu, Text, Icon } from '@gnosis.pm/safe-react-components' -import IconButton from '@material-ui/core/IconButton' - -import { Modal } from 'src/components/Modal' -import { safeAddressFromUrl } from 'src/logic/safe/store/selectors' -import AppCard from 'src/routes/safe/components/Apps/components/AppCard' -import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg' -import { useRouteMatch, Link } from 'react-router-dom' -import { SAFELIST_ADDRESS } from 'src/routes/routes' - -import { useAppList } from '../hooks/useAppList' -import { SAFE_APP_FETCH_STATUS, SafeApp } from '../types' -import AddAppForm from './AddAppForm' -import { AppData } from '../api/fetchSafeAppsList' - -const Wrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; -` - -const StyledLink = styled(Link)` - text-decoration: none; -` - -const centerCSS = css` - display: flex; - align-items: center; - justify-content: center; -` - -const LoadingContainer = styled.div` - width: 100%; - height: 100%; - ${centerCSS}; -` - -const CardsWrapper = styled.div` - width: 100%; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(243px, 1fr)); - column-gap: 20px; - row-gap: 20px; - justify-content: space-evenly; - margin: 0 0 16px 0; -` - -const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; - flex-grow: 1; - align-items: center; -` -const Breadcrumb = styled.div` - height: 51px; -` - -const IconBtn = styled(IconButton)` - position: absolute; - top: 10px; - right: 10px; - z-index: 10; - padding: 5px; - opacity: 0; - - transition: opacity 0.2s ease-in-out; -` - -const AppContainer = styled.div` - position: relative; - - &:hover { - ${IconBtn} { - opacity: 1; - } - } -` - -const isAppLoading = (app: SafeApp) => SAFE_APP_FETCH_STATUS.LOADING === app.fetchStatus -const isCustomApp = (appUrl: string, staticAppsList: AppData[]) => !staticAppsList.some(({ url }) => url === appUrl) - -const AppsList = (): React.ReactElement => { - const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) - const safeAddress = useSelector(safeAddressFromUrl) - const { appList, removeApp, staticAppsList } = useAppList() - const [isAddAppModalOpen, setIsAddAppModalOpen] = useState(false) - const [appToRemove, setAppToRemove] = useState(null) - - const openAddAppModal = () => setIsAddAppModalOpen(true) - - const closeAddAppModal = () => setIsAddAppModalOpen(false) - - if (!appList.length || !safeAddress) { - return ( - - - - ) - } - - return ( - - - {/* TODO: Add navigation breadcrumb. Empty for now to give some top margin */} - - - - - - - - {appList - .filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR) - .map((a) => ( - - - - - {isCustomApp(a.url, staticAppsList) && ( - { - e.stopPropagation() - - setAppToRemove(a) - }} - > - - - )} - - ))} - - - - - - {isAddAppModalOpen && ( - - - Add custom app - - - - )} - - {appToRemove && ( - setAppToRemove(null)}> - setAppToRemove(null)}> - Remove app - - - - This action will remove{' '} - - {appToRemove.name} - {' '} - from the interface - - - - setAppToRemove(null) }} - confirmButtonProps={{ - color: 'error', - onClick: () => { - removeApp(appToRemove.url) - setAppToRemove(null) - }, - text: 'Remove', - }} - /> - - - )} - - ) -} - -export default AppsList diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx deleted file mode 100644 index 8bf9bb6a73..0000000000 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { ReactElement } from 'react' -import styled from 'styled-components' - -import { getNetworkInfo } from 'src/config' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { md, lg } from 'src/theme/variables' -import ModalTitle from 'src/components/ModalTitle' -import Hairline from 'src/components/layout/Hairline' -import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode.d' -import { BasicTxInfo, getParameterElement } from 'src/components/DecodeTxs' - -const { nativeCoin } = getNetworkInfo() - -const Container = styled.div` - max-width: 480px; - padding: ${md} ${lg}; - word-break: break-word; -` - -function isDataDecodedParameterValue(arg: any): arg is DecodedDataParameterValue { - return arg.operation !== undefined -} - -type Props = { - hideDecodedTxData: () => void - onClose: () => void - decodedTxData: DecodedDataParameterValue | DecodedData -} - -export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx }: Props): ReactElement => { - let body - // If we are dealing with a multiSend - // decodedTxData is of type DataDecodedParameter - if (isDataDecodedParameterValue(tx)) { - const txValue = fromTokenUnit(tx.value, nativeCoin.decimals) - - body = ( - <> - - {tx.dataDecoded?.parameters.map((p, index) => getParameterElement(p, index))} - - ) - } else { - // If we are dealing with a single tx - // decodedTxData is of type DecodedData - body = <>{tx.parameters.map((p, index) => getParameterElement(p, index))} - } - - return ( - <> - - - - - {body} - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx deleted file mode 100644 index 5b67522fa1..0000000000 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import React, { ReactElement, useEffect, useMemo, useState } from 'react' -import { EthHashInfo, Text } from '@gnosis.pm/safe-react-components' -import styled from 'styled-components' -import { useDispatch } from 'react-redux' - -import ModalTitle from 'src/components/ModalTitle' -import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' -import { CALL, DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' -import { getExplorerInfo, getNetworkInfo } from 'src/config' -import { web3ReadOnly } from 'src/logic/wallets/getWeb3' -import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -import { TransactionFees } from 'src/components/TransactionsFees' -import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' -import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' -import { lg, md, sm } from 'src/theme/variables' -import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus' -import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' -import { BasicTxInfo, DecodeTxs } from 'src/components/DecodeTxs' -import { fetchTxDecoder } from 'src/utils/decodeTx' -import { DecodedData } from 'src/types/transactions/decode.d' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import Block from 'src/components/layout/Block' -import Divider from 'src/components/Divider' - -import { ConfirmTxModalProps, DecodedTxDetail } from '.' -import Hairline from 'src/components/layout/Hairline' -import { ButtonStatus, Modal } from 'src/components/Modal' - -const { nativeCoin } = getNetworkInfo() - -const Container = styled.div` - max-width: 480px; - padding: ${md} ${lg} 0; -` -const TransactionFeesWrapper = styled.div` - background-color: ${({ theme }) => theme.colors.background}; - padding: ${sm} ${lg}; -` - -const DecodeTxsWrapper = styled.div` - margin: 24px -24px; -` - -const StyledBlock = styled(Block)` - background-color: ${({ theme }) => theme.colors.separator}; - width: fit-content; - padding: 5px 10px; - border-radius: 3px; - margin: 4px 0 0 40px; - - display: flex; - - > :nth-child(1) { - margin-right: 5px; - } -` - -type Props = ConfirmTxModalProps & { - areTxsMalformed: boolean - showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void - hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected -} - -const parseTxValue = (value: string | number): string => { - return web3ReadOnly.utils.toBN(value).toString() -} - -export const ReviewConfirm = ({ - app, - txs, - safeAddress, - ethBalance, - safeName, - params, - hidden, - onUserConfirm, - onClose, - onTxReject, - areTxsMalformed, - showDecodedTxData, -}: Props): ReactElement => { - const isMultiSend = txs.length > 1 - const [decodedData, setDecodedData] = useState(null) - const dispatch = useDispatch() - const explorerUrl = getExplorerInfo(safeAddress) - - const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [ - txs, - isMultiSend, - ]) - const txData: string | undefined = useMemo(() => (isMultiSend ? encodeMultiSendCall(txs) : txs[0]?.data), [ - txs, - isMultiSend, - ]) - const txValue: string | undefined = useMemo( - () => (isMultiSend ? '0' : txs[0]?.value && parseTxValue(txs[0]?.value)), - [txs, isMultiSend], - ) - const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) - const [manualSafeTxGas, setManualSafeTxGas] = useState(0) - const [manualGasPrice, setManualGasPrice] = useState() - const [manualGasLimit, setManualGasLimit] = useState() - - const { - gasLimit, - gasPriceFormatted, - gasEstimation, - isOffChainSignature, - isCreation, - isExecution, - gasCostFormatted, - txEstimationExecutionStatus, - } = useEstimateTransactionGas({ - txData: txData || '', - txRecipient, - operation, - txAmount: txValue, - safeTxGas: manualSafeTxGas, - manualGasPrice, - manualGasLimit, - }) - - const [buttonStatus, setButtonStatus] = useEstimationStatus(txEstimationExecutionStatus) - - // Decode tx data. - useEffect(() => { - const decodeTxData = async () => { - const res = await fetchTxDecoder(txData) - setDecodedData(res) - } - - decodeTxData() - }, [txData]) - - const handleTxRejection = () => { - onTxReject() - onClose() - } - - const handleUserConfirmation = (safeTxHash: string): void => { - onUserConfirm(safeTxHash) - onClose() - } - - const confirmTransactions = async (txParameters: TxParameters) => { - setButtonStatus(ButtonStatus.LOADING) - - await dispatch( - createTransaction( - { - safeAddress, - to: txRecipient, - valueInWei: txValue, - txData, - operation, - origin: app.id, - navigateToTransactionsTab: false, - txNonce: txParameters.safeNonce, - safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined, - ethParameters: txParameters, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - }, - handleUserConfirmation, - handleTxRejection, - ), - ) - - setButtonStatus(ButtonStatus.READY) - } - - const closeEditModalCallback = (txParameters: TxParameters) => { - const oldGasPrice = Number(gasPriceFormatted) - const newGasPrice = Number(txParameters.ethGasPrice) - const oldSafeTxGas = Number(gasEstimation) - const newSafeTxGas = Number(txParameters.safeTxGas) - - if (newGasPrice && oldGasPrice !== newGasPrice) { - setManualGasPrice(txParameters.ethGasPrice) - } - - if (txParameters.ethGasLimit && gasLimit !== txParameters.ethGasLimit) { - setManualGasLimit(txParameters.ethGasLimit) - } - - if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { - setManualSafeTxGas(newSafeTxGas) - } - } - - return ( - - {(txParameters, toggleEditMode) => ( - - )} - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx deleted file mode 100644 index feddb41212..0000000000 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { ReactElement } from 'react' -import { Icon, Text, Title, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' -import styled from 'styled-components' -import { ConfirmTxModalProps } from '.' - -const IconText = styled.div` - display: flex; - align-items: center; - - span { - margin-right: 4px; - } -` - -const FooterWrapper = styled.div` - margin-top: 15px; -` - -export const SafeAppLoadError = ({ onTxReject, onClose }: ConfirmTxModalProps): ReactElement => { - const handleTxRejection = () => { - onTxReject() - onClose() - } - - return ( - <> - - - Transaction error - - - This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this - Safe App for more information. - - - - handleTxRejection()} - handleOk={() => {}} - okDisabled={true} - okText="Submit" - /> - - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx deleted file mode 100644 index 13fa73544c..0000000000 --- a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { ReactElement, useState } from 'react' -import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' - -import Modal from 'src/components/Modal' -import { SafeApp } from 'src/routes/safe/components/Apps/types' -import { TransactionParams } from 'src/routes/safe/components/Apps/components/AppFrame' -import { mustBeEthereumAddress } from 'src/components/forms/validator' -import { SafeAppLoadError } from './SafeAppLoadError' -import { ReviewConfirm } from './ReviewConfirm' -import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode' -import { DecodedTxDetail } from './DecodedTxDetail' - -export type ConfirmTxModalProps = { - isOpen: boolean - app: SafeApp - txs: Transaction[] - params?: TransactionParams - safeAddress: string - safeName: string - ethBalance: string - onUserConfirm: (safeTxHash: string) => void - onTxReject: () => void - onClose: () => void -} - -const isTxValid = (t: Transaction): boolean => { - if (!['string', 'number'].includes(typeof t.value)) { - return false - } - - if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) { - return false - } - - const isAddressValid = mustBeEthereumAddress(t.to) === undefined - return isAddressValid && !!t.data && typeof t.data === 'string' -} - -export type DecodedTxDetail = DecodedDataParameterValue | DecodedData | undefined - -export const ConfirmTxModal = (props: ConfirmTxModalProps): ReactElement | null => { - const [decodedTxDetails, setDecodedTxDetails] = useState() - const areTxsMalformed = props.txs.some((t) => !isTxValid(t)) - - const showDecodedTxData = setDecodedTxDetails - const hideDecodedTxData = () => setDecodedTxDetails(undefined) - - const closeDecodedTxDetail = () => { - hideDecodedTxData() - props.onClose() - } - - return ( - - {areTxsMalformed && } - {decodedTxDetails && ( - - )} - - - ) -} diff --git a/src/routes/safe/components/Apps/components/LegalDisclaimer.tsx b/src/routes/safe/components/Apps/components/LegalDisclaimer.tsx deleted file mode 100644 index bc711b9dd8..0000000000 --- a/src/routes/safe/components/Apps/components/LegalDisclaimer.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { FixedDialog, Text } from '@gnosis.pm/safe-react-components' - -interface OwnProps { - onCancel: () => void - onConfirm: () => void -} - -const LegalDisclaimer = ({ onCancel, onConfirm }: OwnProps): JSX.Element => ( - - - You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not liable for - any loss you may suffer in connection with interacting with the apps, which is at your own risk. You must read - our Terms, which contain more detailed provisions binding on you relating to the apps. - -
- - I have read and understood the{' '} - - Terms - {' '} - and this Disclaimer, and agree to be bound by them. - - - } - onCancel={onCancel} - onConfirm={onConfirm} - title="Disclaimer" - /> -) - -export default LegalDisclaimer diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts deleted file mode 100644 index 68c7f19cda..0000000000 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } from '../utils' -import { AppData } from '../api/fetchSafeAppsList' -import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types' -import { getNetworkId } from 'src/config' - -type UseAppListReturnType = { - appList: SafeApp[] - removeApp: (appUrl: string) => void - staticAppsList: AppData[] -} - -const useAppList = (): UseAppListReturnType => { - const [appList, setAppList] = useState([]) - const [staticAppsList, setStaticAppsList] = useState([]) - - useEffect(() => { - const loadAppsList = async () => { - const remoteAppsList = await getAppsList() - setStaticAppsList(remoteAppsList) - } - - if (!staticAppsList.length) { - loadAppsList() - } - }, [staticAppsList]) - - // Load apps list - // for each URL we return a mocked safe-app with a loading status - // it was developed to speed up initial page load, otherwise the - // app renders a loading until all the safe-apps are fetched. - useEffect(() => { - const fetchAppCallback = (res: SafeApp) => { - setAppList((prevStatus) => { - const cpPrevStatus = [...prevStatus] - const appIndex = cpPrevStatus.findIndex((a) => a.url === res.url) - const newStatus = res.error ? SAFE_APP_FETCH_STATUS.ERROR : SAFE_APP_FETCH_STATUS.SUCCESS - cpPrevStatus[appIndex] = { ...res, fetchStatus: newStatus } - return cpPrevStatus.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) - }) - } - - const loadApps = async () => { - // recover apps from storage (third-party apps added by the user) - const persistedAppList = - (await loadFromStorage<(StoredSafeApp & { networks?: number[] })[]>(APPS_STORAGE_KEY)) || [] - - // backward compatibility. In a previous implementation a safe app could be disabled, that state was - // persisted in the storage. - const customApps = persistedAppList.filter( - (persistedApp) => !staticAppsList.some((staticApp) => staticApp.url === persistedApp.url), - ) - - const apps: SafeApp[] = [...staticAppsList, ...customApps] - // if the app does not expose supported networks, include them. (backward compatible) - .filter((app) => (!app.networks ? true : app.networks.includes(getNetworkId()))) - .map((app) => ({ - ...getEmptySafeApp(), - ...app, - url: app.url.trim(), - })) - - setAppList(apps) - - apps.forEach((app) => { - if (!app.name || app.name === 'unknown') { - // We are using legacy mode, we have to fetch info from manifest - getAppInfoFromUrl(app.url).then(fetchAppCallback) - } else { - // We already have manifest information so we directly add the app - fetchAppCallback(app) - } - }) - } - - loadApps() - }, [staticAppsList]) - - const removeApp = useCallback((appUrl: string): void => { - setAppList((list) => { - const newList = list.filter(({ url }) => url !== appUrl) - const persistedAppList = newList.map(({ url, disabled }) => ({ url, disabled })) - saveToStorage(APPS_STORAGE_KEY, persistedAppList) - - return newList - }) - }, []) - - return { - appList, - staticAppsList, - removeApp, - } -} - -export { useAppList } diff --git a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts deleted file mode 100644 index e40c97fec5..0000000000 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { useSnackbar } from 'notistack' -import { - InterfaceMessageIds, - InterfaceMessageToPayload, - SDKMessageIds, - SDKMessageToPayload, - SDK_MESSAGES, - INTERFACE_MESSAGES, - RequestId, - Transaction, - LowercaseNetworks, -} from '@gnosis.pm/safe-apps-sdk-v1' -import { useDispatch, useSelector } from 'react-redux' -import { useEffect, useCallback, MutableRefObject } from 'react' - -import { getNetworkName, getTxServiceUrl } from 'src/config/' -import { currentSafeWithNames } from 'src/logic/safe/store/selectors' -import { TransactionParams } from '../components/AppFrame' -import { SafeApp } from 'src/routes/safe/components/Apps/types' - -type InterfaceMessageProps = { - messageId: T - data: InterfaceMessageToPayload[T] -} - -type ReturnType = { - sendMessageToIframe: (message: InterfaceMessageProps, requestId?: RequestId) => void -} - -const NETWORK_NAME = getNetworkName() - -const useIframeMessageHandler = ( - selectedApp: SafeApp | undefined, - openConfirmationModal: (txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) => void, - closeModal: () => void, - iframeRef: MutableRefObject, -): ReturnType => { - const { enqueueSnackbar, closeSnackbar } = useSnackbar() - const { address: safeAddress, ethBalance, name: safeName } = useSelector(currentSafeWithNames) - const dispatch = useDispatch() - - const sendMessageToIframe = useCallback( - function (message: InterfaceMessageProps, requestId?: RequestId) { - const requestWithMessage = { - ...message, - requestId: requestId || Math.trunc(window.performance.now()), - version: '0.4.2', - } - - if (iframeRef && selectedApp) { - iframeRef.current?.contentWindow?.postMessage(requestWithMessage, selectedApp.url) - } - }, - [iframeRef, selectedApp], - ) - - useEffect(() => { - const handleIframeMessage = ( - messageId: SDKMessageIds, - messagePayload: SDKMessageToPayload[typeof messageId], - requestId: RequestId, - ): void => { - if (!messageId) { - return - } - - switch (messageId) { - // typescript doesn't narrow type in switch/case statements - // issue: https://github.com/microsoft/TypeScript/issues/20375 - // possible solution: https://stackoverflow.com/a/43879897/7820085 - case SDK_MESSAGES.SEND_TRANSACTIONS: { - if (messagePayload) { - openConfirmationModal( - messagePayload as SDKMessageToPayload[typeof SDK_MESSAGES.SEND_TRANSACTIONS], - undefined, - requestId, - ) - } - break - } - - case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: { - const safeInfoMessage = { - messageId: INTERFACE_MESSAGES.ON_SAFE_INFO, - data: { - safeAddress: safeAddress as string, - network: NETWORK_NAME.toLowerCase() as LowercaseNetworks, - ethBalance: ethBalance as string, - }, - } - const envInfoMessage = { - messageId: INTERFACE_MESSAGES.ENV_INFO, - data: { - txServiceUrl: getTxServiceUrl(), - }, - } - - sendMessageToIframe(safeInfoMessage) - sendMessageToIframe(envInfoMessage) - break - } - default: { - console.error(`ThirdPartyApp: A message was received with an unknown message id ${messageId}.`) - break - } - } - } - const onIframeMessage = async ( - message: MessageEvent<{ - requestId: RequestId - messageId: SDKMessageIds - data: SDKMessageToPayload[SDKMessageIds] - }>, - ) => { - if (message.origin === window.origin) { - return - } - if (!selectedApp?.url.includes(message.origin)) { - console.error(`ThirdPartyApp: A message was received from an unknown origin ${message.origin}`) - return - } - handleIframeMessage(message.data.messageId, message.data.data, message.data.requestId) - } - - window.addEventListener('message', onIframeMessage) - return () => { - window.removeEventListener('message', onIframeMessage) - } - }, [ - closeModal, - closeSnackbar, - dispatch, - enqueueSnackbar, - ethBalance, - openConfirmationModal, - safeAddress, - safeName, - selectedApp, - sendMessageToIframe, - ]) - - return { - sendMessageToIframe, - } -} - -export { useIframeMessageHandler } diff --git a/src/routes/safe/components/Apps/hooks/useLegalConsent.ts b/src/routes/safe/components/Apps/hooks/useLegalConsent.ts deleted file mode 100644 index c9bbf86ada..0000000000 --- a/src/routes/safe/components/Apps/hooks/useLegalConsent.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import { loadFromStorage, saveToStorage } from 'src/utils/storage' - -const APPS_LEGAL_CONSENT_RECEIVED = 'APPS_LEGAL_CONSENT_RECEIVED' - -const useLegalConsent = (): { consentReceived: boolean | undefined; onConsentReceipt: () => void } => { - const [consentReceived, setConsentReceived] = useState() - - useEffect(() => { - const checkLegalDisclaimer = async () => { - const storedConsentReceived = await loadFromStorage(APPS_LEGAL_CONSENT_RECEIVED) - - if (storedConsentReceived) { - setConsentReceived(true) - } else { - setConsentReceived(false) - } - } - - checkLegalDisclaimer() - }, []) - - const onConsentReceipt = useCallback((): void => { - setConsentReceived(true) - saveToStorage(APPS_LEGAL_CONSENT_RECEIVED, true) - }, []) - - return { consentReceived, onConsentReceipt } -} - -export { useLegalConsent } diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx deleted file mode 100644 index f9301b7830..0000000000 --- a/src/routes/safe/components/Apps/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl' - -import AppFrame from './components/AppFrame' -import AppsList from './components/AppsList' - -const Apps = (): React.ReactElement => { - const { getAppUrl } = useSafeAppUrl() - const url = getAppUrl() - if (url) { - return - } else { - return - } -} - -export default Apps diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index feb0d6daf9..2a198c05fa 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -5,9 +5,8 @@ import { SafeApp, SAFE_APP_FETCH_STATUS } from './types' import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { HARMONY_NETWORK } from 'src/config/networks/network.d' import { logError, Errors } from 'src/logic/exceptions/CodedException' -import { AppData, fetchSafeAppsList } from './api/fetchSafeAppsList' export const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' @@ -23,170 +22,7 @@ export type StaticAppInfo = { disabled: boolean networks: number[] } -export const staticAppsList: Array = [ - // 1inch - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUXF1yVGdqUfMbhNyfM3jpP6Bw66cYnKPoWq6iHkhd3Aw`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Aave - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQ3w2ezp2zx3u2LYQHyuNzMrLDJFjyL1rjAFTjNMcQ4cK`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Aave v2 - { - url: `https://app.aave.com/`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - //Balancer Exchange - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Balancer Pool - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVaxypk2FTyfcTS9oZKxmpQziPUTu2VRhhW7sso1mGysf`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // CMM - // Point to a static server to allow app update without Safe deployment - { - url: `https://safe-cmm.gnosis.io`, - disabled: false, - networks: [ETHEREUM_NETWORK.RINKEBY, ETHEREUM_NETWORK.XDAI], - }, - // Compound - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX31xCdhFDmJzoVG33Y6kJtJ5Ujw8r5EJJBrsp8Fbjm7k`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // dHedge - { - url: `https://app.dhedge.org/`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // ENS - { - url: `https://app.ens.domains/`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Gnosis Starter - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmdCwUutYH8GXXNgTShB4cKJ8YJq4PqZ55QxMznKc9DbeS`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Idle - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTvrLwJtyjG8QFHgvqdPhcV5DBMQ7oZceSU4uvPw9vQaj`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Lido finance - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/Qmde8dsa9r8bB59CNGww6LRiaZABuKaJfuzvu94hFkatJC`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Liquity - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmYzTAH6Nzexu35tbWmhVrLYwWj9MdbD1iECejgaGHFk8P`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Mushrooms finance - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT96aES2YA9BssByc6DVizQDkofmKRErs8gJyqWipjyS8`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Pooltogether - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTa21pi77hiT1sLCGy5BeVwcyzExUSp2z7byxZukye8hr`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Request - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTBBaiDQyGa17DJ7DdviyHbc51fTVgf6Z5PW5w2YUTkgR`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, - // Sablier - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/Qmb1Xpfu9mnX4A3trpoVeBZ9sTiNtEuRoFKEiaVXWntDxB`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // Synthetix - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmXLxxczMH4MBEYDeeN9zoiHDzVkeBmB5rBjA3UniPEFcA`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], - }, - // OpenZeppelin - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQovvfYYMUXjZfNbysQDUEXR8nr55iJRwcYgJQGJR7KEA`, - disabled: false, - networks: [ - ETHEREUM_NETWORK.MAINNET, - ETHEREUM_NETWORK.RINKEBY, - //ETHEREUM_NETWORK.ENERGY_WEB_CHAIN, - //ETHEREUM_NETWORK.VOLTA, - // ETHEREUM_NETWORK.XDAI, - ], - }, - // TX-Builder - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZBgEvjqi9Jg8xATr9uUgNUVmnfYiECNxZv9Taux7mzgV`, - disabled: false, - networks: [ - ETHEREUM_NETWORK.MAINNET, - ETHEREUM_NETWORK.RINKEBY, - ETHEREUM_NETWORK.ENERGY_WEB_CHAIN, - ETHEREUM_NETWORK.VOLTA, - ETHEREUM_NETWORK.XDAI, - ], - }, - // Wallet-Connect - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX9B982ZAaBzbm6yBoZUS3uLgcizYA6wW65RCXVRZkG6f`, - disabled: false, - networks: [ - ETHEREUM_NETWORK.MAINNET, - ETHEREUM_NETWORK.RINKEBY, - ETHEREUM_NETWORK.ENERGY_WEB_CHAIN, - ETHEREUM_NETWORK.VOLTA, - ETHEREUM_NETWORK.XDAI, - ], - }, - // Yearn Vaults - { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/Qme9HuPPhgCtgfj1CktvaDKhTesMueGCV2Kui1Sqna3Xs9`, - disabled: false, - networks: [ETHEREUM_NETWORK.MAINNET], - }, -] - -export const getAppsList = async (): Promise => { - let result - try { - result = await fetchSafeAppsList() - } catch (error) { - console.error('Could not fetch remote apps list', error) - } - - return result?.apps && result?.apps.length ? result.apps : staticAppsList -} +export const staticAppsList: Array = [] export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => { try { @@ -219,97 +55,95 @@ export const getEmptySafeApp = (): SafeApp => { } } -export const getAppInfoFromUrl = memoize( - async (appUrl: string): Promise => { - let res = { - ...getEmptySafeApp(), - error: true, - loadingStatus: SAFE_APP_FETCH_STATUS.ERROR, - } - - if (!appUrl?.length) { - return res - } +export const getAppInfoFromUrl = memoize(async (appUrl: string): Promise => { + let res = { + ...getEmptySafeApp(), + error: true, + loadingStatus: SAFE_APP_FETCH_STATUS.ERROR, + } - res.url = appUrl.trim() - const noTrailingSlashUrl = removeLastTrailingSlash(res.url) + if (!appUrl?.length) { + return res + } - try { - const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`, { timeout: 5_000 }) + res.url = appUrl.trim() + const noTrailingSlashUrl = removeLastTrailingSlash(res.url) - // verify imported app fulfil safe requirements - if (!appInfo?.data || !isAppManifestValid(appInfo.data)) { - throw Error('The app does not fulfil the structure required.') - } + try { + const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`, { timeout: 5_000 }) - // the DB origin field has a limit of 100 characters - const originFieldSize = 100 - const jsonDataLength = 20 - const remainingSpace = originFieldSize - res.url.length - jsonDataLength + // verify imported app fulfil safe requirements + if (!appInfo?.data || !isAppManifestValid(appInfo.data)) { + throw Error('The app does not fulfil the structure required.') + } - const appInfoData = { - name: appInfo.data.name, - iconPath: appInfo.data.iconPath, - description: appInfo.data.description, - providedBy: appInfo.data.providedBy, - } + // the DB origin field has a limit of 100 characters + const originFieldSize = 100 + const jsonDataLength = 20 + const remainingSpace = originFieldSize - res.url.length - jsonDataLength - res = { - ...res, - ...appInfoData, - id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }), - error: false, - loadingStatus: SAFE_APP_FETCH_STATUS.SUCCESS, - } + const appInfoData = { + name: appInfo.data.name, + iconPath: appInfo.data.iconPath, + description: appInfo.data.description, + providedBy: appInfo.data.providedBy, + } - if (appInfo.data.iconPath) { - try { - const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 }) - if (/image\/\w/gm.test(iconInfo.headers['content-type'])) { - res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}` - } - } catch (error) { - console.error(`It was not possible to fetch icon from app ${res.url}`) - } - } - return res - } catch (error) { - logError(Errors._900, error.message, { - contexts: { - safeApp: { - url: appUrl, - }, - }, - }) - return res + res = { + ...res, + ...appInfoData, + id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }), + error: false, + loadingStatus: SAFE_APP_FETCH_STATUS.SUCCESS, } - }, -) -export const getIpfsLinkFromEns = memoize( - async (name: string): Promise => { - try { - const content = await getContentFromENS(name) - if (content && content.protocolType === 'ipfs') { - return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/` + if (appInfo.data.iconPath) { + try { + const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 }) + if (/image\/\w/gm.test(iconInfo.headers['content-type'])) { + res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}` + } + } catch (error) { + console.error(`It was not possible to fetch icon from app ${res.url}`) } - } catch (error) { - console.error(error) - return } - }, -) + return res + } catch (error) { + logError(Errors._900, error.message, { + contexts: { + safeApp: { + url: appUrl, + }, + }, + }) + return res + } +}) -export const uniqueApp = (appList: SafeApp[]) => (url: string): string | undefined => { - const newUrl = new URL(url) - const exists = appList.some((a) => { - try { - const currentUrl = new URL(a.url) - return currentUrl.href === newUrl.href - } catch (error) { - console.error('There was a problem trying to validate the URL existence.', error.message) - return false +export const getIpfsLinkFromEns = memoize(async (name: string): Promise => { + try { + const content = await getContentFromENS(name) + if (content && content.protocolType === 'ipfs') { + return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/` } - }) - return exists ? 'This app is already registered.' : undefined -} + } catch (error) { + console.error(error) + return + } +}) + +export const uniqueApp = + (appList: SafeApp[]) => + (url: string): string | undefined => { + const newUrl = new URL(url) + const exists = appList.some((a) => { + try { + const currentUrl = new URL(a.url) + return currentUrl.href === newUrl.href + } catch (error) { + console.error('There was a problem trying to validate the URL existence.', error.message) + return false + } + }) + return exists ? 'This app is already registered.' : undefined + } diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index 2ca8f901c5..9d99fc0e7c 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -26,7 +26,6 @@ import { BalanceData, } from 'src/routes/safe/components/Balances/dataFetcher' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { makeStyles } from '@material-ui/core/styles' import { styles } from './styles' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' @@ -79,16 +78,11 @@ const Coins = (props: Props): React.ReactElement => { const selectedCurrency = useSelector(currentCurrencySelector) const safeTokens = useSelector(extendedSafeTokensSelector) const granted = useSelector(grantedSelector) - const { trackEvent } = useAnalytics() - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' }) - }, [trackEvent]) - - const filteredData: List = useMemo(() => getBalanceData(safeTokens, selectedCurrency), [ - safeTokens, - selectedCurrency, - ]) + const filteredData: List = useMemo( + () => getBalanceData(safeTokens, selectedCurrency), + [safeTokens, selectedCurrency], + ) return ( diff --git a/src/routes/safe/components/Balances/Collectibles/index.tsx b/src/routes/safe/components/Balances/Collectibles/index.tsx index 1d25c0e117..6f95d6e966 100644 --- a/src/routes/safe/components/Balances/Collectibles/index.tsx +++ b/src/routes/safe/components/Balances/Collectibles/index.tsx @@ -9,7 +9,6 @@ import Paragraph from 'src/components/layout/Paragraph' import { nftAssetsFromNftTokensSelector, orderedNFTAssets } from 'src/logic/collectibles/store/selectors' import SendModal from 'src/routes/safe/components/Balances/SendModal' import { fontColor, lg, screenSm, screenXs } from 'src/theme/variables' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d' const useStyles = makeStyles( @@ -78,7 +77,6 @@ const useStyles = makeStyles( ) const Collectibles = (): React.ReactElement => { - const { trackEvent } = useAnalytics() const classes = useStyles() const [selectedToken, setSelectedToken] = React.useState() const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) @@ -86,10 +84,6 @@ const Collectibles = (): React.ReactElement => { const nftTokens = useSelector(orderedNFTAssets) const nftAssetsFromNftTokens = useSelector(nftAssetsFromNftTokensSelector) - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' }) - }, [trackEvent]) - const handleItemSend = (nftToken: NFTToken) => { setSelectedToken(nftToken) setSendNFTsModalOpen(true) diff --git a/src/routes/safe/components/Settings/Advanced/index.tsx b/src/routes/safe/components/Settings/Advanced/index.tsx index 41284e6b84..bfa38c730b 100644 --- a/src/routes/safe/components/Settings/Advanced/index.tsx +++ b/src/routes/safe/components/Settings/Advanced/index.tsx @@ -9,7 +9,6 @@ import { ModulesTable } from './ModulesTable' import Block from 'src/components/layout/Block' import { currentSafe } from 'src/logic/safe/store/selectors' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const InfoText = styled(Text)` margin-top: 16px; @@ -29,11 +28,6 @@ export const Advanced = (): React.ReactElement => { const classes = useStyles() const { nonce, modules } = useSelector(currentSafe) ?? {} const moduleData = modules ? getModuleData(modules) ?? null : null - const { trackEvent } = useAnalytics() - - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Advanced' }) - }, [trackEvent]) return ( <> diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index b75d90886c..0c1d617a9e 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -23,7 +23,6 @@ import Hairline from 'src/components/layout/Hairline' import Heading from 'src/components/layout/Heading' import Paragraph from 'src/components/layout/Paragraph/index' import Row from 'src/components/layout/Row' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' @@ -38,7 +37,6 @@ type Props = { } const ManageOwners = ({ granted, owners }: Props): ReactElement => { - const { trackEvent } = useAnalytics() const classes = useStyles() const [selectedOwner, setSelectedOwner] = useState() @@ -67,10 +65,6 @@ const ManageOwners = ({ granted, owners }: Props): ReactElement => { setSelectedOwner(undefined) } - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) - }, [trackEvent]) - const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const ownerData = getOwnerData(owners) diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index c5e4b25aa2..37bb8cdeea 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -31,7 +31,6 @@ import { currentSafeWithNames, latestMasterContractVersion as latestMasterContractVersionSelector, } from 'src/logic/safe/store/selectors' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { fetchMasterCopies, MasterCopy, MasterCopyDeployer } from 'src/logic/contracts/api/masterCopies' import { getMasterCopyAddressFromProxyAddress } from 'src/logic/contracts/safeContracts' @@ -61,7 +60,6 @@ const SafeDetails = (): ReactElement => { currentVersion: safeCurrentVersion, } = useSelector(currentSafeWithNames) const dispatch = useDispatch() - const { trackEvent } = useAnalytics() const [isModalOpen, setModalOpen] = useState(false) const [safeInfo, setSafeInfo] = useState() @@ -101,10 +99,6 @@ const SafeDetails = (): ReactElement => { : '' } - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Details' }) - }, [trackEvent]) - useEffect(() => { const getMasterCopyInfo = async () => { const masterCopies = await fetchMasterCopies() diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index f487e4c9db..3c9a68fd9e 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -11,7 +11,6 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { grantedSelector } from 'src/routes/safe/container/selector' import { currentSafe } from 'src/logic/safe/store/selectors' -import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { ChangeThresholdModal } from './ChangeThreshold' import { styles } from './style' @@ -28,12 +27,6 @@ const ThresholdSettings = (): React.ReactElement => { setModalOpen((prevOpen) => !prevOpen) } - const { trackEvent } = useAnalytics() - - useEffect(() => { - trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) - }, [trackEvent]) - return ( <> diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index 4368281bf3..096bf68b02 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -17,7 +17,6 @@ export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn' export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' export const TRANSACTIONS_TAB_NEW_BTN_TEST_ID = 'transactions-tab-new-btn' -const Apps = React.lazy(() => import('src/routes/safe/components/Apps')) const Settings = React.lazy(() => import('src/routes/safe/components/Settings')) const Balances = React.lazy(() => import('src/routes/safe/components/Balances')) const TxList = React.lazy(() => import('src/routes/safe/components/Transactions/TxList')) @@ -70,16 +69,6 @@ const Container = (): React.ReactElement => { path={`${matchSafeWithAddress?.path}/transactions`} render={() => wrapInSuspense(, null)} /> - { - if (!featuresEnabled.includes(FEATURES.SAFE_APPS)) { - history.push(`${matchSafeWithAddress?.url}/balances`) - } - return wrapInSuspense(, null) - }} - /> [NOTIFICATIONS_REDUCER_ID]: Map [CURRENCY_VALUES_KEY]: CurrencyValuesState - [COOKIES_REDUCER_ID]: Map [ADDRESS_BOOK_REDUCER_ID]: AddressBookState [CURRENT_SESSION_REDUCER_ID]: CurrentSessionState router: RouterState diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cebef6481f..c2878c6a9f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,35 +1,16 @@ export const APP_ENV = process.env.REACT_APP_ENV export const NODE_ENV = process.env.NODE_ENV export const IS_PRODUCTION = process.env.NODE_ENV === 'production' -export const NETWORK = process.env.REACT_APP_NETWORK?.toUpperCase() || 'RINKEBY' -export const INTERCOM_ID = APP_ENV === 'production' ? process.env.REACT_APP_INTERCOM_ID : 'plssl1fl' -export const GOOGLE_ANALYTICS_ID = process.env.REACT_APP_GOOGLE_ANALYTICS || '' -export const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN || '' -export const PORTIS_ID = process.env.REACT_APP_PORTIS_ID ?? '852b763d-f28b-4463-80cb-846d7ec5806b' -export const FORTMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY ?? 'pk_test_CAD437AA29BE0A40' -/* - * Not being used -export const SQUARELINK_ID = { - [ETHEREUM_NETWORK.RINKEBY]: '46ce08fe50913cfa1b78', - [ETHEREUM_NETWORK.MAINNET]: process.env.REACT_APP_SQUARELINK_ID, - [ETHEREUM_NETWORK.XDAI]: process.env.REACT_APP_SQUARELINK_ID, -} - */ -export const INFURA_TOKEN = process.env.REACT_APP_INFURA_TOKEN || '' +export const NETWORK = process.env.REACT_APP_NETWORK?.toUpperCase() || 'MAINNET' + export const LATEST_SAFE_VERSION = process.env.REACT_APP_LATEST_SAFE_VERSION || '1.1.1' -export const LOADED_SAFE_KEY = 'Gnosis Safe' + export const APP_VERSION = process.env.REACT_APP_APP_VERSION || 'not-defined' -export const OPENSEA_API_KEY = process.env.REACT_APP_OPENSEA_API_KEY || '' export const COLLECTIBLES_SOURCE = process.env.REACT_APP_COLLECTIBLES_SOURCE || 'Gnosis' export const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 -export const ETHERSCAN_API_KEY = process.env.REACT_APP_ETHERSCAN_API_KEY -export const ETHGASSTATION_API_KEY = process.env.REACT_APP_ETHGASSTATION_API_KEY -export const SAFE_APPS_LIST_URL = - process.env.REACT_APP_SAFE_APPS_LIST_URL || - 'https://raw.githubusercontent.com/gnosis/safe-apps-list/main/public/gnosis-default.applist.json' -export const IPFS_GATEWAY = process.env.REACT_APP_IPFS_GATEWAY + export const SPENDING_LIMIT_MODULE_ADDRESS = - process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134' + process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0x965179FbFcf7410634C7A0a258545E136A890cD7' export const KNOWN_MODULES = { [SPENDING_LIMIT_MODULE_ADDRESS]: 'Spending limit', } diff --git a/src/utils/googleAnalytics.ts b/src/utils/googleAnalytics.ts deleted file mode 100644 index 8f29667fc6..0000000000 --- a/src/utils/googleAnalytics.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import ReactGA, { EventArgs } from 'react-ga' -import { getNetworkInfo } from 'src/config' - -import { getGoogleAnalyticsTrackingID } from 'src/config' -import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' -import { loadFromCookie, removeCookie } from 'src/logic/cookies/utils' - -export const SAFE_NAVIGATION_EVENT = 'Safe Navigation' - -export const COOKIES_LIST = [ - { name: '_ga', path: '/' }, - { name: '_gat', path: '/' }, - { name: '_gid', path: '/' }, -] - -let analyticsLoaded = false -export const loadGoogleAnalytics = (): void => { - if (analyticsLoaded) { - return - } - // eslint-disable-next-line no-console - console.log('Loading google analytics...') - const trackingID = getGoogleAnalyticsTrackingID() - const networkInfo = getNetworkInfo() - if (!trackingID) { - console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID') - } else { - ReactGA.initialize(trackingID) - ReactGA.set({ - anonymizeIp: true, - appName: `Gnosis Safe Multisig (${networkInfo.label})`, - appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`, - appVersion: process.env.REACT_APP_APP_VERSION, - }) - analyticsLoaded = true - } -} - -type UseAnalyticsResponse = { - trackPage: (path: string) => void - trackEvent: (event: EventArgs) => void -} - -export const useAnalytics = (): UseAnalyticsResponse => { - const [analyticsAllowed, setAnalyticsAllowed] = useState(false) - - useEffect(() => { - async function fetchCookiesFromStorage() { - const cookiesState = await loadFromCookie(COOKIES_KEY) - if (cookiesState) { - const { acceptedAnalytics } = cookiesState - setAnalyticsAllowed(acceptedAnalytics) - } - } - fetchCookiesFromStorage() - }, []) - - const trackPage = useCallback( - (page) => { - if (!analyticsAllowed || !analyticsLoaded) { - return - } - ReactGA.pageview(page) - }, - [analyticsAllowed], - ) - - const trackEvent = useCallback( - (event: EventArgs) => { - if (!analyticsAllowed || !analyticsLoaded) { - return - } - ReactGA.event(event) - }, - [analyticsAllowed], - ) - - return { trackPage, trackEvent } -} - -// we remove GA cookies manually as react-ga does not provides a utility for it. -export const removeCookies = (): void => { - const subDomain = location.host.split('.').slice(-2).join('.') - COOKIES_LIST.forEach((cookie) => removeCookie(cookie.name, cookie.path, `.${subDomain}`)) -} diff --git a/src/utils/intercom.ts b/src/utils/intercom.ts deleted file mode 100644 index 605ac63d39..0000000000 --- a/src/utils/intercom.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { INTERCOM_ID } from 'src/utils/constants' - -let intercomLoaded = false - -export const isIntercomLoaded = () => intercomLoaded - -// eslint-disable-next-line consistent-return -export const loadIntercom = (): void => { - const APP_ID = INTERCOM_ID - if (!APP_ID) { - console.error('[Intercom] - In order to use Intercom you need to add an appID') - return - } - const d = document - const s = d.createElement('script') - s.type = 'text/javascript' - s.async = true - s.src = `https://widget.intercom.io/widget/${APP_ID}` - const x = d.getElementsByTagName('script')[0] - x?.parentNode?.insertBefore(s, x) - - s.onload = () => { - ;(window as any).Intercom('boot', { - app_id: APP_ID, - consent: true, - }) - intercomLoaded = true - } -} - -export const closeIntercom = (): void => { - if (!isIntercomLoaded()) return - intercomLoaded = false - ;(window as any).Intercom('shutdown') -}