@lifi/widget 3.36.1 → 3.38.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 (90) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/esm/components/AmountInput/PriceFormHelperText.style.js +3 -0
  3. package/dist/esm/components/AmountInput/PriceFormHelperText.style.js.map +1 -1
  4. package/dist/esm/components/AppContainer.js +1 -0
  5. package/dist/esm/components/AppContainer.js.map +1 -1
  6. package/dist/esm/components/Avatar/AccountAvatar.js +3 -3
  7. package/dist/esm/components/Avatar/AccountAvatar.js.map +1 -1
  8. package/dist/esm/components/Avatar/Avatar.d.ts +2 -0
  9. package/dist/esm/components/Avatar/Avatar.js +4 -3
  10. package/dist/esm/components/Avatar/Avatar.js.map +1 -1
  11. package/dist/esm/components/Avatar/ChainBadgeContent.d.ts +7 -0
  12. package/dist/esm/components/Avatar/ChainBadgeContent.js +10 -0
  13. package/dist/esm/components/Avatar/ChainBadgeContent.js.map +1 -0
  14. package/dist/esm/components/Avatar/TokenAvatar.js +3 -3
  15. package/dist/esm/components/Avatar/TokenAvatar.js.map +1 -1
  16. package/dist/esm/components/Chains/AllChainsAvatar.js +31 -5
  17. package/dist/esm/components/Chains/AllChainsAvatar.js.map +1 -1
  18. package/dist/esm/components/Chains/ChainSearchInput.js +7 -1
  19. package/dist/esm/components/Chains/ChainSearchInput.js.map +1 -1
  20. package/dist/esm/components/Header/NavigationHeader.js +5 -1
  21. package/dist/esm/components/Header/NavigationHeader.js.map +1 -1
  22. package/dist/esm/components/RouteCard/RouteCard.js +2 -1
  23. package/dist/esm/components/RouteCard/RouteCard.js.map +1 -1
  24. package/dist/esm/components/SelectTokenButton/SelectTokenButton.js +3 -1
  25. package/dist/esm/components/SelectTokenButton/SelectTokenButton.js.map +1 -1
  26. package/dist/esm/components/TokenList/TokenListItem.js +8 -2
  27. package/dist/esm/components/TokenList/TokenListItem.js.map +1 -1
  28. package/dist/esm/config/version.d.ts +1 -1
  29. package/dist/esm/config/version.js +1 -1
  30. package/dist/esm/hooks/useFilteredByTokenBalances.js +8 -2
  31. package/dist/esm/hooks/useFilteredByTokenBalances.js.map +1 -1
  32. package/dist/esm/hooks/useRoutes.js +10 -19
  33. package/dist/esm/hooks/useRoutes.js.map +1 -1
  34. package/dist/esm/hooks/useTokenSearch.js +15 -8
  35. package/dist/esm/hooks/useTokenSearch.js.map +1 -1
  36. package/dist/esm/hooks/useTokens.js +33 -53
  37. package/dist/esm/hooks/useTokens.js.map +1 -1
  38. package/dist/esm/i18n/en.json +1 -0
  39. package/dist/esm/providers/I18nProvider/I18nProvider.js +27 -3
  40. package/dist/esm/providers/I18nProvider/I18nProvider.js.map +1 -1
  41. package/dist/esm/stores/StoreProvider.js +2 -1
  42. package/dist/esm/stores/StoreProvider.js.map +1 -1
  43. package/dist/esm/stores/chains/ChainOrderStore.js +15 -2
  44. package/dist/esm/stores/chains/ChainOrderStore.js.map +1 -1
  45. package/dist/esm/stores/settings/createSettingsStore.d.ts +2 -0
  46. package/dist/esm/stores/settings/types.d.ts +1 -0
  47. package/dist/esm/stores/settings/utils/getStateValues.js +1 -0
  48. package/dist/esm/stores/settings/utils/getStateValues.js.map +1 -1
  49. package/dist/esm/types/token.d.ts +6 -1
  50. package/dist/esm/types/widget.d.ts +12 -2
  51. package/dist/esm/types/widget.js +2 -0
  52. package/dist/esm/types/widget.js.map +1 -1
  53. package/dist/esm/utils/token.d.ts +12 -1
  54. package/dist/esm/utils/token.js +44 -0
  55. package/dist/esm/utils/token.js.map +1 -1
  56. package/dist/esm/utils/tokenList.js +2 -0
  57. package/dist/esm/utils/tokenList.js.map +1 -1
  58. package/dist/esm/utils/variant.d.ts +2 -0
  59. package/dist/esm/utils/variant.js +10 -0
  60. package/dist/esm/utils/variant.js.map +1 -0
  61. package/package.json +7 -7
  62. package/package.json.tmp +9 -9
  63. package/src/components/AmountInput/PriceFormHelperText.style.tsx +3 -0
  64. package/src/components/AppContainer.tsx +1 -0
  65. package/src/components/Avatar/AccountAvatar.tsx +3 -15
  66. package/src/components/Avatar/Avatar.tsx +6 -7
  67. package/src/components/Avatar/ChainBadgeContent.tsx +22 -0
  68. package/src/components/Avatar/TokenAvatar.tsx +3 -11
  69. package/src/components/Chains/AllChainsAvatar.tsx +56 -8
  70. package/src/components/Chains/ChainSearchInput.tsx +9 -1
  71. package/src/components/Header/NavigationHeader.tsx +5 -1
  72. package/src/components/RouteCard/RouteCard.tsx +2 -1
  73. package/src/components/SelectTokenButton/SelectTokenButton.tsx +9 -1
  74. package/src/components/TokenList/TokenListItem.tsx +23 -1
  75. package/src/config/version.ts +1 -1
  76. package/src/hooks/useFilteredByTokenBalances.ts +8 -4
  77. package/src/hooks/useRoutes.ts +17 -23
  78. package/src/hooks/useTokenSearch.ts +17 -10
  79. package/src/hooks/useTokens.ts +51 -83
  80. package/src/i18n/en.json +1 -0
  81. package/src/providers/I18nProvider/I18nProvider.tsx +39 -2
  82. package/src/stores/StoreProvider.tsx +2 -1
  83. package/src/stores/chains/ChainOrderStore.tsx +18 -2
  84. package/src/stores/settings/types.ts +1 -0
  85. package/src/stores/settings/utils/getStateValues.ts +1 -0
  86. package/src/types/token.ts +5 -0
  87. package/src/types/widget.ts +11 -1
  88. package/src/utils/token.ts +65 -1
  89. package/src/utils/tokenList.ts +2 -0
  90. package/src/utils/variant.ts +16 -0
@@ -1,4 +1,4 @@
1
- import type { Route, Token, TokensResponse } from '@lifi/sdk'
1
+ import type { Route, Token } from '@lifi/sdk'
2
2
  import {
3
3
  ChainType,
4
4
  convertQuoteToRoute,
@@ -6,7 +6,6 @@ import {
6
6
  getRelayerQuote,
7
7
  getRoutes,
8
8
  isGaslessStep,
9
- isTokenMessageSigningAllowed,
10
9
  LiFiErrorCode,
11
10
  } from '@lifi/sdk'
12
11
  import { useAccount } from '@lifi/wallet-management'
@@ -20,8 +19,10 @@ import { useSetExecutableRoute } from '../stores/routes/useSetExecutableRoute.js
20
19
  import { defaultSlippage } from '../stores/settings/createSettingsStore.js'
21
20
  import { useSettings } from '../stores/settings/useSettings.js'
22
21
  import { WidgetEvent } from '../types/events.js'
22
+ import type { TokensByChain } from '../types/token.js'
23
23
  import { getChainTypeFromAddress } from '../utils/chainType.js'
24
24
  import { getQueryKey } from '../utils/queries.js'
25
+ import { updateTokenInCache } from '../utils/token.js'
25
26
  import { useChain } from './useChain.js'
26
27
  import { useDebouncedWatch } from './useDebouncedWatch.js'
27
28
  import { useGasRefuel } from './useGasRefuel.js'
@@ -352,8 +353,7 @@ export const useRoutes = ({ observableRoute }: RoutesProps = {}) => {
352
353
  fromChain.nativeToken.address !== fromTokenAddress &&
353
354
  useRelayerRoutes &&
354
355
  !isBatchingSupported &&
355
- (!observableRoute || isObservableRelayerRoute) &&
356
- isTokenMessageSigningAllowed(fromToken!)
356
+ (!observableRoute || isObservableRelayerRoute)
357
357
 
358
358
  const mainRoutesPromise = shouldUseMainRoutes
359
359
  ? getRoutes(
@@ -447,27 +447,21 @@ export const useRoutes = ({ observableRoute }: RoutesProps = {}) => {
447
447
  // Update local tokens cache to keep priceUSD in sync
448
448
  const { fromToken, toToken } = routesResult.routes[0]
449
449
  ;[fromToken, toToken].forEach((token) => {
450
- queryClient.setQueriesData<TokensResponse>(
450
+ // Update main tokens cache (verified)
451
+ queryClient.setQueriesData<TokensByChain>(
451
452
  { queryKey: [getQueryKey('tokens', keyPrefix)] },
452
- (data) => {
453
- if (data) {
454
- const clonedData = { ...data, tokens: { ...data.tokens } }
455
- const index = clonedData.tokens?.[token.chainId]?.findIndex(
456
- (dataToken) => dataToken.address === token.address
457
- )
458
- if (index >= 0) {
459
- clonedData.tokens[token.chainId] = [
460
- ...clonedData.tokens[token.chainId],
461
- ]
462
- clonedData.tokens[token.chainId][index] = {
463
- ...clonedData.tokens[token.chainId][index],
464
- ...token,
465
- }
466
- }
467
- return clonedData
468
- }
469
- }
453
+ (data) => updateTokenInCache(data, token)
454
+ )
455
+
456
+ // Update search tokens cache (unverified) - matches any search query
457
+ queryClient.setQueriesData<TokensByChain>(
458
+ {
459
+ queryKey: [getQueryKey('tokens-search', keyPrefix)],
460
+ exact: false,
461
+ },
462
+ (data) => updateTokenInCache(data, token)
470
463
  )
464
+
471
465
  queryClient.setQueriesData<Token[]>(
472
466
  {
473
467
  queryKey: [
@@ -3,11 +3,11 @@ import {
3
3
  type ChainId,
4
4
  getToken,
5
5
  type TokenExtended,
6
- type TokensResponse,
7
6
  } from '@lifi/sdk'
8
7
  import { useQuery, useQueryClient } from '@tanstack/react-query'
9
8
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
10
9
  import type { FormType } from '../stores/form/types.js'
10
+ import type { TokensByChain } from '../types/token.js'
11
11
  import { getConfigItemSets, isFormItemAllowed } from '../utils/item.js'
12
12
  import { getQueryKey } from '../utils/queries.js'
13
13
 
@@ -49,21 +49,28 @@ export const useTokenSearch = (
49
49
  return null
50
50
  }
51
51
 
52
- queryClient.setQueriesData<TokensResponse>(
52
+ // Add token to main tokens cache
53
+ queryClient.setQueriesData<TokensByChain>(
53
54
  { queryKey: [getQueryKey('tokens', keyPrefix)] },
54
55
  (data) => {
56
+ if (!data) {
57
+ return data
58
+ }
59
+ const chainTokens = data[chainId as number]
55
60
  if (
56
- data &&
57
- !data.tokens[chainId as number]?.some(
61
+ chainTokens?.some(
58
62
  (t) => t.address.toLowerCase() === token.address.toLowerCase()
59
63
  )
60
64
  ) {
61
- const clonedData = { ...data, tokens: { ...data.tokens } }
62
- clonedData.tokens[chainId as number] = [
63
- ...(clonedData.tokens[chainId as number] ?? []),
64
- token,
65
- ]
66
- return clonedData
65
+ return data
66
+ }
67
+ // Mark token from search as unverified
68
+ return {
69
+ ...data,
70
+ [chainId as number]: [
71
+ ...(chainTokens ?? []),
72
+ { ...token, verified: false },
73
+ ],
67
74
  }
68
75
  }
69
76
  )
@@ -4,17 +4,23 @@ import {
4
4
  getTokens,
5
5
  type TokensExtendedResponse,
6
6
  } from '@lifi/sdk'
7
- import { useQuery, useQueryClient } from '@tanstack/react-query'
7
+ import { useQuery } from '@tanstack/react-query'
8
8
  import { useMemo } from 'react'
9
9
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
10
10
  import type { FormType } from '../stores/form/types.js'
11
+ import type { TokensByChain } from '../types/token.js'
11
12
  import {
12
13
  defaultChainIdsByType,
13
14
  getChainTypeFromAddress,
14
15
  } from '../utils/chainType.js'
15
16
  import { isItemAllowed } from '../utils/item.js'
16
17
  import { getQueryKey } from '../utils/queries.js'
17
- import { filterAllowedTokens } from '../utils/token.js'
18
+ import {
19
+ filterAllowedTokens,
20
+ mergeVerifiedWithSearchTokens,
21
+ } from '../utils/token.js'
22
+
23
+ const refetchInterval = 300_000
18
24
 
19
25
  export const useTokens = (
20
26
  formType?: FormType,
@@ -27,12 +33,8 @@ export const useTokens = (
27
33
  keyPrefix,
28
34
  } = useWidgetConfig()
29
35
 
30
- const { isLoading: isSearchLoading } = useBackgroundTokenSearch(
31
- search,
32
- chainId
33
- )
34
-
35
- const { data, isLoading } = useQuery({
36
+ // Main tokens cache - verified tokens from API
37
+ const { data: verifiedTokens, isLoading } = useQuery({
36
38
  queryKey: [getQueryKey('tokens', keyPrefix)],
37
39
  queryFn: async ({ signal }) => {
38
40
  const chainTypes = [
@@ -41,6 +43,7 @@ export const useTokens = (
41
43
  ChainType.UTXO,
42
44
  ChainType.MVM,
43
45
  ].filter((chainType) => isItemAllowed(chainType, chainsConfig?.types))
46
+
44
47
  const tokensResponse: TokensExtendedResponse = await getTokens(
45
48
  {
46
49
  chainTypes,
@@ -50,45 +53,37 @@ export const useTokens = (
50
53
  },
51
54
  { signal }
52
55
  )
53
- return tokensResponse
54
- },
55
- refetchInterval: 300_000,
56
- staleTime: 300_000,
57
- })
58
-
59
- const allTokens = useMemo(() => {
60
- return filterAllowedTokens(
61
- data?.tokens,
62
- configTokens,
63
- chainsConfig,
64
- formType
65
- )
66
- }, [data?.tokens, configTokens, chainsConfig, formType])
67
56
 
68
- return {
69
- allTokens,
70
- isLoading,
71
- isSearchLoading,
72
- }
73
- }
57
+ // Mark all tokens as verified
58
+ const tokens: TokensByChain = Object.fromEntries(
59
+ Object.entries(tokensResponse.tokens).map(([chainId, tokens]) => [
60
+ chainId,
61
+ tokens.map((token) => ({ ...token, verified: true })),
62
+ ])
63
+ )
74
64
 
75
- /** This hook is used to search for tokens in the background.
76
- * It updates the main tokens cache with the search results,
77
- * if any of the tokens are not already in the cache. */
78
- const useBackgroundTokenSearch = (search?: string, chainId?: number) => {
79
- const { chains: chainsConfig, keyPrefix } = useWidgetConfig()
80
- const queryClient = useQueryClient()
65
+ return tokens
66
+ },
67
+ refetchInterval,
68
+ staleTime: refetchInterval,
69
+ })
81
70
 
82
- const { isLoading: isSearchLoading } = useQuery({
71
+ // Search tokens cache - unverified tokens from search
72
+ const { data: searchTokens, isLoading: isSearchLoading } = useQuery({
83
73
  queryKey: [getQueryKey('tokens-search', keyPrefix), search, chainId],
84
74
  queryFn: async ({ queryKey, signal }) => {
85
- const [, searchQuery, chainId] = queryKey as [string, string, number]
75
+ const [, searchQuery, searchChainId] = queryKey as [
76
+ string,
77
+ string,
78
+ number,
79
+ ]
86
80
  const chainTypes = [
87
81
  ChainType.EVM,
88
82
  ChainType.SVM,
89
83
  ChainType.UTXO,
90
84
  ChainType.MVM,
91
85
  ].filter((chainType) => isItemAllowed(chainType, chainsConfig?.types))
86
+
92
87
  const tokensResponse: TokensExtendedResponse = await getTokens(
93
88
  {
94
89
  chainTypes,
@@ -101,7 +96,7 @@ const useBackgroundTokenSearch = (search?: string, chainId?: number) => {
101
96
  )
102
97
 
103
98
  // If the chainId is not provided, try to get it from the search query
104
- let _chainId = chainId
99
+ let _chainId = searchChainId
105
100
  if (!_chainId) {
106
101
  const chainType = getChainTypeFromAddress(searchQuery)
107
102
  if (chainType) {
@@ -121,57 +116,30 @@ const useBackgroundTokenSearch = (search?: string, chainId?: number) => {
121
116
  }
122
117
  }
123
118
 
124
- // Merge search results into main tokens cache
125
- if (searchQuery) {
126
- queryClient.setQueriesData<TokensExtendedResponse>(
127
- { queryKey: [getQueryKey('tokens', keyPrefix)] },
128
- (data) => {
129
- if (!data) {
130
- return data
131
- }
132
-
133
- const clonedData = { ...data, tokens: { ...data.tokens } }
134
-
135
- Object.entries(tokensResponse.tokens).forEach(
136
- ([chainId, searchTokens]) => {
137
- const chainIdNum = Number(chainId)
138
- const existingTokens = clonedData.tokens[chainIdNum] || []
139
-
140
- const existingTokenAddresses = new Set(
141
- existingTokens.map((token) => token.address.toLowerCase())
142
- )
143
-
144
- // Find tokens in search results that don't exist in the main list
145
- const newTokens = searchTokens.filter(
146
- (searchToken) =>
147
- !existingTokenAddresses.has(
148
- searchToken.address.toLowerCase()
149
- )
150
- )
151
-
152
- // Add new tokens to the main list
153
- if (newTokens.length > 0) {
154
- clonedData.tokens[chainIdNum] = [
155
- ...existingTokens,
156
- ...newTokens,
157
- ]
158
- }
159
- }
160
- )
161
-
162
- return clonedData
163
- }
164
- )
165
- }
119
+ // Mark all search tokens as unverified
120
+ const tokens: TokensByChain = Object.fromEntries(
121
+ Object.entries(tokensResponse.tokens).map(([chainId, tokens]) => [
122
+ chainId,
123
+ tokens.map((token) => ({ ...token, verified: false })),
124
+ ])
125
+ )
166
126
 
167
- return tokensResponse
127
+ return tokens
168
128
  },
169
129
  enabled: !!search,
170
- refetchInterval: 300_000,
171
- staleTime: 300_000,
130
+ refetchInterval,
131
+ staleTime: refetchInterval,
172
132
  })
173
133
 
134
+ // Merge tokens at read time - single place where caches are combined
135
+ const allTokens = useMemo(() => {
136
+ const merged = mergeVerifiedWithSearchTokens(verifiedTokens, searchTokens)
137
+ return filterAllowedTokens(merged, configTokens, chainsConfig, formType)
138
+ }, [verifiedTokens, searchTokens, configTokens, chainsConfig, formType])
139
+
174
140
  return {
175
- isLoading: isSearchLoading,
141
+ allTokens,
142
+ isLoading,
143
+ isSearchLoading,
176
144
  }
177
145
  }
package/src/i18n/en.json CHANGED
@@ -127,6 +127,7 @@
127
127
  "deleteTransactionHistory": "Transaction history is only stored locally and can't be recovered if you delete it.",
128
128
  "fundsLossPrevention": "Always ensure smart contract accounts are properly set up on the destination chain and avoid direct transfers to exchanges to prevent fund loss.",
129
129
  "highValueLoss": "The value of the received tokens is significantly lower than the exchanged tokens and transaction cost.",
130
+ "unverifiedToken": "Unverified token. Always do your own research before proceeding.",
130
131
  "insufficientFunds": "You don't have enough funds to complete the transaction.",
131
132
  "insufficientGas": "You don't have enough gas to complete the transaction. You need to add at least:",
132
133
  "minFromAmountUSD": "Minimum amount is {{amount, currencyExt(currency: USD)}}. Please enter a higher amount.",
@@ -1,6 +1,8 @@
1
1
  import { createInstance, type i18n } from 'i18next'
2
- import { useMemo, useRef } from 'react'
2
+ import { useEffect, useMemo, useRef } from 'react'
3
3
  import { I18nextProvider } from 'react-i18next'
4
+ import { useLanguages } from '../../hooks/useLanguages.js'
5
+ import { useSettingsStore } from '../../stores/settings/SettingsStore.js'
4
6
  import { useSettings } from '../../stores/settings/useSettings.js'
5
7
  import { compactNumberFormatter } from '../../utils/compactNumberFormatter.js'
6
8
  import { currencyExtendedFormatter } from '../../utils/currencyExtendedFormatter.js'
@@ -92,5 +94,40 @@ export const I18nProvider: React.FC<React.PropsWithChildren> = ({
92
94
  return i18n
93
95
  }, [language, languageResources, languages?.default, languageCache])
94
96
 
95
- return <I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider>
97
+ return (
98
+ <I18nextProvider i18n={i18nInstance}>
99
+ {children}
100
+ <DefaultLanguageHandler />
101
+ </I18nextProvider>
102
+ )
103
+ }
104
+
105
+ // Sync language settings internally when config.languages.default changes externally
106
+ const DefaultLanguageHandler: React.FC = () => {
107
+ const { languages } = useWidgetConfig()
108
+ const [lastDefaultLanguage, setValue] = useSettingsStore((state) => [
109
+ state.lastDefaultLanguage,
110
+ state.setValue,
111
+ ])
112
+ const { setLanguageWithCode: setLanguage } = useLanguages()
113
+
114
+ useEffect(() => {
115
+ const currentDefaultLanguage = languages?.default
116
+
117
+ const defaultLanguageChanged =
118
+ currentDefaultLanguage && currentDefaultLanguage !== lastDefaultLanguage
119
+
120
+ if (!defaultLanguageChanged) {
121
+ return
122
+ }
123
+
124
+ const updateLanguage = async () => {
125
+ await setLanguage(currentDefaultLanguage)
126
+ setValue('lastDefaultLanguage', currentDefaultLanguage)
127
+ }
128
+
129
+ updateLanguage()
130
+ }, [languages?.default, setValue, lastDefaultLanguage, setLanguage])
131
+
132
+ return null
96
133
  }
@@ -1,5 +1,6 @@
1
1
  import type { PropsWithChildren } from 'react'
2
2
  import type { WidgetConfigProps } from '../types/widget.js'
3
+ import { getSplitSubvariant } from '../utils/variant.js'
3
4
  import { BookmarkStoreProvider } from './bookmarks/BookmarkStore.js'
4
5
  import { ChainOrderStoreProvider } from './chains/ChainOrderStore.js'
5
6
  import { FormStoreProvider } from './form/FormStore.js'
@@ -16,7 +17,7 @@ export const StoreProvider: React.FC<PropsWithChildren<WidgetConfigProps>> = ({
16
17
  <SplitSubvariantStoreProvider
17
18
  state={
18
19
  config.subvariant === 'split'
19
- ? config.subvariantOptions?.split || 'swap'
20
+ ? getSplitSubvariant(config.subvariantOptions?.split)
20
21
  : undefined
21
22
  }
22
23
  >
@@ -3,8 +3,10 @@ import type { StoreApi } from 'zustand'
3
3
  import { useShallow } from 'zustand/shallow'
4
4
  import type { UseBoundStoreWithEqualityFn } from 'zustand/traditional'
5
5
  import { useChains } from '../../hooks/useChains.js'
6
+ import { useSwapOnly } from '../../hooks/useSwapOnly.js'
6
7
  import { useExternalWalletProvider } from '../../providers/WalletProvider/useExternalWalletProvider.js'
7
8
  import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
9
+ import { HiddenUI } from '../../types/widget.js'
8
10
  import { getConfigItemSets, isItemAllowedForSets } from '../../utils/item.js'
9
11
  import type { FormType } from '../form/types.js'
10
12
  import { useFieldActions } from '../form/useFieldActions.js'
@@ -20,10 +22,11 @@ export function ChainOrderStoreProvider({
20
22
  children,
21
23
  ...props
22
24
  }: PersistStoreProviderProps) {
23
- const { chains: chainsConfig } = useWidgetConfig()
25
+ const { chains: chainsConfig, hiddenUI } = useWidgetConfig()
24
26
  const storeRef = useRef<ChainOrderStore>(null)
25
27
  const { chains } = useChains()
26
28
  const { setFieldValue, getFieldValues } = useFieldActions()
29
+ const swapOnly = useSwapOnly()
27
30
  const { variant, subvariantOptions } = useWidgetConfig()
28
31
  const { externalChainTypes, useExternalWalletProvidersOnly } =
29
32
  useExternalWalletProvider()
@@ -62,13 +65,24 @@ export function ChainOrderStoreProvider({
62
65
  key
63
66
  )
64
67
 
68
+ const isSwapTo = swapOnly && key === 'to'
69
+
65
70
  // Show "All networks" button if there are multiple networks
66
- const showAllNetworks = filteredChains.length > 1
71
+ const showAllNetworks =
72
+ filteredChains.length > 1 &&
73
+ !hiddenUI?.includes(HiddenUI.AllNetworks) &&
74
+ !isSwapTo
67
75
  if (!showAllNetworks) {
68
76
  storeRef.current?.getState().setIsAllNetworks(false, key)
69
77
  }
70
78
  storeRef.current?.getState().setShowAllNetworks(showAllNetworks, key)
71
79
 
80
+ // If swap only, set the to chain to the from chain
81
+ if (isSwapTo) {
82
+ const [fromChainValue] = getFieldValues('fromChain')
83
+ setFieldValue('toChain', fromChainValue)
84
+ }
85
+
72
86
  const [chainValue] = getFieldValues(`${key}Chain`)
73
87
  if (chainValue) {
74
88
  return
@@ -99,6 +113,8 @@ export function ChainOrderStoreProvider({
99
113
  useExternalWalletProvidersOnly,
100
114
  variant,
101
115
  subvariantOptions?.wide?.enableChainSidebar,
116
+ hiddenUI,
117
+ swapOnly,
102
118
  ])
103
119
 
104
120
  return (
@@ -19,6 +19,7 @@ export interface SettingsProps {
19
19
  gasPrice?: string
20
20
  language?: string
21
21
  languageCache?: LanguageResource
22
+ lastDefaultLanguage?: string
22
23
  routePriority?: Order
23
24
  enabledAutoRefuel: boolean
24
25
  slippage?: string
@@ -4,6 +4,7 @@ export const getStateValues = (state: SettingsState): SettingsProps => ({
4
4
  gasPrice: state.gasPrice,
5
5
  language: state.language,
6
6
  languageCache: state.languageCache,
7
+ lastDefaultLanguage: state.lastDefaultLanguage,
7
8
  routePriority: state.routePriority,
8
9
  enabledAutoRefuel: state.enabledAutoRefuel,
9
10
  slippage: state.slippage,
@@ -1,11 +1,13 @@
1
1
  import type {
2
2
  TokenAmount as SDKTokenAmount,
3
3
  TokenAmountExtended as SDKTokenAmountExtended,
4
+ TokenExtended,
4
5
  } from '@lifi/sdk'
5
6
 
6
7
  interface TokenFlags {
7
8
  featured?: boolean
8
9
  popular?: boolean
10
+ verified?: boolean
9
11
  }
10
12
 
11
13
  export interface TokenAmount extends SDKTokenAmount, TokenFlags {}
@@ -13,3 +15,6 @@ export interface TokenAmount extends SDKTokenAmount, TokenFlags {}
13
15
  export interface TokenAmountExtended
14
16
  extends SDKTokenAmountExtended,
15
17
  TokenFlags {}
18
+
19
+ export type TokenWithVerified = TokenExtended & { verified?: boolean }
20
+ export type TokensByChain = Record<number, TokenWithVerified[]>
@@ -38,12 +38,20 @@ import type { DefaultFieldValues } from '../stores/form/types.js'
38
38
  export type WidgetVariant = 'compact' | 'wide' | 'drawer'
39
39
  export type WidgetSubvariant = 'default' | 'split' | 'custom' | 'refuel'
40
40
  export type SplitSubvariant = 'bridge' | 'swap'
41
+ export type SplitSubvariantOptions = {
42
+ defaultTab: SplitSubvariant
43
+ }
41
44
  export type CustomSubvariant = 'checkout' | 'deposit'
42
45
  export type WideSubvariant = {
43
46
  enableChainSidebar?: boolean
44
47
  }
45
48
  export interface SubvariantOptions {
46
- split?: SplitSubvariant
49
+ /**
50
+ * Configure split subvariant behavior:
51
+ * - 'bridge' | 'swap': Single mode without tabs
52
+ * - { defaultTab: 'bridge' | 'swap' }: Tabs mode with configurable default tab
53
+ */
54
+ split?: SplitSubvariant | SplitSubvariantOptions
47
55
  custom?: CustomSubvariant
48
56
  wide?: WideSubvariant
49
57
  }
@@ -116,6 +124,7 @@ export enum HiddenUI {
116
124
  IntegratorStepDetails = 'integratorStepDetails',
117
125
  ReverseTokensButton = 'reverseTokensButton',
118
126
  RouteTokenDescription = 'routeTokenDescription',
127
+ RouteCardPriceImpact = 'routeCardPriceImpact',
119
128
  ChainSelect = 'chainSelect',
120
129
  BridgesSettings = 'bridgesSettings',
121
130
  AddressBookConnectedWallets = 'addressBookConnectedWallets',
@@ -124,6 +133,7 @@ export enum HiddenUI {
124
133
  SearchTokenInput = 'searchTokenInput',
125
134
  InsufficientGasMessage = 'insufficientGasMessage',
126
135
  ContactSupport = 'contactSupport',
136
+ AllNetworks = 'allNetworks',
127
137
  }
128
138
  export type HiddenUIType = `${HiddenUI}`
129
139
 
@@ -1,8 +1,72 @@
1
- import type { BaseToken, TokenExtended } from '@lifi/sdk'
1
+ import type { BaseToken, Token, TokenExtended } from '@lifi/sdk'
2
2
  import type { FormType } from '../stores/form/types.js'
3
+ import type { TokensByChain } from '../types/token.js'
3
4
  import type { WidgetChains, WidgetTokens } from '../types/widget.js'
4
5
  import { getConfigItemSets, isFormItemAllowed } from './item.js'
5
6
 
7
+ /**
8
+ * Merges verified tokens with search tokens.
9
+ * Verified tokens take priority - search tokens are only added if they don't already exist.
10
+ */
11
+ export const mergeVerifiedWithSearchTokens = (
12
+ verifiedTokens?: TokensByChain,
13
+ searchTokens?: TokensByChain
14
+ ): TokensByChain | undefined => {
15
+ if (!verifiedTokens) {
16
+ return searchTokens
17
+ }
18
+ if (!searchTokens) {
19
+ return verifiedTokens
20
+ }
21
+
22
+ const result = { ...verifiedTokens }
23
+
24
+ for (const [chainId, tokens] of Object.entries(searchTokens)) {
25
+ const chainIdNum = Number(chainId)
26
+ const existingTokens = result[chainIdNum] || []
27
+ const existingAddresses = new Set(
28
+ existingTokens.map((t) => t.address.toLowerCase())
29
+ )
30
+
31
+ const newTokens = tokens.filter(
32
+ (t) => !existingAddresses.has(t.address.toLowerCase())
33
+ )
34
+
35
+ if (newTokens.length) {
36
+ result[chainIdNum] = [...existingTokens, ...newTokens]
37
+ }
38
+ }
39
+
40
+ return result
41
+ }
42
+
43
+ /**
44
+ * Updates a token in the cache by chainId and address.
45
+ * Returns a new cache object with the token updated, or the original if not found.
46
+ */
47
+ export const updateTokenInCache = (
48
+ data: TokensByChain | undefined,
49
+ token: Token
50
+ ): TokensByChain | undefined => {
51
+ if (!data) {
52
+ return data
53
+ }
54
+ const chainTokens = data[token.chainId]
55
+ if (!chainTokens) {
56
+ return data
57
+ }
58
+ const index = chainTokens.findIndex((t) => t.address === token.address)
59
+ if (index < 0) {
60
+ return data
61
+ }
62
+ return {
63
+ ...data,
64
+ [token.chainId]: chainTokens.map((t, i) =>
65
+ i === index ? { ...t, ...token } : t
66
+ ),
67
+ }
68
+ }
69
+
6
70
  export const filterAllowedTokens = (
7
71
  dataTokens: { [chainId: number]: TokenExtended[] } | undefined,
8
72
  configTokens?: WidgetTokens,
@@ -91,6 +91,8 @@ const processedTypedTokens = (
91
91
  typedTokens?.forEach((token) => {
92
92
  const tokenAmount = { ...token } as TokenAmount
93
93
  tokenAmount[tokenType] = true
94
+ // Config tokens are explicitly set by integrator, mark as verified
95
+ tokenAmount.verified = true
94
96
 
95
97
  const match = filteredTokensMap.get(token.address)
96
98
  if (match?.priceUSD) {
@@ -0,0 +1,16 @@
1
+ import type {
2
+ SplitSubvariant,
3
+ SplitSubvariantOptions,
4
+ } from '../types/widget.js'
5
+
6
+ export const getSplitSubvariant = (
7
+ split?: SplitSubvariant | SplitSubvariantOptions
8
+ ): SplitSubvariant => {
9
+ if (!split) {
10
+ return 'swap'
11
+ }
12
+ if (typeof split === 'string') {
13
+ return split
14
+ }
15
+ return split.defaultTab
16
+ }