Skip to content

Commit a0437a8

Browse files
ralyodioqwencoder
andcommitted
feat: bulk wallet import for referral payouts
- Create referral_wallets table for multiple wallets per user - Update /api/referrals/import-wallets to bulk upsert all wallets - Add GET/DELETE endpoints for wallet management - Account page now shows all imported wallets with labels - Paste area accepts full CoinPay export (all wallets at once) - Shows import results with each wallet address imported Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent 56e9da0 commit a0437a8

3 files changed

Lines changed: 280 additions & 117 deletions

File tree

src/app/account/account-content.tsx

Lines changed: 147 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useEffect, useCallback } from "react";
44
import Link from "next/link";
55
import { useAuth } from "@/lib/auth-context";
66
import { authHeaders } from "@/lib/auth-client";
77
import { parseWalletPaste, formatWalletCopyText } from "@/lib/wallet-import";
88

9+
interface ReferralWallet {
10+
id: string;
11+
cryptocurrency: string;
12+
wallet_address: string;
13+
label: string | null;
14+
is_primary: boolean;
15+
created_at: string;
16+
}
17+
918
export default function AccountContent() {
1019
const { signedIn, profile, loading, signOut, refreshProfile } = useAuth();
1120
const [editing, setEditing] = useState(false);
@@ -19,7 +28,35 @@ export default function AccountContent() {
1928
const [copied, setCopied] = useState(false);
2029
const [walletPasteOpen, setWalletPasteOpen] = useState(false);
2130
const [walletPasteText, setWalletPasteText] = useState("");
22-
const [walletImportResult, setWalletImportResult] = useState<{ imported: number; errors: string[] } | null>(null);
31+
const [walletImportResult, setWalletImportResult] = useState<{
32+
imported: Array<{ coin: string; address: string; action: string }>;
33+
errors: string[];
34+
} | null>(null);
35+
const [referralWallets, setReferralWallets] = useState<ReferralWallet[]>([]);
36+
const [loadingWallets, setLoadingWallets] = useState(false);
37+
38+
const fetchReferralWallets = useCallback(async () => {
39+
const token = localStorage.getItem("tc_access_token");
40+
if (!token) return;
41+
setLoadingWallets(true);
42+
try {
43+
const res = await fetch("/api/referrals/import-wallets", {
44+
headers: { Authorization: `Bearer ${token}` },
45+
});
46+
if (res.ok) {
47+
const data = await res.json();
48+
setReferralWallets(data.wallets || []);
49+
}
50+
} catch {
51+
// ignore
52+
} finally {
53+
setLoadingWallets(false);
54+
}
55+
}, []);
56+
57+
useEffect(() => {
58+
fetchReferralWallets();
59+
}, [fetchReferralWallets]);
2360

2461
useEffect(() => {
2562
if (profile) {
@@ -98,25 +135,42 @@ export default function AccountContent() {
98135
const handleWalletImport = async () => {
99136
const parsed = parseWalletPaste(walletPasteText);
100137
if (parsed.wallets.length === 0) {
101-
setWalletImportResult({ imported: 0, errors: ["No valid wallet addresses found. Use CoinPay 'Copy All Addresses' format."] });
138+
setWalletImportResult({ imported: [], errors: ["No valid wallet addresses found. Use CoinPay 'Copy All Addresses' format."] });
102139
return;
103140
}
104141

105-
// Auto-select: use the first parsed wallet
106-
const wallet = parsed.wallets[0];
107-
setWalletAddress(wallet.address);
142+
try {
143+
const token = localStorage.getItem("tc_access_token");
144+
const res = await fetch("/api/referrals/import-wallets", {
145+
method: "POST",
146+
headers: {
147+
"Content-Type": "application/json",
148+
Authorization: `Bearer ${token}`,
149+
},
150+
body: JSON.stringify({ paste_text: walletPasteText }),
151+
});
108152

109-
// Map coin to payout_crypto
110-
const payoutMap: Record<string, string> = {
111-
"BTC": "BTC", "ETH": "ETH", "SOL": "SOL", "USDT": "USDT", "USDC": "USDC",
112-
"USDC_ETH": "USDC", "USDC_SOL": "USDC", "USDC_POL": "USDC",
113-
"USDT_ETH": "USDT", "USDT_SOL": "USDT", "USDT_POL": "USDT",
114-
"BNB": "BNB", "XRP": "XRP", "ADA": "ADA", "DOGE": "DOGE", "POL": "POL", "BCH": "BCH",
115-
};
116-
setPayoutCrypto(payoutMap[wallet.coin] || wallet.coin);
153+
const data = await res.json();
117154

118-
setWalletPasteText("");
119-
setWalletImportResult({ imported: parsed.wallets.length, errors: [] });
155+
if (!res.ok) {
156+
setWalletImportResult({
157+
imported: [],
158+
errors: [data.error || "Import failed"],
159+
});
160+
return;
161+
}
162+
163+
setWalletImportResult({
164+
imported: data.imported || [],
165+
errors: data.errors?.map((e: { coin: string; error: string }) => `${e.coin}: ${e.error}`) || [],
166+
});
167+
168+
setWalletPasteText("");
169+
setWalletPasteOpen(false);
170+
await fetchReferralWallets();
171+
} catch {
172+
setWalletImportResult({ imported: [], errors: ["Network error during import"] });
173+
}
120174
};
121175

122176
return (
@@ -251,80 +305,89 @@ export default function AccountContent() {
251305
<p className="text-white text-xl font-bold">${profile?.total_referral_earnings_usd?.toFixed(2) || "0.00"}</p>
252306
</div>
253307
<div>
254-
<span className="text-sm text-tc-text-dim">Payout Wallet</span>
255-
{editing ? (
256-
<div className="space-y-2 mt-1">
257-
<select
258-
value={payoutCrypto}
259-
onChange={(e) => setPayoutCrypto(e.target.value)}
260-
className="w-full bg-tc-darker border border-tc-border rounded-lg px-3 py-2 text-white focus:outline-none focus:border-tc-green/50"
261-
>
262-
<option value="USDT">USDT</option>
263-
<option value="BTC">BTC</option>
264-
<option value="ETH">ETH</option>
265-
<option value="SOL">SOL</option>
266-
<option value="USDC">USDC</option>
267-
</select>
268-
<div className="flex gap-2">
269-
<input
270-
value={walletAddress}
271-
onChange={(e) => setWalletAddress(e.target.value)}
272-
placeholder="Wallet address"
273-
className="flex-1 bg-tc-darker border border-tc-border rounded-lg px-3 py-2 text-white focus:outline-none focus:border-tc-green/50"
274-
/>
275-
<button
276-
type="button"
277-
onClick={() => setWalletPasteOpen(!walletPasteOpen)}
278-
className="shrink-0 bg-tc-green/10 border border-tc-green/30 text-tc-green px-3 py-2 rounded-lg text-sm hover:bg-tc-green/20 transition-colors"
279-
title="Paste from CoinPay"
280-
>
281-
📋
282-
</button>
283-
</div>
284-
{walletPasteOpen && (
285-
<div className="mt-2">
286-
<textarea
287-
value={walletPasteText}
288-
onChange={(e) => setWalletPasteText(e.target.value)}
289-
placeholder={`Paste from CoinPay "Copy All Addresses":\nBTC: bc1q...\nUSDC_SOL: FX8Q...`}
290-
rows={3}
291-
className="w-full rounded-lg border border-tc-border bg-tc-darker px-3 py-2 text-white placeholder:text-tc-text-dim/50 focus:outline-none focus:border-tc-green/50 font-mono text-xs"
292-
/>
293-
<div className="flex gap-2 mt-2">
294-
<button
295-
type="button"
296-
onClick={handleWalletImport}
297-
disabled={!walletPasteText.trim()}
298-
className="bg-tc-green text-black px-3 py-1.5 rounded-lg text-sm font-semibold hover:bg-tc-green-dim disabled:opacity-40 disabled:cursor-not-allowed"
299-
>
300-
Import Wallet
301-
</button>
302-
<button
303-
type="button"
304-
onClick={() => { setWalletPasteOpen(false); setWalletPasteText(""); }}
305-
className="text-tc-text-dim text-sm hover:text-white"
306-
>
307-
Cancel
308-
</button>
309-
</div>
310-
{walletImportResult && (
311-
<p className={`text-xs mt-2 ${walletImportResult.errors.length > 0 ? "text-red-400" : "text-tc-green"}`}>
312-
{walletImportResult.errors.length > 0
313-
? walletImportResult.errors[0]
314-
: `✅ Wallet imported: ${walletAddress.slice(0, 8)}...${walletAddress.slice(-6)}`
315-
}
316-
</p>
317-
)}
318-
</div>
308+
<span className="text-sm text-tc-text-dim">Payout Wallets</span>
309+
{loadingWallets ? (
310+
<p className="text-tc-text-dim text-xs mt-1">Loading...</p>
311+
) : referralWallets.length > 0 ? (
312+
<div className="space-y-1 mt-1">
313+
{referralWallets.slice(0, 3).map((w) => (
314+
<p key={w.cryptocurrency} className="text-white text-xs font-mono flex items-center gap-1">
315+
<span className="text-tc-green font-bold w-20 shrink-0">{w.cryptocurrency}</span>
316+
<span className="truncate">{w.wallet_address}</span>
317+
{w.is_primary && <span className="text-[10px] bg-tc-green/10 text-tc-green px-1 rounded"></span>}
318+
</p>
319+
))}
320+
{referralWallets.length > 3 && (
321+
<p className="text-tc-text-dim text-xs">+{referralWallets.length - 3} more</p>
319322
)}
320323
</div>
321324
) : (
322-
<p className="text-white text-sm truncate">
323-
{profile?.wallet_address ? `${profile.payout_crypto}: ${profile.wallet_address}` : "Not set — click Edit to add"}
324-
</p>
325+
<p className="text-white text-xs mt-1">None — import below</p>
325326
)}
326327
</div>
327328
</div>
329+
330+
{/* Bulk Wallet Import */}
331+
<div className="mt-4 border-t border-tc-border pt-4">
332+
<div className="flex items-center justify-between mb-2">
333+
<h3 className="text-sm font-semibold text-white">Wallet Addresses</h3>
334+
<button
335+
type="button"
336+
onClick={() => setWalletPasteOpen(!walletPasteOpen)}
337+
className="text-xs text-tc-green hover:underline"
338+
>
339+
{walletPasteOpen ? "▲ Close" : "📋 Import from CoinPay"}
340+
</button>
341+
</div>
342+
343+
{walletPasteOpen && (
344+
<div>
345+
<textarea
346+
value={walletPasteText}
347+
onChange={(e) => setWalletPasteText(e.target.value)}
348+
placeholder={`Paste from CoinPay "Copy All Addresses":\nBTC: bc1q...\nETH: 0x...\nUSDC_SOL: FX8Q...\nUSDC_POL: 0x...`}
349+
rows={5}
350+
className="w-full rounded-lg border border-tc-border bg-tc-darker px-3 py-2 text-white placeholder:text-tc-text-dim/50 focus:outline-none focus:border-tc-green/50 font-mono text-xs"
351+
/>
352+
<div className="flex gap-2 mt-2">
353+
<button
354+
type="button"
355+
onClick={handleWalletImport}
356+
disabled={!walletPasteText.trim()}
357+
className="bg-tc-green text-black px-4 py-1.5 rounded-lg text-sm font-semibold hover:bg-tc-green-dim disabled:opacity-40 disabled:cursor-not-allowed"
358+
>
359+
Import All Wallets
360+
</button>
361+
<button
362+
type="button"
363+
onClick={() => { setWalletPasteOpen(false); setWalletPasteText(""); setWalletImportResult(null); }}
364+
className="text-tc-text-dim text-sm hover:text-white"
365+
>
366+
Cancel
367+
</button>
368+
</div>
369+
{walletImportResult && (
370+
<div className="mt-2 text-xs">
371+
{walletImportResult.imported.length > 0 && (
372+
<div className="text-tc-green">
373+
✅ Imported {walletImportResult.imported.length} wallet(s):
374+
{walletImportResult.imported.map((w, i) => (
375+
<span key={i} className="inline-block mr-2 mt-1 px-2 py-0.5 bg-tc-green/10 rounded font-mono">
376+
{w.coin}: {w.address.slice(0, 8)}{w.address.slice(-6)}
377+
</span>
378+
))}
379+
</div>
380+
)}
381+
{walletImportResult.errors.length > 0 && (
382+
<div className="text-red-400 mt-1">
383+
{walletImportResult.errors.join(", ")}
384+
</div>
385+
)}
386+
</div>
387+
)}
388+
</div>
389+
)}
390+
</div>
328391
</div>
329392
</div>
330393

0 commit comments

Comments
 (0)