0xtrails 0.1.13 → 0.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 (216) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +11 -2
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/apiClient.d.ts +1 -1
  5. package/dist/apiClient.d.ts.map +1 -1
  6. package/dist/{proxyCaller.d.ts → balanceInjector.d.ts} +5 -4
  7. package/dist/balanceInjector.d.ts.map +1 -0
  8. package/dist/{ccip-D3gTQONK.js → ccip-D6ToCrWc.js} +12 -12
  9. package/dist/cctp.d.ts.map +1 -1
  10. package/dist/cctpqueue.d.ts +3 -3
  11. package/dist/cctpqueue.d.ts.map +1 -1
  12. package/dist/chains.d.ts.map +1 -1
  13. package/dist/config.d.ts +17 -3
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/constants.d.ts +5 -4
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/contractUtils.d.ts +2 -0
  18. package/dist/contractUtils.d.ts.map +1 -1
  19. package/dist/customChains.d.ts +24 -0
  20. package/dist/customChains.d.ts.map +1 -0
  21. package/dist/{index-CnUM7lKf.js → index-BqgeTLL8.js} +34072 -30146
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +411 -400
  25. package/dist/intentEntrypoint.d.ts +96 -0
  26. package/dist/intentEntrypoint.d.ts.map +1 -0
  27. package/dist/intents.d.ts +5 -3
  28. package/dist/intents.d.ts.map +1 -1
  29. package/dist/metaTxnMonitor.d.ts.map +1 -1
  30. package/dist/morpho.d.ts.map +1 -1
  31. package/dist/pools.d.ts +3 -1
  32. package/dist/pools.d.ts.map +1 -1
  33. package/dist/prepareSend.d.ts +8 -2
  34. package/dist/prepareSend.d.ts.map +1 -1
  35. package/dist/prices.d.ts +1 -1
  36. package/dist/prices.d.ts.map +1 -1
  37. package/dist/relaySdk.d.ts.map +1 -1
  38. package/dist/relayer.d.ts.map +1 -1
  39. package/dist/toast.d.ts +9 -0
  40. package/dist/toast.d.ts.map +1 -0
  41. package/dist/tokenBalances.d.ts +6 -2
  42. package/dist/tokenBalances.d.ts.map +1 -1
  43. package/dist/tokens.d.ts.map +1 -1
  44. package/dist/trails.d.ts +6 -5
  45. package/dist/trails.d.ts.map +1 -1
  46. package/dist/trailsClient.d.ts +12 -0
  47. package/dist/trailsClient.d.ts.map +1 -0
  48. package/dist/transactions.d.ts +8 -0
  49. package/dist/transactions.d.ts.map +1 -1
  50. package/dist/wallets.d.ts.map +1 -1
  51. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  52. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  53. package/dist/widget/components/AccountSettings.d.ts +7 -0
  54. package/dist/widget/components/AccountSettings.d.ts.map +1 -0
  55. package/dist/widget/components/ChainList.d.ts +0 -1
  56. package/dist/widget/components/ChainList.d.ts.map +1 -1
  57. package/dist/widget/components/ClassicSwap.d.ts +46 -0
  58. package/dist/widget/components/ClassicSwap.d.ts.map +1 -0
  59. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  60. package/dist/widget/components/ConnectedWallets.d.ts +9 -0
  61. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -0
  62. package/dist/widget/components/DebugMenu.d.ts.map +1 -1
  63. package/dist/widget/components/DebugScreensList.d.ts.map +1 -1
  64. package/dist/widget/components/DebugToast.d.ts +3 -0
  65. package/dist/widget/components/DebugToast.d.ts.map +1 -0
  66. package/dist/widget/components/Earn.d.ts.map +1 -1
  67. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  68. package/dist/widget/components/Fund.d.ts +44 -0
  69. package/dist/widget/components/Fund.d.ts.map +1 -0
  70. package/dist/widget/components/Identicon.d.ts +9 -0
  71. package/dist/widget/components/Identicon.d.ts.map +1 -0
  72. package/dist/widget/components/Pay.d.ts +46 -0
  73. package/dist/widget/components/Pay.d.ts.map +1 -0
  74. package/dist/widget/components/Receive.d.ts.map +1 -1
  75. package/dist/widget/components/RecentTokens.d.ts.map +1 -1
  76. package/dist/widget/components/Recipients.d.ts +9 -0
  77. package/dist/widget/components/Recipients.d.ts.map +1 -0
  78. package/dist/widget/components/RefundWarning.d.ts +9 -0
  79. package/dist/widget/components/RefundWarning.d.ts.map +1 -0
  80. package/dist/widget/components/SimpleSwap.d.ts.map +1 -1
  81. package/dist/widget/components/Swap.d.ts.map +1 -1
  82. package/dist/widget/components/SwapSettings.d.ts +1 -5
  83. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  84. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  85. package/dist/widget/components/ThemeSyncer.d.ts +6 -0
  86. package/dist/widget/components/ThemeSyncer.d.ts.map +1 -0
  87. package/dist/widget/components/Toast.d.ts +24 -0
  88. package/dist/widget/components/Toast.d.ts.map +1 -0
  89. package/dist/widget/components/TokenList.d.ts.map +1 -1
  90. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  91. package/dist/widget/components/TruncatedAddress.d.ts +2 -0
  92. package/dist/widget/components/TruncatedAddress.d.ts.map +1 -1
  93. package/dist/widget/components/UserPreferences.d.ts +7 -0
  94. package/dist/widget/components/UserPreferences.d.ts.map +1 -0
  95. package/dist/widget/hooks/useBalanceVisible.d.ts +1 -0
  96. package/dist/widget/hooks/useBalanceVisible.d.ts.map +1 -1
  97. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  98. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  99. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  100. package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
  101. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
  102. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +54 -0
  103. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -0
  104. package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
  105. package/dist/widget/hooks/usePayMessage.d.ts +34 -0
  106. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -0
  107. package/dist/widget/hooks/useRecipients.d.ts +17 -0
  108. package/dist/widget/hooks/useRecipients.d.ts.map +1 -0
  109. package/dist/widget/hooks/useSelectedRecipient.d.ts +12 -0
  110. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -0
  111. package/dist/widget/hooks/useSendForm.d.ts +2 -0
  112. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  113. package/dist/widget/hooks/useSwapAmount.d.ts +13 -0
  114. package/dist/widget/hooks/useSwapAmount.d.ts.map +1 -0
  115. package/dist/widget/hooks/useSwapSettings.d.ts +16 -0
  116. package/dist/widget/hooks/useSwapSettings.d.ts.map +1 -0
  117. package/dist/widget/hooks/useTargetAmount.d.ts +5 -0
  118. package/dist/widget/hooks/useTargetAmount.d.ts.map +1 -0
  119. package/dist/widget/hooks/useTheme.d.ts +14 -0
  120. package/dist/widget/hooks/useTheme.d.ts.map +1 -0
  121. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  122. package/dist/widget/index.js +2 -2
  123. package/dist/widget/widget.d.ts +9 -0
  124. package/dist/widget/widget.d.ts.map +1 -1
  125. package/package.json +29 -28
  126. package/src/aave.ts +6 -1
  127. package/src/analytics.ts +103 -53
  128. package/src/apiClient.ts +1 -1
  129. package/src/{proxyCaller.ts → balanceInjector.ts} +22 -17
  130. package/src/cctp.ts +6 -2
  131. package/src/cctpqueue.ts +7 -7
  132. package/src/chains.ts +8 -0
  133. package/src/config.ts +40 -9
  134. package/src/constants.ts +11 -8
  135. package/src/contractUtils.ts +33 -2
  136. package/src/customChains.ts +24 -0
  137. package/src/index.ts +11 -1
  138. package/src/intentEntrypoint.ts +253 -0
  139. package/src/intents.ts +87 -54
  140. package/src/metaTxnMonitor.ts +1 -0
  141. package/src/morpho.ts +13 -2
  142. package/src/pools.ts +68 -86
  143. package/src/prepareSend.ts +437 -207
  144. package/src/prices.ts +51 -7
  145. package/src/relaySdk.ts +6 -4
  146. package/src/relayer.ts +2 -0
  147. package/src/toast.ts +110 -0
  148. package/src/tokenBalances.ts +112 -20
  149. package/src/tokens.ts +70 -7
  150. package/src/trails.ts +80 -77
  151. package/src/trailsClient.ts +45 -0
  152. package/src/transactions.ts +27 -35
  153. package/src/umd.tsx +1 -1
  154. package/src/wallets.ts +2 -1
  155. package/src/widget/assets/sequence-logo.svg +15 -0
  156. package/src/widget/compiled.css +2 -2
  157. package/src/widget/components/AccountActionsDropdown.tsx +18 -159
  158. package/src/widget/components/AccountIntentTransactionHistory.tsx +346 -63
  159. package/src/widget/components/AccountSettings.tsx +96 -0
  160. package/src/widget/components/ChainFilterDropdown.tsx +1 -1
  161. package/src/widget/components/ChainList.tsx +10 -20
  162. package/src/widget/components/ClassicSwap.tsx +923 -0
  163. package/src/widget/components/ConfigDisplay.tsx +8 -5
  164. package/src/widget/components/ConnectedWallets.tsx +260 -0
  165. package/src/widget/components/DebugMenu.tsx +2 -0
  166. package/src/widget/components/DebugScreensList.tsx +3 -0
  167. package/src/widget/components/DebugToast.tsx +63 -0
  168. package/src/widget/components/Earn.tsx +108 -116
  169. package/src/widget/components/EarnPools.tsx +2 -4
  170. package/src/widget/components/EarnPoolsFilters.tsx +6 -6
  171. package/src/widget/components/Fund.tsx +1245 -0
  172. package/src/widget/components/FundMethods.tsx +1 -1
  173. package/src/widget/components/FundSendForm.tsx +1 -1
  174. package/src/widget/components/Identicon.tsx +158 -0
  175. package/src/widget/components/Pay.tsx +1088 -0
  176. package/src/widget/components/PaySendForm.tsx +1 -1
  177. package/src/widget/components/QuoteDetails.tsx +1 -1
  178. package/src/widget/components/Receipt.tsx +1 -1
  179. package/src/widget/components/Receive.tsx +4 -2
  180. package/src/widget/components/RecentTokens.tsx +2 -1
  181. package/src/widget/components/Recipients.tsx +448 -0
  182. package/src/widget/components/RefundWarning.tsx +61 -0
  183. package/src/widget/components/ScreenHeader.tsx +1 -1
  184. package/src/widget/components/SimpleSwap.tsx +74 -58
  185. package/src/widget/components/Swap.tsx +35 -853
  186. package/src/widget/components/SwapSettings.tsx +5 -11
  187. package/src/widget/components/ThemeProvider.tsx +32 -0
  188. package/src/widget/components/ThemeSyncer.tsx +47 -0
  189. package/src/widget/components/Toast.tsx +315 -0
  190. package/src/widget/components/TokenList.tsx +2 -34
  191. package/src/widget/components/TokenSelector.tsx +3 -3
  192. package/src/widget/components/TransactionDetails.tsx +153 -13
  193. package/src/widget/components/TruncatedAddress.tsx +5 -1
  194. package/src/widget/components/UserPreferences.tsx +156 -0
  195. package/src/widget/components/WalletList.tsx +1 -1
  196. package/src/widget/hooks/useBalanceVisible.tsx +40 -2
  197. package/src/widget/hooks/useCheckout.ts +13 -0
  198. package/src/widget/hooks/useCurrentScreen.tsx +3 -0
  199. package/src/widget/hooks/useDebugScreens.ts +12 -2
  200. package/src/widget/hooks/useDefaultTokenSelection.tsx +475 -0
  201. package/src/widget/hooks/useIntentTransactionHistory.ts +212 -0
  202. package/src/widget/hooks/usePayMessage.tsx +370 -0
  203. package/src/widget/hooks/useRecipients.ts +168 -0
  204. package/src/widget/hooks/useSelectedRecipient.tsx +48 -0
  205. package/src/widget/hooks/useSendForm.ts +179 -26
  206. package/src/widget/hooks/useSwapAmount.tsx +50 -0
  207. package/src/widget/hooks/useSwapSettings.tsx +100 -0
  208. package/src/widget/hooks/useTargetAmount.ts +23 -0
  209. package/src/widget/hooks/useTheme.tsx +80 -0
  210. package/src/widget/hooks/useTokenList.ts +20 -11
  211. package/src/widget/index.css +45 -21
  212. package/src/widget/widget.tsx +164 -68
  213. package/dist/address.d.ts +0 -2
  214. package/dist/address.d.ts.map +0 -1
  215. package/dist/proxyCaller.d.ts.map +0 -1
  216. package/src/address.ts +0 -6
@@ -0,0 +1,1088 @@
1
+ import { ChevronRight, Search, Loader2, ChevronDown } from "lucide-react"
2
+ import { useEffect, useState, useMemo, useRef, useCallback } from "react"
3
+ import type React from "react"
4
+ import type { Account, WalletClient } from "viem"
5
+ import type { TransactionState } from "../../transactions.js"
6
+ import type { OnCompleteProps } from "../hooks/useSendForm.js"
7
+ import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
8
+ import { useSendForm } from "../hooks/useSendForm.js"
9
+ import { TradeType } from "../../prepareSend.js"
10
+ import { useOriginSelectedToken } from "../hooks/useOriginSelectedToken.js"
11
+ import { useDestinationSelectedToken } from "../hooks/useDestinationSelectedToken.js"
12
+ import { useSelectedRecipient } from "../hooks/useSelectedRecipient.js"
13
+ import { useSwapAmount } from "../hooks/useSwapAmount.js"
14
+ import { useTokenList } from "../hooks/useTokenList.js"
15
+ import { useMode } from "../hooks/useMode.js"
16
+ import { useCurrentScreen } from "../hooks/useCurrentScreen.js"
17
+ import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js" // Context version
18
+ import { usePayMessage } from "../hooks/usePayMessage.js"
19
+ import { TokenImage } from "./TokenImage.js"
20
+ import { Identicon } from "./Identicon.js"
21
+ import { TokenSelector } from "./TokenSelector.js"
22
+ import { ScreenHeader } from "./ScreenHeader.js"
23
+ import { ChainList } from "./ChainList.js"
24
+ import { QuoteDetails } from "./QuoteDetails.js"
25
+ import { ErrorDisplay } from "./ErrorDisplay.js"
26
+ import { getChainInfo } from "../../chains.js"
27
+ import { formatUsdAmountDisplay } from "../../tokenBalances.js"
28
+ import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
29
+ import { truncateAddress } from "../../utils.js"
30
+ import type { PrepareSendQuote } from "../../prepareSend.js"
31
+ import type { SupportedToken } from "../../tokens.js"
32
+ import { logger } from "../../logger.js"
33
+ import { RefundWarning } from "./RefundWarning.js"
34
+
35
+ interface PayProps {
36
+ selectedToken?: any // Origin token (optional - user can select)
37
+ onBack?: () => void
38
+ account: Account
39
+ walletClient: WalletClient
40
+ onTransactionStateChange: (transactionStates: TransactionState[]) => void
41
+ onError: (error: Error | string | null) => void
42
+ onWaitingForWalletConfirm: (props: PrepareSendQuote) => void
43
+ onConfirm: () => void
44
+ onComplete: (result: OnCompleteProps) => void
45
+ onSend: (amount: string, recipient: string) => void
46
+ paymasterUrls?: Array<{ chainId: number; url: string }>
47
+ gasless?: boolean
48
+ setWalletConfirmRetryHandler: (handler: () => Promise<void>) => void
49
+ quoteProvider?: string
50
+ fundMethod?: string
51
+ onNavigateToMeshConnect?: (
52
+ props: {
53
+ toTokenSymbol: string
54
+ toTokenAmount: string
55
+ toChainId: number
56
+ toRecipientAddress: string
57
+ },
58
+ quote?: PrepareSendQuote | null,
59
+ ) => void
60
+ checkoutOnHandlers?: CheckoutOnHandlers
61
+ recentTokens?: SupportedToken[]
62
+ onRecentTokenSelect?: (token: SupportedToken) => void
63
+ onTrackToken?: (token: any) => void
64
+ toRecipient?: string
65
+ toAmount?: string
66
+ toChainId?: number
67
+ toToken?: string
68
+ toCalldata?: string
69
+ onAmountUpdate?: (amount: string) => void
70
+ }
71
+
72
+ export const Pay: React.FC<PayProps> = ({
73
+ selectedToken: initialOriginToken,
74
+ onBack,
75
+ account,
76
+ walletClient,
77
+ onTransactionStateChange,
78
+ onError,
79
+ onWaitingForWalletConfirm,
80
+ onConfirm,
81
+ onComplete,
82
+ onSend,
83
+ paymasterUrls,
84
+ gasless,
85
+ setWalletConfirmRetryHandler,
86
+ quoteProvider,
87
+ fundMethod,
88
+ onNavigateToMeshConnect,
89
+ checkoutOnHandlers,
90
+ recentTokens,
91
+ onRecentTokenSelect,
92
+ onTrackToken,
93
+ toRecipient,
94
+ toAmount,
95
+ toChainId,
96
+ toToken,
97
+ toCalldata,
98
+ onAmountUpdate,
99
+ }) => {
100
+ const { mode } = useMode()
101
+ const { selectedToken: globalOriginToken, setSelectedToken: setOriginToken } =
102
+ useOriginSelectedToken()
103
+ const {
104
+ selectedToken: globalDestinationToken,
105
+ setSelectedToken: setDestinationToken,
106
+ } = useDestinationSelectedToken()
107
+ const { selectedRecipient, setSelectedRecipient } = useSelectedRecipient()
108
+ const { setAmount: setGlobalAmount } = useSwapAmount()
109
+ const { setCurrentScreen } = useCurrentScreen()
110
+
111
+ // Use new default token selection hook
112
+ const {
113
+ defaultOriginToken,
114
+ defaultDestinationToken,
115
+ isLoading: isLoadingDefaults,
116
+ } = useDefaultTokenSelection()
117
+
118
+ // Use pay message hook for payment request display
119
+ const { message: payMessage } = usePayMessage()
120
+
121
+ // Use global origin token state or initial prop
122
+ const originToken = globalOriginToken || initialOriginToken
123
+
124
+ // Local state for pay-specific functionality
125
+ const [tokenAmountForBackend, setTokenAmountForBackend] = useState("")
126
+ const [inputDisplayValue, setInputDisplayValue] = useState("")
127
+ const [showOriginTokenSelector, setShowOriginTokenSelector] = useState(false)
128
+ const [showOriginChainList, setShowOriginChainList] = useState(false)
129
+ const [showDestinationTokenSelector, setShowDestinationTokenSelector] =
130
+ useState(false)
131
+ const [showDestinationChainList, setShowDestinationChainList] =
132
+ useState(false)
133
+ const inputRef = useRef<HTMLInputElement>(null)
134
+ const lastAutoSelectedRef = useRef<string | null>(null)
135
+
136
+ // Get sorted tokens to auto-select origin and destination tokens
137
+ const { filteredTokensFormatted } = useTokenList({
138
+ onContinue: () => {}, // Not used for auto-selection
139
+ onError: () => {}, // Not used for auto-selection
140
+ fundMethod: undefined,
141
+ allSupportedTokens: true, // Show all tokens for destination selection
142
+ })
143
+
144
+ // Use useSendForm for quote functionality with EXACT_OUTPUT trade type
145
+ const {
146
+ amountUsdDisplay,
147
+ isLoadingQuote,
148
+ prepareSendQuote,
149
+ setAmount: setSendFormAmount,
150
+ handleSubmit,
151
+ isSubmitting,
152
+ buttonText,
153
+ isValidRecipient,
154
+ selectedDestToken,
155
+ setSelectedDestToken,
156
+ setSelectedDestinationChain,
157
+ quoteError,
158
+ quoteErrorPrettified,
159
+ isSameTokenWithoutCustomCalldata,
160
+ destinationTokenAddress,
161
+ isValidCustomToken,
162
+ isSenderContractOnOrigin,
163
+ isSenderContractOnDestination,
164
+ } = useSendForm({
165
+ account,
166
+ toAmount: tokenAmountForBackend || toAmount, // Use the input amount as target amount for EXACT_OUTPUT
167
+ toRecipient: selectedRecipient || toRecipient || account.address,
168
+ toChainId,
169
+ toToken,
170
+ toCalldata,
171
+ walletClient,
172
+ onTransactionStateChange,
173
+ onError,
174
+ onWaitingForWalletConfirm,
175
+ paymasterUrls,
176
+ gasless,
177
+ onConfirm,
178
+ onComplete,
179
+ onSend,
180
+ selectedToken: originToken as any,
181
+ setWalletConfirmRetryHandler,
182
+ tradeType: TradeType.EXACT_OUTPUT, // Key difference: using EXACT_OUTPUT
183
+ quoteProvider,
184
+ fundMethod,
185
+ mode,
186
+ onNavigateToMeshConnect,
187
+ checkoutOnHandlers,
188
+ })
189
+
190
+ // Auto-select origin token using new hook
191
+ useEffect(() => {
192
+ if (!originToken && !isLoadingDefaults && defaultOriginToken) {
193
+ logger.console.log(
194
+ "[trails-sdk] Auto-selecting origin token:",
195
+ defaultOriginToken,
196
+ )
197
+ setOriginToken(defaultOriginToken as any)
198
+ }
199
+ }, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
200
+
201
+ // Initialize destination token from props or auto-select
202
+ useEffect(() => {
203
+ const currentKey = `${toToken}-${toChainId}`
204
+
205
+ if (
206
+ toToken &&
207
+ toChainId &&
208
+ filteredTokensFormatted?.length > 0 &&
209
+ lastAutoSelectedRef.current !== currentKey
210
+ ) {
211
+ // Find the destination token from props
212
+ const destinationToken = filteredTokensFormatted?.find(
213
+ (token) =>
214
+ token.chainId === Number(toChainId) &&
215
+ (token.contractAddress.toLowerCase() === toToken.toLowerCase() ||
216
+ token.symbol.toLowerCase() === toToken.toLowerCase()),
217
+ )
218
+
219
+ if (destinationToken) {
220
+ const decimals =
221
+ destinationToken.contractInfo?.decimals ??
222
+ (destinationToken as any)?.decimals
223
+
224
+ const formattedToken = {
225
+ ...destinationToken,
226
+ contractInfo: {
227
+ decimals,
228
+ contractAddress: destinationToken.contractAddress,
229
+ symbol: destinationToken.symbol,
230
+ name: destinationToken.name,
231
+ },
232
+ }
233
+
234
+ logger.console.log(
235
+ "[trails-sdk] Auto-selecting destination token from props:",
236
+ formattedToken,
237
+ )
238
+
239
+ setDestinationToken(formattedToken as any)
240
+ setSelectedDestToken({
241
+ symbol: destinationToken.symbol,
242
+ name: destinationToken.name,
243
+ imageUrl: destinationToken.imageUrl,
244
+ decimals,
245
+ })
246
+
247
+ // Set the destination chain
248
+ if (setSelectedDestinationChain) {
249
+ const chainInfo = getChainInfo(Number(toChainId))
250
+ if (chainInfo) {
251
+ setSelectedDestinationChain(chainInfo)
252
+ }
253
+ }
254
+
255
+ // Mark this combination as processed
256
+ lastAutoSelectedRef.current = currentKey
257
+ }
258
+ }
259
+ }, [
260
+ toToken,
261
+ toChainId,
262
+ filteredTokensFormatted,
263
+ setDestinationToken,
264
+ setSelectedDestToken,
265
+ setSelectedDestinationChain,
266
+ ])
267
+
268
+ // Initialize selected recipient from toRecipient prop or default to connected wallet
269
+ useEffect(() => {
270
+ if (toRecipient && !selectedRecipient) {
271
+ setSelectedRecipient(toRecipient)
272
+ } else if (!selectedRecipient && account?.address) {
273
+ // Default to connected wallet address if no recipient is set
274
+ setSelectedRecipient(account.address)
275
+ }
276
+ }, [toRecipient, selectedRecipient, setSelectedRecipient, account?.address])
277
+
278
+ // Initialize and update amount from toAmount prop
279
+ useEffect(() => {
280
+ logger.console.log("[trails-sdk] Pay component toAmount effect:", {
281
+ toAmount,
282
+ tokenAmountForBackend,
283
+ inputDisplayValue,
284
+ willUpdate: !!toAmount && toAmount !== tokenAmountForBackend,
285
+ })
286
+
287
+ // Update amount when toAmount prop changes (for payment requests)
288
+ if (toAmount && toAmount !== tokenAmountForBackend) {
289
+ logger.console.log(
290
+ "[trails-sdk] Updating amount from toAmount prop:",
291
+ toAmount,
292
+ )
293
+ setTokenAmountForBackend(toAmount)
294
+ setInputDisplayValue(toAmount)
295
+ setSendFormAmount(toAmount)
296
+ setGlobalAmount(toAmount)
297
+ }
298
+ }, [
299
+ toAmount,
300
+ tokenAmountForBackend,
301
+ inputDisplayValue,
302
+ setSendFormAmount,
303
+ setGlobalAmount,
304
+ ])
305
+
306
+ // Initialize destination token from global state or default when no toToken prop
307
+ useEffect(() => {
308
+ if (!toToken && !selectedDestToken?.symbol) {
309
+ // Prefer global destination token if set
310
+ const destTokenToUse = globalDestinationToken || defaultDestinationToken
311
+
312
+ if (destTokenToUse && !isLoadingDefaults) {
313
+ logger.console.log(
314
+ "[trails-sdk] Initializing destination token:",
315
+ destTokenToUse,
316
+ )
317
+
318
+ // Set destination token if not already set by global state
319
+ if (!globalDestinationToken && defaultDestinationToken) {
320
+ setDestinationToken(defaultDestinationToken as any)
321
+ }
322
+
323
+ const decimals =
324
+ destTokenToUse.contractInfo?.decimals ??
325
+ (destTokenToUse as any)?.decimals
326
+
327
+ setSelectedDestToken({
328
+ symbol: destTokenToUse.symbol,
329
+ name: destTokenToUse.name,
330
+ imageUrl: destTokenToUse.imageUrl,
331
+ decimals,
332
+ })
333
+
334
+ // Also set the destination chain if available
335
+ if (setSelectedDestinationChain && destTokenToUse.chainId) {
336
+ const chainInfo = getChainInfo(destTokenToUse.chainId)
337
+ if (chainInfo) {
338
+ setSelectedDestinationChain(chainInfo)
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }, [
344
+ toToken,
345
+ globalDestinationToken,
346
+ defaultDestinationToken,
347
+ isLoadingDefaults,
348
+ selectedDestToken?.symbol,
349
+ setDestinationToken,
350
+ setSelectedDestToken,
351
+ setSelectedDestinationChain,
352
+ ])
353
+
354
+ // Auto-focus input field on component mount
355
+ useEffect(() => {
356
+ if (inputRef.current) {
357
+ inputRef.current.focus()
358
+ }
359
+ }, [])
360
+
361
+ // Handle input amount changes with 8 decimal limit and 16 char total limit
362
+ const handleAmountChange = useCallback(
363
+ (value: string) => {
364
+ // Allow empty string
365
+ if (value === "") {
366
+ setInputDisplayValue("")
367
+ setTokenAmountForBackend("")
368
+ setSendFormAmount("")
369
+ setGlobalAmount("")
370
+ onAmountUpdate?.("")
371
+ return
372
+ }
373
+
374
+ // Limit total length to 16 characters
375
+ if (value.length > 16) {
376
+ return
377
+ }
378
+
379
+ // Validate decimal places (max 8 decimals) and allow single decimal point
380
+ const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
381
+ if (!decimalMatch) {
382
+ return // Don't update if invalid format
383
+ }
384
+
385
+ // Store the display value
386
+ setInputDisplayValue(value)
387
+
388
+ // Update the token amount for backend and useSendForm
389
+ setTokenAmountForBackend(value)
390
+ setSendFormAmount(value)
391
+ setGlobalAmount(value)
392
+ onAmountUpdate?.(value)
393
+ },
394
+ [setSendFormAmount, setGlobalAmount, onAmountUpdate],
395
+ )
396
+
397
+ // Get display values based on input type
398
+ const displayAmount = useMemo(() => {
399
+ return inputDisplayValue
400
+ }, [inputDisplayValue])
401
+
402
+ // Dynamic font size based on input length
403
+ const inputStyles = useMemo(() => {
404
+ const inputLength = displayAmount.length
405
+ let fontSize: string
406
+
407
+ if (inputLength > 12) {
408
+ fontSize = "0.875rem"
409
+ } else if (inputLength > 9) {
410
+ fontSize = "1rem"
411
+ } else if (inputLength > 6) {
412
+ fontSize = "1.125rem"
413
+ } else if (inputLength > 3) {
414
+ fontSize = "1.25rem"
415
+ } else {
416
+ fontSize = "1.5rem"
417
+ }
418
+
419
+ return {
420
+ fontSize,
421
+ transition: "all 0.1s ease-in-out",
422
+ }
423
+ }, [displayAmount.length])
424
+
425
+ const handleOriginTokenSelect = useCallback(
426
+ (token: any) => {
427
+ const formattedToken = {
428
+ ...token,
429
+ decimals: token.contractInfo?.decimals || token.decimals,
430
+ contractInfo: {
431
+ decimals: token.contractInfo?.decimals || token.decimals,
432
+ contractAddress: token.contractAddress,
433
+ symbol: token.symbol,
434
+ name: token.name,
435
+ },
436
+ }
437
+
438
+ // Update global origin token state
439
+ setOriginToken(formattedToken as any)
440
+ setShowOriginTokenSelector(false)
441
+ logger.console.log("[trails-sdk] selected origin token", token)
442
+ // Track the token selection
443
+ onTrackToken?.(token)
444
+ },
445
+ [setOriginToken, onTrackToken],
446
+ )
447
+
448
+ const handleDestinationTokenSelect = useCallback(
449
+ (token: any) => {
450
+ const formattedToken = {
451
+ ...token,
452
+ decimals: token.contractInfo?.decimals || token.decimals,
453
+ contractInfo: {
454
+ decimals: token.contractInfo?.decimals || token.decimals,
455
+ contractAddress: token.contractAddress,
456
+ symbol: token.symbol,
457
+ name: token.name,
458
+ },
459
+ }
460
+
461
+ // Update both global destination token state and useSendForm state
462
+ setDestinationToken(formattedToken as any)
463
+ setSelectedDestToken(formattedToken as any)
464
+
465
+ // Update destination chain to match the selected token's chain
466
+ if (setSelectedDestinationChain && token.chainId) {
467
+ const chainInfo = getChainInfo(token.chainId)
468
+ if (chainInfo) {
469
+ setSelectedDestinationChain(chainInfo)
470
+ }
471
+ }
472
+
473
+ setShowDestinationTokenSelector(false)
474
+ logger.console.log("[trails-sdk] selected destination token", token)
475
+ // Track the token selection
476
+ onTrackToken?.(token)
477
+ },
478
+ [
479
+ setDestinationToken,
480
+ setSelectedDestToken,
481
+ setSelectedDestinationChain,
482
+ onTrackToken,
483
+ ],
484
+ )
485
+
486
+ // Show origin chain list screen
487
+ if (showOriginChainList) {
488
+ return (
489
+ <ChainList
490
+ onBack={() => {
491
+ setShowOriginChainList(false)
492
+ setShowOriginTokenSelector(true)
493
+ }}
494
+ />
495
+ )
496
+ }
497
+
498
+ // Show destination chain list screen
499
+ if (showDestinationChainList) {
500
+ return (
501
+ <ChainList
502
+ onBack={() => {
503
+ setShowDestinationChainList(false)
504
+ setShowDestinationTokenSelector(true)
505
+ }}
506
+ />
507
+ )
508
+ }
509
+
510
+ // Show origin token selector screen
511
+ if (showOriginTokenSelector) {
512
+ return (
513
+ <div className="space-y-2">
514
+ <ScreenHeader
515
+ onBack={() => setShowOriginTokenSelector(false)}
516
+ headerContent="Select Token"
517
+ headerContentAlign="left"
518
+ showAccountActions={true}
519
+ />
520
+ <TokenSelector
521
+ onTokenSelect={handleOriginTokenSelect}
522
+ onError={onError}
523
+ fundMethod={fundMethod}
524
+ showContinueButton={false}
525
+ compactMode={false}
526
+ recentTokens={recentTokens}
527
+ onRecentTokenSelect={(token) => {
528
+ const formattedToken = {
529
+ ...token,
530
+ decimals: token.decimals,
531
+ contractInfo: {
532
+ decimals: token.decimals,
533
+ contractAddress: token.contractAddress,
534
+ symbol: token.symbol,
535
+ name: token.name,
536
+ },
537
+ } as any
538
+ setOriginToken(formattedToken)
539
+ setShowOriginTokenSelector(false)
540
+ onRecentTokenSelect?.(token)
541
+ }}
542
+ allSupportedTokens={false}
543
+ chainListScreen={true}
544
+ onNavigateToChainList={() => {
545
+ setShowOriginTokenSelector(false)
546
+ setShowOriginChainList(true)
547
+ }}
548
+ onNavigateToFundMethods={() => {
549
+ setCurrentScreen("fund-methods")
550
+ }}
551
+ />
552
+ </div>
553
+ )
554
+ }
555
+
556
+ // Show destination token selector screen
557
+ if (showDestinationTokenSelector) {
558
+ return (
559
+ <div className="space-y-2">
560
+ <ScreenHeader
561
+ onBack={() => setShowDestinationTokenSelector(false)}
562
+ headerContent="Select Token"
563
+ headerContentAlign="left"
564
+ showAccountActions={true}
565
+ />
566
+ <TokenSelector
567
+ onTokenSelect={handleDestinationTokenSelect}
568
+ onError={onError}
569
+ fundMethod={fundMethod}
570
+ showContinueButton={false}
571
+ compactMode={false}
572
+ recentTokens={recentTokens}
573
+ onRecentTokenSelect={(token) => {
574
+ const formattedToken = {
575
+ ...token,
576
+ decimals: token.decimals,
577
+ contractInfo: {
578
+ decimals: token.decimals,
579
+ contractAddress: token.contractAddress,
580
+ symbol: token.symbol,
581
+ name: token.name,
582
+ },
583
+ } as any
584
+ setDestinationToken(formattedToken)
585
+ setSelectedDestToken(formattedToken)
586
+ setShowDestinationTokenSelector(false)
587
+ onRecentTokenSelect?.(token)
588
+ }}
589
+ allSupportedTokens={true}
590
+ chainListScreen={true}
591
+ onNavigateToChainList={() => {
592
+ setShowDestinationTokenSelector(false)
593
+ setShowDestinationChainList(true)
594
+ }}
595
+ />
596
+ </div>
597
+ )
598
+ }
599
+
600
+ // Check if this is a payment request (all required props are set)
601
+ const isPaymentRequest = !!(toToken && toAmount && toChainId && toRecipient)
602
+
603
+ return (
604
+ <div className="space-y-4">
605
+ <ScreenHeader
606
+ onBack={onBack}
607
+ headerContent="Pay"
608
+ headerContentAlign="left"
609
+ showAccountActions={true}
610
+ />
611
+
612
+ {isPaymentRequest ? (
613
+ /* Payment Request UI */
614
+ <>
615
+ {/* Payment Request Header */}
616
+ <div className="space-y-1 trails-bg-secondary trails-border-radius-container p-3">
617
+ <div className="flex justify-start">
618
+ <div className="flex items-center font-medium trails-text-primary text-sm whitespace-nowrap overflow-hidden">
619
+ {payMessage}
620
+ </div>
621
+ </div>
622
+ </div>
623
+
624
+ {/* Origin Token Selection for Payment Request */}
625
+ <div className="space-y-1">
626
+ <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary">
627
+ {/* Amount to Pay Label */}
628
+ <div className="flex justify-between items-center mb-2">
629
+ <div className="text-sm font-bold trails-text-secondary text-left">
630
+ Pay with
631
+ </div>
632
+ </div>
633
+
634
+ <div className="flex items-center space-x-2">
635
+ {/* Amount Display - Non-editable */}
636
+ <div className="flex-1">
637
+ <div className="flex items-center justify-start">
638
+ <div className="flex items-center">
639
+ <input
640
+ type="text"
641
+ value={prepareSendQuote?.originAmountFormatted || ""}
642
+ readOnly={true}
643
+ className={`bg-transparent border-none outline-none font-bold text-left trails-text-primary ${
644
+ isLoadingQuote ? "animate-pulse" : ""
645
+ }`}
646
+ style={{
647
+ fontSize: inputStyles.fontSize,
648
+ width: prepareSendQuote?.originAmountFormatted
649
+ ? `${Math.max(prepareSendQuote.originAmountFormatted.length - 1 + 0.5, 1)}ch`
650
+ : "1ch",
651
+ minWidth: "1ch",
652
+ maxWidth: "200px",
653
+ padding: "0",
654
+ margin: "0",
655
+ transition: "all 0.1s ease-in-out",
656
+ }}
657
+ />
658
+ <span
659
+ className="font-bold text-gray-400 dark:text-gray-500"
660
+ style={{
661
+ fontSize: inputStyles.fontSize,
662
+ marginLeft: "0.1em",
663
+ padding: "0",
664
+ transition: "all 0.2s ease-in-out",
665
+ }}
666
+ >
667
+ {originToken?.symbol || "TOKEN"}
668
+ </span>
669
+ {isLoadingQuote && (
670
+ <div className="ml-2 animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
671
+ )}
672
+ </div>
673
+ </div>
674
+ </div>
675
+
676
+ {/* Origin Token Selection Button */}
677
+ <button
678
+ type="button"
679
+ onClick={() => setShowOriginTokenSelector(true)}
680
+ className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
681
+ >
682
+ {originToken ? (
683
+ <>
684
+ <TokenImage
685
+ symbol={originToken.symbol}
686
+ imageUrl={originToken.imageUrl}
687
+ chainId={originToken.chainId}
688
+ size={20}
689
+ />
690
+ <span className="font-medium trails-text-primary text-sm">
691
+ {originToken.symbol}
692
+ </span>
693
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
694
+ </>
695
+ ) : (
696
+ <>
697
+ <span className="font-medium trails-text-muted text-sm">
698
+ Select Token
699
+ </span>
700
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
701
+ </>
702
+ )}
703
+ </button>
704
+ </div>
705
+
706
+ {/* Bottom Info Row */}
707
+ <div className="mt-2 flex justify-between items-center">
708
+ {/* USD Amount */}
709
+ <div className="text-xs trails-text-muted">
710
+ {originToken?.symbol &&
711
+ prepareSendQuote?.originAmountFormatted ? (
712
+ <>≈ {prepareSendQuote?.originAmountUsdDisplay || "$0.00"}</>
713
+ ) : (
714
+ <span>&nbsp;</span>
715
+ )}
716
+ </div>
717
+ <div className="text-xs trails-text-muted">
718
+ <span>&nbsp;</span>
719
+ </div>
720
+ </div>
721
+ </div>
722
+ </div>
723
+ </>
724
+ ) : (
725
+ /* Regular Pay UI */
726
+ <>
727
+ {/* Recipient and Origin Token Section */}
728
+ <div className="space-y-1 trails-bg-secondary trails-border-radius-container p-3">
729
+ {/* Recipient Button - Like Fund.tsx */}
730
+ <div className="relative">
731
+ {toRecipient ? (
732
+ /* Display only - recipient is fixed */
733
+ <div className="w-full flex items-center justify-between space-x-3 trails-bg-secondary trails-border-radius-list-button px-4 py-2">
734
+ <div className="text-left">
735
+ <div className="font-medium trails-text-primary text-sm">
736
+ Recipient address
737
+ </div>
738
+ </div>
739
+ <div className="flex items-center space-x-2">
740
+ <div
741
+ className="font-medium trails-text-primary text-sm"
742
+ title={toRecipient}
743
+ >
744
+ {truncateAddress(toRecipient)}
745
+ </div>
746
+ <Identicon
747
+ value={toRecipient}
748
+ size={24}
749
+ className="flex-shrink-0"
750
+ />
751
+ </div>
752
+ </div>
753
+ ) : (
754
+ /* Interactive button - user can select recipient */
755
+ <button
756
+ type="button"
757
+ onClick={() => setCurrentScreen("recipients")}
758
+ className="w-full flex items-center justify-between space-x-3 hover:trails-hover-bg hover:bg-gray-50 dark:hover:bg-gray-700 trails-border-radius-list-button px-4 py-2 transition-all duration-200 cursor-pointer"
759
+ >
760
+ <div className="text-left">
761
+ <div className="font-medium trails-text-primary text-sm">
762
+ Recipient address
763
+ </div>
764
+ </div>
765
+ <div className="flex items-center space-x-2">
766
+ {selectedRecipient ? (
767
+ <>
768
+ <div
769
+ className="font-medium trails-text-primary text-sm"
770
+ title={selectedRecipient}
771
+ >
772
+ {truncateAddress(selectedRecipient)}
773
+ </div>
774
+ <Identicon
775
+ value={selectedRecipient}
776
+ size={24}
777
+ className="flex-shrink-0"
778
+ />
779
+ </>
780
+ ) : (
781
+ <>
782
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
783
+ Select address
784
+ </div>
785
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
786
+ <div className="w-4 h-4 rounded-full bg-gray-400 dark:bg-gray-500"></div>
787
+ </div>
788
+ </>
789
+ )}
790
+ <ChevronRight className="w-4 h-4 trails-text-muted flex-shrink-0" />
791
+ </div>
792
+ </button>
793
+ )}
794
+ </div>
795
+
796
+ {/* Origin Token Selector - Like Fund.tsx */}
797
+ <div className="relative">
798
+ <button
799
+ type="button"
800
+ onClick={() => setShowOriginTokenSelector(true)}
801
+ className="w-full flex items-center justify-between space-x-3 hover:trails-hover-bg hover:bg-gray-50 dark:hover:bg-gray-700 trails-border-radius-list-button px-4 py-2 transition-all duration-200 cursor-pointer"
802
+ >
803
+ <div className="text-left">
804
+ <div className="font-medium trails-text-primary text-sm">
805
+ Pay with
806
+ </div>
807
+ </div>
808
+
809
+ <div className="flex items-center space-x-2">
810
+ {originToken ? (
811
+ <>
812
+ <div className="font-medium trails-text-primary text-sm">
813
+ {isLoadingQuote ? (
814
+ <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
815
+ ) : prepareSendQuote?.originAmountFormatted ? (
816
+ <span>
817
+ {prepareSendQuote.originAmountFormatted}{" "}
818
+ {originToken.symbol}
819
+ </span>
820
+ ) : (
821
+ ""
822
+ )}
823
+ </div>
824
+ <TokenImage
825
+ symbol={originToken.symbol}
826
+ imageUrl={originToken.imageUrl}
827
+ chainId={originToken.chainId}
828
+ size={24}
829
+ />
830
+ </>
831
+ ) : (
832
+ <>
833
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
834
+ Select payment token
835
+ </div>
836
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
837
+ <Search className="w-4 h-4 text-gray-400" />
838
+ </div>
839
+ </>
840
+ )}
841
+ <ChevronRight className="w-4 h-4 trails-text-muted flex-shrink-0" />
842
+ </div>
843
+ </button>
844
+ </div>
845
+ </div>
846
+
847
+ <div className="space-y-1">
848
+ {/* Destination Amount Input Section - Like Fund.tsx but for destination token */}
849
+ <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary">
850
+ {/* Amount to Pay Label */}
851
+ <div className="flex justify-between items-center mb-2">
852
+ <div className="text-sm font-medium trails-text-secondary text-left">
853
+ Recipient receives
854
+ </div>
855
+ </div>
856
+
857
+ <div className="flex items-center space-x-2">
858
+ {/* Amount Input */}
859
+ <div className="flex-1">
860
+ <div className="flex items-center justify-start">
861
+ <div className="flex items-center">
862
+ <input
863
+ ref={inputRef}
864
+ type="text"
865
+ value={displayAmount}
866
+ onChange={(e) => handleAmountChange(e.target.value)}
867
+ placeholder={`0 ${selectedDestToken?.symbol || ""}`}
868
+ readOnly={!!toAmount}
869
+ className={`bg-transparent border-none outline-none font-bold text-left trails-text-primary placeholder-trails-text-primary ${
870
+ isLoadingQuote ? "animate-pulse" : ""
871
+ }`}
872
+ style={{
873
+ fontSize: inputStyles.fontSize,
874
+ width: `${Math.max((displayAmount || "0").length - 1 + 0.5, 1)}ch`,
875
+ minWidth: "1ch",
876
+ maxWidth: "270px",
877
+ padding: "0",
878
+ margin: "0",
879
+ transition: "all 0.1s ease-in-out",
880
+ }}
881
+ inputMode="decimal"
882
+ />
883
+ <span
884
+ className="font-bold text-gray-400 dark:text-gray-500"
885
+ style={{
886
+ fontSize: inputStyles.fontSize,
887
+ marginLeft: "0.1em",
888
+ padding: "0",
889
+ transition: "all 0.2s ease-in-out",
890
+ }}
891
+ >
892
+ {selectedDestToken?.symbol?.slice(0, 4) || "TOKEN"}
893
+ </span>
894
+ {isLoadingQuote && (
895
+ <div className="ml-2 animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
896
+ )}
897
+ </div>
898
+ </div>
899
+ </div>
900
+
901
+ {/* Destination Token Selection Button */}
902
+ {toToken ? (
903
+ /* Non-clickable display when toToken is provided */
904
+ <div className="flex items-center space-x-2 trails-bg-card trails-border-radius-input px-2.5 py-1.5 border trails-border-primary">
905
+ {selectedDestToken ? (
906
+ <>
907
+ <TokenImage
908
+ symbol={selectedDestToken.symbol}
909
+ imageUrl={selectedDestToken.imageUrl}
910
+ chainId={globalDestinationToken?.chainId || toChainId}
911
+ size={20}
912
+ />
913
+ <span className="font-medium trails-text-primary text-sm">
914
+ {selectedDestToken.symbol}
915
+ </span>
916
+ </>
917
+ ) : (
918
+ <span className="font-medium trails-text-muted text-sm">
919
+ Select Token
920
+ </span>
921
+ )}
922
+ </div>
923
+ ) : (
924
+ /* Clickable button when toToken is not provided */
925
+ <button
926
+ type="button"
927
+ onClick={() => setShowDestinationTokenSelector(true)}
928
+ className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
929
+ >
930
+ {selectedDestToken ? (
931
+ <>
932
+ <TokenImage
933
+ symbol={selectedDestToken.symbol}
934
+ imageUrl={selectedDestToken.imageUrl}
935
+ chainId={globalDestinationToken?.chainId || toChainId}
936
+ size={20}
937
+ />
938
+ <span className="font-medium trails-text-primary text-sm">
939
+ {selectedDestToken.symbol}
940
+ </span>
941
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
942
+ </>
943
+ ) : (
944
+ <>
945
+ <span className="font-medium trails-text-muted text-sm">
946
+ Select Token
947
+ </span>
948
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
949
+ </>
950
+ )}
951
+ </button>
952
+ )}
953
+ </div>
954
+
955
+ {/* Bottom Info Row */}
956
+ <div className="mt-2 flex justify-between items-center">
957
+ {/* USD Amount */}
958
+ <div className="text-xs trails-text-muted">
959
+ {selectedDestToken?.symbol && displayAmount ? (
960
+ <>≈ {amountUsdDisplay || "$0.00"}</>
961
+ ) : (
962
+ <span>&nbsp;</span>
963
+ )}
964
+ </div>
965
+ <div className="text-xs trails-text-muted">
966
+ <span>&nbsp;</span>
967
+ </div>
968
+ </div>
969
+ </div>
970
+ </div>
971
+ </>
972
+ )}
973
+
974
+ {/* Exchange/Contract Warning */}
975
+ <RefundWarning
976
+ fundMethod={fundMethod}
977
+ isSenderContractOnOrigin={isSenderContractOnOrigin}
978
+ isSenderContractOnDestination={isSenderContractOnDestination}
979
+ />
980
+
981
+ {/* Error Display */}
982
+ <ErrorDisplay
983
+ errorPrettified={quoteErrorPrettified}
984
+ error={quoteError}
985
+ severity="warning"
986
+ />
987
+
988
+ {prepareSendQuote?.noSufficientBalance ? (
989
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
990
+ <div className="flex items-center space-x-2">
991
+ <svg
992
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
993
+ fill="none"
994
+ stroke="currentColor"
995
+ viewBox="0 0 24 24"
996
+ aria-hidden="true"
997
+ >
998
+ <path
999
+ strokeLinecap="round"
1000
+ strokeLinejoin="round"
1001
+ strokeWidth={2}
1002
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
1003
+ />
1004
+ </svg>
1005
+ <p className="text-sm text-amber-600 dark:text-amber-400">
1006
+ Insufficient balance to complete this transaction
1007
+ </p>
1008
+ </div>
1009
+ </div>
1010
+ ) : prepareSendQuote?.minimumNotMet ? (
1011
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
1012
+ <div className="flex items-center space-x-2">
1013
+ <svg
1014
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
1015
+ fill="none"
1016
+ stroke="currentColor"
1017
+ viewBox="0 0 24 24"
1018
+ aria-hidden="true"
1019
+ >
1020
+ <path
1021
+ strokeLinecap="round"
1022
+ strokeLinejoin="round"
1023
+ strokeWidth={2}
1024
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
1025
+ />
1026
+ </svg>
1027
+ <p className="text-sm text-amber-600 dark:text-amber-400">
1028
+ Please enter an amount above{" "}
1029
+ {formatUsdAmountDisplay(MINIMUM_USD_AMOUNT_FOR_SWAP)} otherwise
1030
+ transfer may fail
1031
+ </p>
1032
+ </div>
1033
+ </div>
1034
+ ) : null}
1035
+
1036
+ {/* Pay Button */}
1037
+ <form onSubmit={handleSubmit}>
1038
+ <button
1039
+ type="submit"
1040
+ disabled={
1041
+ !tokenAmountForBackend ||
1042
+ parseFloat(tokenAmountForBackend) <= 0 ||
1043
+ isSubmitting ||
1044
+ isLoadingQuote ||
1045
+ !isValidRecipient ||
1046
+ !destinationTokenAddress ||
1047
+ !isValidCustomToken ||
1048
+ prepareSendQuote?.noSufficientBalance ||
1049
+ isSameTokenWithoutCustomCalldata ||
1050
+ !prepareSendQuote
1051
+ }
1052
+ className="w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative"
1053
+ >
1054
+ {isSubmitting ? (
1055
+ <div className="flex items-center justify-center">
1056
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
1057
+ <span>{buttonText}</span>
1058
+ </div>
1059
+ ) : isSameTokenWithoutCustomCalldata ? (
1060
+ "Select Different Tokens"
1061
+ ) : prepareSendQuote?.noSufficientBalance ? (
1062
+ "Insufficient Balance"
1063
+ ) : !selectedRecipient ? (
1064
+ "Select recipient address"
1065
+ ) : !selectedDestToken ? (
1066
+ "Select destination token"
1067
+ ) : !tokenAmountForBackend ||
1068
+ parseFloat(tokenAmountForBackend) <= 0 ? (
1069
+ "Enter an amount"
1070
+ ) : !prepareSendQuote && !quoteError ? (
1071
+ "Getting quote..."
1072
+ ) : (
1073
+ buttonText || "Pay"
1074
+ )}
1075
+ </button>
1076
+ </form>
1077
+
1078
+ {/* Quote Details */}
1079
+ {prepareSendQuote && (
1080
+ <div className="space-y-2">
1081
+ <QuoteDetails quote={prepareSendQuote} showContent={true} />
1082
+ </div>
1083
+ )}
1084
+ </div>
1085
+ )
1086
+ }
1087
+
1088
+ export default Pay