@rango-dev/widget-embedded 0.61.0 → 0.61.1-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/ConfirmWalletsModal/ConfirmWalletsModal.d.ts.map +1 -1
  3. package/dist/components/CustomDestination/CustomDestination.d.ts.map +1 -1
  4. package/dist/components/Quote/QuoteCostDetails.d.ts.map +1 -1
  5. package/dist/components/Slippage/Slippage.d.ts.map +1 -1
  6. package/dist/components/SwapDetails/SwapDetails.d.ts.map +1 -1
  7. package/dist/containers/Settings/Lists.d.ts.map +1 -1
  8. package/dist/containers/Wallets/useUpdates.d.ts.map +1 -1
  9. package/dist/hooks/useSwapInput.d.ts.map +1 -1
  10. package/dist/hooks/useSyncNotifications/useSyncNotifications.d.ts.map +1 -1
  11. package/dist/hooks/useWalletList.d.ts.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.js.map +4 -4
  14. package/dist/pages/ConfirmSwapPage.d.ts.map +1 -1
  15. package/dist/pages/Home.d.ts.map +1 -1
  16. package/dist/pages/LiquiditySourcePage.d.ts.map +1 -1
  17. package/dist/pages/SelectBlockchainPage.d.ts.map +1 -1
  18. package/dist/pages/SelectSwapItemPage/SelectSwapItemPage.helpers.d.ts +1 -0
  19. package/dist/pages/SelectSwapItemPage/SelectSwapItemPage.helpers.d.ts.map +1 -1
  20. package/dist/pages/SelectSwapItemPage/SelectSwapItemsPage.d.ts.map +1 -1
  21. package/dist/pages/SwapDetailsPage.d.ts.map +1 -1
  22. package/dist/store/slices/wallets.d.ts +3 -1
  23. package/dist/store/slices/wallets.d.ts.map +1 -1
  24. package/dist/types/event.d.ts +160 -6
  25. package/dist/types/event.d.ts.map +1 -1
  26. package/dist/utils/eventPayloads.d.ts +8 -0
  27. package/dist/utils/eventPayloads.d.ts.map +1 -0
  28. package/dist/utils/events.d.ts +8 -5
  29. package/dist/utils/events.d.ts.map +1 -1
  30. package/dist/widget-embedded.build.json +1 -1
  31. package/package.json +2 -2
  32. package/src/components/ConfirmWalletsModal/ConfirmWalletsModal.tsx +43 -1
  33. package/src/components/CustomDestination/CustomDestination.tsx +18 -0
  34. package/src/components/Quote/QuoteCostDetails.tsx +8 -0
  35. package/src/components/Slippage/Slippage.tsx +21 -0
  36. package/src/components/SwapDetails/SwapDetails.tsx +6 -0
  37. package/src/containers/Settings/Lists.tsx +13 -1
  38. package/src/containers/Wallets/useUpdates.ts +3 -1
  39. package/src/hooks/useSwapInput.ts +37 -1
  40. package/src/hooks/useSyncNotifications/useSyncNotifications.ts +33 -1
  41. package/src/hooks/useWalletList.ts +36 -0
  42. package/src/pages/ConfirmSwapPage.tsx +12 -1
  43. package/src/pages/Home.tsx +39 -14
  44. package/src/pages/LiquiditySourcePage.tsx +28 -0
  45. package/src/pages/SelectBlockchainPage.tsx +10 -0
  46. package/src/pages/SelectSwapItemPage/SelectSwapItemPage.helpers.ts +9 -0
  47. package/src/pages/SelectSwapItemPage/SelectSwapItemsPage.tsx +30 -0
  48. package/src/pages/SwapDetailsPage.tsx +6 -0
  49. package/src/store/slices/wallets.ts +10 -4
  50. package/src/types/event.ts +189 -10
  51. package/src/utils/eventPayloads.ts +39 -0
  52. package/src/utils/events.ts +33 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rango-dev/widget-embedded",
3
- "version": "0.61.0",
3
+ "version": "0.61.1-next.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "source": "./src/index.ts",
@@ -66,4 +66,4 @@
66
66
  "publishConfig": {
67
67
  "access": "public"
68
68
  }
69
- }
69
+ }
@@ -19,6 +19,8 @@ import { WIDGET_UI_ID } from '../../constants';
19
19
  import { getQuoteErrorMessage } from '../../constants/errors';
20
20
  import { useAppStore } from '../../store/AppStore';
21
21
  import { useQuoteStore } from '../../store/quote';
22
+ import { UiEventTypes } from '../../types';
23
+ import { emitUiEvent } from '../../utils/events';
22
24
  import { getBlockchainShortNameFor } from '../../utils/meta';
23
25
  import { isConfirmSwapDisabled } from '../../utils/swap';
24
26
  import { getQuoteChains } from '../../utils/wallets';
@@ -211,6 +213,10 @@ export function ConfirmWalletsModal(props: PropTypes) {
211
213
  const lastSelectedWallets = selectableWallets.filter(
212
214
  (wallet) => wallet.selected
213
215
  );
216
+ emitUiEvent({
217
+ type: UiEventTypes.SWAP_WALLETS_CONFIRMED,
218
+ payload: { routeId: selectedQuote?.requestId ?? '' },
219
+ });
214
220
  setWalletsAsSelected(lastSelectedWallets);
215
221
  selectQuoteWallets(lastSelectedWallets);
216
222
  setQuoteWalletConfirmed(true);
@@ -301,6 +307,36 @@ export function ConfirmWalletsModal(props: PropTypes) {
301
307
  }
302
308
  }, [connectedWallets, nextSelectedWallets]);
303
309
 
310
+ const routeId = selectedQuote?.requestId ?? '';
311
+
312
+ useEffect(() => {
313
+ if (!open) {
314
+ return;
315
+ }
316
+ const pendingChains = requiredChains.filter(
317
+ (chain) =>
318
+ !connectedWallets.some(
319
+ (connectedWallet) => connectedWallet.chain === chain
320
+ )
321
+ );
322
+ emitUiEvent({
323
+ type: UiEventTypes.SWAP_WALLETS_MODAL_SHOWN,
324
+ payload: {
325
+ chainsRequired: requiredChains.length,
326
+ chainsPending: pendingChains.length,
327
+ },
328
+ });
329
+ }, [open]);
330
+
331
+ useEffect(() => {
332
+ if (isInsufficientBalanceModalOpen) {
333
+ emitUiEvent({
334
+ type: UiEventTypes.GAS_WARNING_SHOWN,
335
+ payload: { routeId },
336
+ });
337
+ }
338
+ }, [isInsufficientBalanceModalOpen, routeId]);
339
+
304
340
  const modalContainer = document.getElementById(
305
341
  WIDGET_UI_ID.SWAP_BOX_ID
306
342
  ) as HTMLDivElement;
@@ -383,7 +419,13 @@ export function ConfirmWalletsModal(props: PropTypes) {
383
419
  size="large"
384
420
  type="primary"
385
421
  fullWidth
386
- onClick={onConfirmBalance}>
422
+ onClick={() => {
423
+ emitUiEvent({
424
+ type: UiEventTypes.GAS_WARNING_BYPASSED,
425
+ payload: { routeId },
426
+ });
427
+ onConfirmBalance();
428
+ }}>
387
429
  {i18n.t('Proceed anyway')}
388
430
  </Button>
389
431
  </MessageBox>
@@ -16,6 +16,8 @@ import React, { useEffect, useRef } from 'react';
16
16
 
17
17
  import { useAppStore } from '../../store/AppStore';
18
18
  import { useQuoteStore } from '../../store/quote';
19
+ import { UiEventTypes } from '../../types';
20
+ import { emitUiEvent } from '../../utils/events';
19
21
  import {
20
22
  getBlockchainDisplayNameFor,
21
23
  isValidTokenAddress,
@@ -94,6 +96,22 @@ export function CustomDestination(props: PropTypes) {
94
96
  }
95
97
  }, [configDestination]);
96
98
 
99
+ const lastReportedDestination = useRef<string | null>(null);
100
+ useEffect(() => {
101
+ if (!customDestination) {
102
+ lastReportedDestination.current = null;
103
+ return;
104
+ }
105
+ const isValid = open && isValidTokenAddress(blockchain, customDestination);
106
+ if (isValid && lastReportedDestination.current !== customDestination) {
107
+ lastReportedDestination.current = customDestination;
108
+ emitUiEvent({
109
+ type: UiEventTypes.DESTINATION_ADDRESS_SET,
110
+ payload: { destinationChain: blockchain.name },
111
+ });
112
+ }
113
+ }, [customDestination, open, blockchain]);
114
+
97
115
  return (
98
116
  <Container>
99
117
  <CustomCollapsible
@@ -21,7 +21,9 @@ import {
21
21
  USD_VALUE_MAX_DECIMALS,
22
22
  USD_VALUE_MIN_DECIMALS,
23
23
  } from '../../constants/routing';
24
+ import { UiEventTypes } from '../../types';
24
25
  import { getContainer, getExpanded } from '../../utils/common';
26
+ import { emitUiEvent } from '../../utils/events';
25
27
  import { numberToString } from '../../utils/numbers';
26
28
  import { getFeesGroup, getTotalFeesInUsd, getUsdFee } from '../../utils/swap';
27
29
  import { WatermarkedModal } from '../common/WatermarkedModal';
@@ -84,6 +86,12 @@ export function QuoteCostDetails(props: QuoteCostDetailsProps) {
84
86
  showModalFee
85
87
  ? (e) => {
86
88
  e.stopPropagation();
89
+ if (!open && quote) {
90
+ emitUiEvent({
91
+ type: UiEventTypes.ROUTE_FEE_VIEWED,
92
+ payload: { routeId: quote.requestId },
93
+ });
94
+ }
87
95
  setOpen(!open);
88
96
  }
89
97
  : undefined
@@ -12,7 +12,9 @@ import React from 'react';
12
12
 
13
13
  import { MAX_SLIPPAGE, SLIPPAGES } from '../../constants/swapSettings';
14
14
  import { useAppStore } from '../../store/AppStore';
15
+ import { UiEventTypes } from '../../types';
15
16
  import { getContainer } from '../../utils/common';
17
+ import { emitUiEvent } from '../../utils/events';
16
18
  import { getSlippageValidation } from '../../utils/settings';
17
19
  import { isValidCurrencyFormat } from '../../utils/validation';
18
20
 
@@ -31,6 +33,23 @@ export function Slippage() {
31
33
  const slippageValidation =
32
34
  customSlippage !== null ? getSlippageValidation(customSlippage) : null;
33
35
 
36
+ /** Effective slippage before a change, used as the previous value in events. */
37
+ const effectiveSlippage = customSlippage ?? slippage;
38
+
39
+ const emitSlippageChanged = (previousValue: number, newValue: number) => {
40
+ if (previousValue === newValue) {
41
+ return;
42
+ }
43
+ emitUiEvent({
44
+ type: UiEventTypes.SETTINGS_CHANGED,
45
+ payload: {
46
+ setting: 'slippage',
47
+ previousValue,
48
+ newValue,
49
+ },
50
+ });
51
+ };
52
+
34
53
  const onSlippageValueChange = (
35
54
  event: React.ChangeEvent<HTMLInputElement>
36
55
  ) => {
@@ -43,10 +62,12 @@ export function Slippage() {
43
62
  if (parsedValue > MAX_SLIPPAGE) {
44
63
  slippage = MAX_SLIPPAGE;
45
64
  }
65
+ emitSlippageChanged(effectiveSlippage, slippage);
46
66
  setCustomSlippage(slippage);
47
67
  };
48
68
 
49
69
  const onClickSlippageChip = (slippageItem: number) => {
70
+ emitSlippageChanged(effectiveSlippage, slippageItem);
50
71
  if (customSlippage !== null) {
51
72
  setCustomSlippage(null);
52
73
  }
@@ -34,7 +34,9 @@ import {
34
34
  import { useAppStore } from '../../store/AppStore';
35
35
  import { useNotificationStore } from '../../store/notification';
36
36
  import { useQuoteStore } from '../../store/quote';
37
+ import { UiEventTypes } from '../../types';
37
38
  import { getContainer } from '../../utils/common';
39
+ import { emitUiEvent } from '../../utils/events';
38
40
  import {
39
41
  numberToString,
40
42
  roundedSecondsToString,
@@ -404,6 +406,10 @@ export function SwapDetails(props: SwapDetailsProps) {
404
406
  type="primary"
405
407
  size="large"
406
408
  onClick={() => {
409
+ emitUiEvent({
410
+ type: UiEventTypes.SWAP_RETRIED,
411
+ payload: { routeId: swap.requestId },
412
+ });
407
413
  const swapInput = createRetryQuote(swap, blockchains, findToken);
408
414
  retry(swapInput);
409
415
  setTimeout(() => {
@@ -32,7 +32,9 @@ import { navigationRoutes } from '../../constants/navigationRoutes';
32
32
  import { ThemeModeEnum } from '../../constants/settings';
33
33
  import { useLanguage } from '../../hooks/useLanguage';
34
34
  import { useAppStore } from '../../store/AppStore';
35
+ import { UiEventTypes } from '../../types';
35
36
  import { getContainer } from '../../utils/common';
37
+ import { emitUiEvent } from '../../utils/events';
36
38
  import { getUniqueSwappersGroups, isFeatureHidden } from '../../utils/settings';
37
39
 
38
40
  const ThemeSection = styled('div', {
@@ -257,7 +259,17 @@ export function SettingsLists() {
257
259
  ),
258
260
  start: <InfinityIcon color="gray" size={16} />,
259
261
  end: <Switch checked={infiniteApprove} />,
260
- onClick: toggleInfiniteApprove,
262
+ onClick: () => {
263
+ emitUiEvent({
264
+ type: UiEventTypes.SETTINGS_CHANGED,
265
+ payload: {
266
+ setting: 'infiniteApproval',
267
+ previousValue: infiniteApprove,
268
+ newValue: !infiniteApprove,
269
+ },
270
+ });
271
+ toggleInfiniteApprove();
272
+ },
261
273
  };
262
274
 
263
275
  const themeItem = {
@@ -77,7 +77,9 @@ export function useUpdates(params: UseUpdatesParams): UseUpdates {
77
77
  );
78
78
 
79
79
  if (data.length) {
80
- void newWalletConnected(data, info.namespace, state.derivationPath);
80
+ void newWalletConnected(data, info.namespace, state.derivationPath, {
81
+ walletName: type,
82
+ });
81
83
  }
82
84
  };
83
85
 
@@ -5,8 +5,10 @@ import { useCallback, useEffect, useRef, useState } from 'react';
5
5
 
6
6
  import { useAppStore } from '../store/AppStore';
7
7
  import { useQuoteStore } from '../store/quote';
8
- import { QuoteErrorType } from '../types';
8
+ import { QuoteErrorType, QuoteEventTypes } from '../types';
9
9
  import { debounce } from '../utils/common';
10
+ import { buildSwapEstimatePayload } from '../utils/eventPayloads';
11
+ import { emitQuoteEvent } from '../utils/events';
10
12
  import { isPositiveNumber } from '../utils/numbers';
11
13
  import {
12
14
  generateQuoteWarnings,
@@ -160,6 +162,16 @@ export function useSwapInput({
160
162
  updateQuotePartialState('quotes', res);
161
163
  setSelectedQuote(quote);
162
164
 
165
+ if (res.results?.length && quote) {
166
+ emitQuoteEvent({
167
+ type: QuoteEventTypes.ROUTE_REQUESTED,
168
+ payload: {
169
+ ...buildSwapEstimatePayload(quote),
170
+ routeCount: res.results.length,
171
+ },
172
+ });
173
+ }
174
+
163
175
  throwErrorIfResponseIsNotValid({
164
176
  diagnosisMessages: res.diagnosisMessages,
165
177
  requestId: quote?.requestId || '',
@@ -176,6 +188,30 @@ export function useSwapInput({
176
188
  })
177
189
  .catch((error) => {
178
190
  const quoteError = handleQuoteErrors(error);
191
+ if (quoteError.type === QuoteErrorType.NO_RESULT) {
192
+ emitQuoteEvent({
193
+ type: QuoteEventTypes.ROUTE_NOT_FOUND,
194
+ payload: {
195
+ sourceChain: fromToken.blockchain,
196
+ destinationChain: toToken.blockchain,
197
+ sourceTokenSymbol: fromToken.symbol,
198
+ sourceTokenAddress: fromToken.address,
199
+ destinationTokenSymbol: toToken.symbol,
200
+ destinationTokenAddress: toToken.address,
201
+ inputAmountUsd: inputUsdValue ? inputUsdValue.toNumber() : null,
202
+ },
203
+ });
204
+ } else if (quoteError.type === QuoteErrorType.REQUEST_FAILED) {
205
+ emitQuoteEvent({
206
+ type: QuoteEventTypes.ROUTE_FETCH_FAILED,
207
+ payload: {
208
+ sourceChain: fromToken.blockchain,
209
+ destinationChain: toToken.blockchain,
210
+
211
+ errorCode: String(error?.response?.status ?? ''),
212
+ },
213
+ });
214
+ }
179
215
  if (
180
216
  quoteError.type === QuoteErrorType.NO_RESULT ||
181
217
  quoteError.type === QuoteErrorType.REQUEST_FAILED
@@ -2,6 +2,8 @@ import { useManager } from '@rango-dev/queue-manager-react';
2
2
  import { useEffect } from 'react';
3
3
 
4
4
  import { useNotificationStore } from '../../store/notification';
5
+ import { UiEventTypes } from '../../types';
6
+ import { emitUiEvent } from '../../utils/events';
5
7
  import { getPendingSwaps } from '../../utils/queue';
6
8
 
7
9
  export function useSyncNotifications() {
@@ -15,7 +17,37 @@ export function useSyncNotifications() {
15
17
  !isSynced;
16
18
 
17
19
  if (shouldSyncNotifications) {
18
- syncNotifications(getPendingSwaps(manager));
20
+ const pendingSwaps = getPendingSwaps(manager);
21
+ syncNotifications(pendingSwaps);
22
+
23
+ pendingSwaps.forEach(({ swap }) => {
24
+ const isInProgressMultiStep =
25
+ swap.status === 'running' && swap.steps.length > 1;
26
+ if (!isInProgressMultiStep) {
27
+ return;
28
+ }
29
+ /*
30
+ * The current step is the first one that hasn't succeeded yet. A
31
+ * running swap should always have one; if not (all steps already
32
+ * succeeded), the state is inconsistent/transient, so skip it rather
33
+ * than report a misleading step.
34
+ */
35
+ const currentStepIndex = swap.steps.findIndex(
36
+ (step) => step.status !== 'success'
37
+ );
38
+ if (currentStepIndex === -1) {
39
+ return;
40
+ }
41
+
42
+ emitUiEvent({
43
+ type: UiEventTypes.SWAP_RESUMED,
44
+ payload: {
45
+ routeId: swap.requestId,
46
+ stepCount: swap.steps.length,
47
+ stepNumber: currentStepIndex + 1,
48
+ },
49
+ });
50
+ });
19
51
  }
20
52
  }, [
21
53
  useNotificationStore.persist.hasHydrated(),
@@ -7,6 +7,7 @@ import { detectMobileScreens, WalletTypes } from '@rango-dev/wallets-shared';
7
7
  import { useCallback, useEffect } from 'react';
8
8
 
9
9
  import { useAppStore } from '../store/AppStore';
10
+ import { emitWalletDetected } from '../utils/events';
10
11
  import { configWalletsToWalletName } from '../utils/providers';
11
12
  import {
12
13
  hashWalletsState,
@@ -16,6 +17,26 @@ import {
16
17
 
17
18
  import { useStatefulConnect } from './useStatefulConnect/useStatefulConnect';
18
19
 
20
+ /**
21
+ * Wallet types we've already reported as detected this session. Module-scoped so
22
+ * the `walletDetected` event fires at most once per provider regardless of how
23
+ * often the wallet list re-renders or remounts.
24
+ */
25
+ const detectedWalletsReported = new Set<string>();
26
+
27
+ /**
28
+ * Wallet types that aren't browser-injected extensions (hardware wallets,
29
+ * WalletConnect, TON Connect). Their presence doesn't indicate an installed
30
+ * wallet, so they're excluded from the `walletDetected` signal.
31
+ */
32
+ const NON_INJECTED_WALLET_TYPES = new Set<string>([
33
+ WalletTypes.LEDGER,
34
+ WalletTypes.TREZOR,
35
+ WalletTypes.WALLET_CONNECT_2,
36
+ WalletTypes.TON_CONNECT,
37
+ WalletTypes.DEFAULT,
38
+ ]);
39
+
19
40
  interface Params {
20
41
  chain?: string;
21
42
  }
@@ -59,6 +80,21 @@ export function useWalletList(params?: Params): API {
59
80
 
60
81
  const sortedWallets = sortWalletsBasedOnConnectionState(wallets);
61
82
 
83
+ useEffect(() => {
84
+ wallets.forEach((wallet) => {
85
+ if (NON_INJECTED_WALLET_TYPES.has(wallet.type)) {
86
+ return;
87
+ }
88
+ const isDetected = wallet.state !== WalletState.NOT_INSTALLED;
89
+ if (isDetected && !detectedWalletsReported.has(wallet.type)) {
90
+ detectedWalletsReported.add(wallet.type);
91
+ emitWalletDetected({
92
+ walletName: wallet.title,
93
+ });
94
+ }
95
+ });
96
+ }, [hashWalletsState(wallets)]);
97
+
62
98
  const terminateConnectingWallets = useCallback(() => {
63
99
  const connectingWallets =
64
100
  wallets?.filter((wallet) => wallet.state === WalletState.CONNECTING) ||
@@ -28,7 +28,9 @@ import { useConfirmSwap } from '../hooks/useConfirmSwap';
28
28
  import { useAppStore } from '../store/AppStore';
29
29
  import { useQuoteStore } from '../store/quote';
30
30
  import { useUiStore } from '../store/ui';
31
- import { QuoteErrorType, QuoteWarningType } from '../types';
31
+ import { QuoteErrorType, QuoteWarningType, UiEventTypes } from '../types';
32
+ import { buildSwapEstimatePayload } from '../utils/eventPayloads';
33
+ import { emitUiEvent } from '../utils/events';
32
34
  import { isQuoteWarningConfirmationRequired } from '../utils/quote';
33
35
  import { joinList } from '../utils/ui';
34
36
 
@@ -142,6 +144,15 @@ export function ConfirmSwapPage() {
142
144
  if (shouldShowWarningModal) {
143
145
  setShowQuoteWarningModal(true);
144
146
  } else {
147
+ if (selectedQuote) {
148
+ emitUiEvent({
149
+ type: UiEventTypes.SWAP_STARTED,
150
+ payload: {
151
+ ...buildSwapEstimatePayload(selectedQuote),
152
+ stepCount: selectedQuote.swaps.length,
153
+ },
154
+ });
155
+ }
145
156
  await onConfirm();
146
157
  }
147
158
  };
@@ -23,9 +23,14 @@ import { useSwapMode } from '../hooks/useSwapMode';
23
23
  import { useAppStore } from '../store/AppStore';
24
24
  import { useQuoteStore } from '../store/quote';
25
25
  import { useUiStore } from '../store/ui';
26
- import { UiEventTypes } from '../types';
26
+ import { QuoteEventTypes, UiEventTypes } from '../types';
27
27
  import { isVariantExpandable } from '../utils/configs';
28
- import { emitPreventableEvent } from '../utils/events';
28
+ import { buildSwapEstimatePayload } from '../utils/eventPayloads';
29
+ import {
30
+ emitPreventableEvent,
31
+ emitQuoteEvent,
32
+ emitUiEvent,
33
+ } from '../utils/events';
29
34
  import { getSlippageValidation } from '../utils/settings';
30
35
  import { getSwapButtonState, isTokensIdentical } from '../utils/swap';
31
36
 
@@ -133,6 +138,18 @@ export function Home() {
133
138
  };
134
139
  const onClickOnQuote = (quote: SelectedQuote) => {
135
140
  if (selectedQuote?.requestId !== quote.requestId) {
141
+ if (selectedQuote) {
142
+ emitQuoteEvent({
143
+ type: QuoteEventTypes.ROUTE_CHANGED,
144
+ payload: {
145
+ sourceChain: quote.swaps[0]?.from.blockchain ?? '',
146
+ destinationChain:
147
+ quote.swaps[quote.swaps.length - 1]?.to.blockchain ?? '',
148
+ fromRouteId: selectedQuote.requestId,
149
+ toRouteId: quote.requestId,
150
+ },
151
+ });
152
+ }
136
153
  setShowQuoteWarningModal(false);
137
154
  setSelectedQuote(quote);
138
155
  }
@@ -171,6 +188,25 @@ export function Home() {
171
188
  setIsVisibleExpanded(hasInputs);
172
189
  }, [hasInputs]);
173
190
 
191
+ const handleSwapButtonClick = () => {
192
+ if (swapButtonState.action === 'connect-wallet') {
193
+ emitPreventableEvent({ type: UiEventTypes.CLICK_CONNECT_WALLET }, () => {
194
+ onHandleNavigation(navigationRoutes.wallets);
195
+ });
196
+ } else if (swapButtonState.action === 'confirm-warning') {
197
+ setShowQuoteWarningModal(true);
198
+ } else if (selectedQuote) {
199
+ emitUiEvent({
200
+ type: UiEventTypes.SWAP_INITIATED,
201
+ payload: {
202
+ ...buildSwapEstimatePayload(selectedQuote),
203
+ walletConnected: connectedWallets.length > 0,
204
+ },
205
+ });
206
+ onHandleNavigation(navigationRoutes.confirmSwap);
207
+ }
208
+ };
209
+
174
210
  return (
175
211
  <MainContainer>
176
212
  <Layout
@@ -186,18 +222,7 @@ export function Home() {
186
222
  swapButtonState.action === 'confirm-warning' && <WarningIcon />
187
223
  }
188
224
  fullWidth
189
- onClick={() => {
190
- if (swapButtonState.action === 'connect-wallet') {
191
- emitPreventableEvent(
192
- { type: UiEventTypes.CLICK_CONNECT_WALLET },
193
- () => onHandleNavigation(navigationRoutes.wallets)
194
- );
195
- } else if (swapButtonState.action === 'confirm-warning') {
196
- setShowQuoteWarningModal(true);
197
- } else {
198
- onHandleNavigation(navigationRoutes.confirmSwap);
199
- }
200
- }}>
225
+ onClick={handleSwapButtonClick}>
201
226
  {swapButtonState.title}
202
227
  </Button>
203
228
  }
@@ -21,6 +21,8 @@ import {
21
21
  NotFoundContainer,
22
22
  } from '../components/SettingsContainer';
23
23
  import { useAppStore } from '../store/AppStore';
24
+ import { type LiquiditySourceType, UiEventTypes } from '../types';
25
+ import { emitUiEvent } from '../utils/events';
24
26
  import { containsText } from '../utils/numbers';
25
27
  import { replaceSpacesWithDash } from '../utils/sanitizers';
26
28
  import { getUniqueSwappersGroups } from '../utils/settings';
@@ -56,7 +58,24 @@ export function LiquiditySourcePage({ sourceType }: PropTypes) {
56
58
  liquiditySources.length ===
57
59
  liquiditySources.filter((sourceItem) => sourceItem.selected).length;
58
60
 
61
+ const liquiditySourceType: LiquiditySourceType =
62
+ sourceType === 'Exchanges' ? 'exchanges' : 'bridges';
63
+
59
64
  const toggleAllSources = () => {
65
+ // Selecting all enables every source; deselecting all disables them.
66
+ const enabled = !hasSelectAll;
67
+ emitUiEvent({
68
+ type: UiEventTypes.SETTINGS_CHANGED,
69
+ payload: {
70
+ setting: 'liquiditySource',
71
+ sourceType: liquiditySourceType,
72
+ changeType: enabled ? 'selectAll' : 'deselectAll',
73
+ previousSelectedCount: liquiditySources.filter(
74
+ (sourceItem) => sourceItem.selected
75
+ ).length,
76
+ totalCount: liquiditySources.length,
77
+ },
78
+ });
60
79
  liquiditySources.forEach((sourceItem) => {
61
80
  if (hasSelectAll) {
62
81
  toggleLiquiditySource(sourceItem.groupTitle);
@@ -79,6 +98,15 @@ export function LiquiditySourcePage({ sourceType }: PropTypes) {
79
98
  start: <Image src={logo} size={22} type="circular" />,
80
99
  onClick: () => {
81
100
  if (!campaignMode) {
101
+ emitUiEvent({
102
+ type: UiEventTypes.SETTINGS_CHANGED,
103
+ payload: {
104
+ setting: 'liquiditySource',
105
+ sourceType: liquiditySourceType,
106
+ changeType: 'individual',
107
+ sources: [{ id: groupTitle, enabled: !selected }],
108
+ },
109
+ });
82
110
  toggleLiquiditySource(groupTitle);
83
111
  }
84
112
  },
@@ -16,7 +16,9 @@ import { useNavigateBack } from '../hooks/useNavigateBack';
16
16
  import { useSwapMode } from '../hooks/useSwapMode';
17
17
  import { useAppStore } from '../store/AppStore';
18
18
  import { useQuoteStore } from '../store/quote';
19
+ import { UiEventTypes } from '../types';
19
20
  import { filterBlockchainsWithAtLeastOneToken } from '../utils/common';
21
+ import { emitUiEvent } from '../utils/events';
20
22
 
21
23
  interface PropTypes {
22
24
  type: 'source' | 'destination' | 'custom-token';
@@ -51,6 +53,14 @@ export function SelectBlockchainPage(props: PropTypes) {
51
53
  if (type === 'custom-token') {
52
54
  navigate(`..?blockchain=${blockchain.name}`, { replace: true });
53
55
  } else {
56
+ emitUiEvent({
57
+ type: UiEventTypes.CHAIN_FILTER_APPLIED,
58
+ payload: {
59
+ side: type === 'source' ? 'from' : 'to',
60
+ chain: blockchain.name,
61
+ filterSource: 'expanded',
62
+ },
63
+ });
54
64
  if (type === 'source') {
55
65
  setFromBlockchain(blockchain);
56
66
  } else {
@@ -7,6 +7,15 @@ import {
7
7
  MIN_SEARCH_LENGTH,
8
8
  } from './SelectSwapItemPage.constants';
9
9
 
10
+ export function getTokenSelectionMethod(
11
+ searchedFor: string
12
+ ): 'search' | 'default ' {
13
+ if (searchedFor.trim()) {
14
+ return 'search';
15
+ }
16
+ return 'default ';
17
+ }
18
+
10
19
  export function shouldSearchForCustomTokens(
11
20
  metaSearchResultTokens: Token[],
12
21
  query: string,
@@ -14,8 +14,11 @@ import { useNavigateBack } from '../../hooks/useNavigateBack';
14
14
  import { useSearchCustomTokens } from '../../hooks/useSearchCustomTokens';
15
15
  import { useAppStore } from '../../store/AppStore';
16
16
  import { useQuoteStore } from '../../store/quote';
17
+ import { UiEventTypes } from '../../types';
18
+ import { emitUiEvent } from '../../utils/events';
17
19
 
18
20
  import {
21
+ getTokenSelectionMethod,
19
22
  prepareTokensList,
20
23
  shouldSearchForCustomTokens,
21
24
  } from './SelectSwapItemPage.helpers';
@@ -82,6 +85,31 @@ export function SelectSwapItemsPage(props: PropTypes) {
82
85
  setToToken({ token, meta: { blockchains } });
83
86
  }
84
87
  };
88
+
89
+ const side = type === 'source' ? 'from' : 'to';
90
+
91
+ const emitChainFilterApplied = (blockchain: BlockchainMeta) => {
92
+ emitUiEvent({
93
+ type: UiEventTypes.CHAIN_FILTER_APPLIED,
94
+ payload: { side, chain: blockchain.name, filterSource: 'featured' },
95
+ });
96
+ };
97
+
98
+ const emitTokenSelected = (token: Token) => {
99
+ emitUiEvent({
100
+ type: UiEventTypes.TOKEN_SELECTED,
101
+ payload: {
102
+ side,
103
+ tokenName: token.name,
104
+ tokenSymbol: token.symbol,
105
+ tokenAddress: token.address,
106
+ chain: token.blockchain,
107
+ selectionMethod: getTokenSelectionMethod(searchedFor),
108
+ activeChainFilter: selectedBlockchainName || 'all',
109
+ },
110
+ });
111
+ };
112
+
85
113
  const types = {
86
114
  source: i18n.t('Source'),
87
115
  destination: i18n.t('Destination'),
@@ -111,6 +139,7 @@ export function SelectSwapItemsPage(props: PropTypes) {
111
139
  blockchain={type === 'source' ? fromBlockchain : toBlockchain}
112
140
  onMoreClick={() => navigate(navigationRoutes.blockchains)}
113
141
  onChange={(blockchain) => {
142
+ emitChainFilterApplied(blockchain);
114
143
  updateBlockchain(blockchain);
115
144
  }}
116
145
  />
@@ -142,6 +171,7 @@ export function SelectSwapItemsPage(props: PropTypes) {
142
171
  searchedFor={searchedFor}
143
172
  type={type}
144
173
  onChange={(token) => {
174
+ emitTokenSelected(token);
145
175
  updateToken(token);
146
176
 
147
177
  const tokenBlockchain = blockchains.find(