@lifi/widget 4.1.0 → 4.2.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 (204) hide show
  1. package/dist/esm/AppDefault.js +14 -0
  2. package/dist/esm/AppDefault.js.map +1 -1
  3. package/dist/esm/components/AmountInput/AmountInput.js +2 -1
  4. package/dist/esm/components/AmountInput/AmountInput.js.map +1 -1
  5. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js +4 -2
  6. package/dist/esm/components/AmountInput/AmountInputEndAdornment.js.map +1 -1
  7. package/dist/esm/components/AmountInput/AmountInputEndAdornment.style.js +1 -1
  8. package/dist/esm/components/AmountInput/AmountInputEndAdornment.style.js.map +1 -1
  9. package/dist/esm/components/AmountInput/PriceFormHelperText.js +4 -2
  10. package/dist/esm/components/AmountInput/PriceFormHelperText.js.map +1 -1
  11. package/dist/esm/components/AmountInputCard/AmountInputCard.style.js +98 -0
  12. package/dist/esm/components/AmountInputCard/AmountInputCard.style.js.map +1 -0
  13. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js +25 -0
  14. package/dist/esm/components/AmountInputCard/AmountInputCardPair.js.map +1 -0
  15. package/dist/esm/components/AmountInputCard/BalanceDisplay.js +35 -0
  16. package/dist/esm/components/AmountInputCard/BalanceDisplay.js.map +1 -0
  17. package/dist/esm/components/AmountInputCard/BalanceDisplay.style.js +11 -0
  18. package/dist/esm/components/AmountInputCard/BalanceDisplay.style.js.map +1 -0
  19. package/dist/esm/components/AmountInputCard/FiatValueToggle.js +41 -0
  20. package/dist/esm/components/AmountInputCard/FiatValueToggle.js.map +1 -0
  21. package/dist/esm/components/AmountInputCard/PercentageChips.js +80 -0
  22. package/dist/esm/components/AmountInputCard/PercentageChips.js.map +1 -0
  23. package/dist/esm/components/AmountInputCard/PercentageChips.style.js +43 -0
  24. package/dist/esm/components/AmountInputCard/PercentageChips.style.js.map +1 -0
  25. package/dist/esm/components/AmountInputCard/ReceiveAmountCard.js +137 -0
  26. package/dist/esm/components/AmountInputCard/ReceiveAmountCard.js.map +1 -0
  27. package/dist/esm/components/AmountInputCard/SendAmountCard.js +102 -0
  28. package/dist/esm/components/AmountInputCard/SendAmountCard.js.map +1 -0
  29. package/dist/esm/components/Avatar/Avatar.js +12 -3
  30. package/dist/esm/components/Avatar/Avatar.js.map +1 -1
  31. package/dist/esm/components/Chains/VirtualizedChainList.js +2 -1
  32. package/dist/esm/components/Chains/VirtualizedChainList.js.map +1 -1
  33. package/dist/esm/components/Header/HeaderNavigationTabs.js +14 -1
  34. package/dist/esm/components/Header/HeaderNavigationTabs.js.map +1 -1
  35. package/dist/esm/components/IconCircle/IconCircle.style.js +1 -1
  36. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.js +99 -0
  37. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.js.map +1 -0
  38. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.style.js +52 -0
  39. package/dist/esm/components/LimitOrderSettings/LimitOrderSettings.style.js.map +1 -0
  40. package/dist/esm/components/LimitPriceCard/LimitPriceCard.js +115 -0
  41. package/dist/esm/components/LimitPriceCard/LimitPriceCard.js.map +1 -0
  42. package/dist/esm/components/LimitPriceCard/LimitPriceCard.style.js +89 -0
  43. package/dist/esm/components/LimitPriceCard/LimitPriceCard.style.js.map +1 -0
  44. package/dist/esm/components/LimitPriceCard/LimitPricePresets.js +91 -0
  45. package/dist/esm/components/LimitPriceCard/LimitPricePresets.js.map +1 -0
  46. package/dist/esm/components/RouteCard/RouteProviderCard.js +118 -0
  47. package/dist/esm/components/RouteCard/RouteProviderCard.js.map +1 -0
  48. package/dist/esm/components/Routes/LimitOrderRoutes.js +34 -0
  49. package/dist/esm/components/Routes/LimitOrderRoutes.js.map +1 -0
  50. package/dist/esm/components/Routes/Routes.js +2 -2
  51. package/dist/esm/components/Routes/Routes.js.map +1 -1
  52. package/dist/esm/components/SettingsPanel/SettingsPanel.js +51 -0
  53. package/dist/esm/components/SettingsPanel/SettingsPanel.js.map +1 -0
  54. package/dist/esm/components/SettingsPanel/SettingsPanel.style.js +33 -0
  55. package/dist/esm/components/SettingsPanel/SettingsPanel.style.js.map +1 -0
  56. package/dist/esm/components/SettingsPanel/utils.js +14 -0
  57. package/dist/esm/components/SettingsPanel/utils.js.map +1 -0
  58. package/dist/esm/components/SwapButton/SwapButton.js +50 -0
  59. package/dist/esm/components/SwapButton/SwapButton.js.map +1 -0
  60. package/dist/esm/components/SwapButton/SwapButton.style.js +24 -0
  61. package/dist/esm/components/SwapButton/SwapButton.style.js.map +1 -0
  62. package/dist/esm/components/TokenPillButton/TokenPillButton.js +52 -0
  63. package/dist/esm/components/TokenPillButton/TokenPillButton.js.map +1 -0
  64. package/dist/esm/components/TokenPillButton/TokenPillButton.style.js +39 -0
  65. package/dist/esm/components/TokenPillButton/TokenPillButton.style.js.map +1 -0
  66. package/dist/esm/config/version.d.ts +1 -1
  67. package/dist/esm/config/version.js +1 -1
  68. package/dist/esm/config/version.js.map +1 -1
  69. package/dist/esm/hooks/useChainSearch.js +2 -1
  70. package/dist/esm/hooks/useChainSearch.js.map +1 -1
  71. package/dist/esm/hooks/useLimitMarketRate.js +24 -0
  72. package/dist/esm/hooks/useLimitMarketRate.js.map +1 -0
  73. package/dist/esm/hooks/useLinkedLimitFields.js +49 -0
  74. package/dist/esm/hooks/useLinkedLimitFields.js.map +1 -0
  75. package/dist/esm/hooks/useRoutes.js +23 -6
  76. package/dist/esm/hooks/useRoutes.js.map +1 -1
  77. package/dist/esm/hooks/useTokens.js +16 -14
  78. package/dist/esm/hooks/useTokens.js.map +1 -1
  79. package/dist/esm/hooks/useWideVariant.js +5 -2
  80. package/dist/esm/hooks/useWideVariant.js.map +1 -1
  81. package/dist/esm/i18n/en.json +22 -0
  82. package/dist/esm/index.d.ts +2 -2
  83. package/dist/esm/pages/MainPage/LimitPageContent.js +23 -0
  84. package/dist/esm/pages/MainPage/LimitPageContent.js.map +1 -0
  85. package/dist/esm/pages/MainPage/MainPage.js +19 -22
  86. package/dist/esm/pages/MainPage/MainPage.js.map +1 -1
  87. package/dist/esm/pages/MainPage/MainPageActions.js +28 -0
  88. package/dist/esm/pages/MainPage/MainPageActions.js.map +1 -0
  89. package/dist/esm/pages/MainPage/ReviewButton.js +9 -2
  90. package/dist/esm/pages/MainPage/ReviewButton.js.map +1 -1
  91. package/dist/esm/pages/RoutePriorityPage.js +48 -0
  92. package/dist/esm/pages/RoutePriorityPage.js.map +1 -0
  93. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js +2 -2
  94. package/dist/esm/pages/SettingsPage/BridgeAndExchangeSettings.js.map +1 -1
  95. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js +12 -27
  96. package/dist/esm/pages/SettingsPage/RoutePrioritySettings.js.map +1 -1
  97. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js +133 -0
  98. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippagePage.js.map +1 -0
  99. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js +14 -72
  100. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.js.map +1 -1
  101. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js +26 -14
  102. package/dist/esm/pages/SettingsPage/SlippageSettings/SlippageSettings.style.js.map +1 -1
  103. package/dist/esm/pages/SettingsPage/ThemeSettings.js +4 -0
  104. package/dist/esm/pages/SettingsPage/ThemeSettings.js.map +1 -1
  105. package/dist/esm/pages/TransactionPage/TransactionReview.js +4 -1
  106. package/dist/esm/pages/TransactionPage/TransactionReview.js.map +1 -1
  107. package/dist/esm/providers/I18nProvider/I18nProvider.js +1 -1
  108. package/dist/esm/providers/I18nProvider/enResource.d.ts +1 -1
  109. package/dist/esm/providers/I18nProvider/enResource.js +1 -1
  110. package/dist/esm/providers/I18nProvider/types.d.ts +2 -2
  111. package/dist/esm/providers/SDKClientProvider.js +25 -24
  112. package/dist/esm/providers/SDKClientProvider.js.map +1 -1
  113. package/dist/esm/providers/WidgetProvider/WidgetProvider.js +22 -21
  114. package/dist/esm/providers/WidgetProvider/WidgetProvider.js.map +1 -1
  115. package/dist/esm/stores/form/URLSearchParamsBuilder.js +14 -3
  116. package/dist/esm/stores/form/URLSearchParamsBuilder.js.map +1 -1
  117. package/dist/esm/stores/form/createFormStore.js +6 -2
  118. package/dist/esm/stores/form/createFormStore.js.map +1 -1
  119. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js +3 -0
  120. package/dist/esm/stores/form/getDefaultValuesFromQueryString.js.map +1 -1
  121. package/dist/esm/stores/form/types.d.ts +9 -1
  122. package/dist/esm/stores/form/types.d.ts.map +1 -1
  123. package/dist/esm/stores/form/types.js.map +1 -1
  124. package/dist/esm/stores/form/useClearAmountFields.js +1 -0
  125. package/dist/esm/stores/form/useClearAmountFields.js.map +1 -1
  126. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js +4 -2
  127. package/dist/esm/stores/navigationTabs/useNavigationTabsStore.js.map +1 -1
  128. package/dist/esm/stores/navigationTabs/utils.js +38 -8
  129. package/dist/esm/stores/navigationTabs/utils.js.map +1 -1
  130. package/dist/esm/types/events.d.ts +10 -1
  131. package/dist/esm/types/events.d.ts.map +1 -1
  132. package/dist/esm/types/events.js +2 -0
  133. package/dist/esm/types/events.js.map +1 -1
  134. package/dist/esm/types/widget.d.ts +9 -0
  135. package/dist/esm/types/widget.d.ts.map +1 -1
  136. package/dist/esm/types/widget.js.map +1 -1
  137. package/dist/esm/utils/format.d.ts +1 -1
  138. package/dist/esm/utils/format.d.ts.map +1 -1
  139. package/dist/esm/utils/format.js +2 -2
  140. package/dist/esm/utils/format.js.map +1 -1
  141. package/dist/esm/utils/limitOrder.js +61 -0
  142. package/dist/esm/utils/limitOrder.js.map +1 -0
  143. package/dist/esm/utils/navigationRoutes.d.ts +2 -0
  144. package/dist/esm/utils/navigationRoutes.d.ts.map +1 -1
  145. package/dist/esm/utils/navigationRoutes.js +6 -0
  146. package/dist/esm/utils/navigationRoutes.js.map +1 -1
  147. package/package.json +11 -11
  148. package/src/AppDefault.tsx +16 -0
  149. package/src/components/AmountInput/AmountInputEndAdornment.style.ts +1 -1
  150. package/src/components/AmountInput/AmountInputEndAdornment.tsx +5 -2
  151. package/src/components/AmountInputCard/AmountInputCard.style.tsx +157 -0
  152. package/src/components/AmountInputCard/AmountInputCardPair.tsx +25 -0
  153. package/src/components/AmountInputCard/BalanceDisplay.style.tsx +9 -0
  154. package/src/components/AmountInputCard/BalanceDisplay.tsx +46 -0
  155. package/src/components/AmountInputCard/FiatValueToggle.tsx +55 -0
  156. package/src/components/AmountInputCard/PercentageChips.style.tsx +60 -0
  157. package/src/components/AmountInputCard/PercentageChips.tsx +100 -0
  158. package/src/components/AmountInputCard/ReceiveAmountCard.tsx +219 -0
  159. package/src/components/AmountInputCard/SendAmountCard.tsx +179 -0
  160. package/src/components/Avatar/Avatar.tsx +8 -3
  161. package/src/components/Header/HeaderNavigationTabs.tsx +19 -2
  162. package/src/components/LimitOrderSettings/LimitOrderSettings.style.tsx +69 -0
  163. package/src/components/LimitOrderSettings/LimitOrderSettings.tsx +124 -0
  164. package/src/components/LimitPriceCard/LimitPriceCard.style.tsx +103 -0
  165. package/src/components/LimitPriceCard/LimitPriceCard.tsx +153 -0
  166. package/src/components/LimitPriceCard/LimitPricePresets.tsx +139 -0
  167. package/src/components/RouteCard/RouteProviderCard.tsx +113 -0
  168. package/src/components/Routes/LimitOrderRoutes.tsx +64 -0
  169. package/src/components/Routes/Routes.tsx +3 -2
  170. package/src/components/SettingsPanel/SettingsPanel.style.tsx +41 -0
  171. package/src/components/SettingsPanel/SettingsPanel.tsx +73 -0
  172. package/src/components/SettingsPanel/utils.ts +27 -0
  173. package/src/components/SwapButton/SwapButton.style.tsx +28 -0
  174. package/src/components/SwapButton/SwapButton.tsx +71 -0
  175. package/src/components/TokenPillButton/TokenPillButton.style.tsx +50 -0
  176. package/src/components/TokenPillButton/TokenPillButton.tsx +81 -0
  177. package/src/config/version.ts +1 -1
  178. package/src/hooks/useLimitMarketRate.ts +39 -0
  179. package/src/hooks/useLinkedLimitFields.ts +74 -0
  180. package/src/hooks/useRoutes.ts +45 -4
  181. package/src/hooks/useWideVariant.ts +8 -2
  182. package/src/pages/MainPage/LimitPageContent.tsx +18 -0
  183. package/src/pages/MainPage/MainPage.tsx +43 -30
  184. package/src/pages/MainPage/MainPageActions.tsx +35 -0
  185. package/src/pages/MainPage/ReviewButton.tsx +13 -1
  186. package/src/pages/RoutePriorityPage.tsx +55 -0
  187. package/src/pages/SettingsPage/RoutePrioritySettings.tsx +14 -38
  188. package/src/pages/SettingsPage/SlippageSettings/SlippagePage.tsx +176 -0
  189. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.style.tsx +32 -25
  190. package/src/pages/SettingsPage/SlippageSettings/SlippageSettings.tsx +14 -128
  191. package/src/pages/SettingsPage/ThemeSettings.tsx +4 -0
  192. package/src/stores/form/URLSearchParamsBuilder.tsx +25 -3
  193. package/src/stores/form/createFormStore.ts +7 -3
  194. package/src/stores/form/getDefaultValuesFromQueryString.ts +11 -0
  195. package/src/stores/form/types.ts +14 -1
  196. package/src/stores/form/useClearAmountFields.ts +1 -0
  197. package/src/stores/navigationTabs/types.ts +6 -0
  198. package/src/stores/navigationTabs/useNavigationTabsStore.tsx +8 -3
  199. package/src/stores/navigationTabs/utils.ts +67 -5
  200. package/src/types/events.ts +12 -0
  201. package/src/types/widget.ts +9 -0
  202. package/src/utils/format.ts +6 -2
  203. package/src/utils/limitOrder.ts +88 -0
  204. package/src/utils/navigationRoutes.ts +6 -0
@@ -0,0 +1,73 @@
1
+ import type { BoxProps } from '@mui/material'
2
+ import { useNavigate } from '@tanstack/react-router'
3
+ import type { JSX } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useSplitMode } from '../../stores/navigationTabs/useNavigationTabsStore.js'
6
+ import { useSettingsStore } from '../../stores/settings/SettingsStore.js'
7
+ import { useSettings } from '../../stores/settings/useSettings.js'
8
+ import { navigationRoutes } from '../../utils/navigationRoutes.js'
9
+ import { Card } from '../Card/Card.js'
10
+ import {
11
+ SettingsPanelButton,
12
+ SettingsPanelContainer,
13
+ SettingsPanelTitle,
14
+ SettingsPanelValue,
15
+ } from './SettingsPanel.style.js'
16
+ import {
17
+ bridgeSettingsPanelKeys,
18
+ type SettingsPanelKey,
19
+ swapSettingsPanelKeys,
20
+ } from './utils.js'
21
+
22
+ export const SettingsPanel: React.FC<BoxProps> = (props): JSX.Element => {
23
+ const { t } = useTranslation()
24
+ const navigate = useNavigate()
25
+ const splitMode = useSplitMode()
26
+ const { slippage, routePriority } = useSettings(['slippage', 'routePriority'])
27
+ const [enabledBridges, totalBridges, enabledExchanges, totalExchanges] =
28
+ useSettingsStore((state) => [
29
+ Object.values(state._enabledBridges).filter(Boolean).length,
30
+ Object.values(state._enabledBridges).length,
31
+ Object.values(state._enabledExchanges).filter(Boolean).length,
32
+ Object.values(state._enabledExchanges).length,
33
+ ])
34
+
35
+ const rows =
36
+ splitMode === 'bridge' ? bridgeSettingsPanelKeys : swapSettingsPanelKeys
37
+
38
+ const labelByKey: Record<SettingsPanelKey, string> = {
39
+ routePriority: t('settings.routePriority'),
40
+ exchanges: t('settings.enabledExchanges'),
41
+ bridges: t('settings.enabledBridges'),
42
+ slippage: t('settings.slippage'),
43
+ }
44
+
45
+ const valueByKey: Record<SettingsPanelKey, string> = {
46
+ routePriority:
47
+ routePriority === 'FASTEST'
48
+ ? t('settings.routePriorityOptions.fastest.title')
49
+ : t('settings.routePriorityOptions.cheapest.title'),
50
+ exchanges: `${enabledExchanges}/${totalExchanges}`,
51
+ bridges: `${enabledBridges}/${totalBridges}`,
52
+ slippage: slippage ? `${slippage}%` : t('button.auto'),
53
+ }
54
+
55
+ const handleClick = (key: SettingsPanelKey) => {
56
+ navigate({
57
+ to: `/${navigationRoutes.settings}/${navigationRoutes[key]}`,
58
+ })
59
+ }
60
+
61
+ return (
62
+ <SettingsPanelContainer {...props}>
63
+ {rows.map((key) => (
64
+ <Card key={key}>
65
+ <SettingsPanelButton onClick={() => handleClick(key)} disableRipple>
66
+ <SettingsPanelTitle>{labelByKey[key]}</SettingsPanelTitle>
67
+ <SettingsPanelValue>{valueByKey[key]}</SettingsPanelValue>
68
+ </SettingsPanelButton>
69
+ </Card>
70
+ ))}
71
+ </SettingsPanelContainer>
72
+ )
73
+ }
@@ -0,0 +1,27 @@
1
+ import type { NavigationTabKey } from '../../types/widget.js'
2
+
3
+ export type SettingsPanelKey =
4
+ | 'routePriority'
5
+ | 'exchanges'
6
+ | 'bridges'
7
+ | 'slippage'
8
+
9
+ /** Navigation tabs that surface the settings panel on the main page. */
10
+ export const advancedNavigationTabKeys: NavigationTabKey[] = [
11
+ 'swap-advanced',
12
+ 'bridge-advanced',
13
+ ]
14
+
15
+ // Keys are ordered to match the settings page layout.
16
+ // Swap is same-chain, so route priority and bridges are not relevant.
17
+ export const swapSettingsPanelKeys: SettingsPanelKey[] = [
18
+ 'slippage',
19
+ 'exchanges',
20
+ ]
21
+
22
+ export const bridgeSettingsPanelKeys: SettingsPanelKey[] = [
23
+ 'routePriority',
24
+ 'slippage',
25
+ 'bridges',
26
+ 'exchanges',
27
+ ]
@@ -0,0 +1,28 @@
1
+ import { Box, styled } from '@mui/material'
2
+ import type React from 'react'
3
+ import { Card } from '../Card/Card.js'
4
+
5
+ // Sits just above MUI's appBar layer (1100) so the swap icon stays on top of the
6
+ // two overlapping amount cards it bridges.
7
+ const swapButtonZIndex = 1110
8
+
9
+ export const SwapIconCard: React.FC<React.ComponentProps<typeof Card>> = styled(
10
+ Card
11
+ )(({ theme }) => ({
12
+ height: 32,
13
+ width: 32,
14
+ fontSize: 16,
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ justifyContent: 'center',
18
+ borderRadius: theme.vars.shape.borderRadiusTertiary,
19
+ zIndex: swapButtonZIndex,
20
+ }))
21
+
22
+ export const SwapButtonContainer: React.FC<React.ComponentProps<typeof Box>> =
23
+ styled(Box)(({ theme }) => ({
24
+ display: 'flex',
25
+ justifyContent: 'center',
26
+ alignItems: 'center',
27
+ margin: theme.spacing(-1.5),
28
+ }))
@@ -0,0 +1,71 @@
1
+ import ArrowDownward from '@mui/icons-material/ArrowDownward'
2
+ import type { BoxProps } from '@mui/material'
3
+ import type { JSX } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useAvailableChains } from '../../hooks/useAvailableChains.js'
6
+ import { useToAddressAutoPopulate } from '../../hooks/useToAddressAutoPopulate.js'
7
+ import { useToAddressReset } from '../../hooks/useToAddressReset.js'
8
+ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
9
+ import { useFieldActions } from '../../stores/form/useFieldActions.js'
10
+ import { SwapButtonContainer, SwapIconCard } from './SwapButton.style.js'
11
+
12
+ export const SwapButton: React.FC<BoxProps> = ({
13
+ sx,
14
+ ...props
15
+ }): JSX.Element => {
16
+ const { t } = useTranslation()
17
+ const { mode } = useWidgetConfig()
18
+ const { setFieldValue, getFieldValues } = useFieldActions()
19
+ const { getChainById } = useAvailableChains()
20
+ const { tryResetToAddress } = useToAddressReset()
21
+ const autoPopulateToAddress = useToAddressAutoPopulate()
22
+
23
+ const handleClick = (): void => {
24
+ const [fromChainId, fromToken, toChainId, toToken, toAddress, toAmount] =
25
+ getFieldValues(
26
+ 'fromChain',
27
+ 'fromToken',
28
+ 'toChain',
29
+ 'toToken',
30
+ 'toAddress',
31
+ 'toAmount'
32
+ )
33
+ const newFromAmount =
34
+ mode === 'limit' && toAmount ? (toAmount as string) : ''
35
+ setFieldValue('fromAmount', newFromAmount, { isTouched: true })
36
+ setFieldValue('fromChain', toChainId, { isTouched: true })
37
+ setFieldValue('fromToken', toToken, { isTouched: true })
38
+ setFieldValue('toChain', fromChainId, { isTouched: true })
39
+ setFieldValue('toToken', fromToken, { isTouched: true })
40
+
41
+ const autoPopulatedToAddress = autoPopulateToAddress({
42
+ formType: 'from',
43
+ selectedToAddress: toAddress,
44
+ selectedChainId: toChainId,
45
+ selectedOppositeChainId: fromChainId,
46
+ selectedOppositeTokenAddress: fromToken,
47
+ })
48
+
49
+ if (autoPopulatedToAddress) {
50
+ return
51
+ }
52
+
53
+ const toChain = getChainById(fromChainId)
54
+ if (toChain) {
55
+ tryResetToAddress(toChain)
56
+ }
57
+ }
58
+
59
+ return (
60
+ <SwapButtonContainer sx={sx} {...props}>
61
+ <SwapIconCard
62
+ component="button"
63
+ onClick={handleClick}
64
+ aria-label={t('button.swap')}
65
+ data-testid="widget-swap-tokens-button"
66
+ >
67
+ <ArrowDownward fontSize="inherit" />
68
+ </SwapIconCard>
69
+ </SwapButtonContainer>
70
+ )
71
+ }
@@ -0,0 +1,50 @@
1
+ import { Button, styled, type Theme, Typography } from '@mui/material'
2
+ import type React from 'react'
3
+
4
+ const pillLayout = (theme: Theme) => ({
5
+ height: 36,
6
+ borderRadius: theme.vars.shape.borderRadiusSecondary,
7
+ padding: theme.spacing(0.5, 1),
8
+ display: 'flex',
9
+ alignItems: 'center',
10
+ gap: theme.spacing(1),
11
+ whiteSpace: 'nowrap',
12
+ flexShrink: 0,
13
+ })
14
+
15
+ export const TokenPill: React.FC<React.ComponentProps<typeof Button>> = styled(
16
+ Button
17
+ )(({ theme }) => ({
18
+ ...pillLayout(theme),
19
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 4%, transparent)`,
20
+ border: '1px solid transparent',
21
+ transition: theme.transitions.create(['background-color'], {
22
+ duration: theme.transitions.duration.short,
23
+ }),
24
+ '&:hover': {
25
+ backgroundColor: `color-mix(in srgb, ${theme.vars.palette.common.onBackground} 8%, transparent)`,
26
+ },
27
+ }))
28
+
29
+ export const TokenSelectPill: React.FC<React.ComponentProps<typeof Button>> =
30
+ styled(TokenPill)({ minWidth: 0 })
31
+
32
+ export const TokenPillSymbol: React.FC<
33
+ React.ComponentProps<typeof Typography>
34
+ > = styled(Typography)(({ theme }) => ({
35
+ fontSize: 14,
36
+ fontWeight: 700,
37
+ lineHeight: '18px',
38
+ overflow: 'hidden',
39
+ textOverflow: 'ellipsis',
40
+ whiteSpace: 'nowrap',
41
+ color: theme.vars.palette.text.primary,
42
+ }))
43
+
44
+ export const TokenPillLabel: React.FC<React.ComponentProps<typeof Typography>> =
45
+ styled(Typography)(({ theme }) => ({
46
+ fontSize: 16,
47
+ fontWeight: 700,
48
+ lineHeight: '20px',
49
+ color: theme.vars.palette.text.secondary,
50
+ }))
@@ -0,0 +1,81 @@
1
+ import { useNavigate } from '@tanstack/react-router'
2
+ import type { JSX } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { useChain } from '../../hooks/useChain.js'
5
+ import { useToken } from '../../hooks/useToken.js'
6
+ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
7
+ import type { FormTypeProps } from '../../stores/form/types.js'
8
+ import { FormKeyHelper } from '../../stores/form/types.js'
9
+ import { useFieldValues } from '../../stores/form/useFieldValues.js'
10
+ import type { DisabledUIConfig } from '../../types/widget.js'
11
+ import { navigationRoutes } from '../../utils/navigationRoutes.js'
12
+ import { AvatarBadgedDefault } from '../Avatar/Avatar.js'
13
+ import { TokenAvatar } from '../Avatar/TokenAvatar.js'
14
+ import {
15
+ TokenPill,
16
+ TokenPillLabel,
17
+ TokenPillSymbol,
18
+ TokenSelectPill,
19
+ } from './TokenPillButton.style.js'
20
+
21
+ export const TokenPillButton: React.FC<FormTypeProps> = ({
22
+ formType,
23
+ }): JSX.Element => {
24
+ const { t } = useTranslation()
25
+ const navigate = useNavigate()
26
+ const { disabledUI, mode } = useWidgetConfig()
27
+
28
+ const tokenKey = FormKeyHelper.getTokenKey(formType)
29
+ const [chainId, tokenAddress] = useFieldValues(
30
+ FormKeyHelper.getChainKey(formType),
31
+ tokenKey
32
+ )
33
+
34
+ const { chain } = useChain(chainId)
35
+ const { token } = useToken(chainId, tokenAddress)
36
+
37
+ const isSelected = !!(chain && token)
38
+
39
+ const handleClick = (): void => {
40
+ navigate({
41
+ to:
42
+ formType === 'from'
43
+ ? navigationRoutes.fromToken
44
+ : mode === 'refuel'
45
+ ? navigationRoutes.toTokenNative
46
+ : navigationRoutes.toToken,
47
+ })
48
+ }
49
+
50
+ const isDisabled = !!disabledUI?.[tokenKey as keyof DisabledUIConfig]
51
+
52
+ const testId = `widget-${formType}-token-button`
53
+
54
+ if (isSelected) {
55
+ return (
56
+ <TokenPill
57
+ onClick={isDisabled ? undefined : handleClick}
58
+ disabled={isDisabled}
59
+ data-testid={testId}
60
+ >
61
+ <TokenAvatar
62
+ token={token}
63
+ chain={chain}
64
+ tokenAvatarSize={24}
65
+ chainAvatarSize={12}
66
+ />
67
+ <TokenPillSymbol>{token.symbol}</TokenPillSymbol>
68
+ </TokenPill>
69
+ )
70
+ }
71
+
72
+ return (
73
+ <TokenSelectPill
74
+ onClick={isDisabled ? undefined : handleClick}
75
+ data-testid={testId}
76
+ >
77
+ <AvatarBadgedDefault chain={chain} avatarSize={24} badgeSize={12} />
78
+ <TokenPillLabel>{t('main.select')}</TokenPillLabel>
79
+ </TokenSelectPill>
80
+ )
81
+ }
@@ -1,2 +1,2 @@
1
1
  export const name = '@lifi/widget'
2
- export const version = '4.1.0'
2
+ export const version = '4.2.0'
@@ -0,0 +1,39 @@
1
+ import { FormKeyHelper } from '../stores/form/types.js'
2
+ import { useFieldValues } from '../stores/form/useFieldValues.js'
3
+ import { useToken } from './useToken.js'
4
+
5
+ /**
6
+ * Canonical limit-order market rate (receive token per send token), derived
7
+ * from the tokens' USD prices: 1 send token ≈ sendPrice / receivePrice receive
8
+ * tokens. This is the spot ratio — what the price card's "Market" reference and
9
+ * proximity indicator compare against. Returns `undefined` until both token
10
+ * prices are known.
11
+ */
12
+ export const useLimitMarketRate = (): number | undefined => {
13
+ const [fromChainId, fromTokenAddress, toChainId, toTokenAddress] =
14
+ useFieldValues(
15
+ FormKeyHelper.getChainKey('from'),
16
+ FormKeyHelper.getTokenKey('from'),
17
+ FormKeyHelper.getChainKey('to'),
18
+ FormKeyHelper.getTokenKey('to')
19
+ )
20
+ const { token: fromToken } = useToken(fromChainId, fromTokenAddress)
21
+ const { token: toToken } = useToken(toChainId, toTokenAddress)
22
+
23
+ const fromPrice = Number(fromToken?.priceUSD)
24
+ const toPrice = Number(toToken?.priceUSD)
25
+
26
+ // A `priceUSD` of `"0"` (or missing) means the price is unknown — there's no
27
+ // meaningful spot ratio to derive, and dividing by a zero `toPrice` would be
28
+ // `Infinity`, so the falsy guard intentionally suppresses the rate here.
29
+ if (
30
+ !fromPrice ||
31
+ !toPrice ||
32
+ Number.isNaN(fromPrice) ||
33
+ Number.isNaN(toPrice)
34
+ ) {
35
+ return undefined
36
+ }
37
+
38
+ return fromPrice / toPrice
39
+ }
@@ -0,0 +1,74 @@
1
+ import { FormKeyHelper } from '../stores/form/types.js'
2
+ import { useFieldActions } from '../stores/form/useFieldActions.js'
3
+ import { useFieldValues } from '../stores/form/useFieldValues.js'
4
+ import { deriveLimitPrice, deriveReceiveAmount } from '../utils/limitOrder.js'
5
+ import { useToken } from './useToken.js'
6
+
7
+ export interface LinkedLimitFields {
8
+ limitPrice: string
9
+ /** Set the send amount (explicit user edit); derives receiveAmount when a price is set. */
10
+ setSendAmount: (value: string) => void
11
+ /** Set the limit price (canonical: receive per send); derives receiveAmount when a send amount is set. */
12
+ setLimitPrice: (value: string) => void
13
+ }
14
+
15
+ /**
16
+ * Implements the linked-field derivation for limit orders:
17
+ * - `limitPrice = receiveAmount / sendAmount`
18
+ * - editing sendAmount recomputes receiveAmount, holding the current price
19
+ * - editing limitPrice (with a send amount set) recomputes receiveAmount
20
+ * - sendAmount never changes without an explicit user edit
21
+ *
22
+ * The receive amount (`toAmount`) is read-only in the UI — it is derived here
23
+ * from the send amount and limit price, then sent to the routes request; the
24
+ * receive card only displays the resulting quote.
25
+ *
26
+ * The limit price is NOT stored: it is purely a function of the `fromAmount`
27
+ * and `toAmount` form fields (canonical receive-per-send). The UI layer handles
28
+ * base/quote display inversion. Because it's derived, editing the send amount
29
+ * only has to preserve the pre-edit price.
30
+ */
31
+ export const useLinkedLimitFields = (): LinkedLimitFields => {
32
+ const { setFieldValue } = useFieldActions()
33
+ const [toChainId, toTokenAddress, sendAmount, receiveAmount] = useFieldValues(
34
+ FormKeyHelper.getChainKey('to'),
35
+ FormKeyHelper.getTokenKey('to'),
36
+ FormKeyHelper.getAmountKey('from'),
37
+ FormKeyHelper.getAmountKey('to')
38
+ )
39
+ const { token: toToken } = useToken(toChainId, toTokenAddress)
40
+
41
+ const limitPrice = deriveLimitPrice(sendAmount, receiveAmount)
42
+
43
+ const setSendAmount = (value: string): void => {
44
+ // Hold the pre-edit price (derived from the current fields) so changing the
45
+ // send amount rescales the receive amount instead of moving the price.
46
+ setFieldValue('fromAmount', value, { isDirty: true, isTouched: true })
47
+ const nextReceive = deriveReceiveAmount(
48
+ value,
49
+ limitPrice,
50
+ toToken?.decimals
51
+ )
52
+ if (nextReceive) {
53
+ setFieldValue('toAmount', nextReceive, { isDirty: true })
54
+ }
55
+ }
56
+
57
+ const setLimitPrice = (value: string): void => {
58
+ // Write unconditionally (including '') so the receive amount tracks the
59
+ // price both ways — clearing the price clears the derived receive amount
60
+ // instead of leaving a stale value that re-derives the old price.
61
+ const nextReceive = deriveReceiveAmount(
62
+ sendAmount,
63
+ value,
64
+ toToken?.decimals
65
+ )
66
+ setFieldValue('toAmount', nextReceive, { isDirty: true })
67
+ }
68
+
69
+ return {
70
+ limitPrice,
71
+ setSendAmount,
72
+ setLimitPrice,
73
+ }
74
+ }
@@ -18,6 +18,7 @@ import { useCallback, useMemo } from 'react'
18
18
  import { useSDKClient } from '../providers/SDKClientProvider.js'
19
19
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
20
20
  import { useFieldValues } from '../stores/form/useFieldValues.js'
21
+ import { useNavigationTabsStore } from '../stores/navigationTabs/useNavigationTabsStore.js'
21
22
  import { useIntermediateRoutesStore } from '../stores/routes/useIntermediateRoutesStore.js'
22
23
  import { useSetExecutableRoute } from '../stores/routes/useSetExecutableRoute.js'
23
24
  import { defaultSlippage } from '../stores/settings/createSettingsStore.js'
@@ -70,6 +71,10 @@ export const useRoutes = ({
70
71
  const queryClient = useQueryClient()
71
72
  const emitter = useWidgetEvents()
72
73
  const swapOnly = useSwapOnly()
74
+ const isPrivate = useNavigationTabsStore(
75
+ (state) => state.activeTab === 'private'
76
+ )
77
+
73
78
  const {
74
79
  disabledBridges,
75
80
  disabledExchanges,
@@ -88,11 +93,12 @@ export const useRoutes = ({
88
93
  'slippage',
89
94
  ])
90
95
  const [fromTokenAmount] = useDebouncedWatch(500, 'fromAmount')
96
+ // Debounce toAmount like fromAmount to avoid a request per keystroke
97
+ const [toTokenAmount] = useDebouncedWatch(500, 'toAmount')
91
98
  const [
92
99
  fromChainId,
93
100
  fromTokenAddress,
94
101
  toAddress,
95
- toTokenAmount,
96
102
  toChainId,
97
103
  toTokenAddress,
98
104
  contractCalls,
@@ -100,11 +106,14 @@ export const useRoutes = ({
100
106
  'fromChain',
101
107
  'fromToken',
102
108
  'toAddress',
103
- 'toAmount',
104
109
  'toChain',
105
110
  'toToken',
106
111
  'contractCalls'
107
112
  )
113
+ const [validUntilDuration, partiallyFillable] = useFieldValues(
114
+ 'validUntil',
115
+ 'partiallyFillable'
116
+ )
108
117
  const { token: fromToken } = useToken(fromChainId, fromTokenAddress)
109
118
  const { token: toToken } = useToken(toChainId, toTokenAddress)
110
119
  const { chain: fromChain } = useChain(fromChainId)
@@ -117,7 +126,15 @@ export const useRoutes = ({
117
126
  const { isBatchingSupported, isBatchingSupportedLoading } =
118
127
  useIsBatchingSupported(fromChain, account.address)
119
128
 
120
- const hasAmount = Number(fromTokenAmount) > 0 || Number(toTokenAmount) > 0
129
+ // In limit mode toAmount is the user's limit target, derived from the sell
130
+ // amount and limit price. It's keyed and sent to the backend so changing the
131
+ // limit fetches a fresh quote. The receive card is read-only (it displays the
132
+ // quote and never writes toAmount back), so keying on it can't loop. Both a
133
+ // sell amount and a limit price (→ toAmount) are required before fetching.
134
+ const hasAmount =
135
+ mode === 'limit'
136
+ ? Number(fromTokenAmount) > 0 && Number(toTokenAmount) > 0
137
+ : Number(fromTokenAmount) > 0 || Number(toTokenAmount) > 0
121
138
 
122
139
  const contractCallQuoteEnabled: boolean =
123
140
  mode === 'custom' ? Boolean(contractCalls && account.address) : true
@@ -169,6 +186,8 @@ export const useRoutes = ({
169
186
  toChain?.id as number,
170
187
  toToken?.address as string,
171
188
  toTokenAmount,
189
+ validUntilDuration,
190
+ partiallyFillable,
172
191
  contractCalls,
173
192
  slippage,
174
193
  swapOnly,
@@ -184,6 +203,7 @@ export const useRoutes = ({
184
203
  feeConfig?.fee,
185
204
  disableMessageSigning,
186
205
  !!isBatchingSupported,
206
+ isPrivate,
187
207
  observableRoute?.id,
188
208
  ] as const,
189
209
  [
@@ -196,6 +216,8 @@ export const useRoutes = ({
196
216
  toChain?.id,
197
217
  toToken?.address,
198
218
  toTokenAmount,
219
+ validUntilDuration,
220
+ partiallyFillable,
199
221
  contractCalls,
200
222
  slippage,
201
223
  swapOnly,
@@ -212,6 +234,7 @@ export const useRoutes = ({
212
234
  feeConfig?.fee,
213
235
  disableMessageSigning,
214
236
  isBatchingSupported,
237
+ isPrivate,
215
238
  observableRoute?.id,
216
239
  ]
217
240
  )
@@ -233,6 +256,8 @@ export const useRoutes = ({
233
256
  toChainId,
234
257
  toTokenAddress,
235
258
  toTokenAmount,
259
+ validUntilDuration,
260
+ partiallyFillable,
236
261
  contractCalls,
237
262
  slippage = defaultSlippage,
238
263
  swapOnly,
@@ -248,13 +273,16 @@ export const useRoutes = ({
248
273
  configuredFee,
249
274
  disableMessageSigning,
250
275
  isBatchingSupported,
276
+ isPrivate,
251
277
  // _observableRouteId must be the last element in the query key
252
278
  _observableRouteId,
253
279
  ],
254
280
  signal,
255
281
  }) => {
256
282
  const fromAmount = parseUnits(fromTokenAmount, fromToken!.decimals)
257
- const toAmount = parseUnits(toTokenAmount, toToken!.decimals)
283
+ const toAmount = toTokenAmount
284
+ ? parseUnits(toTokenAmount, toToken!.decimals)
285
+ : undefined
258
286
  const formattedSlippage = slippage
259
287
  ? Number.parseFloat(slippage) / 100
260
288
  : defaultSlippage
@@ -360,6 +388,8 @@ export const useRoutes = ({
360
388
  const shouldUseMainRoutes =
361
389
  !observableRoute || !isObservableRelayerRoute
362
390
  const shouldUseRelayerQuote =
391
+ // Relayer quotes don't support limit-order params
392
+ mode !== 'limit' &&
363
393
  fromAddress &&
364
394
  fromChain?.chainType === ChainType.EVM &&
365
395
  fromChain.permit2 &&
@@ -370,6 +400,15 @@ export const useRoutes = ({
370
400
  !isBatchingSupported &&
371
401
  (!observableRoute || isObservableRelayerRoute)
372
402
 
403
+ const limitOrderRouteParams =
404
+ mode === 'limit'
405
+ ? {
406
+ toAmount: toAmount?.toString(),
407
+ validUntil: Math.floor(Date.now() / 1000) + validUntilDuration,
408
+ partiallyFillable: partiallyFillable,
409
+ }
410
+ : undefined
411
+
373
412
  const mainRoutesPromise = shouldUseMainRoutes
374
413
  ? getRoutes(
375
414
  sdkClient,
@@ -385,6 +424,7 @@ export const useRoutes = ({
385
424
  enabledRefuel && gasRecommendationFromAmount
386
425
  ? gasRecommendationFromAmount
387
426
  : undefined,
427
+ ...limitOrderRouteParams,
388
428
  options: {
389
429
  allowSwitchChain:
390
430
  mode === 'refuel' ? false : allowSwitchChain,
@@ -410,6 +450,7 @@ export const useRoutes = ({
410
450
  slippage: formattedSlippage,
411
451
  fee: calculatedFee || configuredFee,
412
452
  executionType: disableMessageSigning ? 'transaction' : 'all',
453
+ ...(isPrivate && { private: true }),
413
454
  },
414
455
  },
415
456
  { signal }
@@ -1,13 +1,19 @@
1
1
  import type { Theme } from '@mui/material'
2
2
  import { useMediaQuery } from '@mui/material'
3
3
  import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js'
4
+ import { useNavigationTabsStore } from '../stores/navigationTabs/useNavigationTabsStore.js'
5
+ import { getTabVariant } from '../stores/navigationTabs/utils.js'
4
6
 
5
7
  const defaultExpandableWidth = 852
6
8
 
7
9
  export const useWideVariant = (): boolean => {
8
- const { variant, showSingleRoute } = useWidgetConfig()
10
+ const config = useWidgetConfig()
11
+ const activeTab = useNavigationTabsStore((store) => store.activeTab)
12
+ // The active tab can pin its own variant (e.g. the limit tab is compact),
13
+ // falling back to the widget config's variant when it doesn't.
14
+ const variant = getTabVariant(config, activeTab)
9
15
  const expandableAllowed = useMediaQuery((theme: Theme) =>
10
16
  theme.breakpoints.up(defaultExpandableWidth)
11
17
  )
12
- return variant === 'wide' && expandableAllowed && !showSingleRoute
18
+ return variant === 'wide' && expandableAllowed && !config.showSingleRoute
13
19
  }
@@ -0,0 +1,18 @@
1
+ import { AmountInputCardPair } from '../../components/AmountInputCard/AmountInputCardPair.js'
2
+ import { LimitOrderSettings } from '../../components/LimitOrderSettings/LimitOrderSettings.js'
3
+ import { PageContainer } from '../../components/PageContainer.js'
4
+ import { LimitOrderRoutes } from '../../components/Routes/LimitOrderRoutes.js'
5
+ import { MainPageActions } from './MainPageActions.js'
6
+
7
+ const marginSx = { marginBottom: 2 }
8
+
9
+ export const LimitPageContent: React.FC = () => {
10
+ return (
11
+ <PageContainer topGutters>
12
+ <AmountInputCardPair sx={marginSx} />
13
+ <LimitOrderSettings sx={marginSx} />
14
+ <LimitOrderRoutes sx={marginSx} />
15
+ <MainPageActions />
16
+ </PageContainer>
17
+ )
18
+ }