diff --git a/src/components/SafeListSidebar/index.tsx b/src/components/SafeListSidebar/index.tsx index 19fe5ff5..8aeeb77b 100644 --- a/src/components/SafeListSidebar/index.tsx +++ b/src/components/SafeListSidebar/index.tsx @@ -1,7 +1,7 @@ +import React, { useEffect, useMemo, useState } from 'react' import Drawer from '@material-ui/core/Drawer' import SearchIcon from '@material-ui/icons/Search' import SearchBar from 'material-ui-search-bar' -import * as React from 'react' import { connect } from 'react-redux' import SafeList from './SafeList' @@ -16,12 +16,11 @@ import Link from 'src/components/layout/Link' import Row from 'src/components/layout/Row' import { WELCOME_ADDRESS } from 'src/routes/routes' import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { AppReduxState } from 'src/store' -const { useEffect, useMemo, useState } = React - export const SafeListSidebarContext = React.createContext({ isOpen: false, toggleSidebar: () => {}, @@ -39,12 +38,7 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault const [isOpen, setIsOpen] = useState(false) const [filter, setFilter] = useState('') const classes = useSidebarStyles() - - useEffect(() => { - setTimeout(() => { - setFilter('') - }, 300) - }, [isOpen]) + const { trackEvent } = useAnalytics() const searchClasses = { input: classes.searchInput, @@ -54,6 +48,9 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault } const toggleSidebar = () => { + if (!isOpen) { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Safe List Sidebar' }) + } setIsOpen((prevIsOpen) => !prevIsOpen) } @@ -73,6 +70,12 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault const filteredSafes = useMemo(() => filterBy(filter, safes), [safes, filter]) + useEffect(() => { + setTimeout(() => { + setFilter('') + }, 300) + }, [isOpen]) + return ( { const columns = generateColumns() @@ -53,6 +54,11 @@ const AddressBookTable = ({ classes }) => { const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = 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/index.tsx b/src/routes/safe/components/Apps/index.tsx index 2cd30315..d54dd1b0 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -19,6 +19,7 @@ import { import { isSameURL } from 'src/utils/url' import { useIframeMessageHandler } from './hooks/useIframeMessageHandler' import ConfirmTransactionModal from './components/ConfirmTransactionModal' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const centerCSS = css` display: flex; @@ -64,6 +65,7 @@ const Apps = (): React.ReactElement => { ) const iframeRef = useRef() + const { trackEvent } = useAnalytics() const granted = useSelector(grantedSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) @@ -111,6 +113,7 @@ const Apps = (): React.ReactElement => { [selectedAppId], ) + // Auto Select app first App useEffect(() => { const selectFirstEnabledApp = () => { const firstEnabledApp = appList.find((a) => !a.disabled) @@ -124,7 +127,14 @@ const Apps = (): React.ReactElement => { if (initialSelect || currentAppWasDisabled) { selectFirstEnabledApp() } - }, [appList, selectedApp, selectedAppId]) + }, [appList, selectedApp, selectedAppId, trackEvent]) + + // track GA + useEffect(() => { + if (selectedApp) { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Apps', label: selectedApp.name }) + } + }, [selectedApp, trackEvent]) const handleIframeLoad = useCallback(() => { const iframe = iframeRef.current diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index ac2feaa4..d9b1bc63 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -1,9 +1,9 @@ +import React, { useEffect } from 'react' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' import { makeStyles } from '@material-ui/core/styles' import { List } from 'immutable' -import React from 'react' import { useSelector } from 'react-redux' import { styles } from './styles' @@ -29,6 +29,7 @@ import { } from 'src/routes/safe/components/Balances/dataFetcher' import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector' import { Skeleton } from '@material-ui/lab' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const useStyles = makeStyles(styles as any) @@ -61,6 +62,11 @@ const Coins = (props: Props): React.ReactElement => { const currencyValues = useSelector(safeFiatBalancesListSelector) const granted = useSelector(grantedSelector) const [filteredData, setFilteredData] = React.useState>(List()) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' }) + }, [trackEvent]) React.useMemo(() => { setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate)) diff --git a/src/routes/safe/components/Balances/Collectibles/index.tsx b/src/routes/safe/components/Balances/Collectibles/index.tsx index f8153d6d..d3a88e23 100644 --- a/src/routes/safe/components/Balances/Collectibles/index.tsx +++ b/src/routes/safe/components/Balances/Collectibles/index.tsx @@ -1,6 +1,6 @@ +import React, { useEffect } from 'react' import Card from '@material-ui/core/Card' import { makeStyles } from '@material-ui/core/styles' -import React from 'react' import { useSelector } from 'react-redux' import Item from './components/Item' @@ -10,6 +10,7 @@ import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collec import SendModal from 'src/routes/safe/components/Balances/SendModal' import { safeSelector } from 'src/logic/safe/store/selectors' import { fontColor, lg, screenSm, screenXs } from 'src/theme/variables' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const useStyles = makeStyles({ cardInner: { @@ -74,13 +75,18 @@ const useStyles = makeStyles({ }, } as any) -const Collectibles = () => { +const Collectibles = (): React.ReactElement => { const classes = useStyles() const [selectedToken, setSelectedToken] = React.useState({}) const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) const { address, ethBalance, name } = useSelector(safeSelector) const nftTokens = useSelector(nftTokensSelector) const activeAssetsList = useSelector(activeNftAssetsListSelector) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' }) + }, [trackEvent]) const handleItemSend = (nftToken) => { setSelectedToken(nftToken) diff --git a/src/routes/safe/components/Settings/Advanced/index.tsx b/src/routes/safe/components/Settings/Advanced/index.tsx index 9628ec7e..4f3bafa3 100644 --- a/src/routes/safe/components/Settings/Advanced/index.tsx +++ b/src/routes/safe/components/Settings/Advanced/index.tsx @@ -1,6 +1,6 @@ import { Loader, Text, theme, Title } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' -import React from 'react' +import React, { useEffect } from 'react' import { useSelector } from 'react-redux' import styled from 'styled-components' @@ -10,6 +10,7 @@ import ModulesTable from './ModulesTable' import Block from 'src/components/layout/Block' import { safeModulesSelector, safeNonceSelector } from 'src/logic/safe/store/selectors' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const useStyles = makeStyles(styles) @@ -39,10 +40,14 @@ const LoadingModules = (): React.ReactElement => { const Advanced = (): React.ReactElement => { const classes = useStyles() - const nonce = useSelector(safeNonceSelector) const modules = useSelector(safeModulesSelector) const moduleData = getModuleData(modules) ?? 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/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index 984b0612..9bf77dab 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -33,7 +33,7 @@ export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn' const useStyles = makeStyles(styles) type OwnProps = { - isOpen: true + isOpen: boolean onClose: () => void ownerAddress: string selectedOwnerName: string diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index 437d4632..461ed4a8 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -1,9 +1,10 @@ +import React, { useState, useEffect } from 'react' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' -import React from 'react' +import { List } from 'immutable' import RemoveOwnerIcon from '../assets/icons/bin.svg' @@ -28,6 +29,9 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph/index' import Row from 'src/components/layout/Row' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook' +import { SafeOwner } from 'src/logic/safe/store/models/safe' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' @@ -35,166 +39,167 @@ export const ADD_OWNER_BTN_TEST_ID = 'add-owner-btn' export const REPLACE_OWNER_BTN_TEST_ID = 'replace-owner-btn' export const OWNERS_ROW_TEST_ID = 'owners-row' -class ManageOwners extends React.Component { - constructor(props) { - super(props) +const useStyles = makeStyles(styles) - this.state = { - selectedOwnerAddress: undefined, - selectedOwnerName: undefined, - showAddOwner: false, - showRemoveOwner: false, - showReplaceOwner: false, - showEditOwner: false, - } - } - - onShow = (action, row?: any) => () => { - this.setState({ - [`show${action}`]: true, - selectedOwnerAddress: row && row.address, - selectedOwnerName: row && row.name, - }) - } - - onHide = (action) => () => { - this.setState({ - [`show${action}`]: false, - selectedOwnerAddress: undefined, - selectedOwnerName: undefined, - }) - } - - render() { - const { addressBook, classes, granted, owners } = this.props - const { - selectedOwnerAddress, - selectedOwnerName, - showAddOwner, - showEditOwner, - showRemoveOwner, - showReplaceOwner, - } = this.state - const columns = generateColumns() - const autoColumns = columns.filter((c) => !c.custom) - const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners) - const ownerData = getOwnerData(ownersAdbk) - - return ( - <> - - - Manage Safe Owners - - - Add, remove and replace owners or rename existing owners. Owner names are only stored locally and never - shared with Gnosis or any third parties. - - - - {(sortedData) => - sortedData.map((row, index) => ( - = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} - data-testid={OWNERS_ROW_TEST_ID} - key={index} - tabIndex={-1} - > - {autoColumns.map((column: any) => ( - - {column.id === OWNERS_TABLE_ADDRESS_ID ? ( - - ) : ( - row[column.id] - )} - - ))} - - - Edit owner - {granted && ( - <> - Replace owner - {ownerData.size > 1 && ( - Remove owner - )} - - )} - - - - )) - } -
-
-
- {granted && ( - <> - - - - - - - - )} - - - - - - ) - } +type Props = { + addressBook: unknown + granted: boolean + owners: List } -export default withStyles(styles as any)(ManageOwners) +const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactElement => { + const { trackEvent } = useAnalytics() + const classes = useStyles() + + const [selectedOwnerAddress, setSelectedOwnerAddress] = useState() + const [selectedOwnerName, setSelectedOwnerName] = useState() + const [modalsStatus, setModalStatus] = useState({ + showAddOwner: false, + showRemoveOwner: false, + showReplaceOwner: false, + showEditOwner: false, + }) + + const onShow = (action, row?: any) => () => { + setModalStatus((prevState) => ({ + ...prevState, + [`show${action}`]: !prevState[`show${action}`], + })) + setSelectedOwnerAddress(row && row.address) + setSelectedOwnerName(row && row.name) + } + + const onHide = (action) => () => { + setModalStatus((prevState) => ({ + ...prevState, + [`show${action}`]: !Boolean(prevState[`show${action}`]), + })) + setSelectedOwnerAddress(undefined) + setSelectedOwnerName(undefined) + } + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) + }, [trackEvent]) + + const columns = generateColumns() + const autoColumns = columns.filter((c) => !c.custom) + const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookEntryProps, owners) + const ownerData = getOwnerData(ownersAdbk) + + return ( + <> + + + Manage Safe Owners + + + Add, remove and replace owners or rename existing owners. Owner names are only stored locally and never shared + with Gnosis or any third parties. + + + + {(sortedData) => + sortedData.map((row, index) => ( + = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} + data-testid={OWNERS_ROW_TEST_ID} + key={index} + > + {autoColumns.map((column: any) => ( + + {column.id === OWNERS_TABLE_ADDRESS_ID ? ( + + ) : ( + row[column.id] + )} + + ))} + + + Edit owner + {granted && ( + <> + Replace owner + {ownerData.size > 1 && ( + Remove owner + )} + + )} + + + + )) + } +
+
+
+ {granted && ( + <> + + + + + + + + )} + + + + + + ) +} + +export default ManageOwners diff --git a/src/routes/safe/components/Settings/ManageOwners/style.ts b/src/routes/safe/components/Settings/ManageOwners/style.ts index 21cb5f9e..957a20d0 100644 --- a/src/routes/safe/components/Settings/ManageOwners/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/style.ts @@ -1,6 +1,7 @@ import { lg, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ formContainer: { minHeight: '420px', }, diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index 14a7b11d..c04ed03a 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core/styles' -import React from 'react' +import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -29,6 +29,7 @@ import { safeNeedsUpdateSelector, safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn' @@ -44,6 +45,7 @@ const SafeDetails = (): React.ReactElement => { const safeName = useSelector(safeNameSelector) const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector) const safeCurrentVersion = useSelector(safeCurrentVersionSelector) + const { trackEvent } = useAnalytics() const [isModalOpen, setModalOpen] = React.useState(false) const safeAddress = useSelector(safeParamAddressFromStateSelector) @@ -63,6 +65,10 @@ const SafeDetails = (): React.ReactElement => { setModalOpen(true) } + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Details' }) + }, [trackEvent]) + return ( <> diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index 36ae420a..c6bcccf4 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -1,6 +1,6 @@ import { withStyles } from '@material-ui/core/styles' import { withSnackbar } from 'notistack' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import ChangeThreshold from './ChangeThreshold' @@ -22,6 +22,7 @@ import { safeParamAddressFromStateSelector, safeThresholdSelector, } from 'src/logic/safe/store/selectors' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => { const [isModalOpen, setModalOpen] = useState(false) @@ -52,6 +53,12 @@ const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => { ) } + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' }) + }, [trackEvent]) + return ( <> diff --git a/src/routes/safe/components/Transactions/TxsTable/index.tsx b/src/routes/safe/components/Transactions/TxsTable/index.tsx index 57033ba4..e7e43cdc 100644 --- a/src/routes/safe/components/Transactions/TxsTable/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/index.tsx @@ -7,7 +7,7 @@ import { withStyles } from '@material-ui/core/styles' import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandMore from '@material-ui/icons/ExpandMore' import cn from 'classnames' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' import ExpandedTxComponent from './ExpandedTx' @@ -21,6 +21,7 @@ import Block from 'src/components/layout/Block' import Row from 'src/components/layout/Row' import { safeCancellationTransactionsSelector } from 'src/logic/safe/store/selectors' import { extendedTransactionsSelector } from 'src/logic/safe/store/selectors/transactions' +import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' export const TRANSACTION_ROW_TEST_ID = 'transaction-row' @@ -28,6 +29,11 @@ const TxsTable = ({ classes }) => { const [expandedTx, setExpandedTx] = useState(null) const cancellationTransactions = useSelector(safeCancellationTransactionsSelector) const transactions = useSelector(extendedTransactionsSelector) + const { trackEvent } = useAnalytics() + + useEffect(() => { + trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Transactions' }) + }, [trackEvent]) const handleTxExpand = (safeTxHash) => { setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash)) diff --git a/src/utils/googleAnalytics.ts b/src/utils/googleAnalytics.ts index d66c8602..091f6a40 100644 --- a/src/utils/googleAnalytics.ts +++ b/src/utils/googleAnalytics.ts @@ -1,12 +1,14 @@ import { useCallback, useEffect, useState } from 'react' -import GoogleAnalytics from 'react-ga' +import GoogleAnalytics, { EventArgs } from 'react-ga' import { getGoogleAnalyticsTrackingID } from 'src/config' import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' import { loadFromCookie } from 'src/logic/cookies/utils' +export const SAFE_NAVIGATION_EVENT = 'Safe Navigation' + let analyticsLoaded = false -export const loadGoogleAnalytics = () => { +export const loadGoogleAnalytics = (): void => { if (analyticsLoaded) { return } @@ -22,7 +24,12 @@ export const loadGoogleAnalytics = () => { } } -export const useAnalytics = () => { +type UseAnalyticsResponse = { + trackPage: (path: string) => void + trackEvent: (event: EventArgs) => void +} + +export const useAnalytics = (): UseAnalyticsResponse => { const [analyticsAllowed, setAnalyticsAllowed] = useState(false) useEffect(() => { @@ -37,18 +44,24 @@ export const useAnalytics = () => { }, []) const trackPage = useCallback( - (page, options = {}) => { + (page) => { if (!analyticsAllowed || !analyticsLoaded) { return } - GoogleAnalytics.set({ - page, - ...options, - }) GoogleAnalytics.pageview(page) }, [analyticsAllowed], ) - return { trackPage } + const trackEvent = useCallback( + (event: EventArgs) => { + if (!analyticsAllowed || !analyticsLoaded) { + return + } + GoogleAnalytics.event(event) + }, + [analyticsAllowed], + ) + + return { trackPage, trackEvent } }