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 ;
+ }}
+
+
+