Skip to content

Commit c501c85

Browse files
author
CloudLobster
committed
fix: claim page - single button flow (auth+claim in one tap)
No auto-sign, no two-step. User sees one green button: 'Claim $0.10 USDC' → taps → sign popup → auth → claim → done. All from a user-initiated click event.
1 parent 987b641 commit c501c85

File tree

1 file changed

+51
-160
lines changed

1 file changed

+51
-160
lines changed

web/src/pages/Claim.tsx

Lines changed: 51 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22
import { useParams, useNavigate } from 'react-router-dom';
33
import { 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-z0-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

Comments
 (0)