diff --git a/index.html b/index.html index 0d8addc4..2eaa699a 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,22 @@ Vortex + + + + + + + + +
diff --git a/src/components/FeeCollapse/index.tsx b/src/components/FeeCollapse/index.tsx index 63158c1e..65de11bd 100644 --- a/src/components/FeeCollapse/index.tsx +++ b/src/components/FeeCollapse/index.tsx @@ -5,6 +5,7 @@ import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import Big from 'big.js'; import { roundDownToSignificantDecimals } from '../../helpers/parseNumbers'; import { OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../../constants/tokenConfig'; +import { useEventsContext } from '../../contexts/events'; const FEES_RATE = 0.05; // 0.5% fee rate @@ -27,8 +28,12 @@ interface CollapseProps { export const FeeCollapse: FC = ({ fromAmount, toAmount, toCurrency }) => { const [isOpen, setIsOpen] = useState(false); + const { trackEvent } = useEventsContext(); - const toggleIsOpen = () => setIsOpen((state) => !state); + const toggleIsOpen = () => { + trackEvent({ event: 'click_details' }); + setIsOpen((state) => !state); + }; const outputToken = toCurrency ? OUTPUT_TOKEN_CONFIG[toCurrency] : undefined; diff --git a/src/components/NumericInput/index.tsx b/src/components/NumericInput/index.tsx index 7e80615e..6de9743b 100644 --- a/src/components/NumericInput/index.tsx +++ b/src/components/NumericInput/index.tsx @@ -1,5 +1,6 @@ import { Input } from 'react-daisyui'; import { UseFormRegisterReturn } from 'react-hook-form'; +import { useEventsContext } from '../../contexts/events'; export function exceedsMaxDecimals(value: unknown, maxDecimals: number) { if (value === undefined || value === null) return true; @@ -53,31 +54,38 @@ export const NumericInput = ({ autoFocus, disabled, disableStyles = false, -}: NumericInputProps) => ( -
- handleOnKeyPress(e, maxDecimals)} - onInput={handleOnInput} - pattern="^[0-9]*[.,]?[0-9]*$" - placeholder="0.0" - readOnly={readOnly} - spellcheck="false" - step="any" - type="text" - inputmode="decimal" - value={defaultValue} - autoFocus={autoFocus} - disabled={disabled} - {...register} - /> -
-); +}: NumericInputProps) => { + const { trackEvent } = useEventsContext(); + + return ( +
+ handleOnKeyPress(e, maxDecimals)} + onInput={(e: KeyboardEvent) => { + trackEvent({ event: 'amount_type' }); + handleOnInput(e); + }} + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder="0.0" + readOnly={readOnly} + spellcheck="false" + step="any" + type="text" + inputmode="decimal" + value={defaultValue} + autoFocus={autoFocus} + disabled={disabled} + {...register} + /> +
+ ); +}; diff --git a/src/contexts/events.tsx b/src/contexts/events.tsx new file mode 100644 index 00000000..d737f5f9 --- /dev/null +++ b/src/contexts/events.tsx @@ -0,0 +1,121 @@ +import { createContext } from 'preact'; +import { PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'preact/compat'; +import { useAccount } from 'wagmi'; +import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; +import { OfframpingState } from '../services/offrampingFlow'; + +declare global { + interface Window { + dataLayer: Record[]; + } +} + +const UNIQUE_EVENT_TYPES = ['amount_type', 'click_details', 'click_support']; + +export interface AmountTypeEvent { + event: `amount_type`; +} + +export interface ClickDetailsEvent { + event: 'click_details'; +} + +export interface WalletConnectEvent { + event: 'wallet_connect'; + wallet_action: 'connect' | 'disconnect' | 'change'; +} + +export interface TransactionEvent { + event: 'transaction_confirmation' | 'kyc_completed' | 'transaction_success' | 'transaction_failure'; + from_asset: string; + to_asset: string; + from_amount: string; + to_amount: string; +} + +export interface ClickSupportEvent { + event: 'click_support'; + transaction_status: 'success' | 'failure'; +} + +export type TrackableEvent = + | AmountTypeEvent + | ClickDetailsEvent + | WalletConnectEvent + | TransactionEvent + | ClickSupportEvent; + +type EventType = TrackableEvent['event']; + +type UseEventsContext = ReturnType; +const useEvents = () => { + const [_, setTrackedEventTypes] = useState>(new Set()); + + const previousAddress = useRef<`0x${string}` | undefined>(undefined); + const { address } = useAccount(); + + const trackEvent = useCallback( + (event: TrackableEvent) => { + setTrackedEventTypes((trackedEventTypes) => { + if (UNIQUE_EVENT_TYPES.includes(event.event)) { + if (trackedEventTypes.has(event.event)) { + return trackedEventTypes; + } else { + trackedEventTypes = new Set(trackedEventTypes); + trackedEventTypes.add(event.event); + } + } + console.log('Push data layer', event); + + window.dataLayer.push(event); + + return trackedEventTypes; + }); + }, + [setTrackedEventTypes], + ); + + useEffect(() => { + const wasConnected = previousAddress.current !== undefined; + const isConnected = address !== undefined; + + if (!isConnected) { + trackEvent({ event: 'wallet_connect', wallet_action: 'disconnect' }); + } else { + trackEvent({ event: 'wallet_connect', wallet_action: wasConnected ? 'change' : 'connect' }); + } + + previousAddress.current = address; + }, [address]); + + return { + trackEvent, + }; +}; + +const Context = createContext(undefined); + +export const useEventsContext = () => { + const contextValue = useContext(Context); + if (contextValue === undefined) { + throw new Error('Context must be inside a Provider'); + } + + return contextValue; +}; + +export function EventsProvider({ children }: PropsWithChildren) { + const useEventsResult = useEvents(); + + return {children}; +} + +export function createTransactionEvent(type: TransactionEvent['event'], state: OfframpingState) { + return { + event: type, + from_asset: INPUT_TOKEN_CONFIG[state.inputTokenType].assetSymbol, + to_asset: OUTPUT_TOKEN_CONFIG[state.outputTokenType].stellarAsset.code.string, + from_amount: state.inputAmount.units, + to_amount: state.outputAmount.units, + }; +} diff --git a/src/hooks/useMainProcess.ts b/src/hooks/useMainProcess.ts index e8397a3f..562c7fe6 100644 --- a/src/hooks/useMainProcess.ts +++ b/src/hooks/useMainProcess.ts @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; // Configs, Types, constants import { createStellarEphemeralSecret, sep24First } from '../services/anchor'; import { ExecutionInput } from '../types'; -import { OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; +import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; import { fetchTomlValues, sep10, sep24Second } from '../services/anchor'; // Utils @@ -20,6 +20,7 @@ import { } from '../services/offrampingFlow'; import { EventStatus, GenericEvent } from '../components/GenericEvent'; import Big from 'big.js'; +import { createTransactionEvent, useEventsContext } from '../contexts/events'; export const useMainProcess = () => { // EXAMPLE mocking states @@ -39,12 +40,19 @@ export const useMainProcess = () => { const [sep24Url, setSep24Url] = useState(undefined); const [sep24Id, setSep24Id] = useState(undefined); const wagmiConfig = useConfig(); + const { trackEvent } = useEventsContext(); const [events, setEvents] = useState([]); const updateHookStateFromState = (state: OfframpingState | undefined) => { setOfframpingPhase(state?.phase); setSep24Id(state?.sep24Id); + + if (state?.phase === 'success') { + trackEvent(createTransactionEvent('transaction_success', state)); + } else if (state?.phase === 'failure') { + trackEvent(createTransactionEvent('transaction_failure', state)); + } }; useEffect(() => { @@ -64,6 +72,13 @@ export const useMainProcess = () => { (async () => { setOfframpingStarted(true); + trackEvent({ + event: 'transaction_confirmation', + from_asset: INPUT_TOKEN_CONFIG[inputTokenType].assetSymbol, + to_asset: OUTPUT_TOKEN_CONFIG[outputTokenType].stellarAsset.code.string, + from_amount: amountInUnits, + to_amount: Big(minAmountOutUnits).round(2, 0).toFixed(2, 0), + }); try { const stellarEphemeralSecret = createStellarEphemeralSecret(); @@ -99,6 +114,8 @@ export const useMainProcess = () => { sepResult: secondSep24Response, }); + trackEvent(createTransactionEvent('kyc_completed', initialState)); + updateHookStateFromState(initialState); } catch (error) { console.error('Some error occurred initializing the offramping process', error); diff --git a/src/main.tsx b/src/main.tsx index efc5752a..fb0083bd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,6 +14,7 @@ import { App } from './app'; import defaultTheme from './theme'; import { GlobalState, GlobalStateContext, GlobalStateProvider } from './GlobalStateProvider'; import { wagmiConfig } from './wagmiConfig'; +import { EventsProvider } from './contexts/events'; const queryClient = new QueryClient(); @@ -24,14 +25,16 @@ render( - - - {(globalState) => { - const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState; - return ; - }} - - + + + + {(globalState) => { + const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState; + return ; + }} + + +