Skip to content

Commit

Permalink
46 integrate google analytics query into the prototype (#86)
Browse files Browse the repository at this point in the history
* Add basic event tracking

* Add tracked events
  • Loading branch information
TorstenStueber authored Aug 8, 2024
1 parent e1088d1 commit 8532087
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 38 deletions.
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
<title>Vortex</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Google Tag Manager -->
<script>
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-T8JZSLD8');
</script>
<!-- End Google Tag Manager -->

<link
href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@400;600&display=swap"
rel="stylesheet"
Expand All @@ -18,6 +34,17 @@
</head>

<body style="background-color: #fff">
<!-- Google Tag Manager (noscript) -->
<noscript
><iframe
src="https://www.googletagmanager.com/ns.html?id=GTM-T8JZSLD8"
height="0"
width="0"
style="display: none; visibility: hidden"
></iframe
></noscript>
<!-- End Google Tag Manager (noscript) -->

<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
7 changes: 6 additions & 1 deletion src/components/FeeCollapse/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,8 +28,12 @@ interface CollapseProps {

export const FeeCollapse: FC<CollapseProps> = ({ 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;

Expand Down
64 changes: 36 additions & 28 deletions src/components/NumericInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,31 +54,38 @@ export const NumericInput = ({
autoFocus,
disabled,
disableStyles = false,
}: NumericInputProps) => (
<div className={disableStyles ? 'flex-grow' : 'flex-grow text-black font-outfit'}>
<Input
autocomplete="off"
autocorrect="off"
autocapitalize="none"
className={
disableStyles
? 'border-0 bg-transparent focus:outline-none px-4 ' + additionalStyle
: 'input-ghost w-full text-lg pl-2 focus:outline-none text-accent-content ' + additionalStyle
}
minlength="1"
onKeyPress={(e: KeyboardEvent) => 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}
/>
</div>
);
}: NumericInputProps) => {
const { trackEvent } = useEventsContext();

return (
<div className={disableStyles ? 'flex-grow' : 'flex-grow text-black font-outfit'}>
<Input
autocomplete="off"
autocorrect="off"
autocapitalize="none"
className={
disableStyles
? 'border-0 bg-transparent focus:outline-none px-4 ' + additionalStyle
: 'input-ghost w-full text-lg pl-2 focus:outline-none text-accent-content ' + additionalStyle
}
minlength="1"
onKeyPress={(e: KeyboardEvent) => 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}
/>
</div>
);
};
121 changes: 121 additions & 0 deletions src/contexts/events.tsx
Original file line number Diff line number Diff line change
@@ -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<string, any>[];
}
}

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<typeof useEvents>;
const useEvents = () => {
const [_, setTrackedEventTypes] = useState<Set<EventType>>(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<UseEventsContext | undefined>(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 <Context.Provider value={useEventsResult}>{children}</Context.Provider>;
}

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,
};
}
19 changes: 18 additions & 1 deletion src/hooks/useMainProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -39,12 +40,19 @@ export const useMainProcess = () => {
const [sep24Url, setSep24Url] = useState<string | undefined>(undefined);
const [sep24Id, setSep24Id] = useState<string | undefined>(undefined);
const wagmiConfig = useConfig();
const { trackEvent } = useEventsContext();

const [events, setEvents] = useState<GenericEvent[]>([]);

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(() => {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 11 additions & 8 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -24,14 +25,16 @@ render(
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<GlobalStateProvider>
<GlobalStateContext.Consumer>
{(globalState) => {
const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState;
return <App />;
}}
</GlobalStateContext.Consumer>
</GlobalStateProvider>
<EventsProvider>
<GlobalStateProvider>
<GlobalStateContext.Consumer>
{(globalState) => {
const { tenantRPC, getThemeName = () => undefined } = globalState as GlobalState;
return <App />;
}}
</GlobalStateContext.Consumer>
</GlobalStateProvider>
</EventsProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
Expand Down

0 comments on commit 8532087

Please sign in to comment.