diff --git a/deploy/likedao/templates/react-app.config.yaml b/deploy/likedao/templates/react-app.config.yaml index 1d06789f..b31e7e8a 100644 --- a/deploy/likedao/templates/react-app.config.yaml +++ b/deploy/likedao/templates/react-app.config.yaml @@ -35,6 +35,11 @@ data: endpoint: {{ .Values.reactApp.graphqlEndpoint | quote }}, }, authEndpoint: {{ .Values.reactApp.authEndpoint | quote }}, + {{ if .Values.reactApp.googleAnalyticsId }} + googleAnalyticsId: {{ .Values.reactApp.googleAnalyticsId | quote }}, + {{ else }} + googleAnalyticsId: null, + {{ end }} chainLinks: [ {{- range .Values.reactApp.chainLinks }} {{- with .}} diff --git a/react-app/config/config.template.js b/react-app/config/config.template.js index 099831e6..d8e194eb 100644 --- a/react-app/config/config.template.js +++ b/react-app/config/config.template.js @@ -22,6 +22,7 @@ window.appConfig = { endpoint: "http://localhost:8080/graphql", }, authEndpoint: "http://localhost:8080/auth", + googleAnalyticsId: null, chainLinks: [], footerLinks: { githubLink: "https://github.com/likecoin/likedao", diff --git a/react-app/package.json b/react-app/package.json index 2eccc942..db38afff 100644 --- a/react-app/package.json +++ b/react-app/package.json @@ -38,6 +38,7 @@ "react": "^18.1.0", "react-avatar": "^5.0.1", "react-dom": "^18.1.0", + "react-ga": "^3.3.1", "react-hook-form": "^7.31.3", "react-paginate": "^8.1.3", "react-popper": "^2.3.0", diff --git a/react-app/src/clients/baseWallet.ts b/react-app/src/clients/baseWallet.ts index 4284347c..2e8ea1a2 100644 --- a/react-app/src/clients/baseWallet.ts +++ b/react-app/src/clients/baseWallet.ts @@ -13,15 +13,17 @@ export interface ArbitrarySigner { export class BaseWallet { protected chainInfo: ChainInfo; - + public type: string; public offlineSigner: OfflineSigner; public provider: WalletProvider; constructor( + type: string, chainInfo: ChainInfo, offlineSigner: OfflineSigner, provider: WalletProvider ) { + this.type = type; this.offlineSigner = offlineSigner; this.chainInfo = chainInfo; this.provider = provider; diff --git a/react-app/src/clients/keplrClient.ts b/react-app/src/clients/keplrClient.ts index a5250dca..4b0091aa 100644 --- a/react-app/src/clients/keplrClient.ts +++ b/react-app/src/clients/keplrClient.ts @@ -5,9 +5,17 @@ import { newSignDataMessage, SignDataMessageResponse, } from "../models/cosmos/tx"; -import { ArbitrarySigner, BaseWallet } from "./baseWallet"; +import { ArbitrarySigner, BaseWallet, WalletProvider } from "./baseWallet"; export class KeplrWallet extends BaseWallet { + private constructor( + chainInfo: ChainInfo, + offlineSigner: OfflineSigner, + provider: WalletProvider + ) { + super("keplr", chainInfo, offlineSigner, provider); + } + static async connect(chainInfo: ChainInfo): Promise { const keplrClient = window.keplr; diff --git a/react-app/src/clients/walletConnectClient.ts b/react-app/src/clients/walletConnectClient.ts index ca0e132c..2f9f3597 100644 --- a/react-app/src/clients/walletConnectClient.ts +++ b/react-app/src/clients/walletConnectClient.ts @@ -32,7 +32,7 @@ export class WalletConnectWallet extends BaseWallet { provider: WalletProvider, connector: WalletConnect ) { - super(chainInfo, offlineSigner, provider); + super("walletConnect", chainInfo, offlineSigner, provider); this.connector = connector; } diff --git a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx index f3c7d36e..efdf9f75 100644 --- a/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx +++ b/react-app/src/components/ProposalDetailScreen/ProposalDetailScreen.tsx @@ -3,6 +3,7 @@ import cn from "classnames"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import BigNumber from "bignumber.js"; +import * as ReactGA from "react-ga"; import AppRoutes from "../../navigation/AppRoutes"; import Paper from "../common/Paper/Paper"; import { @@ -71,6 +72,13 @@ const ProposalDetailScreen: React.FC = () => { return; } + ReactGA.event({ + category: "Reaction", + action: "Set on proposal", + value: proposalId!, + label: type, + }); + try { await reactionAPI.setReaction( ReactionTargetType.Proposal, @@ -82,7 +90,7 @@ const ProposalDetailScreen: React.FC = () => { toast.error(translate("ProposalDetail.setReaction.failure")); } }, - [requestState, reactionAPI, wallet, translate] + [requestState, wallet, proposalId, reactionAPI, translate] ); const onUnsetReaction = useCallback(async (): Promise => { @@ -95,6 +103,12 @@ const ProposalDetailScreen: React.FC = () => { return; } + ReactGA.event({ + category: "Reaction", + action: "Unset on proposal", + value: proposalId!, + }); + try { await reactionAPI.unsetReaction( ReactionTargetType.Proposal, @@ -104,7 +118,7 @@ const ProposalDetailScreen: React.FC = () => { console.error("Error while setting reaction", e); toast.error(translate("ProposalDetail.setReaction.failure")); } - }, [requestState, reactionAPI, wallet, translate]); + }, [requestState, reactionAPI, proposalId, wallet, translate]); const handleOpenVoteModal = useCallback(() => { if (wallet.status !== ConnectionStatus.Connected) { diff --git a/react-app/src/config/Config.ts b/react-app/src/config/Config.ts index 2759f02a..4440ed3e 100644 --- a/react-app/src/config/Config.ts +++ b/react-app/src/config/Config.ts @@ -45,6 +45,7 @@ export interface IConfig { endpoint: string; }; authEndpoint: string; + googleAnalyticsId: string | null; chainLinks: ChainLink[]; footerLinks: FooterLinks; } @@ -74,6 +75,7 @@ const defaultConfig: IConfig = { }, authEndpoint: "http://localhost:8080/auth", chainLinks: [], + googleAnalyticsId: null, footerLinks: { githubLink: "https://github.com/likecoin/likedao", tokenLinks: [ diff --git a/react-app/src/navigation/AppRouter.tsx b/react-app/src/navigation/AppRouter.tsx index cc673761..8f85831e 100644 --- a/react-app/src/navigation/AppRouter.tsx +++ b/react-app/src/navigation/AppRouter.tsx @@ -12,51 +12,54 @@ import AppScaffold from "../components/AppScaffold/AppScaffold"; import ProposalDetailRouter from "../components/ProposalDetailScreen/ProposalDetailRouter"; import ValidatorDetailScreen from "../components/ValidatorDetailScreen/ValidatorDetailScreen"; import ValidatorScreen from "../components/ValidatorScreen/ValidatorScreen"; +import AnalyticsProvider from "../providers/AnalyticsProvider"; import AppRoutes from "./AppRoutes"; const AppRouter: React.FC = () => { const wallet = useWallet(); return ( - - }> - } /> - } /> - } /> - } /> - } - /> - } - /> + + + }> + } /> + } /> + } /> + } /> + } + /> + } + /> + } + /> + } /> + } + /> + + } + path={AppRoutes.NotFound} + element={} /> - } /> } + path={AppRoutes.ErrorInvalidAddress} + element={} /> - - - } - /> - } - /> - } /> - + } /> + - {wallet.status === ConnectionStatus.Connecting && ( - - )} + {wallet.status === ConnectionStatus.Connecting && ( + + )} + ); }; diff --git a/react-app/src/providers/AnalyticsProvider.tsx b/react-app/src/providers/AnalyticsProvider.tsx new file mode 100644 index 00000000..8048eff3 --- /dev/null +++ b/react-app/src/providers/AnalyticsProvider.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from "react"; +import * as ReactGA from "react-ga"; +import { useLocation } from "react-router-dom"; +import Config from "../config/Config"; + +interface AnalyticsProviderProps { + children?: React.ReactNode; +} + +const AnalyticsContext = React.createContext(null as any); + +const AnalyticsProvider: React.FC = (props) => { + const { children } = props; + const [isInitialized, setIsInitialized] = useState(false); + const location = useLocation(); + + useEffect(() => { + if (!isInitialized) return; + + ReactGA.pageview(location.pathname + location.search); + }, [isInitialized, location]); + + useEffect(() => { + if (Config.googleAnalyticsId) { + ReactGA.initialize(Config.googleAnalyticsId); + } + setIsInitialized(true); + }, []); + + return ( + + {isInitialized && children} + + ); +}; + +export default AnalyticsProvider; diff --git a/react-app/src/providers/WalletProvider.tsx b/react-app/src/providers/WalletProvider.tsx index 6545905c..02e1a7ba 100644 --- a/react-app/src/providers/WalletProvider.tsx +++ b/react-app/src/providers/WalletProvider.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { AccountData } from "@cosmjs/proto-signing"; import { toast } from "react-toastify"; +import * as ReactGA from "react-ga"; import { KeplrWallet } from "../clients/keplrClient"; import { BaseWallet, @@ -118,6 +119,11 @@ const WalletProvider: React.FC = (props) => { ); }) .finally(() => { + ReactGA.event({ + category: "Wallet", + action: "Disconnect", + label: activeWallet.type, + }); setActiveWallet(null); setAutoConnectWalletType(null); }); @@ -148,6 +154,12 @@ const WalletProvider: React.FC = (props) => { setAccountBalance(accountBalance); setAutoConnectWalletType(AutoConnectWalletType.Keplr); setWalletStatus(ConnectionStatus.Connected); + + ReactGA.event({ + category: "Wallet", + action: "Connect", + label: wallet.type, + }); // Validate the token in case the logged in wallet has a different account than the authenticated address await authAPI.validate(account.address); } catch (err: unknown) { @@ -190,6 +202,12 @@ const WalletProvider: React.FC = (props) => { setAccountBalance(accountBalance); setAutoConnectWalletType(AutoConnectWalletType.WalletConnect); setWalletStatus(ConnectionStatus.Connected); + + ReactGA.event({ + category: "Wallet", + action: "Connect", + label: wallet.type, + }); // Validate the token in case the logged in wallet has a different account than the authenticated address await authAPI.validate(account.address); } catch (err: unknown) { diff --git a/react-app/yarn.lock b/react-app/yarn.lock index 6d233db4..9d337772 100644 --- a/react-app/yarn.lock +++ b/react-app/yarn.lock @@ -8650,6 +8650,11 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-ga@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.1.tgz#d8e1f4e05ec55ed6ff944dcb14b99011dfaf9504" + integrity sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ== + react-hook-form@^7.31.3: version "7.31.3" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.31.3.tgz#b61bafb9a7435f91695351a7a9f714d8c4df0121"