Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add input_amount to events #451

Merged
merged 13 commits into from
Feb 20, 2025
2 changes: 1 addition & 1 deletion src/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, JSX } from 'react';
import { FC } from 'react';
import { create } from 'zustand';
import { motion, AnimatePresence } from 'motion/react';

Expand Down
1 change: 1 addition & 0 deletions src/components/FeeComparison/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function FeeProviderRow({
targetAssetSymbol,
providerPrice,
vortexPrice,
trackQuote,
error,
]);

Expand Down
2 changes: 1 addition & 1 deletion src/components/InputKeys/SelectionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent, useState } from 'react';
import { useState } from 'react';
import { InputTokenType, OutputTokenType } from '../../constants/tokenConfig';
import { Dialog } from '../Dialog';
import { Skeleton } from '../Skeleton';
Expand Down
9 changes: 6 additions & 3 deletions src/components/Nabla/useSwapForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import {
getBaseOutputTokenDetails,
getInputTokenDetails,
InputTokenType,
isStellarOutputToken,
OutputTokenType,
} from '../../constants/tokenConfig';
import { debounce } from '../../helpers/function';
import { storageService } from '../../services/storage/local';
import schema, { SwapFormValues } from './schema';
import { getCaseSensitiveNetwork } from '../../helpers/networks';
import { useNetwork } from '../../contexts/network';
import { useFormStoreActions } from '../../stores/formStore';

type SwapSettings = {
from: string;
Expand All @@ -41,6 +41,7 @@ export const useSwapForm = () => {
const [isTokenSelectModalVisible, setIsTokenSelectModalVisible] = useState(false);
const [tokenSelectModalType, setTokenModalType] = useState<TokenSelectType>('from');
const { selectedNetwork, setSelectedNetwork } = useNetwork();
const { setFromAmount } = useFormStoreActions();

const initialState = useMemo(() => {
const searchParams = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -130,11 +131,13 @@ export const useSwapForm = () => {

const fromAmount: Big | undefined = useMemo(() => {
try {
return new Big(fromAmountString);
const fromAmount = new Big(fromAmountString);
setFromAmount(fromAmount);
return fromAmount;
} catch {
return undefined;
}
}, [fromAmountString]);
}, [fromAmountString, setFromAmount]);

const openTokenSelectModal = useCallback((type: TokenSelectType) => {
setTokenModalType(type);
Expand Down
16 changes: 12 additions & 4 deletions src/contexts/events.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createContext } from 'react';
import { PropsWithChildren, useCallback, useContext, useEffect, useRef } from 'react';
import Big from 'big.js';
import * as Sentry from '@sentry/react';
import { getBaseOutputTokenDetails, getInputTokenDetails } from '../constants/tokenConfig';
import { OfframpingState } from '../services/offrampingFlow';
import { calculateTotalReceive } from '../components/FeeCollapse';
Expand All @@ -11,6 +10,7 @@ import { getNetworkId, isNetworkEVM, Networks } from '../helpers/networks';
import { LocalStorageKeys } from '../hooks/useLocalStorage';
import { storageService } from '../services/storage/local';
import { useNetwork } from './network';
import { useFromAmount } from '../stores/formStore';

declare global {
interface Window {
Expand All @@ -19,7 +19,6 @@ declare global {
}

const UNIQUE_EVENT_TYPES: TrackableEvent['event'][] = [
'amount_type',
'click_details',
'click_support',
'transaction_confirmation',
Expand All @@ -33,7 +32,8 @@ const UNIQUE_EVENT_TYPES: TrackableEvent['event'][] = [
];

export interface AmountTypeEvent {
event: `amount_type`;
event: 'amount_type';
input_amount: string;
}

export interface ClickDetailsEvent {
Expand All @@ -43,7 +43,9 @@ export interface ClickDetailsEvent {
export interface WalletConnectEvent {
event: 'wallet_connect';
wallet_action: 'connect' | 'disconnect' | 'change';
input_amount?: string;
account_address?: string;
network_selected?: string;
}

export interface OfframpingParameters {
Expand Down Expand Up @@ -105,6 +107,7 @@ export interface NetworkChangeEvent {

export interface FormErrorEvent {
event: 'form_error';
input_amount: string;
error_message:
| 'insufficient_balance'
| 'insufficient_liquidity'
Expand Down Expand Up @@ -149,6 +152,7 @@ const useEvents = () => {
const previousChainId = useRef<number | undefined>(undefined);
const firstRender = useRef(true);
const { selectedNetwork } = useNetwork();
const fromAmount = useFromAmount();

const scheduledQuotes = useRef<
| {
Expand Down Expand Up @@ -279,12 +283,16 @@ const useEvents = () => {
event: 'wallet_connect',
wallet_action: 'disconnect',
account_address: previous,
input_amount: fromAmount ? fromAmount.toString() : '0',
network_selected: getNetworkId(selectedNetwork).toString(),
});
} else if (wasChanged) {
trackEvent({
event: 'wallet_connect',
wallet_action: wasConnected ? 'change' : 'connect',
account_address: address,
input_amount: fromAmount ? fromAmount.toString() : '0',
network_selected: getNetworkId(selectedNetwork).toString(),
});
}

Expand All @@ -293,7 +301,7 @@ const useEvents = () => {
} else {
storageService.remove(storageKey);
}
}, [selectedNetwork, address, trackEvent]);
}, [fromAmount, selectedNetwork, address, trackEvent]);

return {
trackEvent,
Expand Down
19 changes: 16 additions & 3 deletions src/hooks/nabla/useTokenAmountOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
getBaseOutputTokenDetails,
getInputTokenDetailsOrDefault,
InputTokenType,
isStellarOutputToken,
OutputTokenType,
} from '../../constants/tokenConfig';
import { Networks } from '../../helpers/networks';
Expand Down Expand Up @@ -127,7 +126,11 @@ export function useTokenOutAmount({
},
parseError: (error) => {
const insufficientLiquidityMessage = () => {
trackEvent({ event: 'form_error', error_message: 'insufficient_liquidity' });
trackEvent({
event: 'form_error',
error_message: 'insufficient_liquidity',
input_amount: amountIn ? amountIn : '0',
});
return 'Insufficient liquidity for this exchange. Please try a smaller amount or try again later.';
};

Expand Down Expand Up @@ -161,7 +164,17 @@ export function useTokenOutAmount({
} else {
setError('root', { type: 'custom', message: error });
}
}, [error, isLoading, fetchStatus, initializing, debouncedAmountBigDecimal, fromAmountString, clearErrors, setError]);
}, [
error,
isLoading,
fetchStatus,
initializing,
debouncedAmountBigDecimal,
fromAmountString,
clearErrors,
setError,
debouncedFromAmountString,
]);

const isInputStable = debouncedFromAmountString === fromAmountString;
const stableAmountInUnits = isInputStable ? debouncedFromAmountString : undefined;
Expand Down
1 change: 0 additions & 1 deletion src/pages/progress/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
getInputTokenDetailsOrDefault,
getOutputTokenDetails,
isStellarOutputTokenDetails,
OutputTokenDetailsSpacewalk,
} from '../../constants/tokenConfig';
import { Networks, isNetworkEVM, getNetworkDisplayName } from '../../helpers/networks';
import { OfframpingState } from '../../services/offrampingFlow';
Expand Down
2 changes: 1 addition & 1 deletion src/pages/progress/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const useProgressUpdate = (
}, 100);

return () => clearInterval(progressUpdateInterval);
}, [currentPhase, currentPhaseIndex, setDisplayedPercentage, setShowCheckmark]);
}, [currentPhase, currentPhaseIndex, displayedPercentage, setDisplayedPercentage, setShowCheckmark]);
};

export const OFFRAMPING_PHASE_SECONDS: Record<OfframpingPhase, number> = {
Expand Down
40 changes: 35 additions & 5 deletions src/pages/swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { isNetworkEVM } from '../../helpers/networks';
import { useInputTokenBalance } from '../../hooks/useInputTokenBalance';
import { useTokenOutAmount } from '../../hooks/nabla/useTokenAmountOut';
import { useMainProcess } from '../../hooks/offramp/useMainProcess';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useSwapUrlParams } from './useSwapUrlParams';

import { BaseLayout } from '../../layouts';
Expand Down Expand Up @@ -195,6 +196,23 @@ export const SwapPage = () => {
to,
} = useSwapForm();

// We need to keep track of the amount the user has entered. We use a debounced value to avoid tracking the amount while the user is typing.
const debouncedFromAmount = useDebouncedValue(fromAmount, 1000);
// Tracks if the user has interacted with the input field.
const [fromAmountFieldTouched, setFromAmountFieldTouched] = useState(false);

useEffect(() => {
if (fromAmountFieldTouched) {
// We need this check to avoid tracking the amount for the default value of fromAmount.
if (debouncedFromAmount !== fromAmount) return;

trackEvent({
event: 'amount_type',
input_amount: debouncedFromAmount ? debouncedFromAmount.toString() : '0',
});
}
}, [fromAmountFieldTouched, debouncedFromAmount, fromAmount, trackEvent]);

useSwapUrlParams({ form, feeComparisonRef });

const fromToken = getInputTokenDetailsOrDefault(selectedNetwork, from);
Expand Down Expand Up @@ -295,7 +313,7 @@ export const SwapPage = () => {
onClick={() => openTokenSelectModal('from')}
onChange={() => {
// User interacted with the input field
trackEvent({ event: 'amount_type' });
setFromAmountFieldTouched(true);
// This also enables the quote tracking events
trackQuote.current = true;
}}
Expand All @@ -304,15 +322,19 @@ export const SwapPage = () => {
<UserBalance token={fromToken} onClick={(amount: string) => form.setValue('fromAmount', amount)} />
</>
),
[form, fromToken, openTokenSelectModal, trackEvent],
[form, fromToken, openTokenSelectModal],
);

function getCurrentErrorMessage() {
if (isDisconnected) return;

if (typeof userInputTokenBalance === 'string') {
if (Big(userInputTokenBalance).lt(fromAmount ?? 0)) {
trackEvent({ event: 'form_error', error_message: 'insufficient_balance' });
trackEvent({
event: 'form_error',
error_message: 'insufficient_balance',
input_amount: fromAmount ? fromAmount.toString() : '0',
});
return `Insufficient balance. Your balance is ${userInputTokenBalance} ${fromToken?.assetSymbol}.`;
}
}
Expand All @@ -326,15 +348,23 @@ export const SwapPage = () => {

if (fromAmount && exchangeRate && maxAmountUnits.lt(fromAmount.mul(exchangeRate))) {
console.log(exchangeRate, fromAmount!.mul(exchangeRate).toNumber());
trackEvent({ event: 'form_error', error_message: 'more_than_maximum_withdrawal' });
trackEvent({
event: 'form_error',
error_message: 'more_than_maximum_withdrawal',
input_amount: fromAmount ? fromAmount.toString() : '0',
});
return `Maximum withdrawal amount is ${stringifyBigWithSignificantDecimals(maxAmountUnits, 2)} ${
toToken.fiat.symbol
}.`;
}

if (amountOut !== undefined) {
if (!config.test.overwriteMinimumTransferAmount && minAmountUnits.gt(amountOut)) {
trackEvent({ event: 'form_error', error_message: 'less_than_minimum_withdrawal' });
trackEvent({
event: 'form_error',
error_message: 'less_than_minimum_withdrawal',
input_amount: fromAmount ? fromAmount.toString() : '0',
});
return `Minimum withdrawal amount is ${stringifyBigWithSignificantDecimals(minAmountUnits, 2)} ${
toToken.fiat.symbol
}.`;
Expand Down
37 changes: 37 additions & 0 deletions src/stores/formStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { create } from 'zustand';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pendulum-chain/devs let me know if you like the addition of this state, if you prefer it to be more minimal (only fromAmount) or to avoid this altogether.

Although it adds extra code, I found it less confusing than passing fromAmount to the event context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with keeping it the way you implemented it 👍

import Big from 'big.js';
import { InputTokenDetails, BaseInputTokenDetails } from '../constants/tokenConfig';

interface FormState {
fromAmount?: Big;
fromToken?: InputTokenDetails;
toToken?: BaseInputTokenDetails;
}

interface FormStoreActions {
setFromAmount: (amount?: Big) => void;
setFromToken: (token?: InputTokenDetails) => void;
setToToken: (token?: BaseInputTokenDetails) => void;
}

type FormStore = FormState & {
actions: FormStoreActions;
};

const useFormStore = create<FormStore>((set) => ({
fromAmount: undefined,
fromToken: undefined,
toToken: undefined,

actions: {
setFromAmount: (amount?: Big) => set({ fromAmount: amount }),
setFromToken: (token?: InputTokenDetails) => set({ fromToken: token }),
setToToken: (token?: BaseInputTokenDetails) => set({ toToken: token }),
},
}));

export const useFromAmount = () => useFormStore((state) => state.fromAmount);
export const useFromToken = () => useFormStore((state) => state.fromToken);
export const useToToken = () => useFormStore((state) => state.toToken);

export const useFormStoreActions = () => useFormStore((state) => state.actions);
Loading