@rango-dev/widget-embedded 0.30.1 → 0.31.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 (128) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/components/ConfirmWalletsModal/ConfirmWallets.styles.d.ts +1 -653
  3. package/dist/components/ConfirmWalletsModal/ConfirmWallets.styles.d.ts.map +1 -1
  4. package/dist/components/ConfirmWalletsModal/ConfirmWalletsModal.d.ts.map +1 -1
  5. package/dist/components/ConfirmWalletsModal/WalletList.d.ts.map +1 -1
  6. package/dist/components/CustomDestination/CustomDestination.d.ts +4 -0
  7. package/dist/components/CustomDestination/CustomDestination.d.ts.map +1 -0
  8. package/dist/components/CustomDestination/CustomDestination.styles.d.ts +495 -0
  9. package/dist/components/CustomDestination/CustomDestination.styles.d.ts.map +1 -0
  10. package/dist/components/CustomDestination/CustomDestination.types.d.ts +7 -0
  11. package/dist/components/CustomDestination/CustomDestination.types.d.ts.map +1 -0
  12. package/dist/components/FilterSelector/FilterSelector.d.ts +4 -0
  13. package/dist/components/FilterSelector/FilterSelector.d.ts.map +1 -0
  14. package/dist/components/FilterSelector/FilterSelector.style.d.ts +962 -0
  15. package/dist/components/FilterSelector/FilterSelector.style.d.ts.map +1 -0
  16. package/dist/components/FilterSelector/FilterSelector.type.d.ts +14 -0
  17. package/dist/components/FilterSelector/FilterSelector.type.d.ts.map +1 -0
  18. package/dist/components/FilterSelector/FilterSelectorContent.d.ts +4 -0
  19. package/dist/components/FilterSelector/FilterSelectorContent.d.ts.map +1 -0
  20. package/dist/components/FilterSelector/index.d.ts +2 -0
  21. package/dist/components/FilterSelector/index.d.ts.map +1 -0
  22. package/dist/components/Quote/Quote.styles.d.ts +3 -3
  23. package/dist/components/RefreshModal/RefreshModal.styles.d.ts +3 -3
  24. package/dist/components/SearchInput/SearchInput.d.ts.map +1 -1
  25. package/dist/components/SwapDetails/SwapDetails.d.ts.map +1 -1
  26. package/dist/components/SwapDetails/SwapDetails.styles.d.ts.map +1 -1
  27. package/dist/components/SwapDetailsModal/SwapDetailsModal.WalletState.d.ts.map +1 -1
  28. package/dist/components/TokenList/TokenList.d.ts.map +1 -1
  29. package/dist/containers/Inputs/Inputs.d.ts.map +1 -1
  30. package/dist/containers/Wallets/Wallets.d.ts.map +1 -1
  31. package/dist/containers/WidgetInfo/WidgetInfo.d.ts.map +1 -1
  32. package/dist/containers/WidgetInfo/WidgetInfo.types.d.ts +5 -1
  33. package/dist/containers/WidgetInfo/WidgetInfo.types.d.ts.map +1 -1
  34. package/dist/hooks/useObserveBalanceChanges/index.d.ts +2 -0
  35. package/dist/hooks/useObserveBalanceChanges/index.d.ts.map +1 -0
  36. package/dist/hooks/useObserveBalanceChanges/useObserveBalanceChanges.d.ts +4 -0
  37. package/dist/hooks/useObserveBalanceChanges/useObserveBalanceChanges.d.ts.map +1 -0
  38. package/dist/hooks/usePrepareBlockchainList/usePrepareBlockchainList.helpers.d.ts +2 -2
  39. package/dist/hooks/useTheme.d.ts.map +1 -1
  40. package/dist/hooks/useUpdateQuoteInputs/index.d.ts +2 -0
  41. package/dist/hooks/useUpdateQuoteInputs/index.d.ts.map +1 -0
  42. package/dist/hooks/useUpdateQuoteInputs/useUpdateQuoteInputs.d.ts +3 -0
  43. package/dist/hooks/useUpdateQuoteInputs/useUpdateQuoteInputs.d.ts.map +1 -0
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/index.js.map +4 -4
  48. package/dist/pages/HistoryPage.d.ts.map +1 -1
  49. package/dist/pages/Home.d.ts.map +1 -1
  50. package/dist/pages/WalletsPage.d.ts.map +1 -1
  51. package/dist/services/cacheService.d.ts +21 -0
  52. package/dist/services/cacheService.d.ts.map +1 -0
  53. package/dist/store/notification.d.ts.map +1 -1
  54. package/dist/store/slices/config.d.ts +2 -1
  55. package/dist/store/slices/config.d.ts.map +1 -1
  56. package/dist/store/slices/data.d.ts +2 -1
  57. package/dist/store/slices/data.d.ts.map +1 -1
  58. package/dist/store/utils/data.d.ts +20 -0
  59. package/dist/store/utils/data.d.ts.map +1 -0
  60. package/dist/store/utils/data.test.d.ts +2 -0
  61. package/dist/store/utils/data.test.d.ts.map +1 -0
  62. package/dist/store/utils/index.d.ts +2 -0
  63. package/dist/store/utils/index.d.ts.map +1 -0
  64. package/dist/test-utils/fixtures.d.ts +1 -0
  65. package/dist/test-utils/fixtures.d.ts.map +1 -1
  66. package/dist/types/config.d.ts +5 -0
  67. package/dist/types/config.d.ts.map +1 -1
  68. package/dist/types/event.d.ts +21 -15
  69. package/dist/types/event.d.ts.map +1 -1
  70. package/dist/types/notification.d.ts +1 -0
  71. package/dist/types/notification.d.ts.map +1 -1
  72. package/dist/types/quote.d.ts +9 -1
  73. package/dist/types/quote.d.ts.map +1 -1
  74. package/dist/types/wallets.d.ts +1 -1
  75. package/dist/types/wallets.d.ts.map +1 -1
  76. package/dist/utils/events.d.ts +8 -0
  77. package/dist/utils/events.d.ts.map +1 -0
  78. package/dist/utils/settings.d.ts +1 -1
  79. package/dist/utils/swap.d.ts +1 -1
  80. package/dist/utils/swap.d.ts.map +1 -1
  81. package/dist/widget-embedded.build.json +1 -1
  82. package/package.json +10 -10
  83. package/src/components/ConfirmWalletsModal/ConfirmWallets.styles.ts +1 -49
  84. package/src/components/ConfirmWalletsModal/ConfirmWalletsModal.tsx +61 -179
  85. package/src/components/ConfirmWalletsModal/WalletList.tsx +16 -4
  86. package/src/components/CustomDestination/CustomDestination.styles.ts +45 -0
  87. package/src/components/CustomDestination/CustomDestination.tsx +148 -0
  88. package/src/components/CustomDestination/CustomDestination.types.ts +7 -0
  89. package/src/components/FilterSelector/FilterSelector.style.ts +95 -0
  90. package/src/components/FilterSelector/FilterSelector.tsx +42 -0
  91. package/src/components/FilterSelector/FilterSelector.type.ts +15 -0
  92. package/src/components/FilterSelector/FilterSelectorContent.tsx +60 -0
  93. package/src/components/FilterSelector/index.ts +1 -0
  94. package/src/components/SearchInput/SearchInput.tsx +2 -0
  95. package/src/components/SwapDetails/SwapDetails.styles.ts +10 -0
  96. package/src/components/SwapDetails/SwapDetails.tsx +25 -13
  97. package/src/components/SwapDetailsModal/SwapDetailsModal.WalletState.tsx +28 -13
  98. package/src/components/TokenList/TokenList.tsx +8 -2
  99. package/src/containers/Inputs/Inputs.tsx +1 -0
  100. package/src/containers/Wallets/Wallets.tsx +7 -2
  101. package/src/containers/WidgetInfo/WidgetInfo.tsx +26 -0
  102. package/src/containers/WidgetInfo/WidgetInfo.types.ts +5 -1
  103. package/src/hooks/useObserveBalanceChanges/index.ts +1 -0
  104. package/src/hooks/useObserveBalanceChanges/useObserveBalanceChanges.ts +68 -0
  105. package/src/hooks/useTheme.ts +11 -5
  106. package/src/hooks/useUpdateQuoteInputs/index.ts +1 -0
  107. package/src/hooks/useUpdateQuoteInputs/useUpdateQuoteInputs.ts +50 -0
  108. package/src/index.ts +4 -0
  109. package/src/pages/HistoryPage.tsx +180 -26
  110. package/src/pages/Home.tsx +6 -3
  111. package/src/pages/WalletsPage.tsx +29 -13
  112. package/src/services/cacheService.ts +35 -0
  113. package/src/store/notification.ts +13 -1
  114. package/src/store/quote.ts +1 -1
  115. package/src/store/slices/config.ts +39 -1
  116. package/src/store/slices/data.test.ts +84 -0
  117. package/src/store/slices/data.ts +44 -24
  118. package/src/store/utils/data.test.ts +175 -0
  119. package/src/store/utils/data.ts +117 -0
  120. package/src/store/utils/index.ts +1 -0
  121. package/src/test-utils/fixtures.ts +14 -7
  122. package/src/types/config.ts +3 -1
  123. package/src/types/event.ts +32 -16
  124. package/src/types/notification.ts +1 -0
  125. package/src/types/quote.ts +11 -0
  126. package/src/types/wallets.ts +1 -1
  127. package/src/utils/events.ts +27 -0
  128. package/src/utils/swap.ts +4 -4
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  BlockchainAndTokenConfig,
5
5
  QuoteEventData,
6
6
  Tokens,
7
+ UiEventData,
7
8
  WalletEventData,
8
9
  WalletInfoWithExtra,
9
10
  WidgetColors,
@@ -71,6 +72,7 @@ import { widgetEventEmitter } from './services/eventEmitter';
71
72
  import {
72
73
  WidgetEvents as MainEvents,
73
74
  QuoteEventTypes,
75
+ UiEventTypes,
74
76
  WalletEventTypes,
75
77
  WidgetEvents,
76
78
  } from './types';
@@ -108,6 +110,7 @@ export type {
108
110
  WidgetVariant,
109
111
  WalletEventData,
110
112
  QuoteEventData,
113
+ UiEventData,
111
114
  WalletInfoWithExtra,
112
115
  };
113
116
  export {
@@ -133,6 +136,7 @@ export {
133
136
  MainEvents,
134
137
  QuoteEventTypes,
135
138
  WalletEventTypes,
139
+ UiEventTypes,
136
140
  RouteEventType,
137
141
  StepEventType,
138
142
  StepExecutionEventStatus,
@@ -1,15 +1,30 @@
1
- import type { PendingSwap, PendingSwapStep } from 'rango-types';
2
-
3
1
  import { i18n } from '@lingui/core';
4
2
  import { useManager } from '@rango-dev/queue-manager-react';
5
- import { Divider, NotFound, styled } from '@rango-dev/ui';
6
- import React, { useState } from 'react';
3
+ import {
4
+ Button,
5
+ darkTheme,
6
+ Divider,
7
+ MessageBox,
8
+ NotFound,
9
+ styled,
10
+ Typography,
11
+ } from '@rango-dev/ui';
12
+ import {
13
+ type PendingSwap,
14
+ type PendingSwapStep,
15
+ TransactionStatus,
16
+ } from 'rango-types';
17
+ import React, { useMemo, useState } from 'react';
7
18
  import { useNavigate } from 'react-router-dom';
8
19
 
20
+ import { WatermarkedModal } from '../components/common/WatermarkedModal';
21
+ import { FilterSelector } from '../components/FilterSelector';
22
+ import { SuffixContainer } from '../components/HeaderButtons/HeaderButtons.styles';
9
23
  import { HistoryGroupedList } from '../components/HistoryGroupedList';
10
24
  import { NotFoundContainer } from '../components/HistoryGroupedList/HistoryGroupedList.styles';
11
25
  import { Layout, PageContainer } from '../components/Layout';
12
26
  import { SearchInput } from '../components/SearchInput';
27
+ import { getContainer } from '../utils/common';
13
28
  import { groupSwapsByDate } from '../utils/date';
14
29
  import { containsText } from '../utils/numbers';
15
30
  import { getPendingSwaps } from '../utils/queue';
@@ -23,6 +38,33 @@ const HistoryGroupedListContainer = styled('div', {
23
38
  height: '100%',
24
39
  });
25
40
 
41
+ const SearchAndFilterBar = styled('div', {
42
+ display: 'flex',
43
+ justifyContent: 'center',
44
+ alignItems: 'center',
45
+ });
46
+
47
+ const Description = styled('div', {
48
+ '._typography': {
49
+ color: '$neutral700',
50
+ [`.${darkTheme}&`]: {
51
+ color: '$neutral900',
52
+ },
53
+ },
54
+ });
55
+
56
+ const transactionStatusFilters = [
57
+ {
58
+ id: TransactionStatus.SUCCESS,
59
+ title: i18n.t('Complete'),
60
+ },
61
+ {
62
+ id: TransactionStatus.RUNNING,
63
+ title: i18n.t('Running'),
64
+ },
65
+ { id: TransactionStatus.FAILED, title: i18n.t('Failed') },
66
+ ];
67
+
26
68
  const isStepContainsText = (steps: PendingSwapStep[], value: string) => {
27
69
  if (!steps?.length) {
28
70
  return false;
@@ -41,50 +83,116 @@ export function HistoryPage() {
41
83
  const { manager, state } = useManager();
42
84
  const list: PendingSwap[] = getPendingSwaps(manager).map(({ swap }) => swap);
43
85
  const [searchedFor, setSearchedFor] = useState<string>('');
86
+ const [openFilterSelector, setOpenFilterSelector] = useState(false);
44
87
  const loading = !state.loadedFromPersistor;
45
-
88
+ const [filterBy, setFilterBy] = useState('');
89
+ const [openClearModal, setOpenClearModal] = useState(false);
46
90
  const searchHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
47
91
  const value = event.target.value;
48
92
  setSearchedFor(value);
49
93
  };
50
94
 
51
- let filteredList = list;
52
- if (searchedFor) {
53
- filteredList = list.filter(
54
- (swap) =>
55
- containsText(swap.inputAmount, searchedFor) ||
56
- containsText(swap.status, searchedFor) ||
57
- isStepContainsText(swap.steps, searchedFor) ||
58
- containsText(swap.requestId, searchedFor)
59
- );
60
- }
95
+ const filteredList = useMemo(() => {
96
+ if (!searchedFor && !filterBy) {
97
+ return list;
98
+ }
99
+
100
+ return list.filter((swap) => {
101
+ const { inputAmount, status, steps, requestId } = swap;
102
+
103
+ const matchesSearch =
104
+ !searchedFor ||
105
+ containsText(inputAmount, searchedFor) ||
106
+ containsText(status, searchedFor) ||
107
+ isStepContainsText(steps, searchedFor) ||
108
+ containsText(requestId, searchedFor);
109
+
110
+ const matchesFilter = !filterBy || filterBy === status;
111
+
112
+ return matchesSearch && matchesFilter;
113
+ });
114
+ }, [list, searchedFor, filterBy]);
61
115
 
62
116
  const isEmpty = !filteredList?.length && !loading;
63
117
 
118
+ const onCloseModal = () => setOpenClearModal(false);
119
+ const onClear = async () => {
120
+ try {
121
+ await manager?.clearQueue();
122
+
123
+ setOpenClearModal(false);
124
+ } catch (e) {
125
+ console.log(e);
126
+ }
127
+ };
128
+ const isClearButtonDisabled = useMemo(() => {
129
+ return !list.some(
130
+ (swap) =>
131
+ swap.status === TransactionStatus.SUCCESS ||
132
+ swap.status === TransactionStatus.FAILED
133
+ );
134
+ }, [list]);
135
+
64
136
  return (
65
137
  <Layout
66
138
  header={{
67
139
  title: i18n.t('History'),
140
+ suffix: (
141
+ <SuffixContainer>
142
+ <Button
143
+ disabled={isClearButtonDisabled}
144
+ variant="ghost"
145
+ size="xsmall"
146
+ onClick={() => setOpenClearModal(true)}>
147
+ <Typography size="medium" variant="label" color="error">
148
+ {i18n.t('Clear all Txs')}
149
+ </Typography>
150
+ </Button>
151
+ </SuffixContainer>
152
+ ),
68
153
  }}>
69
154
  <PageContainer>
70
- <SearchInput
71
- setValue={setSearchedFor}
72
- fullWidth
73
- variant="contained"
74
- placeholder={i18n.t('Search Transaction')}
75
- autoFocus
76
- onChange={searchHandler}
77
- value={searchedFor}
78
- />
155
+ <SearchAndFilterBar>
156
+ <SearchInput
157
+ setValue={setSearchedFor}
158
+ fullWidth
159
+ variant="contained"
160
+ placeholder={i18n.t('Search Transaction')}
161
+ autoFocus
162
+ onChange={searchHandler}
163
+ style={{ height: 36 }}
164
+ value={searchedFor}
165
+ />
166
+ <Divider size={10} direction="horizontal" />
167
+
168
+ <FilterSelector
169
+ filterBy={filterBy}
170
+ open={openFilterSelector}
171
+ onOpenChange={(open) => setOpenFilterSelector(open)}
172
+ onClickItem={(id) => setFilterBy(id)}
173
+ list={transactionStatusFilters}
174
+ />
175
+ </SearchAndFilterBar>
176
+
79
177
  <Divider size="16" />
80
178
  <HistoryGroupedListContainer>
81
179
  {isEmpty && (
82
180
  <NotFoundContainer>
83
181
  <Divider size={32} />
84
182
  <NotFound
85
- title={i18n.t('No results found')}
183
+ title={
184
+ searchedFor
185
+ ? i18n.t('No results found')
186
+ : i18n.t('No transactions')
187
+ }
188
+ titleColor={!searchedFor ? '$info' : undefined}
189
+ hasIcon={!!searchedFor}
86
190
  description={
87
- searchedFor ? i18n.t('Try using different keywords') : ''
191
+ searchedFor
192
+ ? i18n.t('Try using different keywords')
193
+ : i18n.t(
194
+ 'Your transaction history is stored locally and will appear here after you start a swap'
195
+ )
88
196
  }
89
197
  />
90
198
  </NotFoundContainer>
@@ -99,6 +207,52 @@ export function HistoryPage() {
99
207
  )}
100
208
  </HistoryGroupedListContainer>
101
209
  </PageContainer>
210
+ <WatermarkedModal
211
+ open={openClearModal}
212
+ onClose={onCloseModal}
213
+ container={getContainer()}>
214
+ <Divider size={20} />
215
+ <MessageBox
216
+ type="warning"
217
+ title={i18n.t('Clear History')}
218
+ description={
219
+ <Description>
220
+ <Typography variant="body" size="medium">
221
+ {i18n.t(
222
+ 'This would clear all succeeded/failed transactions from widget. Do you wish to proceed?'
223
+ )}
224
+ </Typography>
225
+ <Divider size={'24'} />
226
+
227
+ <Typography variant="body" size="small">
228
+ {i18n.t(
229
+ 'Hint: This does not clear your transaction history on blockchain. Only hides them in this page.'
230
+ )}
231
+ </Typography>
232
+ </Description>
233
+ }
234
+ />
235
+ <Divider size={40} />
236
+
237
+ <Divider size={10} />
238
+ <Button
239
+ variant="contained"
240
+ type="primary"
241
+ size="large"
242
+ onClick={onClear}>
243
+ {i18n.t('Yes, Clear history')}
244
+ </Button>
245
+ <Divider size={10} />
246
+ <Button
247
+ variant="outlined"
248
+ type="primary"
249
+ size="large"
250
+ onClick={onCloseModal}>
251
+ <Typography variant="title" size="medium" color="primary">
252
+ {i18n.t('No, Cancel')}
253
+ </Typography>
254
+ </Button>
255
+ </WatermarkedModal>
102
256
  </Layout>
103
257
  );
104
258
  }
@@ -19,7 +19,9 @@ import { useAppStore } from '../store/AppStore';
19
19
  import { useQuoteStore } from '../store/quote';
20
20
  import { useUiStore } from '../store/ui';
21
21
  import { useWalletsStore } from '../store/wallets';
22
+ import { UiEventTypes } from '../types';
22
23
  import { isVariantExpandable } from '../utils/configs';
24
+ import { emitPreventableEvent } from '../utils/events';
23
25
  import { getSwapButtonState, isTokensIdentical } from '../utils/swap';
24
26
 
25
27
  const MainContainer = styled('div', {
@@ -115,14 +117,12 @@ export function Home() {
115
117
  navigate(route);
116
118
  }
117
119
  };
118
-
119
120
  const onClickOnQuote = (quote: SelectedQuote) => {
120
121
  if (selectedQuote?.requestId !== quote.requestId) {
121
122
  setShowQuoteWarningModal(false);
122
123
  setSelectedQuote(quote);
123
124
  }
124
125
  };
125
-
126
126
  return (
127
127
  <MainContainer>
128
128
  <Layout
@@ -138,7 +138,10 @@ export function Home() {
138
138
  fullWidth
139
139
  onClick={() => {
140
140
  if (swapButtonState.action === 'connect-wallet') {
141
- onHandleNavigation(navigationRoutes.wallets);
141
+ emitPreventableEvent(
142
+ { type: UiEventTypes.CLICK_CONNECT_WALLET },
143
+ () => onHandleNavigation(navigationRoutes.wallets)
144
+ );
142
145
  } else if (swapButtonState.action === 'confirm-warning') {
143
146
  setShowQuoteWarningModal(true);
144
147
  } else {
@@ -80,12 +80,14 @@ export function WalletsPage() {
80
80
  setSelectedWalletType(type);
81
81
  }, TIME_TO_IGNORE_MODAL);
82
82
  },
83
- onConnect: () => {
83
+ onConnect: (type) => {
84
84
  if (modalTimerId) {
85
85
  clearTimeout(modalTimerId);
86
86
  }
87
87
  setTimeout(() => {
88
- setOpenModal(false);
88
+ if (selectedWalletType === type) {
89
+ setOpenModal(false);
90
+ }
89
91
  }, TIME_TO_CLOSE_MODAL);
90
92
  },
91
93
  });
@@ -126,21 +128,35 @@ export function WalletsPage() {
126
128
 
127
129
  const filteredWallets = filterWalletsByCategory(list, blockchainCategory);
128
130
 
129
- const handleWalletItemClick = (type: string, wallet: WalletInfoWithExtra) => {
131
+ const handleWalletItemClick = (wallet: WalletInfoWithExtra) => {
130
132
  if (isSingleWalletActive(list, config.multiWallets)) {
131
133
  return;
132
134
  }
133
135
 
134
- if (!!wallet.namespaces && wallet.state === WalletState.DISCONNECTED) {
135
- setNamespacesModalState({
136
- open: true,
137
- providerType: type,
138
- providerImage: wallet.image,
139
- availableNamespaces: wallet.namespaces,
140
- singleNamespace: wallet.singleNamespace,
141
- });
136
+ if (
137
+ !!wallet.namespaces?.length &&
138
+ wallet.state === WalletState.DISCONNECTED
139
+ ) {
140
+ if (wallet.namespaces.length > 1) {
141
+ setNamespacesModalState({
142
+ open: true,
143
+ providerType: wallet.type,
144
+ providerImage: wallet.image,
145
+ availableNamespaces: wallet.namespaces,
146
+ singleNamespace: wallet.singleNamespace,
147
+ });
148
+ } else if (wallet.needsDerivationPath) {
149
+ setDerivationPathModalState({
150
+ open: true,
151
+ providerType: wallet.type,
152
+ providerImage: wallet.image,
153
+ namespace: wallet.namespaces[0],
154
+ });
155
+ } else {
156
+ void handleClick(wallet.type);
157
+ }
142
158
  } else {
143
- void handleClick(type);
159
+ void handleClick(wallet.type);
144
160
  }
145
161
  };
146
162
 
@@ -208,7 +224,7 @@ export function WalletsPage() {
208
224
  key={key}
209
225
  {...wallet}
210
226
  container={getContainer()}
211
- onClick={(type) => handleWalletItemClick(type, wallet)}
227
+ onClick={() => handleWalletItemClick(wallet)}
212
228
  isLoading={fetchMetaStatus === 'loading'}
213
229
  disabled={!isActiveTab}
214
230
  />
@@ -0,0 +1,35 @@
1
+ import type { Token } from 'rango-sdk';
2
+
3
+ interface CacheServiceInterface<V> {
4
+ get<K extends keyof V>(key: K): V[K] | undefined;
5
+ set<K extends keyof V>(key: K, value: V[K]): void;
6
+ remove<K extends keyof V>(key: K): void;
7
+ clear(): void;
8
+ }
9
+
10
+ class CacheService<T> implements CacheServiceInterface<T> {
11
+ #cache: Map<string, T[keyof T]> = new Map();
12
+
13
+ get<K extends keyof T>(key: K): T[K] | undefined {
14
+ return this.#cache.get(key as string) as T[K];
15
+ }
16
+
17
+ set<K extends keyof T>(key: K, value: T[K]): void {
18
+ this.#cache.set(key as string, value);
19
+ }
20
+
21
+ remove<K extends keyof T>(key: K): void {
22
+ this.#cache.delete(key as string);
23
+ }
24
+
25
+ clear(): void {
26
+ this.#cache.clear();
27
+ }
28
+ }
29
+
30
+ export type CachedEntries = {
31
+ supportedSourceTokens: Token[];
32
+ supportedDestinationTokens: Token[];
33
+ };
34
+
35
+ export const cacheService = new CacheService<CachedEntries>();
@@ -36,6 +36,7 @@ export const useNotificationStore = createSelectors(
36
36
  creationTime: Date.now(),
37
37
  requestId: route.requestId,
38
38
  route: {
39
+ creationTime: parseInt(route.creationTime),
39
40
  from: {
40
41
  blockchain: fromStep.fromBlockchain,
41
42
  address: fromStep.fromSymbolAddress,
@@ -67,7 +68,18 @@ export const useNotificationStore = createSelectors(
67
68
  getNotifications: () => {
68
69
  const { isSynced, notifications } = get();
69
70
  return isSynced
70
- ? notifications.sort((a, b) => b.creationTime - a.creationTime)
71
+ ? notifications.sort((a, b) => {
72
+ if (a.route.creationTime && !b.route.creationTime) {
73
+ return -1;
74
+ }
75
+ if (!a.route.creationTime && b.route.creationTime) {
76
+ return 1;
77
+ }
78
+ if (!a.route.creationTime && !b.route.creationTime) {
79
+ return b.creationTime - a.creationTime;
80
+ }
81
+ return b.route.creationTime - a.route.creationTime;
82
+ })
71
83
  : [];
72
84
  },
73
85
  syncNotifications: (swaps) => {
@@ -224,7 +224,7 @@ export const useQuoteStore = createSelectors(
224
224
  set((state) => ({
225
225
  inputAmount: amount,
226
226
  ...(!amount && {
227
- outputAmount: new BigNumber(0),
227
+ outputAmount: null,
228
228
  outputUsdValue: new BigNumber(0),
229
229
  selectedQuote: null,
230
230
  }),
@@ -1,7 +1,11 @@
1
+ import type { DataSlice } from './data';
1
2
  import type { SettingsSlice } from './settings';
2
3
  import type { WidgetConfig } from '../../types';
3
4
  import type { StateCreatorWithInitialData } from '../app';
4
5
 
6
+ import { cacheService } from '../../services/cacheService';
7
+ import { matchTokensFromConfigWithMeta } from '../utils';
8
+
5
9
  export const DEFAULT_CONFIG: WidgetConfig = {
6
10
  apiKey: '',
7
11
  title: undefined,
@@ -54,7 +58,7 @@ export interface ConfigSlice {
54
58
 
55
59
  export const createConfigSlice: StateCreatorWithInitialData<
56
60
  WidgetConfig,
57
- ConfigSlice & SettingsSlice,
61
+ ConfigSlice & SettingsSlice & DataSlice,
58
62
  ConfigSlice
59
63
  > = (initialData, set, get) => {
60
64
  return {
@@ -87,6 +91,40 @@ export const createConfigSlice: StateCreatorWithInitialData<
87
91
  // Actions
88
92
  updateConfig: (nextConfig: WidgetConfig) => {
89
93
  const currentConfig = get().config;
94
+ const {
95
+ _tokensMapByTokenHash: tokensMapByTokenHash,
96
+ _tokensMapByBlockchainName: tokensMapByBlockchainName,
97
+ } = get();
98
+
99
+ const supportedSourceTokens = matchTokensFromConfigWithMeta({
100
+ type: 'source',
101
+ config: {
102
+ blockchains: nextConfig.from?.blockchains,
103
+ tokens: nextConfig.from?.tokens,
104
+ },
105
+ meta: {
106
+ tokensMapByBlockchainName,
107
+ tokensMapByTokenHash,
108
+ },
109
+ });
110
+
111
+ const supportedDestinationTokens = matchTokensFromConfigWithMeta({
112
+ type: 'destination',
113
+ config: {
114
+ blockchains: nextConfig.to?.blockchains,
115
+ tokens: nextConfig.to?.tokens,
116
+ },
117
+ meta: {
118
+ tokensMapByBlockchainName,
119
+ tokensMapByTokenHash,
120
+ },
121
+ });
122
+
123
+ cacheService.set('supportedSourceTokens', supportedSourceTokens);
124
+ cacheService.set(
125
+ 'supportedDestinationTokens',
126
+ supportedDestinationTokens
127
+ );
90
128
 
91
129
  set({
92
130
  config: {
@@ -3,6 +3,7 @@ import type { EvmBlockchainMeta, Token } from 'rango-sdk';
3
3
 
4
4
  import { assert, beforeEach, describe, expect, test } from 'vitest';
5
5
 
6
+ import { cacheService } from '../../services/cacheService';
6
7
  import {
7
8
  createEvmBlockchain,
8
9
  createInitialAppStore,
@@ -17,6 +18,7 @@ let customTokens: [Token, Token, Token];
17
18
  let rangoBlockchain: EvmBlockchainMeta;
18
19
 
19
20
  beforeEach(() => {
21
+ cacheService.clear();
20
22
  rangoBlockchain = createEvmBlockchain();
21
23
 
22
24
  customTokens = [
@@ -52,6 +54,10 @@ beforeEach(() => {
52
54
  customTokens.forEach((token) => {
53
55
  const tokenHash = createTokenHash(token);
54
56
  initData._tokensMapByTokenHash.set(tokenHash, token);
57
+ if (!initData._tokensMapByBlockchainName[token.blockchain]) {
58
+ initData._tokensMapByBlockchainName[token.blockchain] = [];
59
+ }
60
+ initData._tokensMapByBlockchainName[token.blockchain].push(tokenHash);
55
61
  });
56
62
 
57
63
  const appStore = createAppStore();
@@ -351,3 +357,81 @@ describe('search in tokens', () => {
351
357
  expect(secondResult).toBe('RNG');
352
358
  });
353
359
  });
360
+
361
+ describe('supported tokens from config', () => {
362
+ test('Should ensure tokens include only tokens from the config', () => {
363
+ const rangoToken = customTokens[0];
364
+ const djangoToken = customTokens[1];
365
+
366
+ const configTokens = [rangoToken, djangoToken];
367
+
368
+ appStoreState = updateAppStoreConfig(appStoreState, {
369
+ from: {
370
+ tokens: configTokens,
371
+ },
372
+ to: { tokens: configTokens },
373
+ });
374
+
375
+ let sourceTokens = appStoreState.tokens({
376
+ type: 'source',
377
+ });
378
+
379
+ let destinationTokens = appStoreState.tokens({
380
+ type: 'destination',
381
+ });
382
+
383
+ expect(configTokens).toMatchObject(sourceTokens);
384
+ expect(configTokens).toMatchObject(destinationTokens);
385
+
386
+ appStoreState = updateAppStoreConfig(appStoreState, {
387
+ from: {
388
+ blockchains: [rangoBlockchain.name],
389
+ tokens: {
390
+ [rangoBlockchain.name]: {
391
+ tokens: configTokens,
392
+ isExcluded: false,
393
+ },
394
+ },
395
+ },
396
+ to: {
397
+ blockchains: [rangoBlockchain.name],
398
+ tokens: {
399
+ [rangoBlockchain.name]: {
400
+ tokens: configTokens,
401
+ isExcluded: false,
402
+ },
403
+ },
404
+ },
405
+ });
406
+
407
+ sourceTokens = appStoreState.tokens({
408
+ type: 'source',
409
+ });
410
+
411
+ destinationTokens = appStoreState.tokens({
412
+ type: 'destination',
413
+ });
414
+
415
+ expect(sourceTokens).toMatchObject(sourceTokens);
416
+ expect(configTokens).toMatchObject(destinationTokens);
417
+ });
418
+
419
+ test('Check tokens calculation is caching', () => {
420
+ const rangoToken = customTokens[0];
421
+ const djangoToken = customTokens[1];
422
+
423
+ appStoreState = updateAppStoreConfig(appStoreState, {
424
+ from: {
425
+ tokens: [rangoToken, djangoToken],
426
+ },
427
+ });
428
+
429
+ expect(cacheService.get('supportedSourceTokens')?.length ?? 0).toBe(0);
430
+
431
+ appStoreState.tokens({
432
+ type: 'source',
433
+ });
434
+
435
+ expect(cacheService.get('supportedSourceTokens')?.length ?? 0).toBe(2);
436
+ });
437
+ });