1- import { useState , useEffect } from 'react' ;
1+ import { useState , useEffect , useCallback } from 'react' ;
22import { useParams , useNavigate } from 'react-router-dom' ;
33import { useAccount , useConnect , useSignMessage } from 'wagmi' ;
44
@@ -27,129 +27,74 @@ export default function Claim() {
2727 const [ loading , setLoading ] = useState ( true ) ;
2828 const [ error , setError ] = useState ( '' ) ;
2929
30- // Auth + claim state
31- const [ token , setToken ] = useState ( '' ) ;
32- const [ handle , setHandle ] = useState ( '' ) ;
33- const [ step , setStep ] = useState < 'idle' | 'checking' | 'need-register' | 'ready' | 'claiming' | 'success' > ( 'idle' ) ;
34- const [ regHandle , setRegHandle ] = useState ( '' ) ;
35- const [ regError , setRegError ] = useState ( '' ) ;
30+ const [ status , setStatus ] = useState ( '' ) ;
31+ const [ statusError , setStatusError ] = useState ( '' ) ;
3632 const [ claimResult , setClaimResult ] = useState < any > ( null ) ;
37- const [ claimError , setClaimError ] = useState ( '' ) ;
3833
3934 // Fetch claim info
4035 useEffect ( ( ) => {
4136 if ( ! id ) return ;
4237 fetch ( `${ API_BASE } /api/claim/${ id } ` )
4338 . then ( r => r . json ( ) )
44- . then ( data => {
45- if ( data . error ) setError ( data . error ) ;
46- else setClaim ( data ) ;
47- } )
39+ . then ( data => { if ( data . error ) setError ( data . error ) ; else setClaim ( data ) ; } )
4840 . catch ( ( ) => setError ( 'Failed to load claim' ) )
4941 . finally ( ( ) => setLoading ( false ) ) ;
5042 } , [ id ] ) ;
5143
52- // When wallet connects → auto authenticate
53- useEffect ( ( ) => {
54- if ( ! address || token ) return ;
55- autoAuth ( address ) ;
56- } , [ address ] ) ;
44+ // One-click: auth + claim in a single user-initiated action
45+ const handleAuthAndClaim = useCallback ( async ( ) => {
46+ if ( ! address || ! id ) return ;
47+ setStatusError ( '' ) ;
5748
58- async function autoAuth ( wallet : string ) {
59- setStep ( 'checking' ) ;
60- setClaimError ( '' ) ;
6149 try {
62- // 1. Check if registered
63- const checkRes = await fetch ( `${ API_BASE } /api/register/check/${ wallet } ` ) ;
50+ // 1. Check registration
51+ setStatus ( 'Checking account...' ) ;
52+ const checkRes = await fetch ( `${ API_BASE } /api/register/check/${ address } ` ) ;
6453 const checkData = await checkRes . json ( ) ;
6554
66- if ( ! checkData . registered || ! checkData . handle ) {
67- setStep ( 'need-register' ) ;
55+ if ( ! checkData . registered ) {
56+ setStatus ( '' ) ;
57+ setStatusError ( 'No BaseMail account found. Please register at basemail.ai first, then come back to claim.' ) ;
6858 return ;
6959 }
7060
7161 // 2. SIWE auth
72- setHandle ( checkData . handle ) ;
62+ setStatus ( 'Preparing sign-in...' ) ;
7363 const startRes = await fetch ( `${ API_BASE } /api/auth/start` , {
7464 method : 'POST' ,
7565 headers : { 'Content-Type' : 'application/json' } ,
76- body : JSON . stringify ( { address : wallet } ) ,
66+ body : JSON . stringify ( { address } ) ,
7767 } ) ;
7868 const { nonce, message } = await startRes . json ( ) ;
79- let signature : string ;
80- try {
81- signature = await signMessageAsync ( { message } ) ;
82- } catch {
83- throw new Error ( 'Wallet signature failed — tap the button below to retry' ) ;
84- }
8569
70+ setStatus ( 'Please sign in your wallet...' ) ;
71+ const signature = await signMessageAsync ( { message } ) ;
72+
73+ setStatus ( 'Verifying...' ) ;
8674 const verifyRes = await fetch ( `${ API_BASE } /api/auth/verify` , {
8775 method : 'POST' ,
8876 headers : { 'Content-Type' : 'application/json' } ,
89- body : JSON . stringify ( { address : wallet , signature, nonce, message } ) ,
77+ body : JSON . stringify ( { address, signature, nonce, message } ) ,
9078 } ) ;
9179 const verifyData = await verifyRes . json ( ) ;
80+ if ( ! verifyData . token ) throw new Error ( verifyData . error || 'Authentication failed' ) ;
9281
93- if ( verifyData . token ) {
94- setToken ( verifyData . token ) ;
95- if ( verifyData . handle ) setHandle ( verifyData . handle ) ;
96- setStep ( 'ready' ) ;
97- } else {
98- throw new Error ( verifyData . error || 'Auth failed' ) ;
99- }
100- } catch ( e : any ) {
101- setClaimError ( e . message || 'Authentication failed' ) ;
102- setStep ( 'idle' ) ;
103- }
104- }
105-
106- async function handleRegister ( ) {
107- if ( ! address || ! regHandle ) return ;
108- setRegError ( '' ) ;
109- try {
110- // SIWE + register in one flow
111- const startRes = await fetch ( `${ API_BASE } /api/auth/start` , {
112- method : 'POST' ,
113- headers : { 'Content-Type' : 'application/json' } ,
114- body : JSON . stringify ( { address } ) ,
115- } ) ;
116- const { nonce, message } = await startRes . json ( ) ;
117- const signature = await signMessageAsync ( { message } ) ;
118-
119- const regRes = await fetch ( `${ API_BASE } /api/register` , {
82+ // 3. Claim
83+ setStatus ( 'Claiming USDC...' ) ;
84+ const claimRes = await fetch ( `${ API_BASE } /api/claim/${ id } ` , {
12085 method : 'POST' ,
121- headers : { 'Content-Type' : 'application/json' } ,
122- body : JSON . stringify ( { wallet : address , handle : regHandle , signature, nonce } ) ,
86+ headers : { 'Content-Type' : 'application/json' , Authorization : `Bearer ${ verifyData . token } ` } ,
12387 } ) ;
124- const regData = await regRes . json ( ) ;
125- if ( ! regRes . ok ) throw new Error ( regData . error || 'Registration failed' ) ;
88+ const claimData = await claimRes . json ( ) ;
89+ if ( ! claimRes . ok ) throw new Error ( claimData . error || 'Claim failed' ) ;
12690
127- setToken ( regData . token ) ;
128- setHandle ( regData . handle || regHandle ) ;
129- setStep ( 'ready' ) ;
91+ setClaimResult ( { ...claimData , handle : verifyData . handle || checkData . handle } ) ;
92+ setStatus ( '' ) ;
13093 } catch ( e : any ) {
131- setRegError ( e . message ) ;
94+ setStatusError ( e . message || 'Failed' ) ;
95+ setStatus ( '' ) ;
13296 }
133- }
134-
135- async function handleClaim ( ) {
136- if ( ! token || ! id ) return ;
137- setStep ( 'claiming' ) ;
138- setClaimError ( '' ) ;
139- try {
140- const res = await fetch ( `${ API_BASE } /api/claim/${ id } ` , {
141- method : 'POST' ,
142- headers : { 'Content-Type' : 'application/json' , Authorization : `Bearer ${ token } ` } ,
143- } ) ;
144- const data = await res . json ( ) ;
145- if ( ! res . ok ) throw new Error ( data . error || 'Claim failed' ) ;
146- setClaimResult ( data ) ;
147- setStep ( 'success' ) ;
148- } catch ( e : any ) {
149- setClaimError ( e . message ) ;
150- setStep ( 'ready' ) ;
151- }
152- }
97+ } , [ address , id , signMessageAsync ] ) ;
15398
15499 const networkLabel = claim ?. network === 'base-mainnet' ? 'Base' : 'Base Sepolia (Testnet)' ;
155100 const explorerBase = claim ?. network === 'base-mainnet' ? 'https://basescan.org' : 'https://sepolia.basescan.org' ;
@@ -164,7 +109,6 @@ export default function Claim() {
164109 return (
165110 < div className = "min-h-screen bg-[#0a0a0f] flex items-center justify-center p-4" >
166111 < div className = "max-w-md w-full" >
167- { /* Logo */ }
168112 < div className = "text-center mb-6" >
169113 < a href = "/" className = "text-2xl font-bold text-white" >
170114 < span className = "text-purple-400" > Base</ span > Mail
@@ -179,7 +123,7 @@ export default function Claim() {
179123 < div className = "text-4xl mb-3" > ❌</ div >
180124 < p className = "text-red-400" > { error } </ p >
181125 </ div >
182- ) : claim && step === 'success' && claimResult ? (
126+ ) : claim && claimResult ? (
183127 /* ── Success ── */
184128 < div className = "text-center py-6" >
185129 < div className = "text-5xl mb-4" > ✅</ div >
@@ -188,24 +132,20 @@ export default function Claim() {
188132 < span className = "text-white font-bold text-2xl" > ${ claim . amount_usdc . toFixed ( 2 ) } </ span > USDC
189133 </ p >
190134 < p className = "text-gray-500 text-sm mb-4" >
191- From { claim . sender } → { claimResult . claimer }
135+ From { claim . sender } → { claimResult . handle || claimResult . claimer }
192136 </ p >
193137 { claimResult . release_tx && (
194- < a
195- href = { `${ explorerBase } /tx/${ claimResult . release_tx } ` }
138+ < a href = { `${ explorerBase } /tx/${ claimResult . release_tx } ` }
196139 target = "_blank" rel = "noopener noreferrer"
197- className = "text-purple-400 hover:text-purple-300 text-xs underline block mb-4"
198- >
140+ className = "text-purple-400 hover:text-purple-300 text-xs underline block mb-4" >
199141 View transaction on BaseScan ↗
200142 </ a >
201143 ) }
202144 < p className = "text-gray-500 text-xs mb-4" >
203145 A receipt email has been delivered to your BaseMail inbox.
204146 </ p >
205- < button
206- onClick = { ( ) => navigate ( '/dashboard' ) }
207- className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition"
208- >
147+ < button onClick = { ( ) => navigate ( '/dashboard' ) }
148+ className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition" >
209149 Open Dashboard
210150 </ button >
211151 </ div >
@@ -243,78 +183,29 @@ export default function Claim() {
243183 < p > Claimed USDC will appear as a receipt email in your BaseMail inbox.</ p >
244184 </ div >
245185
246- { /* Step 1: Connect wallet */ }
247186 { ! isConnected ? (
187+ /* Connect wallet */
248188 < div className = "space-y-2" >
249189 { connectors . map ( ( connector ) => (
250- < button
251- key = { connector . id }
190+ < button key = { connector . id }
252191 onClick = { ( ) => connect ( { connector } ) }
253- className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition"
254- >
192+ className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition" >
255193 🔗 Connect { connector . name }
256194 </ button >
257195 ) ) }
258196 </ div >
259- ) : step === 'checking' ? (
260- < div className = "text-center text-gray-400 py-4 animate-pulse" >
261- Checking account...
262- </ div >
263- ) : step === 'need-register' ? (
264- /* Register new account */
265- < div >
266- < p className = "text-gray-400 text-sm mb-3" >
267- Create a free BaseMail account to claim your USDC:
268- </ p >
269- < div className = "flex gap-2 mb-2" >
270- < input
271- type = "text"
272- value = { regHandle }
273- onChange = { ( e ) => setRegHandle ( e . target . value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] / g, '' ) ) }
274- placeholder = "choose a handle"
275- className = "flex-1 bg-[#0a0a0f] border border-gray-700 rounded-lg px-3 py-2.5 text-white font-mono text-sm focus:outline-none focus:border-purple-500"
276- />
277- < span className = "text-gray-500 text-sm self-center" > @basemail.ai</ span >
278- </ div >
279- { regError && < p className = "text-red-400 text-xs mb-2" > { regError } </ p > }
280- < button
281- onClick = { handleRegister }
282- disabled = { ! regHandle || regHandle . length < 3 }
283- className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition disabled:opacity-50"
284- >
285- Create Account & Claim
286- </ button >
287- </ div >
288- ) : step === 'ready' ? (
289- /* Authenticated — claim button */
290- < div >
291- < p className = "text-gray-400 text-sm mb-3" >
292- Claiming as < span className = "text-white font-mono" > { handle } @basemail.ai</ span >
293- </ p >
294- < button
295- onClick = { handleClaim }
296- className = "w-full bg-green-600 text-white py-3 rounded-lg font-bold hover:bg-green-500 transition"
297- >
298- ✅ Claim ${ claim . amount_usdc . toFixed ( 2 ) } USDC
299- </ button >
300- </ div >
301- ) : step === 'claiming' ? (
302- < div className = "text-center text-gray-400 py-4 animate-pulse" >
303- Claiming USDC...
304- </ div >
197+ ) : status ? (
198+ /* Processing */
199+ < div className = "text-center text-gray-400 py-4 animate-pulse" > { status } </ div >
305200 ) : (
306- /* idle — retry auth */
307- < div >
308- < button
309- onClick = { ( ) => address && autoAuth ( address ) }
310- className = "w-full bg-purple-600 text-white py-3 rounded-lg font-medium hover:bg-purple-500 transition"
311- >
312- ✍️ Authenticate & Claim
313- </ button >
314- </ div >
201+ /* One button does everything: auth + claim */
202+ < button onClick = { handleAuthAndClaim }
203+ className = "w-full bg-green-600 text-white py-3 rounded-lg font-bold hover:bg-green-500 transition" >
204+ ✅ Claim ${ claim . amount_usdc . toFixed ( 2 ) } USDC
205+ </ button >
315206 ) }
316207
317- { claimError && < p className = "text-red-400 text-sm mt-3" > { claimError } </ p > }
208+ { statusError && < p className = "text-red-400 text-sm mt-3" > { statusError } </ p > }
318209 </ >
319210 ) }
320211 </ >
0 commit comments