0xtrails 0.1.2 → 0.1.3
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.
- package/dist/analytics.d.ts +68 -1
- package/dist/analytics.d.ts.map +1 -1
- package/dist/{ccip-BmFTEOaB.js → ccip-CWd4g9uZ.js} +1 -1
- package/dist/chains.d.ts +9 -3
- package/dist/chains.d.ts.map +1 -1
- package/dist/ens.d.ts +7 -0
- package/dist/ens.d.ts.map +1 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/{index-BPsVj7zK.js → index-BTUBzx4R.js} +23624 -21770
- package/dist/index.js +2 -2
- package/dist/lifi.d.ts +4 -0
- package/dist/lifi.d.ts.map +1 -0
- package/dist/mode.d.ts +1 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/prepareSend.d.ts +3 -1
- package/dist/prepareSend.d.ts.map +1 -1
- package/dist/prices.d.ts +2 -0
- package/dist/prices.d.ts.map +1 -1
- package/dist/relaySdk.d.ts.map +1 -1
- package/dist/relayer.d.ts.map +1 -1
- package/dist/tokenBalances.d.ts.map +1 -1
- package/dist/tokens.d.ts +2 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/trails.d.ts +3 -3
- package/dist/trails.d.ts.map +1 -1
- package/dist/transactions.d.ts.map +1 -1
- package/dist/wallets.d.ts +247 -5
- package/dist/wallets.d.ts.map +1 -1
- package/dist/widget/components/ChainFilterDropdown.d.ts +2 -0
- package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
- package/dist/widget/components/ConnectWallet.d.ts +1 -0
- package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
- package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
- package/dist/widget/components/FundSendForm.d.ts +2 -2
- package/dist/widget/components/FundSendForm.d.ts.map +1 -1
- package/dist/widget/components/PaySendForm.d.ts +2 -2
- package/dist/widget/components/PaySendForm.d.ts.map +1 -1
- package/dist/widget/components/QrCode.d.ts +1 -1
- package/dist/widget/components/QrCode.d.ts.map +1 -1
- package/dist/widget/components/RefundAddressInput.d.ts +13 -0
- package/dist/widget/components/RefundAddressInput.d.ts.map +1 -0
- package/dist/widget/components/Swap.d.ts +43 -0
- package/dist/widget/components/Swap.d.ts.map +1 -0
- package/dist/widget/components/TokenList.d.ts +0 -2
- package/dist/widget/components/TokenList.d.ts.map +1 -1
- package/dist/widget/components/TokenSelector.d.ts +26 -0
- package/dist/widget/components/TokenSelector.d.ts.map +1 -0
- package/dist/widget/components/WalletConnect.d.ts.map +1 -1
- package/dist/widget/components/WalletConnectionPending.d.ts +12 -0
- package/dist/widget/components/WalletConnectionPending.d.ts.map +1 -0
- package/dist/widget/components/WalletList.d.ts.map +1 -1
- package/dist/widget/hooks/useAmountUsd.d.ts +1 -3
- package/dist/widget/hooks/useAmountUsd.d.ts.map +1 -1
- package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
- package/dist/widget/hooks/useSendForm.d.ts +6 -4
- package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
- package/dist/widget/hooks/useTokenList.d.ts +2 -3
- package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
- package/dist/widget/index.js +1 -1
- package/dist/widget/widget.d.ts.map +1 -1
- package/package.json +9 -6
- package/src/aave.ts +13 -13
- package/src/analytics.ts +87 -4
- package/src/chains.ts +45 -7
- package/src/constants.ts +4 -4
- package/src/ens.ts +17 -0
- package/src/error.ts +16 -1
- package/src/lifi.ts +58 -0
- package/src/mode.ts +1 -1
- package/src/morpho.ts +3 -3
- package/src/pools.ts +18 -18
- package/src/prepareSend.ts +35 -3
- package/src/prices.ts +21 -0
- package/src/relaySdk.ts +1 -0
- package/src/relayer.ts +8 -0
- package/src/tokenBalances.ts +3 -0
- package/src/tokens.ts +85 -19
- package/src/trails.ts +2 -2
- package/src/transactions.ts +1 -0
- package/src/wallets.ts +275 -35
- package/src/widget/compiled.css +1 -1
- package/src/widget/components/ChainFilterDropdown.tsx +42 -33
- package/src/widget/components/ChainImage.tsx +1 -1
- package/src/widget/components/ConnectWallet.tsx +92 -128
- package/src/widget/components/DebugScreensDropdown.tsx +3 -0
- package/src/widget/components/FundSendForm.tsx +17 -3
- package/src/widget/components/PaySendForm.tsx +16 -2
- package/src/widget/components/QRCodeDeposit.tsx +1 -1
- package/src/widget/components/QrCode.tsx +277 -16
- package/src/widget/components/Receipt.tsx +1 -1
- package/src/widget/components/RefundAddressInput.tsx +149 -0
- package/src/widget/components/Swap.tsx +648 -0
- package/src/widget/components/TokenList.tsx +27 -363
- package/src/widget/components/TokenSelector.tsx +405 -0
- package/src/widget/components/WalletConnect.tsx +9 -7
- package/src/widget/components/WalletConnectionPending.tsx +157 -0
- package/src/widget/components/WalletList.tsx +6 -5
- package/src/widget/hooks/useAmountUsd.ts +3 -8
- package/src/widget/hooks/useCheckout.ts +3 -2
- package/src/widget/hooks/useSendForm.ts +66 -32
- package/src/widget/hooks/useTokenList.ts +158 -106
- package/src/widget/widget.tsx +335 -72
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { Search, ChevronLeft } from "lucide-react"
|
|
2
|
+
import type React from "react"
|
|
3
|
+
import { useEffect, useRef, useState, useMemo } from "react"
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion"
|
|
5
|
+
import type { Token, TokenFormatted } from "../hooks/useTokenList.js"
|
|
6
|
+
import { useTokenList } from "../hooks/useTokenList.js"
|
|
7
|
+
import { TokenImage } from "./TokenImage.js"
|
|
8
|
+
import type { Mode } from "../../mode.js"
|
|
9
|
+
import type { SupportedToken } from "../../tokens.js"
|
|
10
|
+
import { RecentTokens } from "./RecentTokens.js"
|
|
11
|
+
import { getChainInfo } from "../../chains.js"
|
|
12
|
+
import { ChainFilterDropdown } from "./ChainFilterDropdown.js"
|
|
13
|
+
|
|
14
|
+
interface TokenSelectorProps {
|
|
15
|
+
onTokenSelect: (selectedToken: Token) => void
|
|
16
|
+
targetAmountUsd?: number | null
|
|
17
|
+
targetAmountUsdFormatted?: string | null
|
|
18
|
+
onError: (error: Error | string | null) => void
|
|
19
|
+
mode?: Mode
|
|
20
|
+
recentTokens?: SupportedToken[]
|
|
21
|
+
onRecentTokenSelect?: (token: SupportedToken) => void
|
|
22
|
+
fundMethod?: string | null
|
|
23
|
+
onNavigateToFundMethods?: () => void
|
|
24
|
+
showContinueButton?: boolean
|
|
25
|
+
selectedToken?: Token | null
|
|
26
|
+
onContinue?: (selectedToken: Token) => void
|
|
27
|
+
showInsufficientBalance?: boolean
|
|
28
|
+
totalBalanceUsd?: number
|
|
29
|
+
totalBalanceUsdFormatted?: string
|
|
30
|
+
compactMode?: boolean
|
|
31
|
+
allSupportedTokens?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const TokenSelector: React.FC<TokenSelectorProps> = ({
|
|
35
|
+
onTokenSelect,
|
|
36
|
+
targetAmountUsd,
|
|
37
|
+
onError,
|
|
38
|
+
recentTokens = [],
|
|
39
|
+
onRecentTokenSelect,
|
|
40
|
+
fundMethod,
|
|
41
|
+
onNavigateToFundMethods,
|
|
42
|
+
showContinueButton = false,
|
|
43
|
+
selectedToken,
|
|
44
|
+
onContinue,
|
|
45
|
+
showInsufficientBalance = false,
|
|
46
|
+
compactMode = false,
|
|
47
|
+
allSupportedTokens = false,
|
|
48
|
+
}) => {
|
|
49
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
50
|
+
const [filterByChainId, setFilterByChainId] = useState<number | null>(null)
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
// Auto-focus the search input when component mounts
|
|
54
|
+
if (searchInputRef.current) {
|
|
55
|
+
searchInputRef.current.focus()
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
searchQuery,
|
|
61
|
+
setSearchQuery,
|
|
62
|
+
handleTokenSelect,
|
|
63
|
+
filteredTokens,
|
|
64
|
+
isLoadingTokens,
|
|
65
|
+
isTokenSelected,
|
|
66
|
+
filteredTokensFormatted,
|
|
67
|
+
balanceError,
|
|
68
|
+
} = useTokenList({
|
|
69
|
+
onContinue: onTokenSelect, // Use onTokenSelect for the hook
|
|
70
|
+
targetAmountUsd,
|
|
71
|
+
onError,
|
|
72
|
+
fundMethod,
|
|
73
|
+
allSupportedTokens,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Apply chain filter to tokens
|
|
77
|
+
const chainFilteredTokens = useMemo(() => {
|
|
78
|
+
if (filterByChainId === null) {
|
|
79
|
+
return filteredTokensFormatted
|
|
80
|
+
}
|
|
81
|
+
return filteredTokensFormatted.filter(
|
|
82
|
+
(token) => token.chainId === filterByChainId,
|
|
83
|
+
)
|
|
84
|
+
}, [filteredTokensFormatted, filterByChainId])
|
|
85
|
+
|
|
86
|
+
// Get unique chains from all tokens (not filtered by chain)
|
|
87
|
+
const uniqueChains = useMemo(() => {
|
|
88
|
+
const chainIds = new Set(
|
|
89
|
+
filteredTokensFormatted.map((token) => token.chainId),
|
|
90
|
+
)
|
|
91
|
+
return Array.from(chainIds)
|
|
92
|
+
.map((chainId) => {
|
|
93
|
+
const chainInfo = getChainInfo(chainId)
|
|
94
|
+
return {
|
|
95
|
+
chainId,
|
|
96
|
+
name: chainInfo?.name || `Chain ${chainId}`,
|
|
97
|
+
imageUrl: "", // We'll use TokenImage component for chain icons
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
101
|
+
}, [filteredTokensFormatted])
|
|
102
|
+
|
|
103
|
+
// Filter recent tokens to only show ones that exist in the current token list and match search
|
|
104
|
+
const filteredRecentTokens = recentTokens.filter((recentToken) => {
|
|
105
|
+
// First check if this recent token exists in the current token list
|
|
106
|
+
const existsInTokenList = chainFilteredTokens.some(
|
|
107
|
+
(token) =>
|
|
108
|
+
token.contractAddress.toLowerCase() ===
|
|
109
|
+
recentToken.contractAddress.toLowerCase() &&
|
|
110
|
+
token.chainId === recentToken.chainId,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if (!existsInTokenList) return false
|
|
114
|
+
|
|
115
|
+
// Then apply search filtering
|
|
116
|
+
if (!searchQuery.trim()) return true
|
|
117
|
+
|
|
118
|
+
const query = searchQuery.toLowerCase()
|
|
119
|
+
return (
|
|
120
|
+
recentToken.symbol.toLowerCase().includes(query) ||
|
|
121
|
+
recentToken.name.toLowerCase().includes(query) ||
|
|
122
|
+
recentToken.contractAddress.toLowerCase().includes(query)
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Handle recent token selection by finding the actual token from the list
|
|
127
|
+
const handleRecentTokenSelect = (recentToken: SupportedToken) => {
|
|
128
|
+
// Find the actual token from the filtered tokens list
|
|
129
|
+
const actualToken = chainFilteredTokens.find(
|
|
130
|
+
(token) =>
|
|
131
|
+
token.contractAddress.toLowerCase() ===
|
|
132
|
+
recentToken.contractAddress.toLowerCase() &&
|
|
133
|
+
token.chainId === recentToken.chainId,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (actualToken) {
|
|
137
|
+
// Use the actual token with balance info
|
|
138
|
+
handleTokenSelect(actualToken)
|
|
139
|
+
} else if (onRecentTokenSelect) {
|
|
140
|
+
// Fallback to the original handler if token not found in current list
|
|
141
|
+
onRecentTokenSelect(recentToken)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
{/* Search Field */}
|
|
148
|
+
<div className="relative mb-2">
|
|
149
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
150
|
+
<Search className={`h-5 w-5 ${"text-gray-500 dark:text-gray-500"}`} />
|
|
151
|
+
</div>
|
|
152
|
+
<input
|
|
153
|
+
ref={searchInputRef}
|
|
154
|
+
type="text"
|
|
155
|
+
value={searchQuery}
|
|
156
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
157
|
+
placeholder="Search tokens"
|
|
158
|
+
className="block w-full pl-10 pr-12 py-2 border border-solid border-gray-200 dark:border-gray-700 trails-border-radius-input focus:ring-2 focus:ring-blue-500 focus:border-blue-500 trails-input"
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
{/* Chain Filter Dropdown */}
|
|
162
|
+
<div className="absolute inset-y-0 right-0 flex items-center">
|
|
163
|
+
<ChainFilterDropdown
|
|
164
|
+
chains={uniqueChains}
|
|
165
|
+
selectedChainId={filterByChainId}
|
|
166
|
+
onChainSelect={setFilterByChainId}
|
|
167
|
+
compactMode={compactMode}
|
|
168
|
+
nestedMode={true}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Recent Tokens */}
|
|
174
|
+
<AnimatePresence>
|
|
175
|
+
{!isLoadingTokens && filteredRecentTokens.length > 0 && (
|
|
176
|
+
<motion.div
|
|
177
|
+
initial={{ opacity: 0, height: 0 }}
|
|
178
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
179
|
+
exit={{ opacity: 0, height: 0 }}
|
|
180
|
+
transition={{ duration: 0.05, ease: "easeOut" }}
|
|
181
|
+
>
|
|
182
|
+
<RecentTokens
|
|
183
|
+
recentTokens={filteredRecentTokens}
|
|
184
|
+
onTokenSelect={handleRecentTokenSelect}
|
|
185
|
+
/>
|
|
186
|
+
</motion.div>
|
|
187
|
+
)}
|
|
188
|
+
</AnimatePresence>
|
|
189
|
+
|
|
190
|
+
{isLoadingTokens && (
|
|
191
|
+
<div className="text-center py-4">
|
|
192
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 mx-auto border-solid border-black dark:border-white"></div>
|
|
193
|
+
<p className="mt-2 text-gray-500 dark:text-gray-400">
|
|
194
|
+
Loading your token balances...
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{!isLoadingTokens && !balanceError && filteredTokens.length === 0 && (
|
|
200
|
+
<div className="text-center py-4 rounded-lg trails-bg-secondary">
|
|
201
|
+
<p className="text-gray-500 dark:text-gray-400">
|
|
202
|
+
{searchQuery.trim()
|
|
203
|
+
? "No tokens found matching your search."
|
|
204
|
+
: fundMethod === "qr-code" || fundMethod === "exchange"
|
|
205
|
+
? ""
|
|
206
|
+
: "No available tokens found"}
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{/* Token List */}
|
|
212
|
+
{chainFilteredTokens.length > 0 && (
|
|
213
|
+
<div
|
|
214
|
+
className={`${compactMode ? "max-h-48" : "max-h-[24vh]"} overflow-y-auto trails-scrollbar space-y-1`}
|
|
215
|
+
>
|
|
216
|
+
<AnimatePresence mode="popLayout">
|
|
217
|
+
{chainFilteredTokens.map((token: TokenFormatted) => {
|
|
218
|
+
const {
|
|
219
|
+
symbol,
|
|
220
|
+
imageUrl,
|
|
221
|
+
chainId,
|
|
222
|
+
contractAddress,
|
|
223
|
+
balanceUsdFormatted,
|
|
224
|
+
tokenName,
|
|
225
|
+
priceUsd,
|
|
226
|
+
balanceFormatted,
|
|
227
|
+
isSufficientBalance,
|
|
228
|
+
} = token
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<motion.button
|
|
232
|
+
key={`${chainId}-${contractAddress}`}
|
|
233
|
+
layout
|
|
234
|
+
initial={{ opacity: 0, y: 20 }}
|
|
235
|
+
animate={{ opacity: 1, y: 0 }}
|
|
236
|
+
exit={{ opacity: 0, y: -20 }}
|
|
237
|
+
transition={{
|
|
238
|
+
duration: 0.05,
|
|
239
|
+
ease: "easeOut",
|
|
240
|
+
layout: { duration: 0.08, ease: "easeOut" },
|
|
241
|
+
}}
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={() => handleTokenSelect(token)}
|
|
244
|
+
title={
|
|
245
|
+
!isSufficientBalance &&
|
|
246
|
+
fundMethod !== "qr-code" &&
|
|
247
|
+
fundMethod !== "exchange"
|
|
248
|
+
? "Insufficient balance for this token"
|
|
249
|
+
: undefined
|
|
250
|
+
}
|
|
251
|
+
className={`w-full py-2 px-4 flex items-center space-x-3 transition-all duration-200 trails-bg-secondary trails-hover-bg trails-border-radius-list-button ${
|
|
252
|
+
isTokenSelected(token) ? "trails-bg-card" : ""
|
|
253
|
+
} ${!isSufficientBalance && fundMethod !== "qr-code" && fundMethod !== "exchange" ? "opacity-75 cursor-not-allowed" : "cursor-pointer"}`}
|
|
254
|
+
>
|
|
255
|
+
<div className="relative flex-shrink-0 mr-2">
|
|
256
|
+
<div
|
|
257
|
+
className={`rounded-full flex items-center justify-center bg-gray-100 dark:bg-gray-700 ${
|
|
258
|
+
!isSufficientBalance &&
|
|
259
|
+
fundMethod !== "qr-code" &&
|
|
260
|
+
fundMethod !== "exchange"
|
|
261
|
+
? "opacity-80"
|
|
262
|
+
: ""
|
|
263
|
+
}`}
|
|
264
|
+
>
|
|
265
|
+
{contractAddress ? (
|
|
266
|
+
<TokenImage
|
|
267
|
+
symbol={symbol}
|
|
268
|
+
imageUrl={imageUrl}
|
|
269
|
+
chainId={chainId}
|
|
270
|
+
size={32}
|
|
271
|
+
/>
|
|
272
|
+
) : (
|
|
273
|
+
<span
|
|
274
|
+
className={`text-base font-medium text-gray-600 dark:text-gray-300 ${
|
|
275
|
+
!isSufficientBalance &&
|
|
276
|
+
fundMethod !== "qr-code" &&
|
|
277
|
+
fundMethod !== "exchange"
|
|
278
|
+
? "opacity-70"
|
|
279
|
+
: ""
|
|
280
|
+
}`}
|
|
281
|
+
>
|
|
282
|
+
{symbol}
|
|
283
|
+
</span>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="flex-1 min-w-0 text-left">
|
|
289
|
+
<h3
|
|
290
|
+
className={`text-sm font-medium truncate text-gray-900 dark:text-white ${
|
|
291
|
+
!isSufficientBalance &&
|
|
292
|
+
fundMethod !== "qr-code" &&
|
|
293
|
+
fundMethod !== "exchange"
|
|
294
|
+
? "opacity-70"
|
|
295
|
+
: ""
|
|
296
|
+
}`}
|
|
297
|
+
>
|
|
298
|
+
{tokenName}
|
|
299
|
+
</h3>
|
|
300
|
+
<p
|
|
301
|
+
className={`text-xs text-gray-500 dark:text-gray-400 ${
|
|
302
|
+
!isSufficientBalance &&
|
|
303
|
+
fundMethod !== "qr-code" &&
|
|
304
|
+
fundMethod !== "exchange"
|
|
305
|
+
? "opacity-70"
|
|
306
|
+
: ""
|
|
307
|
+
}`}
|
|
308
|
+
>
|
|
309
|
+
{symbol}
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{fundMethod !== "qr-code" && fundMethod !== "exchange" && (
|
|
314
|
+
<div className="text-right flex-shrink-0">
|
|
315
|
+
{priceUsd > 0 ? (
|
|
316
|
+
<>
|
|
317
|
+
<p
|
|
318
|
+
className={`text-sm font-medium text-gray-900 dark:text-white ${
|
|
319
|
+
!isSufficientBalance &&
|
|
320
|
+
fundMethod !== "qr-code" &&
|
|
321
|
+
fundMethod !== "exchange"
|
|
322
|
+
? "opacity-70"
|
|
323
|
+
: ""
|
|
324
|
+
}`}
|
|
325
|
+
>
|
|
326
|
+
{balanceUsdFormatted}
|
|
327
|
+
</p>
|
|
328
|
+
<p
|
|
329
|
+
className={`text-xs text-gray-500 dark:text-gray-400 ${
|
|
330
|
+
!isSufficientBalance &&
|
|
331
|
+
fundMethod !== "qr-code" &&
|
|
332
|
+
fundMethod !== "exchange"
|
|
333
|
+
? "opacity-70"
|
|
334
|
+
: ""
|
|
335
|
+
}`}
|
|
336
|
+
>
|
|
337
|
+
{balanceFormatted}
|
|
338
|
+
</p>
|
|
339
|
+
</>
|
|
340
|
+
) : (
|
|
341
|
+
<p
|
|
342
|
+
className={`text-sm font-medium text-gray-900 dark:text-white ${
|
|
343
|
+
!isSufficientBalance &&
|
|
344
|
+
fundMethod !== "qr-code" &&
|
|
345
|
+
fundMethod !== "exchange"
|
|
346
|
+
? "opacity-70"
|
|
347
|
+
: ""
|
|
348
|
+
}`}
|
|
349
|
+
>
|
|
350
|
+
{balanceFormatted}
|
|
351
|
+
</p>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</motion.button>
|
|
356
|
+
)
|
|
357
|
+
})}
|
|
358
|
+
</AnimatePresence>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{showInsufficientBalance &&
|
|
363
|
+
fundMethod !== "qr-code" &&
|
|
364
|
+
fundMethod !== "exchange" && (
|
|
365
|
+
<div
|
|
366
|
+
className={`text-left py-3 px-4 rounded-lg ${"bg-amber-500/10 border border-solid border-amber-500/30"}`}
|
|
367
|
+
>
|
|
368
|
+
<p className={`text-xs font-medium ${"text-amber-400"}`}>
|
|
369
|
+
Insufficient balance
|
|
370
|
+
</p>
|
|
371
|
+
<p className={`text-xs mt-1 ${"text-amber-300"}`}>
|
|
372
|
+
You do not have enough funds to reach the target amount
|
|
373
|
+
</p>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{showContinueButton && onContinue && selectedToken && (
|
|
378
|
+
<div className="space-y-4">
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
onClick={() => onContinue(selectedToken)}
|
|
382
|
+
disabled={!selectedToken}
|
|
383
|
+
className={`w-full font-semibold py-3 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`}
|
|
384
|
+
>
|
|
385
|
+
Continue
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* Pay with another method button */}
|
|
391
|
+
{onNavigateToFundMethods && !isLoadingTokens && (
|
|
392
|
+
<div className="text-center pt-2 pb-1">
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={onNavigateToFundMethods}
|
|
396
|
+
className="inline-flex items-center justify-center space-x-2 px-4 py-2 text-sm font-medium trails-text-secondary trails-bg-secondary trails-hover-bg trails-border trails-border-radius-button transition-all duration-200 cursor-pointer"
|
|
397
|
+
>
|
|
398
|
+
<ChevronLeft className="h-4 w-4" />
|
|
399
|
+
<span>Pay with another method</span>
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
@@ -5,7 +5,7 @@ import { QrCode } from "./QrCode.js"
|
|
|
5
5
|
import { TruncatedAddress } from "./TruncatedAddress.js"
|
|
6
6
|
import { getWalletConnectProjectId } from "../../config.js"
|
|
7
7
|
import { ScreenHeader } from "./ScreenHeader.js"
|
|
8
|
-
import {
|
|
8
|
+
import { getWalletIcon, getWalletName } from "../../wallets.js"
|
|
9
9
|
|
|
10
10
|
export interface WalletConnectProps {
|
|
11
11
|
onBack: () => void
|
|
@@ -186,7 +186,7 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
|
|
|
186
186
|
|
|
187
187
|
const title = useMemo(() => {
|
|
188
188
|
const walletName = getWalletName(selectedWalletId)
|
|
189
|
-
if (walletName) {
|
|
189
|
+
if (walletName && walletName !== "WalletConnect") {
|
|
190
190
|
return `Scan code to connect to ${walletName}`
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -265,10 +265,10 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
|
|
|
265
265
|
<div className="flex flex-col items-center">
|
|
266
266
|
<QrCode
|
|
267
267
|
url={wcUri}
|
|
268
|
-
size={
|
|
268
|
+
size={300}
|
|
269
269
|
imageUrl={
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
getWalletIcon(selectedWalletId) ||
|
|
271
|
+
getWalletIcon("walletconnect")
|
|
272
272
|
}
|
|
273
273
|
/>
|
|
274
274
|
<button
|
|
@@ -298,7 +298,7 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
|
|
|
298
298
|
</div>
|
|
299
299
|
) : (
|
|
300
300
|
<div className="flex flex-col items-center">
|
|
301
|
-
<div className="w-[
|
|
301
|
+
<div className="w-[300px] h-[300px] bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center trails-border-radius-container">
|
|
302
302
|
<div className="text-center">
|
|
303
303
|
<div className="animate-spin rounded-full h-8 w-8 border-solid border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
304
304
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
@@ -310,7 +310,9 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
|
|
|
310
310
|
)}
|
|
311
311
|
|
|
312
312
|
<div className="mt-3 text-xs text-gray-600 dark:text-gray-400">
|
|
313
|
-
{status === "pending" &&
|
|
313
|
+
{status === "pending" && wcUri && (
|
|
314
|
+
<span>Waiting for confirmation…</span>
|
|
315
|
+
)}
|
|
314
316
|
</div>
|
|
315
317
|
</>
|
|
316
318
|
)}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ChevronLeft } from "lucide-react"
|
|
2
|
+
import type React from "react"
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { useWallets } from "../../wallets.js"
|
|
5
|
+
|
|
6
|
+
interface WalletConnectionPendingProps {
|
|
7
|
+
onBack: () => void
|
|
8
|
+
onRetry: () => void
|
|
9
|
+
selectedWalletId: string
|
|
10
|
+
isConnecting: boolean
|
|
11
|
+
error: string | null
|
|
12
|
+
showRetry?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const WalletConnectionPending: React.FC<
|
|
16
|
+
WalletConnectionPendingProps
|
|
17
|
+
> = ({
|
|
18
|
+
onBack,
|
|
19
|
+
onRetry,
|
|
20
|
+
selectedWalletId,
|
|
21
|
+
isConnecting,
|
|
22
|
+
error,
|
|
23
|
+
showRetry = false,
|
|
24
|
+
}) => {
|
|
25
|
+
const [showContent, setShowContent] = useState(false)
|
|
26
|
+
const [showTimeoutWarning, setShowTimeoutWarning] = useState(false)
|
|
27
|
+
const { wallets: allWallets } = useWallets()
|
|
28
|
+
|
|
29
|
+
const walletConfig = allWallets.find((w) => w.id === selectedWalletId)
|
|
30
|
+
const walletName = walletConfig?.name || selectedWalletId
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setShowContent(true)
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
() => {
|
|
39
|
+
if (isConnecting && !error) {
|
|
40
|
+
setShowTimeoutWarning(true)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
2 * 60 * 1000,
|
|
44
|
+
) // 2 minutes
|
|
45
|
+
|
|
46
|
+
return () => clearTimeout(timer)
|
|
47
|
+
}, [isConnecting, error])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="space-y-6">
|
|
51
|
+
<div className="flex items-center relative">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={onBack}
|
|
55
|
+
className="absolute left-0 top-0 p-2 rounded-full transition-colors cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 hover:trails-hover-bg text-gray-600 dark:text-gray-400"
|
|
56
|
+
>
|
|
57
|
+
<ChevronLeft className="h-6 w-6" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="flex flex-col justify-center min-h-full space-y-6 pt-8">
|
|
62
|
+
<div className="text-center">
|
|
63
|
+
<div
|
|
64
|
+
className={`mx-auto flex items-center justify-center transition-all duration-500 ease-out relative mb-4 ${showContent ? "transform -translate-y-8" : ""}`}
|
|
65
|
+
>
|
|
66
|
+
{error ? (
|
|
67
|
+
<div className={`h-24 w-24`} />
|
|
68
|
+
) : (
|
|
69
|
+
<div className="animate-spin rounded-full h-24 w-24 border-solid border-b-2 border-blue-500 dark:border-blue-400" />
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<div className="absolute">
|
|
73
|
+
{walletConfig?.icon && typeof walletConfig.icon === "string" ? (
|
|
74
|
+
<img
|
|
75
|
+
src={walletConfig.icon}
|
|
76
|
+
alt={walletName}
|
|
77
|
+
className="h-16 w-16"
|
|
78
|
+
/>
|
|
79
|
+
) : (
|
|
80
|
+
<div className="h-16 w-16 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
|
81
|
+
<span className="text-2xl font-bold text-gray-600 dark:text-gray-400">
|
|
82
|
+
{walletName.charAt(0).toUpperCase()}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div
|
|
90
|
+
className={`mb-2 transition-all duration-500 ease-out ${showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
|
91
|
+
>
|
|
92
|
+
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
|
93
|
+
{error ? "Connection failed" : `Waiting for ${walletName}…`}
|
|
94
|
+
</h2>
|
|
95
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
|
96
|
+
{error
|
|
97
|
+
? "There was an issue connecting to your wallet. Please try again."
|
|
98
|
+
: "Please approve the connection request in your wallet"}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Timeout Warning */}
|
|
104
|
+
{showTimeoutWarning && !error && (
|
|
105
|
+
<div
|
|
106
|
+
className={`transition-all duration-500 ease-out delay-150 ${showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
|
107
|
+
>
|
|
108
|
+
<div className="p-4 trails-border-radius-container text-sm bg-yellow-50 border border-solid border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-700/50">
|
|
109
|
+
<div className="flex items-start space-x-3">
|
|
110
|
+
<svg
|
|
111
|
+
className="w-5 h-5 mt-0.5 flex-shrink-0 text-yellow-600 dark:text-yellow-400"
|
|
112
|
+
fill="none"
|
|
113
|
+
viewBox="0 0 24 24"
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
>
|
|
116
|
+
<title>Warning</title>
|
|
117
|
+
<path
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
strokeLinejoin="round"
|
|
120
|
+
strokeWidth={2}
|
|
121
|
+
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"
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
<div>
|
|
125
|
+
<p className="font-medium text-yellow-800 dark:text-yellow-300">
|
|
126
|
+
Connection is taking longer than expected
|
|
127
|
+
</p>
|
|
128
|
+
<p className="mt-1 text-xs text-yellow-700 dark:text-yellow-400">
|
|
129
|
+
This connection request is taking longer than expected.
|
|
130
|
+
Please reach out to support for help if the issue persists.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Retry Button */}
|
|
139
|
+
{(error || showRetry) && (
|
|
140
|
+
<div
|
|
141
|
+
className={`mb-2 transition-all duration-500 ease-out delay-300 ${showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
|
142
|
+
>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={onRetry}
|
|
146
|
+
className="w-full px-4 py-2 trails-border-radius-button font-medium transition-colors cursor-pointer bg-blue-500 hover:bg-blue-600 text-white dark:bg-blue-600 dark:hover:bg-blue-700"
|
|
147
|
+
>
|
|
148
|
+
Try Again
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default WalletConnectionPending
|
|
@@ -3,7 +3,7 @@ import type React from "react"
|
|
|
3
3
|
import { ChevronRight, Search } from "lucide-react"
|
|
4
4
|
import { Wallet } from "lucide-react"
|
|
5
5
|
import { ScreenHeader } from "./ScreenHeader.js"
|
|
6
|
-
import {
|
|
6
|
+
import { topShownWallets } from "../../wallets.js"
|
|
7
7
|
import type { WalletConfig } from "../../wallets.js"
|
|
8
8
|
|
|
9
9
|
export interface WalletListProps {
|
|
@@ -23,8 +23,8 @@ export const WalletList: React.FC<WalletListProps> = ({
|
|
|
23
23
|
return walletOptions
|
|
24
24
|
.filter((wallet) => wallet.id !== "injected")
|
|
25
25
|
.sort((a, b) => {
|
|
26
|
-
const aIndex =
|
|
27
|
-
const bIndex =
|
|
26
|
+
const aIndex = topShownWallets.indexOf(a.id)
|
|
27
|
+
const bIndex = topShownWallets.indexOf(b.id)
|
|
28
28
|
|
|
29
29
|
// If both are featured, maintain featured order
|
|
30
30
|
if (aIndex !== -1 && bIndex !== -1) {
|
|
@@ -84,14 +84,15 @@ export const WalletList: React.FC<WalletListProps> = ({
|
|
|
84
84
|
<button
|
|
85
85
|
type="button"
|
|
86
86
|
key={wallet.id}
|
|
87
|
+
data-wallet-id={wallet.id}
|
|
87
88
|
onClick={() => handleWalletSelect(wallet.id)}
|
|
88
89
|
className="w-full flex items-center justify-between cursor-pointer font-semibold py-4 px-6 trails-border-radius-large-button transition-all duration-200 trails-bg-secondary trails-hover-bg trails-text-primary"
|
|
89
90
|
>
|
|
90
91
|
<div className="flex items-center space-x-3">
|
|
91
|
-
{wallet.
|
|
92
|
+
{wallet.icon ? (
|
|
92
93
|
<>
|
|
93
94
|
<img
|
|
94
|
-
src={wallet.
|
|
95
|
+
src={wallet.icon}
|
|
95
96
|
alt={wallet.name}
|
|
96
97
|
className="h-6 w-6"
|
|
97
98
|
/>
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import type { SequenceAPIClient } from "@0xsequence/api"
|
|
2
1
|
import { useQuery } from "@tanstack/react-query"
|
|
3
2
|
import { useTokenPrice } from "../../prices.js"
|
|
4
3
|
import { formatUsdAmountDisplay } from "../../tokenBalances.js"
|
|
5
4
|
import { useTokenAddress } from "../../tokens.js"
|
|
5
|
+
import { useAPIClient } from "../../apiClient.js"
|
|
6
6
|
|
|
7
7
|
type UseAmountUsdProps = {
|
|
8
8
|
amount?: string | null
|
|
9
9
|
token?: string | null
|
|
10
10
|
chainId?: number | null
|
|
11
|
-
apiClient: SequenceAPIClient
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export function useAmountUsd({
|
|
15
|
-
amount,
|
|
16
|
-
token,
|
|
17
|
-
chainId,
|
|
18
|
-
apiClient,
|
|
19
|
-
}: UseAmountUsdProps): {
|
|
13
|
+
export function useAmountUsd({ amount, token, chainId }: UseAmountUsdProps): {
|
|
20
14
|
amountUsd: number | null
|
|
21
15
|
amountUsdFormatted: string
|
|
22
16
|
} {
|
|
17
|
+
const apiClient = useAPIClient()
|
|
23
18
|
const isTokenAddress = token?.startsWith("0x")
|
|
24
19
|
const resolvedTokenAddress = useTokenAddress({
|
|
25
20
|
chainId,
|