@wopr-network/platform-ui-core 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { motion } from "framer-motion";
|
|
4
|
-
import { Bitcoin } from "lucide-react";
|
|
5
|
-
import { useState } from "react";
|
|
4
|
+
import { Bitcoin, Check, CircleDollarSign, Copy } from "lucide-react";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
7
|
import { Button } from "@/components/ui/button";
|
|
7
8
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createCryptoCheckout,
|
|
11
|
+
createStablecoinCheckout,
|
|
12
|
+
type StablecoinCheckoutResult,
|
|
13
|
+
} from "@/lib/api";
|
|
9
14
|
import { cn } from "@/lib/utils";
|
|
10
15
|
import { isAllowedRedirectUrl } from "@/lib/validate-redirect-url";
|
|
11
16
|
|
|
@@ -16,12 +21,109 @@ const CRYPTO_AMOUNTS = [
|
|
|
16
21
|
{ value: 100, label: "$100" },
|
|
17
22
|
];
|
|
18
23
|
|
|
24
|
+
const STABLECOIN_TOKENS = [
|
|
25
|
+
{ token: "USDC", label: "USDC", chain: "base", chainLabel: "Base" },
|
|
26
|
+
{ token: "USDT", label: "USDT", chain: "base", chainLabel: "Base" },
|
|
27
|
+
{ token: "DAI", label: "DAI", chain: "base", chainLabel: "Base" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
type PaymentMethod = "btc" | "stablecoin";
|
|
31
|
+
|
|
32
|
+
function CopyButton({ text }: { text: string }) {
|
|
33
|
+
const [copied, setCopied] = useState(false);
|
|
34
|
+
|
|
35
|
+
const handleCopy = useCallback(() => {
|
|
36
|
+
navigator.clipboard.writeText(text);
|
|
37
|
+
setCopied(true);
|
|
38
|
+
setTimeout(() => setCopied(false), 2000);
|
|
39
|
+
}, [text]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Button variant="ghost" size="sm" onClick={handleCopy} className="h-7 px-2">
|
|
43
|
+
{copied ? <Check className="h-3.5 w-3.5 text-primary" /> : <Copy className="h-3.5 w-3.5" />}
|
|
44
|
+
</Button>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function StablecoinDeposit({
|
|
49
|
+
checkout,
|
|
50
|
+
onReset,
|
|
51
|
+
}: {
|
|
52
|
+
checkout: StablecoinCheckoutResult;
|
|
53
|
+
onReset: () => void;
|
|
54
|
+
}) {
|
|
55
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
56
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
// TODO: poll charge status via tRPC query when backend endpoint exists
|
|
60
|
+
// For now, show the deposit address and let the user confirm manually
|
|
61
|
+
return () => {
|
|
62
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const displayAmount = `${checkout.amountUsd} ${checkout.token}`;
|
|
67
|
+
|
|
68
|
+
if (confirmed) {
|
|
69
|
+
return (
|
|
70
|
+
<motion.div
|
|
71
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
72
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
73
|
+
className="rounded-md border border-primary/25 bg-primary/5 p-4 text-center"
|
|
74
|
+
>
|
|
75
|
+
<Check className="mx-auto h-8 w-8 text-primary" />
|
|
76
|
+
<p className="mt-2 text-sm font-medium">Payment detected. Credits will appear shortly.</p>
|
|
77
|
+
</motion.div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="space-y-4">
|
|
83
|
+
<div className="rounded-md border p-4 space-y-3">
|
|
84
|
+
<div className="flex items-center justify-between">
|
|
85
|
+
<p className="text-sm text-muted-foreground">
|
|
86
|
+
Send exactly{" "}
|
|
87
|
+
<span className="font-mono font-bold text-foreground">{displayAmount}</span> to:
|
|
88
|
+
</p>
|
|
89
|
+
<Badge variant="outline" className="text-xs">
|
|
90
|
+
{checkout.token} on {checkout.chain}
|
|
91
|
+
</Badge>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
|
|
95
|
+
<code className="flex-1 text-xs font-mono break-all text-foreground">
|
|
96
|
+
{checkout.depositAddress}
|
|
97
|
+
</code>
|
|
98
|
+
<CopyButton text={checkout.depositAddress} />
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<p className="text-xs text-muted-foreground">
|
|
102
|
+
Only send {checkout.token} on the {checkout.chain} network. Other tokens or chains will be
|
|
103
|
+
lost.
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="flex gap-2">
|
|
108
|
+
<Button variant="ghost" size="sm" onClick={onReset}>
|
|
109
|
+
Cancel
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</motion.div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
19
116
|
export function BuyCryptoCreditPanel() {
|
|
117
|
+
const [method, setMethod] = useState<PaymentMethod>("stablecoin");
|
|
20
118
|
const [selected, setSelected] = useState<number | null>(null);
|
|
119
|
+
const [selectedToken, setSelectedToken] = useState(STABLECOIN_TOKENS[0]);
|
|
21
120
|
const [loading, setLoading] = useState(false);
|
|
22
121
|
const [error, setError] = useState<string | null>(null);
|
|
122
|
+
const [stablecoinCheckout, setStablecoinCheckout] = useState<StablecoinCheckoutResult | null>(
|
|
123
|
+
null,
|
|
124
|
+
);
|
|
23
125
|
|
|
24
|
-
async function
|
|
126
|
+
async function handleBtcCheckout() {
|
|
25
127
|
if (selected === null) return;
|
|
26
128
|
setLoading(true);
|
|
27
129
|
setError(null);
|
|
@@ -39,6 +141,30 @@ export function BuyCryptoCreditPanel() {
|
|
|
39
141
|
}
|
|
40
142
|
}
|
|
41
143
|
|
|
144
|
+
async function handleStablecoinCheckout() {
|
|
145
|
+
if (selected === null) return;
|
|
146
|
+
setLoading(true);
|
|
147
|
+
setError(null);
|
|
148
|
+
try {
|
|
149
|
+
const result = await createStablecoinCheckout(
|
|
150
|
+
selected,
|
|
151
|
+
selectedToken.token,
|
|
152
|
+
selectedToken.chain,
|
|
153
|
+
);
|
|
154
|
+
setStablecoinCheckout(result);
|
|
155
|
+
} catch {
|
|
156
|
+
setError("Checkout failed. Please try again.");
|
|
157
|
+
} finally {
|
|
158
|
+
setLoading(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleReset() {
|
|
163
|
+
setStablecoinCheckout(null);
|
|
164
|
+
setSelected(null);
|
|
165
|
+
setError(null);
|
|
166
|
+
}
|
|
167
|
+
|
|
42
168
|
return (
|
|
43
169
|
<motion.div
|
|
44
170
|
initial={{ opacity: 0, y: 8 }}
|
|
@@ -48,47 +174,108 @@ export function BuyCryptoCreditPanel() {
|
|
|
48
174
|
<Card>
|
|
49
175
|
<CardHeader>
|
|
50
176
|
<CardTitle className="flex items-center gap-2">
|
|
51
|
-
|
|
177
|
+
{method === "btc" ? (
|
|
178
|
+
<Bitcoin className="h-4 w-4 text-amber-500" />
|
|
179
|
+
) : (
|
|
180
|
+
<CircleDollarSign className="h-4 w-4 text-blue-500" />
|
|
181
|
+
)}
|
|
52
182
|
Pay with Crypto
|
|
53
183
|
</CardTitle>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
184
|
+
<div className="flex gap-2 pt-1">
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={() => {
|
|
188
|
+
setMethod("stablecoin");
|
|
189
|
+
handleReset();
|
|
190
|
+
}}
|
|
191
|
+
className={cn(
|
|
192
|
+
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
|
193
|
+
method === "stablecoin"
|
|
194
|
+
? "bg-primary/10 text-primary"
|
|
195
|
+
: "text-muted-foreground hover:text-foreground",
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
Stablecoin
|
|
199
|
+
</button>
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={() => {
|
|
203
|
+
setMethod("btc");
|
|
204
|
+
handleReset();
|
|
205
|
+
}}
|
|
206
|
+
className={cn(
|
|
207
|
+
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
|
208
|
+
method === "btc"
|
|
209
|
+
? "bg-amber-500/10 text-amber-500"
|
|
210
|
+
: "text-muted-foreground hover:text-foreground",
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
BTC
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
57
216
|
</CardHeader>
|
|
58
217
|
<CardContent className="space-y-4">
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
218
|
+
{stablecoinCheckout ? (
|
|
219
|
+
<StablecoinDeposit checkout={stablecoinCheckout} onReset={handleReset} />
|
|
220
|
+
) : (
|
|
221
|
+
<>
|
|
222
|
+
{method === "stablecoin" && (
|
|
223
|
+
<div className="flex gap-2">
|
|
224
|
+
{STABLECOIN_TOKENS.map((t) => (
|
|
225
|
+
<button
|
|
226
|
+
key={`${t.token}:${t.chain}`}
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => setSelectedToken(t)}
|
|
229
|
+
className={cn(
|
|
230
|
+
"rounded-md border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
231
|
+
selectedToken.token === t.token && selectedToken.chain === t.chain
|
|
232
|
+
? "border-primary bg-primary/5 text-primary"
|
|
233
|
+
: "border-border text-muted-foreground hover:text-foreground",
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
{t.label}
|
|
237
|
+
</button>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
243
|
+
{CRYPTO_AMOUNTS.map((amt) => (
|
|
244
|
+
<motion.button
|
|
245
|
+
key={amt.value}
|
|
246
|
+
type="button"
|
|
247
|
+
onClick={() => setSelected(amt.value)}
|
|
248
|
+
whileHover={{ scale: 1.03 }}
|
|
249
|
+
whileTap={{ scale: 0.98 }}
|
|
250
|
+
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
|
251
|
+
className={cn(
|
|
252
|
+
"flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
|
|
253
|
+
selected === amt.value
|
|
254
|
+
? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
|
|
255
|
+
: "border-border",
|
|
256
|
+
)}
|
|
257
|
+
>
|
|
258
|
+
<span className="text-lg font-bold">{amt.label}</span>
|
|
259
|
+
</motion.button>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
264
|
+
|
|
265
|
+
<Button
|
|
266
|
+
onClick={method === "btc" ? handleBtcCheckout : handleStablecoinCheckout}
|
|
267
|
+
disabled={selected === null || loading}
|
|
268
|
+
variant="outline"
|
|
269
|
+
className="w-full sm:w-auto"
|
|
78
270
|
>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
variant="outline"
|
|
88
|
-
className="w-full sm:w-auto"
|
|
89
|
-
>
|
|
90
|
-
{loading ? "Opening checkout..." : "Pay with crypto"}
|
|
91
|
-
</Button>
|
|
271
|
+
{loading
|
|
272
|
+
? "Creating checkout..."
|
|
273
|
+
: method === "btc"
|
|
274
|
+
? "Pay with BTC"
|
|
275
|
+
: `Pay with ${selectedToken.label}`}
|
|
276
|
+
</Button>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
92
279
|
</CardContent>
|
|
93
280
|
</Card>
|
|
94
281
|
</motion.div>
|
package/src/lib/api.ts
CHANGED
|
@@ -1336,6 +1336,23 @@ export async function createCryptoCheckout(
|
|
|
1336
1336
|
return trpcVanilla.billing.cryptoCheckout.mutate({ amountUsd });
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
|
+
export interface StablecoinCheckoutResult {
|
|
1340
|
+
depositAddress: string;
|
|
1341
|
+
amountRaw: string;
|
|
1342
|
+
amountUsd: number;
|
|
1343
|
+
chain: string;
|
|
1344
|
+
token: string;
|
|
1345
|
+
referenceId: string;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
export async function createStablecoinCheckout(
|
|
1349
|
+
amountUsd: number,
|
|
1350
|
+
token: string,
|
|
1351
|
+
chain: string,
|
|
1352
|
+
): Promise<StablecoinCheckoutResult> {
|
|
1353
|
+
return trpcVanilla.billing.stablecoinCheckout.mutate({ amountUsd, token, chain });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1339
1356
|
// --- Dividend types ---
|
|
1340
1357
|
|
|
1341
1358
|
export interface DividendWalletStats {
|
package/src/lib/trpc-types.ts
CHANGED
|
@@ -113,6 +113,7 @@ type AppRouterRecord = {
|
|
|
113
113
|
affiliateStats: AnyTRPCQueryProcedure;
|
|
114
114
|
affiliateReferrals: AnyTRPCQueryProcedure;
|
|
115
115
|
cryptoCheckout: AnyTRPCMutationProcedure;
|
|
116
|
+
stablecoinCheckout: AnyTRPCMutationProcedure;
|
|
116
117
|
autoTopupSettings: AnyTRPCQueryProcedure;
|
|
117
118
|
updateAutoTopupSettings: AnyTRPCMutationProcedure;
|
|
118
119
|
accountStatus: AnyTRPCQueryProcedure;
|