1+ import React , { useState } from 'react' ;
2+ import { Button } from './ui/button' ;
3+ import { Copy , Clock , Trash2 , Wallet } from 'lucide-react' ;
4+ import { ethers } from 'ethers' ;
5+ import { MembershipState } from '@waku/rln' ;
6+ import { useRLN } from '../contexts/rln/RLNContext' ;
7+ import { toast } from 'sonner' ;
8+
9+ interface MembershipDetailsProps {
10+ membershipInfo : {
11+ address : string ;
12+ chainId : string ;
13+ treeIndex : number ;
14+ rateLimit : number ;
15+ idCommitment : string ;
16+ startBlock : number ;
17+ endBlock : number ;
18+ state : MembershipState ;
19+ depositAmount : ethers . BigNumber ;
20+ activeDuration : number ;
21+ gracePeriodDuration : number ;
22+ holder : string ;
23+ token : string ;
24+ } ;
25+ copyToClipboard : ( text : string ) => void ;
26+ hash : string ;
27+ }
28+
29+ export function MembershipDetails ( { membershipInfo, copyToClipboard, hash } : MembershipDetailsProps ) {
30+ const { extendMembership, eraseMembership, withdrawDeposit } = useRLN ( ) ;
31+ const [ isLoading , setIsLoading ] = useState < { [ key : string ] : boolean } > ( { } ) ;
32+ const [ password , setPassword ] = useState ( '' ) ;
33+ const [ showPasswordInput , setShowPasswordInput ] = useState ( false ) ;
34+ const [ actionType , setActionType ] = useState < 'extend' | 'erase' | 'withdraw' | null > ( null ) ;
35+
36+ const handleAction = async ( type : 'extend' | 'erase' | 'withdraw' ) => {
37+ if ( ! password ) {
38+ setActionType ( type ) ;
39+ setShowPasswordInput ( true ) ;
40+ return ;
41+ }
42+
43+ setIsLoading ( prev => ( { ...prev , [ type ] : true } ) ) ;
44+ try {
45+ let result ;
46+ switch ( type ) {
47+ case 'extend' :
48+ result = await extendMembership ( hash , password ) ;
49+ break ;
50+ case 'erase' :
51+ result = await eraseMembership ( hash , password ) ;
52+ break ;
53+ case 'withdraw' :
54+ result = await withdrawDeposit ( hash , password ) ;
55+ break ;
56+ }
57+
58+ if ( result . success ) {
59+ toast . success ( `Successfully ${ type } ed membership` ) ;
60+ setPassword ( '' ) ;
61+ setShowPasswordInput ( false ) ;
62+ setActionType ( null ) ;
63+ } else {
64+ toast . error ( result . error || `Failed to ${ type } membership` ) ;
65+ }
66+ } catch ( err ) {
67+ toast . error ( err instanceof Error ? err . message : `Failed to ${ type } membership` ) ;
68+ } finally {
69+ setIsLoading ( prev => ( { ...prev , [ type ] : false } ) ) ;
70+ }
71+ } ;
72+
73+ // Check if membership is in grace period
74+ const isInGracePeriod = membershipInfo . state === MembershipState . GracePeriod ;
75+
76+ // Check if membership is erased and awaiting withdrawal
77+ const canWithdraw = membershipInfo . state === MembershipState . ErasedAwaitsWithdrawal ;
78+
79+ // Check if membership can be erased (Active or GracePeriod)
80+ const canErase = membershipInfo . state === MembershipState . Active || membershipInfo . state === MembershipState . GracePeriod ;
81+
82+ return (
83+ < div className = "mt-3 space-y-2 border-t border-terminal-border/40 pt-3 animate-in fade-in-50 duration-300" >
84+ < div className = "flex items-center justify-between mb-2" >
85+ < div className = "flex items-center" >
86+ < span className = "text-primary font-mono font-medium mr-2" > { ">" } </ span >
87+ < h3 className = "text-sm font-mono font-semibold text-primary" >
88+ Membership Details
89+ </ h3 >
90+ </ div >
91+ < div className = "flex items-center space-x-2" >
92+ { isInGracePeriod && (
93+ < Button
94+ variant = "outline"
95+ size = "sm"
96+ className = "text-warning-DEFAULT hover:text-warning-DEFAULT hover:border-warning-DEFAULT flex items-center gap-1"
97+ onClick = { ( ) => handleAction ( 'extend' ) }
98+ disabled = { isLoading . extend }
99+ >
100+ < Clock className = "w-3 h-3" />
101+ < span > { isLoading . extend ? 'Extending...' : 'Extend' } </ span >
102+ </ Button >
103+ ) }
104+ { canErase && (
105+ < Button
106+ variant = "outline"
107+ size = "sm"
108+ className = "text-destructive hover:text-destructive hover:border-destructive flex items-center gap-1"
109+ onClick = { ( ) => handleAction ( 'erase' ) }
110+ disabled = { isLoading . erase }
111+ >
112+ < Trash2 className = "w-3 h-3" />
113+ < span > { isLoading . erase ? 'Erasing...' : 'Erase' } </ span >
114+ </ Button >
115+ ) }
116+ { canWithdraw && (
117+ < Button
118+ variant = "outline"
119+ size = "sm"
120+ className = "text-accent hover:text-accent hover:border-accent flex items-center gap-1"
121+ onClick = { ( ) => handleAction ( 'withdraw' ) }
122+ disabled = { isLoading . withdraw }
123+ >
124+ < Wallet className = "w-3 h-3" />
125+ < span > { isLoading . withdraw ? 'Withdrawing...' : 'Withdraw' } </ span >
126+ </ Button >
127+ ) }
128+ </ div >
129+ </ div >
130+
131+ { showPasswordInput && (
132+ < div className = "mb-4 space-y-2 border-b border-terminal-border pb-4" >
133+ < input
134+ type = "password"
135+ value = { password }
136+ onChange = { ( e ) => setPassword ( e . target . value ) }
137+ placeholder = "Enter keystore password"
138+ className = "w-full px-3 py-2 border border-terminal-border rounded-md bg-terminal-background text-foreground font-mono focus:ring-1 focus:ring-accent focus:border-accent text-sm"
139+ />
140+ < div className = "flex space-x-2" >
141+ < Button
142+ variant = "default"
143+ size = "sm"
144+ onClick = { ( ) => handleAction ( actionType ! ) }
145+ disabled = { ! password || isLoading [ actionType ! ] }
146+ >
147+ Confirm
148+ </ Button >
149+ < Button
150+ variant = "ghost"
151+ size = "sm"
152+ onClick = { ( ) => {
153+ setShowPasswordInput ( false ) ;
154+ setPassword ( '' ) ;
155+ setActionType ( null ) ;
156+ } }
157+ >
158+ Cancel
159+ </ Button >
160+ </ div >
161+ </ div >
162+ ) }
163+
164+ < div className = "space-y-2 text-xs font-mono" >
165+ < div className = "grid grid-cols-2 gap-4" >
166+ { /* Membership State */ }
167+ < div >
168+ < span className = "text-muted-foreground text-xs" > State:</ span >
169+ < div className = "text-accent" > { membershipInfo . state || 'N/A' } </ div >
170+ </ div >
171+
172+ { /* Basic Info */ }
173+ < div >
174+ < span className = "text-muted-foreground text-xs" > Chain ID:</ span >
175+ < div className = "text-accent" > { membershipInfo . chainId } </ div >
176+ </ div >
177+ < div >
178+ < span className = "text-muted-foreground text-xs" > Rate Limit:</ span >
179+ < div className = "text-accent" > { membershipInfo . rateLimit } msg/epoch</ div >
180+ </ div >
181+
182+ { /* Contract Info */ }
183+ < div >
184+ < span className = "text-muted-foreground text-xs" > Contract Address:</ span >
185+ < div className = "text-accent truncate hover:text-clip flex items-center" >
186+ { membershipInfo . address }
187+ < Button
188+ variant = "ghost"
189+ size = "sm"
190+ className = "h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
191+ onClick = { ( ) => membershipInfo . address && copyToClipboard ( membershipInfo . address ) }
192+ >
193+ < Copy className = "h-3 w-3" />
194+ </ Button >
195+ </ div >
196+ </ div >
197+
198+ { /* Member Details */ }
199+ < div >
200+ < span className = "text-muted-foreground text-xs" > Member Index:</ span >
201+ < div className = "text-accent" > { membershipInfo . treeIndex || 'N/A' } </ div >
202+ </ div >
203+ < div >
204+ < span className = "text-muted-foreground text-xs" > ID Commitment:</ span >
205+ < div className = "text-accent truncate hover:text-clip flex items-center" >
206+ { membershipInfo . idCommitment || 'N/A' }
207+ { membershipInfo . idCommitment && (
208+ < Button
209+ variant = "ghost"
210+ size = "sm"
211+ className = "h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
212+ onClick = { ( ) => membershipInfo . idCommitment && copyToClipboard ( membershipInfo . idCommitment ) }
213+ >
214+ < Copy className = "h-3 w-3" />
215+ </ Button >
216+ ) }
217+ </ div >
218+ </ div >
219+
220+ { /* Block Information */ }
221+ < div >
222+ < span className = "text-muted-foreground text-xs" > Start Block:</ span >
223+ < div className = "text-accent" > { membershipInfo . startBlock || 'N/A' } </ div >
224+ </ div >
225+ < div >
226+ < span className = "text-muted-foreground text-xs" > End Block:</ span >
227+ < div className = "text-accent" > { membershipInfo . endBlock || 'N/A' } </ div >
228+ </ div >
229+
230+ { /* Duration Information */ }
231+ < div >
232+ < span className = "text-muted-foreground text-xs" > Active Duration:</ span >
233+ < div className = "text-accent" > { membershipInfo . activeDuration ? `${ membershipInfo . activeDuration } blocks` : 'N/A' } </ div >
234+ </ div >
235+ < div >
236+ < span className = "text-muted-foreground text-xs" > Grace Period:</ span >
237+ < div className = "text-accent" > { membershipInfo . gracePeriodDuration ? `${ membershipInfo . gracePeriodDuration } blocks` : 'N/A' } </ div >
238+ </ div >
239+
240+ { /* Token Information */ }
241+ < div >
242+ < span className = "text-muted-foreground text-xs" > Token Address:</ span >
243+ < div className = "text-accent truncate hover:text-clip flex items-center" >
244+ { membershipInfo . token || 'N/A' }
245+ { membershipInfo . token && (
246+ < Button
247+ variant = "ghost"
248+ size = "sm"
249+ className = "h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
250+ onClick = { ( ) => membershipInfo . token && copyToClipboard ( membershipInfo . token ) }
251+ >
252+ < Copy className = "h-3 w-3" />
253+ </ Button >
254+ ) }
255+ </ div >
256+ </ div >
257+ < div >
258+ < span className = "text-muted-foreground text-xs" > Deposit Amount:</ span >
259+ < div className = "text-accent" >
260+ { membershipInfo . depositAmount ? `${ ethers . utils . formatEther ( membershipInfo . depositAmount ) } ETH` : 'N/A' }
261+ </ div >
262+ </ div >
263+
264+ { /* Holder Information */ }
265+ < div className = "col-span-2" >
266+ < span className = "text-muted-foreground text-xs" > Holder Address:</ span >
267+ < div className = "text-accent truncate hover:text-clip flex items-center" >
268+ { membershipInfo . holder || 'N/A' }
269+ { membershipInfo . holder && (
270+ < Button
271+ variant = "ghost"
272+ size = "sm"
273+ className = "h-5 w-5 p-0 ml-1 text-muted-foreground hover:text-accent"
274+ onClick = { ( ) => membershipInfo . holder && copyToClipboard ( membershipInfo . holder ) }
275+ >
276+ < Copy className = "h-3 w-3" />
277+ </ Button >
278+ ) }
279+ </ div >
280+ </ div >
281+ </ div >
282+ </ div >
283+ </ div >
284+ ) ;
285+ }
0 commit comments