diff --git a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx index 6f9ebe139c..39b868b2f7 100644 --- a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx +++ b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx @@ -10,11 +10,7 @@ import { SafeAppsTag } from '@/config/constants' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import { WalletConnectContext } from '@/services/walletconnect/WalletConnectContext' - -const isWalletConnectSafeApp = (app: SafeAppData): boolean => { - const WALLET_CONNECT = /wallet-connect/ - return WALLET_CONNECT.test(app.url) -} +import { isWalletConnectSafeApp } from '@/services/walletconnect/utils' const FeaturedAppCard = ({ app }: { app: SafeAppData }) => ( @@ -62,7 +58,7 @@ export const FeaturedApps = ({ stackedLayout }: { stackedLayout: boolean }): Rea > {featuredApps?.map((app) => ( - {isWalletConnectSafeApp(app) ? ( + {isWalletConnectSafeApp(app.url) ? ( diff --git a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx index d2c18301b3..ba1aa23212 100644 --- a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx +++ b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx @@ -14,6 +14,7 @@ import PlusIcon from '@/public/images/common/plus.svg' import MinusIcon from '@/public/images/common/minus.svg' import EthHashInfo from '@/components/common/EthHashInfo' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwnerFlowProps }) => { const dispatch = useAppDispatch() @@ -49,6 +50,7 @@ export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwn trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: safe.threshold }) trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) + trackEvent({ ...TX_EVENTS.CREATE, label: params.removedOwner ? TX_TYPES.owner_swap : TX_TYPES.owner_add }) } return ( diff --git a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx index 6c1aa11d61..e9e7068a25 100644 --- a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx +++ b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx @@ -8,6 +8,7 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { ChangeThresholdFlowFieldNames } from '@/components/tx-flow/flows/ChangeThreshold' import type { ChangeThresholdFlowProps } from '@/components/tx-flow/flows/ChangeThreshold' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' import commonCss from '@/components/tx-flow/common/styles.module.css' @@ -24,6 +25,7 @@ const ReviewChangeThreshold = ({ params }: { params: ChangeThresholdFlowProps }) const onChangeThreshold = () => { trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: newThreshold }) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.owner_threshold_change }) } return ( diff --git a/src/components/tx-flow/flows/ConfirmBatch/index.tsx b/src/components/tx-flow/flows/ConfirmBatch/index.tsx index 27e53d211f..daf1ae5580 100644 --- a/src/components/tx-flow/flows/ConfirmBatch/index.tsx +++ b/src/components/tx-flow/flows/ConfirmBatch/index.tsx @@ -1,4 +1,4 @@ -import { type ReactElement, useContext, useEffect } from 'react' +import { type ReactElement, useContext, useEffect, useCallback } from 'react' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' @@ -9,6 +9,8 @@ import TxLayout from '../../common/TxLayout' import BatchIcon from '@/public/images/common/batch.svg' import { useDraftBatch } from '@/hooks/useDraftBatch' import BatchTxList from '@/components/batch/BatchSidebar/BatchTxList' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' type ConfirmBatchProps = { onSubmit: () => void @@ -32,8 +34,13 @@ const ConfirmBatch = ({ onSubmit }: ConfirmBatchProps): ReactElement => { createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) }, [batchTxs, setSafeTx, setSafeTxError]) + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.batch }) + onSubmit() + }, [onSubmit]) + return ( - + ) diff --git a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx index 5ab7d1a2a2..15cb0b27e0 100644 --- a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx +++ b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx @@ -17,6 +17,7 @@ import type { SpendingLimitState } from '@/store/spendingLimitsSlice' import type { NewSpendingLimitFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' import { SafeTxContext } from '../../SafeTxProvider' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowProps }) => { const [existingSpendingLimit, setExistingSpendingLimit] = useState() @@ -53,6 +54,8 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr ...SETTINGS_EVENTS.SPENDING_LIMIT.RESET_PERIOD, label: resetTime, }) + + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.spending_limit_add }) } const existingAmount = existingSpendingLimit diff --git a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx index c9024eb02f..baa97927fb 100644 --- a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx @@ -1,4 +1,4 @@ -import { type ReactElement, useEffect, useContext } from 'react' +import { type ReactElement, useEffect, useContext, useCallback } from 'react' import { Grid, Typography } from '@mui/material' import SendToBlock from '@/components/tx-flow/flows/TokenTransfer/SendToBlock' import { createNftTransferParams } from '@/services/tx/tokenTransferParams' @@ -8,6 +8,8 @@ import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { SafeTxContext } from '../../SafeTxProvider' import { NftItems } from '@/components/tx-flow/flows/NftTransfer/SendNftBatch' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' type ReviewNftBatchProps = { params: NftTransferParams @@ -38,8 +40,13 @@ const ReviewNftBatch = ({ params, onSubmit, txNonce }: ReviewNftBatchProps): Rea promise.then(setSafeTx).catch(setSafeTxError) }, [safeAddress, params, setSafeTx, setSafeTxError]) + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.transfer_nft }) + onSubmit() + }, [onSubmit]) + return ( - + diff --git a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx index 91db5ae55f..24b2f66251 100644 --- a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx +++ b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx @@ -4,11 +4,17 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { createRejectTx } from '@/services/tx/tx-sender' import { useContext, useEffect } from 'react' import { SafeTxContext } from '../../SafeTxProvider' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' type RejectTxProps = { txNonce: number } +const onSubmit = () => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.rejection }) +} + const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => { const { setSafeTx, setSafeTxError, setNonce } = useContext(SafeTxContext) @@ -19,7 +25,7 @@ const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => { }, [txNonce, setNonce, setSafeTx, setSafeTxError]) return ( - {}} isBatchable={false}> + To reject the transaction, a separate rejection transaction will be created to replace the original one. diff --git a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx index e2155eb66e..6ac5920984 100644 --- a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx +++ b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx @@ -7,6 +7,12 @@ import { trackEvent, SETTINGS_EVENTS } from '@/services/analytics' import { createRemoveGuardTx } from '@/services/tx/tx-sender' import { type RemoveGuardFlowProps } from '.' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_GUARD) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.guard_remove }) +} export const ReviewRemoveGuard = ({ params }: { params: RemoveGuardFlowProps }) => { const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) @@ -21,10 +27,6 @@ export const ReviewRemoveGuard = ({ params }: { params: RemoveGuardFlowProps }) } }, [safeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_GUARD) - } - return ( ({ color: palette.primary.light })}>Transaction guard diff --git a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx index 01a11b223d..d6fb40da21 100644 --- a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx +++ b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx @@ -7,6 +7,12 @@ import { createRemoveModuleTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { type RemoveModuleFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_MODULE) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.module_remove }) +} export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps }) => { const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) @@ -21,10 +27,6 @@ export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps } } }, [safeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_MODULE) - } - return ( diff --git a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx index 676a2b1561..59c5498744 100644 --- a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx @@ -11,6 +11,7 @@ import MinusIcon from '@/public/images/common/minus.svg' import { SafeTxContext } from '../../SafeTxProvider' import type { RemoveOwnerFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' import commonCss from '@/components/tx-flow/common/styles.module.css' @@ -29,6 +30,7 @@ export const ReviewRemoveOwner = ({ params }: { params: RemoveOwnerFlowProps }): const onFormSubmit = () => { trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: safe.threshold }) trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.owner_remove }) } return ( diff --git a/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx b/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx index df046f6cc6..087cf68705 100644 --- a/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx +++ b/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx @@ -13,6 +13,12 @@ import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmount import { safeFormatUnits } from '@/utils/formatters' import SpendingLimitLabel from '@/components/common/SpendingLimitLabel' import { createTx } from '@/services/tx/tx-sender' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.SPENDING_LIMIT.LIMIT_REMOVED) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.spending_limit_remove }) +} export const RemoveSpendingLimit = ({ params }: { params: SpendingLimitState }) => { const { setSafeTx, setSafeTxError } = useContext(SafeTxContext) @@ -42,10 +48,6 @@ export const RemoveSpendingLimit = ({ params }: { params: SpendingLimitState }) createTx(txParams).then(setSafeTx).catch(setSafeTxError) }, [chainId, params.beneficiary, params.token, setSafeTx, setSafeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.SPENDING_LIMIT.LIMIT_REMOVED) - } - return ( {token && ( diff --git a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx index 4580b6021f..6b4f57c3f8 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx @@ -17,13 +17,18 @@ import ApprovalEditor from '@/components/tx/ApprovalEditor' import { getInteractionTitle, isTxValid } from '@/components/safe-apps/utils' import ErrorMessage from '@/components/tx/ErrorMessage' import { asError } from '@/services/exceptions/utils' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { isWalletConnectSafeApp } from '@/services/walletconnect/utils' type ReviewSafeAppsTxProps = { safeAppsTx: SafeAppsTxParams + onSubmit?: (txId: string, safeTxHash: string) => void } const ReviewSafeAppsTx = ({ safeAppsTx: { txs, requestId, params, appId, app }, + onSubmit, }: ReviewSafeAppsTxProps): ReactElement => { const { safe } = useSafeInfo() const onboard = useOnboard() @@ -54,11 +59,22 @@ const ReviewSafeAppsTx = ({ if (!safeTx || !onboard) return trackSafeAppTxCount(Number(appId)) + let safeTxHash = '' try { - await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId, txId) + safeTxHash = await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId, txId) } catch (error) { setSafeTxError(asError(error)) } + + // Track tx creation + if (safeTx.signatures.size === 0) { + trackEvent({ + ...TX_EVENTS.CREATE, + label: isWalletConnectSafeApp(app?.url || '') ? TX_TYPES.walletconnect : TX_TYPES.safeapps, + }) + } + + onSubmit?.(txId, safeTxHash) } const origin = useMemo(() => getTxOrigin(app), [app]) diff --git a/src/components/tx-flow/flows/SafeAppsTx/index.tsx b/src/components/tx-flow/flows/SafeAppsTx/index.tsx index a5f6bdf329..297d13f569 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/index.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/index.tsx @@ -12,14 +12,20 @@ export type SafeAppsTxParams = { params?: SendTransactionRequestParams } -const SafeAppsTxFlow = ({ data }: { data: SafeAppsTxParams }) => { +const SafeAppsTxFlow = ({ + data, + onSubmit, +}: { + data: SafeAppsTxParams + onSubmit?: (txId: string, safeTxHash: string) => void +}) => { return ( } step={0} > - + ) } diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index d331e10b96..dfd2cd79a9 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -38,6 +38,8 @@ import InfoBox from '@/components/safe-messages/InfoBox' import { DecodedMsg } from '@/components/safe-messages/DecodedMsg' import TxCard from '@/components/tx-flow/common/TxCard' import { dispatchPreparedSignature } from '@/services/safe-messages/safeMsgNotifications' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' const createSkeletonMessage = (confirmationsRequired: number): SafeMessage => { return { @@ -169,19 +171,14 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr const { safe } = useSafeInfo() const isOwner = useIsSafeOwner() const wallet = useWallet() + useHighlightHiddenTab() const { decodedMessage, safeMessageMessage, safeMessageHash } = useDecodedSafeMessage(message, safe) const [safeMessage, setSafeMessage] = useSafeMessage(safeMessageHash) - - useHighlightHiddenTab() - - const decodedMessageAsString = - typeof decodedMessage === 'string' ? decodedMessage : JSON.stringify(decodedMessage, null, 2) - + const isPlainTextMessage = typeof decodedMessage === 'string' + const decodedMessageAsString = isPlainTextMessage ? decodedMessage : JSON.stringify(decodedMessage, null, 2) const hasSigned = !!safeMessage?.confirmations.some(({ owner }) => owner.value === wallet?.address) - const isFullySigned = !!safeMessage?.preparedSignature - const isDisabled = !isOwner || hasSigned const { onSign, submitError } = useSyncSafeMessageSigner( @@ -195,9 +192,15 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr const handleSign = async () => { const updatedMessage = await onSign() + if (updatedMessage) { setSafeMessage(updatedMessage) } + + // Track first signature as creation + if (updatedMessage?.confirmations.length === 1) { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.typed_message }) + } } const onContinue = async () => { diff --git a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx index ab535b7ede..d940608ac5 100644 --- a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx +++ b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx @@ -25,6 +25,8 @@ import useHighlightHiddenTab from '@/hooks/useHighlightHiddenTab' import { type SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { asError } from '@/services/exceptions/utils' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export type SignMessageOnChainProps = { app?: SafeAppData @@ -98,6 +100,12 @@ const ReviewSignMessageOnChain = ({ message, method, requestId }: SignMessageOnC const handleSubmit = async () => { if (!safeTx || !onboard) return + + // Track the creation of a typed message + if (isTypedMessage && safeTx.signatures.size === 1) { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.typed_message }) + } + try { await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId) } catch (error) { diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx index 98e2535f31..bbacba80e8 100644 --- a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx @@ -1,9 +1,10 @@ -import { type ReactElement } from 'react' +import { useCallback, type ReactElement } from 'react' import { type TokenTransferParams, TokenTransferType } from '@/components/tx-flow/flows/TokenTransfer/index' import ReviewTokenTransfer from '@/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer' import ReviewSpendingLimitTx from '@/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' -// TODO: Split this into separate flows const ReviewTokenTx = ({ params, onSubmit, @@ -15,10 +16,15 @@ const ReviewTokenTx = ({ }): ReactElement => { const isSpendingLimitTx = params.type === TokenTransferType.spendingLimit + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.transfer_token }) + onSubmit() + }, [onSubmit]) + return isSpendingLimitTx ? ( - + ) : ( - + ) } diff --git a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx index 62239c188f..8e3f7e7d7f 100644 --- a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx +++ b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx @@ -9,6 +9,12 @@ import { createUpdateSafeTxs } from '@/services/tx/safeUpdateParams' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '../../SafeTxProvider' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' + +const onSubmit = () => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.safe_update }) +} export const UpdateSafeReview = () => { const { safe, safeLoaded } = useSafeInfo() @@ -25,7 +31,7 @@ export const UpdateSafeReview = () => { }, [chain, safe, safeLoaded, setNonce, setSafeTx, setSafeTxError]) return ( - null}> + Update now to take advantage of new features and the highest security standards available. diff --git a/src/components/tx/ExecuteCheckbox/index.tsx b/src/components/tx/ExecuteCheckbox/index.tsx index 43e1ad6720..7ec0bf2a56 100644 --- a/src/components/tx/ExecuteCheckbox/index.tsx +++ b/src/components/tx/ExecuteCheckbox/index.tsx @@ -12,7 +12,7 @@ const ExecuteCheckbox = ({ onChange }: { onChange: (checked: boolean) => void }) const handleChange = (_: ChangeEvent, value: string) => { const checked = value === 'true' - trackEvent({ ...MODALS_EVENTS.EXECUTE_TX, label: checked }) + trackEvent({ ...MODALS_EVENTS.TOGGLE_EXECUTE_TX, label: checked }) dispatch(setTransactionExecution(checked)) onChange(checked) } diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index ec64f35893..5518a827a0 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -96,8 +96,8 @@ const ExecuteForm = ({ } // On success - setTxFlow(, undefined, false) onSubmit(executedTxId) + setTxFlow(, undefined, false) } const cannotPropose = !isOwner && !onlyExecute diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx index ce02977c13..e68a47c76f 100644 --- a/src/components/tx/SignOrExecuteForm/SignForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx @@ -63,9 +63,12 @@ const SignForm = ({ return } - // On success + // On successful sign + if (!isAddingToBatch) { + onSubmit(resultTxId) + } + setTxFlow(undefined) - onSubmit(resultTxId) } const onBatchClick = (e: SyntheticEvent) => { diff --git a/src/components/tx/security/tenderly/index.tsx b/src/components/tx/security/tenderly/index.tsx index 4f1487205e..243eda41d6 100644 --- a/src/components/tx/security/tenderly/index.tsx +++ b/src/components/tx/security/tenderly/index.tsx @@ -18,6 +18,8 @@ import sharedCss from '@/components/tx/security/shared/styles.module.css' import { TxInfoContext } from '@/components/tx-flow/TxInfoProvider' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import InfoIcon from '@/public/images/notifications/info.svg' +import Track from '@/components/common/Track' +import { MODALS_EVENTS } from '@/services/analytics' export type TxSimulationProps = { transactions?: SimulationTxParams['transactions'] @@ -112,15 +114,17 @@ const TxSimulationBlock = ({ transactions, disabled, gasLimit }: TxSimulationPro ) ) : ( - + + + )} diff --git a/src/services/analytics/__tests__/gtm.test.ts b/src/services/analytics/__tests__/gtm.test.ts index 33eacac65c..5381ec244c 100644 --- a/src/services/analytics/__tests__/gtm.test.ts +++ b/src/services/analytics/__tests__/gtm.test.ts @@ -1,18 +1,131 @@ -import { normalizeAppName } from '../gtm' +import * as gtm from '../gtm' +import TagManager from '../TagManager' +import { EventType, DeviceType } from '../types' -const FAKE_SAFE_APP_NAME = 'Safe App' -const FAKE_DOMAIN = 'http://domain.crypto' +// Mock dependencies +jest.mock('../TagManager', () => ({ + initialize: jest.fn(), + dataLayer: jest.fn(), + enableCookies: jest.fn(), + disableCookies: jest.fn(), + setUserProperty: jest.fn(), +})) describe('gtm', () => { - describe('normalizeAppName', () => { - it('should return the app name if is not an URL', () => { - expect(normalizeAppName(FAKE_SAFE_APP_NAME)).toBe(FAKE_SAFE_APP_NAME) + // Reset mocks before each test + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('gtmTrack', () => { + it('should send correct data to the dataLayer', () => { + const mockEventData = { + event: EventType.CLICK, + category: 'testCategory', + action: 'testAction', + chainId: '1234', + label: 'testLabel', + } + + gtm.gtmTrack(mockEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + event: mockEventData.event, + eventCategory: mockEventData.category, + eventAction: mockEventData.action, + chainId: mockEventData.chainId, + eventLabel: mockEventData.label, + appVersion: expect.any(String), + deviceType: DeviceType.DESKTOP, + }), + ) + }) + + it('should set the chain ID correctly', () => { + const testChainId = '1234' + gtm.gtmSetChainId(testChainId) + + const mockEventData = { + event: EventType.CLICK, + category: 'testCategory', + action: 'testAction', + label: 'testLabel', + } + + gtm.gtmTrack(mockEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + event: mockEventData.event, + eventCategory: mockEventData.category, + eventAction: mockEventData.action, + chainId: testChainId, + eventLabel: mockEventData.label, + appVersion: expect.any(String), + deviceType: DeviceType.DESKTOP, + }), + ) }) + }) + + describe('gtmTrackSafeApp', () => { + it('should send correct data to the dataLayer for a Safe App event', () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { + pathname: '/apps', + }, + }) + + const mockEventData = { + event: EventType.SAFE_APP, + category: 'testCategory', + action: 'testAction', + label: 'testLabel', + chainId: '1234', + } + + const mockAppName = 'Test App' + const mockSdkEventData = { + method: 'testMethod', + ethMethod: 'testEthMethod', + version: '1.0.0', + } + + gtm.gtmTrackSafeApp(mockEventData, mockAppName, mockSdkEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + appVersion: expect.any(String), + chainId: expect.any(String), + deviceType: DeviceType.DESKTOP, + event: EventType.SAFE_APP, + eventAction: 'testAction', + eventCategory: 'testCategory', + eventLabel: 'testLabel', + safeAddress: '', + safeAppEthMethod: '', + safeAppMethod: '', + safeAppName: 'Test App', + safeAppSDKVersion: '', + }), + ) + }) + + describe('normalizeAppName', () => { + const FAKE_SAFE_APP_NAME = 'Safe App' + const FAKE_DOMAIN = 'http://domain.crypto' + + it('should return the app name if is not an URL', () => { + expect(gtm.normalizeAppName(FAKE_SAFE_APP_NAME)).toBe(FAKE_SAFE_APP_NAME) + }) - it('should strip the querystring or hash when is an URL', () => { - expect(normalizeAppName(FAKE_DOMAIN)).toBe(FAKE_DOMAIN) - expect(normalizeAppName(`${FAKE_DOMAIN}?q1=query1&q2=query2`)).toBe(FAKE_DOMAIN) - expect(normalizeAppName(`${FAKE_DOMAIN}#hash`)).toBe(FAKE_DOMAIN) + it('should strip the querystring or hash when is an URL', () => { + expect(gtm.normalizeAppName(FAKE_DOMAIN)).toBe(FAKE_DOMAIN) + expect(gtm.normalizeAppName(`${FAKE_DOMAIN}?q1=query1&q2=query2`)).toBe(FAKE_DOMAIN) + expect(gtm.normalizeAppName(`${FAKE_DOMAIN}#hash`)).toBe(FAKE_DOMAIN) + }) }) }) }) diff --git a/src/services/analytics/events/modals.ts b/src/services/analytics/events/modals.ts index d13418b99c..6d81dd8139 100644 --- a/src/services/analytics/events/modals.ts +++ b/src/services/analytics/events/modals.ts @@ -31,8 +31,8 @@ export const MODALS_EVENTS = { action: 'Estimation', category: MODALS_CATEGORY, }, - EXECUTE_TX: { - action: 'Execute transaction', + TOGGLE_EXECUTE_TX: { + action: 'Toggle execute transaction', category: MODALS_CATEGORY, }, USE_SPENDING_LIMIT: { @@ -44,18 +44,10 @@ export const MODALS_EVENTS = { action: 'Simulate transaction', category: MODALS_CATEGORY, }, - REJECT_TX: { - action: 'Reject transaction', - category: MODALS_CATEGORY, - }, EDIT_APPROVALS: { action: 'Edit approval', category: MODALS_CATEGORY, }, - PROPOSE_TX: { - action: 'Propose transaction', - category: MODALS_CATEGORY, - }, ACCEPT_RISK: { action: 'Accept transaction risk', category: MODALS_CATEGORY, diff --git a/src/services/analytics/events/overview.ts b/src/services/analytics/events/overview.ts index 369dad15eb..9c8dc47202 100644 --- a/src/services/analytics/events/overview.ts +++ b/src/services/analytics/events/overview.ts @@ -96,4 +96,9 @@ export const OVERVIEW_EVENTS = { action: 'Click on SEP5 allocation button', category: OVERVIEW_CATEGORY, }, + SAFE_VIEWED: { + event: EventType.META, + action: 'Safe viewed', + category: OVERVIEW_CATEGORY, + }, } diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts new file mode 100644 index 0000000000..7f33f5676c --- /dev/null +++ b/src/services/analytics/events/transactions.ts @@ -0,0 +1,40 @@ +import { EventType } from '../types' + +export enum TX_TYPES { + // Owner txs + owner_add = 'owner_add', + owner_remove = 'owner_remove', + owner_swap = 'owner_swap', + owner_threshold_change = 'owner_threshold_change', + + // Module txs + guard_remove = 'guard_remove', + module_remove = 'module_remove', + spending_limit_remove = 'spending_limit_remove', + spending_limit_add = 'spending_limit_add', + + // Safe txs + safe_update = 'safe_update', + + // Transfers + transfer_token = 'transfer_token', + transfer_nft = 'transfer_nft', + + // Other + batch = 'batch', + rejection = 'rejection', + typed_message = 'typed_message', + safeapps = 'safeapps', + walletconnect = 'walletconnect', +} + +const TX_CATEGORY = 'transactions' + +export const TX_EVENTS = { + CREATE: { + event: EventType.META, + action: 'Create transaction', + category: TX_CATEGORY, + // label: TX_TYPES, + }, +} diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 6f13652e7f..2a87cbc003 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -22,6 +22,7 @@ import { SAFE_APPS_SDK_CATEGORY } from './events' import { getAbTest } from '../tracking/abTesting' import type { AbTest } from '../tracking/abTesting' import { AppRoutes } from '@/config/routes' +import packageJson from '../../../package.json' type GTMEnvironment = 'LIVE' | 'LATEST' | 'DEVELOPMENT' type GTMEnvironmentArgs = Required> @@ -42,6 +43,7 @@ const GTM_ENV_AUTH: Record = { } const commonEventParams = { + appVersion: packageJson.version, chainId: '', deviceType: DeviceType.DESKTOP, safeAddress: '', diff --git a/src/services/analytics/useGtm.ts b/src/services/analytics/useGtm.ts index 854a63741e..e8a29a93f8 100644 --- a/src/services/analytics/useGtm.ts +++ b/src/services/analytics/useGtm.ts @@ -14,6 +14,7 @@ import { gtmSetDeviceType, gtmSetSafeAddress, gtmSetUserProperty, + gtmTrack, } from '@/services/analytics/gtm' import { useAppSelector } from '@/store' import { CookieType, selectCookies } from '@/store/cookiesSlice' @@ -25,6 +26,7 @@ import { useMediaQuery } from '@mui/material' import { AnalyticsUserProperties, DeviceType } from './types' import useSafeAddress from '@/hooks/useSafeAddress' import useWallet from '@/hooks/wallets/useWallet' +import { OVERVIEW_EVENTS } from './events' const useGtm = () => { const chainId = useChainId() @@ -72,6 +74,10 @@ const useGtm = () => { // Set safe address for all GTM events useEffect(() => { gtmSetSafeAddress(safeAddress) + + if (safeAddress) { + gtmTrack(OVERVIEW_EVENTS.SAFE_VIEWED) + } }, [safeAddress]) // Track page views – anonymized by default. diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx index 4d4ea0ff51..57bc87b834 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -271,6 +271,7 @@ describe('useSafeWalletProvider', () => { ], params: { safeTxGas: 0 }, }, + onSubmit: expect.any(Function), }) expect(resp).toBeInstanceOf(Promise) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx index ae04d26073..289f75a62e 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx @@ -131,23 +131,12 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | code: RpcErrorCode.USER_REJECTED, message: 'User rejected transaction', }) - unsubscribe() } - const unsubscribeSignaturePrepared = txSubscribe( - TxEvent.SAFE_APPS_REQUEST, - async ({ safeAppRequestId, safeTxHash, txId }) => { - if (safeAppRequestId === id) { - const txHash = txId ? pendingTxs.current[txId] : undefined - resolve({ safeTxHash, txHash }) - unsubscribe() - } - }, - ) - - const unsubscribe = () => { + const onSubmit = (txId: string, safeTxHash: string) => { + const txHash = pendingTxs.current[txId] onClose = () => {} - unsubscribeSignaturePrepared() + resolve({ safeTxHash, txHash }) } setTxFlow( @@ -159,6 +148,7 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | txs: transactions, params: params.params, }} + onSubmit={onSubmit} />, onClose, ) diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index 570aa0e0ea..2013ecc84f 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -312,10 +312,11 @@ export const dispatchSafeAppsTx = async ( onboard: OnboardAPI, chainId: SafeInfo['chainId'], txId?: string, -) => { +): Promise => { const sdk = await getSafeSDKWithSigner(onboard, chainId) const safeTxHash = await sdk.getTransactionHash(safeTx) txDispatch(TxEvent.SAFE_APPS_REQUEST, { safeAppRequestId, safeTxHash, txId }) + return safeTxHash } export const dispatchTxRelay = async ( diff --git a/src/services/walletconnect/utils.ts b/src/services/walletconnect/utils.ts index 047e1003ad..bd3f903646 100644 --- a/src/services/walletconnect/utils.ts +++ b/src/services/walletconnect/utils.ts @@ -62,3 +62,8 @@ export const getPeerName = (peer: SessionTypes.Struct['peer'] | ProposalTypes.St export const splitError = (message: string): string[] => { return message.split(/: (.+)/).slice(0, 2) } + +export const isWalletConnectSafeApp = (url: string): boolean => { + const WALLET_CONNECT = /wallet-connect/ + return WALLET_CONNECT.test(url) +}