1
- import { useState , Fragment , useMemo } from "react" ;
1
+ import { Fragment , useMemo , useRef , useEffect } from "react" ;
2
2
import { Listbox , Transition } from "@headlessui/react" ;
3
3
import { getChains , NATIVE } from "common" ;
4
- import { useAccount } from "wagmi" ;
5
- import { getBalance } from "@wagmi/core" ;
6
- import { config } from "../../app/wagmi" ;
7
- import React from "react" ;
8
- import { zeroAddress } from "viem" ;
9
4
import { stringToBlobUrl } from "common" ;
10
5
import { Checkbox } from "@chakra-ui/react" ;
6
+ import { zeroAddress } from "viem" ;
7
+ import { useDonateToGitcoin } from "./DonateToGitcoinContext" ;
8
+ import React from "react" ;
9
+
10
+ type TokenFilter = {
11
+ chainId : number ;
12
+ addresses : string [ ] ;
13
+ } ;
14
+
15
+ export type DonationDetails = {
16
+ chainId : number ;
17
+ tokenAddress : string ;
18
+ amount : string ;
19
+ } ;
11
20
12
21
type DonateToGitcoinProps = {
13
22
divider ?: "none" | "top" | "bottom" ;
23
+ tokenFilters ?: TokenFilter [ ] ;
14
24
} ;
15
25
16
- export function DonateToGitcoin ( { divider = "none" } : DonateToGitcoinProps ) {
17
- const [ isEnabled , setIsEnabled ] = useState ( false ) ;
18
- const chains = getChains ( ) . filter ( ( c ) => c . type === "mainnet" ) ;
19
- const [ selectedChainId , setSelectedChainId ] = useState < number | null > ( null ) ;
20
- const [ selectedToken , setSelectedToken ] = useState < string > ( "" ) ;
21
- const [ amount , setAmount ] = useState < string > ( "" ) ;
22
- const [ tokenBalances , setTokenBalances ] = useState <
23
- { token : string ; balance : number } [ ]
24
- > ( [ ] ) ;
26
+ const AmountInput = React . memo ( function AmountInput ( {
27
+ amount,
28
+ isAmountValid,
29
+ selectedToken,
30
+ selectedTokenBalance,
31
+ tokenDetails,
32
+ handleAmountChange,
33
+ } : {
34
+ amount : string ;
35
+ isAmountValid : boolean ;
36
+ selectedToken : string ;
37
+ selectedTokenBalance : number ;
38
+ tokenDetails ?: { code : string } ;
39
+ handleAmountChange : ( e : React . ChangeEvent < HTMLInputElement > ) => void ;
40
+ } ) {
41
+ const inputRef = useRef < HTMLInputElement > ( null ) ;
42
+
43
+ useEffect ( ( ) => {
44
+ if ( inputRef . current ) {
45
+ inputRef . current . focus ( ) ;
46
+ }
47
+ } , [ ] ) ;
48
+
49
+ return (
50
+ < div className = "relative flex-grow max-w-[200px]" >
51
+ < input
52
+ ref = { inputRef }
53
+ type = "text"
54
+ className = { `w-full rounded-lg border py-2 px-3 text-sm shadow-sm hover:border-gray-300 ${
55
+ isAmountValid ? "border-gray-200" : "border-red-300"
56
+ } `}
57
+ value = { amount }
58
+ onChange = { handleAmountChange }
59
+ placeholder = "Enter amount"
60
+ max = { selectedTokenBalance }
61
+ />
62
+ { selectedToken && (
63
+ < div className = "absolute right-3 top-2.5 text-xs text-gray-500" >
64
+ { tokenDetails ?. code }
65
+ </ div >
66
+ ) }
67
+ </ div >
68
+ ) ;
69
+ } ) ;
70
+
71
+ function DonateToGitcoinContent ( {
72
+ divider = "none" ,
73
+ tokenFilters,
74
+ } : DonateToGitcoinProps ) {
75
+ const {
76
+ isEnabled,
77
+ selectedChainId,
78
+ selectedToken,
79
+ amount,
80
+ tokenBalances,
81
+ selectedTokenBalance,
82
+ handleAmountChange,
83
+ handleTokenChange,
84
+ handleChainChange,
85
+ handleCheckboxChange,
86
+ } = useDonateToGitcoin ( ) ;
87
+
88
+ // Filter chains based on tokenFilters
89
+ const chains = useMemo ( ( ) => {
90
+ const allChains = getChains ( ) . filter ( ( c ) => c . type === "mainnet" ) ;
91
+ if ( ! tokenFilters ) return allChains ;
92
+ return allChains . filter ( ( chain ) =>
93
+ tokenFilters . some ( ( filter ) => filter . chainId === chain . id )
94
+ ) ;
95
+ } , [ tokenFilters ] ) ;
25
96
26
97
const selectedChain = selectedChainId
27
98
? chains . find ( ( c ) => c . id === selectedChainId )
28
99
: null ;
29
100
const tokenDetails = selectedChain ?. tokens . find (
30
101
( t ) => t . address === selectedToken
31
102
) ;
32
- const { address } = useAccount ( ) ;
33
103
34
- const handleCheckboxChange = ( checked : boolean ) => {
35
- setIsEnabled ( checked ) ;
36
- if ( ! checked ) {
37
- // Reset form when checkbox is unchecked
38
- setSelectedChainId ( null ) ;
39
- setSelectedToken ( "" ) ;
40
- setAmount ( "" ) ;
41
- setTokenBalances ( [ ] ) ;
42
- }
43
- } ;
44
-
45
- React . useEffect ( ( ) => {
46
- if ( ! address || ! selectedChain ) return ;
47
-
48
- const fetchBalances = async ( ) => {
49
- const balances = await Promise . all (
50
- selectedChain . tokens
51
- . filter ( ( token ) => token . address !== zeroAddress )
52
- . map ( async ( token ) => {
53
- const { value } = await getBalance ( config , {
54
- address,
55
- token :
56
- token . address . toLowerCase ( ) === NATIVE . toLowerCase ( )
57
- ? undefined
58
- : token . address ,
59
- chainId : selectedChainId ,
60
- } ) ;
61
- return {
62
- token : token . address ,
63
- balance : Number ( value ) / 10 ** ( token . decimals || 18 ) ,
64
- } ;
65
- } )
66
- ) ;
67
- setTokenBalances ( balances ) ;
68
- } ;
69
-
70
- fetchBalances ( ) ;
71
- } , [ address , selectedChainId , selectedChain ] ) ;
72
-
73
- const selectedTokenBalance =
74
- tokenBalances . find (
75
- ( b ) => b . token . toLowerCase ( ) === selectedToken . toLowerCase ( )
76
- ) ?. balance || 0 ;
77
-
78
- const isAmountValid = useMemo ( ( ) => {
79
- if ( ! amount || ! selectedToken ) return true ;
80
- const numAmount = Number ( amount ) ;
81
- return numAmount > 0 && numAmount <= selectedTokenBalance ;
82
- } , [ amount , selectedToken , selectedTokenBalance ] ) ;
104
+ // Filter tokens based on tokenFilters
105
+ const filteredTokens = useMemo ( ( ) => {
106
+ if ( ! selectedChain || ! tokenFilters ) return selectedChain ?. tokens ;
107
+ const chainFilter = tokenFilters . find (
108
+ ( f ) => f . chainId === selectedChain . id
109
+ ) ;
110
+ if ( ! chainFilter ) return selectedChain . tokens ;
111
+ return selectedChain . tokens . filter ( ( token ) =>
112
+ chainFilter . addresses
113
+ . map ( ( addr ) => addr . toLowerCase ( ) )
114
+ . includes ( token . address . toLowerCase ( ) )
115
+ ) ;
116
+ } , [ selectedChain , tokenFilters ] ) ;
83
117
84
118
const borderClass = useMemo ( ( ) => {
85
119
switch ( divider ) {
@@ -92,6 +126,16 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
92
126
}
93
127
} , [ divider ] ) ;
94
128
129
+ const isAmountValid = useMemo ( ( ) => {
130
+ if ( ! amount || ! selectedToken ) return true ;
131
+ const numAmount = Number ( amount ) ;
132
+ return (
133
+ ! isNaN ( numAmount ) &&
134
+ ( amount . endsWith ( "." ) || numAmount > 0 ) &&
135
+ numAmount <= selectedTokenBalance
136
+ ) ;
137
+ } , [ amount , selectedToken , selectedTokenBalance ] ) ;
138
+
95
139
return (
96
140
< div className = { `flex flex-col justify-center mt-2 py-4 ${ borderClass } ` } >
97
141
< div className = { `${ ! isEnabled ? "opacity-50" : "" } ` } >
@@ -129,12 +173,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
129
173
< select
130
174
className = "bg-transparent border-none focus:ring-0 text-sm flex-grow font-medium"
131
175
value = { selectedChainId || "" }
132
- onChange = { ( e ) => {
133
- const newChainId = Number ( e . target . value ) ;
134
- setSelectedChainId ( newChainId || null ) ;
135
- setSelectedToken ( "" ) ;
136
- setAmount ( "" ) ;
137
- } }
176
+ onChange = { handleChainChange }
138
177
>
139
178
< option value = "" > Select chain</ option >
140
179
{ chains
@@ -150,7 +189,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
150
189
151
190
{ selectedChain && (
152
191
< div className = "flex items-center gap-3" >
153
- < Listbox value = { selectedToken } onChange = { setSelectedToken } >
192
+ < Listbox value = { selectedToken } onChange = { handleTokenChange } >
154
193
< div className = "relative" >
155
194
< Listbox . Button className = "relative w-40 cursor-default rounded-lg border border-gray-200 bg-white py-2 pl-3 pr-8 text-left text-sm shadow-sm hover:border-gray-300" >
156
195
{ selectedToken ? (
@@ -183,10 +222,9 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
183
222
style = { { maxHeight : "40vh" } }
184
223
>
185
224
< div className = "max-h-[40vh] overflow-y-auto" >
186
- { selectedChain ?. tokens
225
+ { ( filteredTokens || [ ] )
187
226
. filter ( ( token ) => token . address !== zeroAddress )
188
227
. sort ( ( a , b ) => {
189
- // NATIVE token always first
190
228
if (
191
229
a . address . toLowerCase ( ) === NATIVE . toLowerCase ( )
192
230
)
@@ -196,7 +234,6 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
196
234
)
197
235
return 1 ;
198
236
199
- // Get balances
200
237
const balanceA =
201
238
tokenBalances . find (
202
239
( b ) =>
@@ -210,7 +247,6 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
210
247
b . token . toLowerCase ( )
211
248
) ?. balance || 0 ;
212
249
213
- // Sort by balance (highest to lowest)
214
250
if ( balanceA === 0 && balanceB === 0 ) return 0 ;
215
251
if ( balanceA === 0 ) return 1 ;
216
252
if ( balanceB === 0 ) return - 1 ;
@@ -248,23 +284,14 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
248
284
</ div >
249
285
</ Listbox >
250
286
251
- < div className = "relative flex-grow max-w-[200px]" >
252
- < input
253
- type = "number"
254
- className = { `w-full rounded-lg border py-2 px-3 text-sm shadow-sm hover:border-gray-300 ${
255
- isAmountValid ? "border-gray-200" : "border-red-300"
256
- } `}
257
- value = { amount }
258
- onChange = { ( e ) => setAmount ( e . target . value ) }
259
- placeholder = "Enter amount"
260
- max = { selectedTokenBalance }
261
- />
262
- { selectedToken && (
263
- < div className = "absolute right-3 top-2.5 text-xs text-gray-500" >
264
- { tokenDetails ?. code }
265
- </ div >
266
- ) }
267
- </ div >
287
+ < AmountInput
288
+ amount = { amount }
289
+ isAmountValid = { isAmountValid }
290
+ selectedToken = { selectedToken }
291
+ selectedTokenBalance = { selectedTokenBalance }
292
+ tokenDetails = { tokenDetails }
293
+ handleAmountChange = { handleAmountChange }
294
+ />
268
295
</ div >
269
296
) }
270
297
@@ -278,3 +305,7 @@ export function DonateToGitcoin({ divider = "none" }: DonateToGitcoinProps) {
278
305
</ div >
279
306
) ;
280
307
}
308
+
309
+ export function DonateToGitcoin ( props : DonateToGitcoinProps ) {
310
+ return < DonateToGitcoinContent { ...props } /> ;
311
+ }
0 commit comments