@wopr-network/platform-ui-core 1.2.0 → 1.4.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,11 +1,18 @@
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 { createCryptoCheckout } from "@/lib/api";
9
+ import {
10
+ createCryptoCheckout,
11
+ createEthCheckout,
12
+ createStablecoinCheckout,
13
+ type EthCheckoutResult,
14
+ type StablecoinCheckoutResult,
15
+ } from "@/lib/api";
9
16
  import { cn } from "@/lib/utils";
10
17
  import { isAllowedRedirectUrl } from "@/lib/validate-redirect-url";
11
18
 
@@ -16,12 +23,134 @@ const CRYPTO_AMOUNTS = [
16
23
  { value: 100, label: "$100" },
17
24
  ];
18
25
 
26
+ const STABLECOIN_TOKENS = [
27
+ { token: "USDC", label: "USDC", chain: "base", chainLabel: "Base" },
28
+ { token: "USDT", label: "USDT", chain: "base", chainLabel: "Base" },
29
+ { token: "DAI", label: "DAI", chain: "base", chainLabel: "Base" },
30
+ ];
31
+
32
+ type PaymentMethod = "btc" | "stablecoin" | "eth";
33
+
34
+ /** Format wei (BigInt string) to ETH string without Number precision loss. */
35
+ function formatWeiToEth(weiStr: string): string {
36
+ const wei = BigInt(weiStr);
37
+ const divisor = BigInt("1000000000000000000");
38
+ const whole = wei / divisor;
39
+ const frac = wei % divisor;
40
+ const fracStr = frac.toString().padStart(18, "0").slice(0, 6);
41
+ return `${whole}.${fracStr}`;
42
+ }
43
+
44
+ function CopyButton({ text }: { text: string }) {
45
+ const [copied, setCopied] = useState(false);
46
+
47
+ const handleCopy = useCallback(() => {
48
+ navigator.clipboard.writeText(text);
49
+ setCopied(true);
50
+ setTimeout(() => setCopied(false), 2000);
51
+ }, [text]);
52
+
53
+ return (
54
+ <Button variant="ghost" size="sm" onClick={handleCopy} className="h-7 px-2">
55
+ {copied ? <Check className="h-3.5 w-3.5 text-primary" /> : <Copy className="h-3.5 w-3.5" />}
56
+ </Button>
57
+ );
58
+ }
59
+
60
+ function StablecoinDeposit({
61
+ checkout,
62
+ onReset,
63
+ }: {
64
+ checkout: StablecoinCheckoutResult;
65
+ onReset: () => void;
66
+ }) {
67
+ const [confirmed, _setConfirmed] = useState(false);
68
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
69
+
70
+ useEffect(() => {
71
+ // TODO: poll charge status via tRPC query when backend endpoint exists
72
+ // For now, show the deposit address and let the user confirm manually
73
+ return () => {
74
+ if (pollRef.current) clearInterval(pollRef.current);
75
+ };
76
+ }, []);
77
+
78
+ const displayAmount = `${checkout.amountUsd} ${checkout.token}`;
79
+
80
+ if (confirmed) {
81
+ return (
82
+ <motion.div
83
+ initial={{ opacity: 0, scale: 0.95 }}
84
+ animate={{ opacity: 1, scale: 1 }}
85
+ className="rounded-md border border-primary/25 bg-primary/5 p-4 text-center"
86
+ >
87
+ <Check className="mx-auto h-8 w-8 text-primary" />
88
+ <p className="mt-2 text-sm font-medium">Payment detected. Credits will appear shortly.</p>
89
+ </motion.div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="space-y-4">
95
+ <div className="rounded-md border p-4 space-y-3">
96
+ <div className="flex items-center justify-between">
97
+ <p className="text-sm text-muted-foreground">
98
+ Send exactly{" "}
99
+ <span className="font-mono font-bold text-foreground">{displayAmount}</span> to:
100
+ </p>
101
+ <Badge variant="outline" className="text-xs">
102
+ {checkout.token} on {checkout.chain}
103
+ </Badge>
104
+ </div>
105
+
106
+ <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
107
+ <code className="flex-1 text-xs font-mono break-all text-foreground">
108
+ {checkout.depositAddress}
109
+ </code>
110
+ <CopyButton text={checkout.depositAddress} />
111
+ </div>
112
+
113
+ <p className="text-xs text-muted-foreground">
114
+ Only send {checkout.token} on the {checkout.chain} network. Other tokens or chains will be
115
+ lost.
116
+ </p>
117
+ </div>
118
+
119
+ <div className="flex gap-2">
120
+ <Button variant="ghost" size="sm" onClick={onReset}>
121
+ Cancel
122
+ </Button>
123
+ </div>
124
+ </motion.div>
125
+ );
126
+ }
127
+
19
128
  export function BuyCryptoCreditPanel() {
129
+ const [method, setMethod] = useState<PaymentMethod>("stablecoin");
20
130
  const [selected, setSelected] = useState<number | null>(null);
131
+ const [selectedToken, setSelectedToken] = useState(STABLECOIN_TOKENS[0]);
21
132
  const [loading, setLoading] = useState(false);
22
133
  const [error, setError] = useState<string | null>(null);
134
+ const [stablecoinCheckout, setStablecoinCheckout] = useState<StablecoinCheckoutResult | null>(
135
+ null,
136
+ );
137
+ const [ethCheckout, setEthCheckout] = useState<EthCheckoutResult | null>(null);
23
138
 
24
- async function handleCheckout() {
139
+ async function handleEthCheckout() {
140
+ if (selected === null) return;
141
+ setLoading(true);
142
+ setError(null);
143
+ try {
144
+ const result = await createEthCheckout(selected, "base");
145
+ setEthCheckout(result);
146
+ } catch {
147
+ setError("Checkout failed. Please try again.");
148
+ } finally {
149
+ setLoading(false);
150
+ }
151
+ }
152
+
153
+ async function handleBtcCheckout() {
25
154
  if (selected === null) return;
26
155
  setLoading(true);
27
156
  setError(null);
@@ -39,6 +168,31 @@ export function BuyCryptoCreditPanel() {
39
168
  }
40
169
  }
41
170
 
171
+ async function handleStablecoinCheckout() {
172
+ if (selected === null) return;
173
+ setLoading(true);
174
+ setError(null);
175
+ try {
176
+ const result = await createStablecoinCheckout(
177
+ selected,
178
+ selectedToken.token,
179
+ selectedToken.chain,
180
+ );
181
+ setStablecoinCheckout(result);
182
+ } catch {
183
+ setError("Checkout failed. Please try again.");
184
+ } finally {
185
+ setLoading(false);
186
+ }
187
+ }
188
+
189
+ function handleReset() {
190
+ setStablecoinCheckout(null);
191
+ setEthCheckout(null);
192
+ setSelected(null);
193
+ setError(null);
194
+ }
195
+
42
196
  return (
43
197
  <motion.div
44
198
  initial={{ opacity: 0, y: 8 }}
@@ -48,47 +202,167 @@ export function BuyCryptoCreditPanel() {
48
202
  <Card>
49
203
  <CardHeader>
50
204
  <CardTitle className="flex items-center gap-2">
51
- <Bitcoin className="h-4 w-4 text-amber-500" />
205
+ {method === "btc" ? (
206
+ <Bitcoin className="h-4 w-4 text-amber-500" />
207
+ ) : method === "eth" ? (
208
+ <CircleDollarSign className="h-4 w-4 text-indigo-500" />
209
+ ) : (
210
+ <CircleDollarSign className="h-4 w-4 text-blue-500" />
211
+ )}
52
212
  Pay with Crypto
53
213
  </CardTitle>
54
- <p className="text-xs text-muted-foreground">
55
- Pay with BTC or other cryptocurrencies. Minimum $10.
56
- </p>
214
+ <div className="flex gap-2 pt-1">
215
+ <button
216
+ type="button"
217
+ onClick={() => {
218
+ setMethod("stablecoin");
219
+ handleReset();
220
+ }}
221
+ className={cn(
222
+ "rounded-full px-3 py-1 text-xs font-medium transition-colors",
223
+ method === "stablecoin"
224
+ ? "bg-primary/10 text-primary"
225
+ : "text-muted-foreground hover:text-foreground",
226
+ )}
227
+ >
228
+ Stablecoin
229
+ </button>
230
+ <button
231
+ type="button"
232
+ onClick={() => {
233
+ setMethod("eth");
234
+ handleReset();
235
+ }}
236
+ className={cn(
237
+ "rounded-full px-3 py-1 text-xs font-medium transition-colors",
238
+ method === "eth"
239
+ ? "bg-indigo-500/10 text-indigo-500"
240
+ : "text-muted-foreground hover:text-foreground",
241
+ )}
242
+ >
243
+ ETH
244
+ </button>
245
+ <button
246
+ type="button"
247
+ onClick={() => {
248
+ setMethod("btc");
249
+ handleReset();
250
+ }}
251
+ className={cn(
252
+ "rounded-full px-3 py-1 text-xs font-medium transition-colors",
253
+ method === "btc"
254
+ ? "bg-amber-500/10 text-amber-500"
255
+ : "text-muted-foreground hover:text-foreground",
256
+ )}
257
+ >
258
+ BTC
259
+ </button>
260
+ </div>
57
261
  </CardHeader>
58
262
  <CardContent className="space-y-4">
59
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
60
- {CRYPTO_AMOUNTS.map((amt) => (
61
- <motion.button
62
- key={amt.value}
63
- type="button"
64
- onClick={() => setSelected(amt.value)}
65
- whileHover={{ scale: 1.03 }}
66
- whileTap={{ scale: 0.98 }}
67
- transition={{
68
- type: "spring",
69
- stiffness: 400,
70
- damping: 25,
71
- }}
72
- className={cn(
73
- "flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
74
- selected === amt.value
75
- ? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
76
- : "border-border",
77
- )}
263
+ {ethCheckout ? (
264
+ <motion.div
265
+ initial={{ opacity: 0, y: 8 }}
266
+ animate={{ opacity: 1, y: 0 }}
267
+ className="space-y-4"
268
+ >
269
+ <div className="rounded-md border p-4 space-y-3">
270
+ <div className="flex items-center justify-between">
271
+ <p className="text-sm text-muted-foreground">
272
+ Send approximately{" "}
273
+ <span className="font-mono font-bold text-foreground">
274
+ {formatWeiToEth(ethCheckout.expectedWei)} ETH
275
+ </span>{" "}
276
+ (${ethCheckout.amountUsd}) to:
277
+ </p>
278
+ <Badge variant="outline" className="text-xs">
279
+ ETH on {ethCheckout.chain}
280
+ </Badge>
281
+ </div>
282
+ <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
283
+ <code className="flex-1 text-xs font-mono break-all text-foreground">
284
+ {ethCheckout.depositAddress}
285
+ </code>
286
+ <CopyButton text={ethCheckout.depositAddress} />
287
+ </div>
288
+ <p className="text-xs text-muted-foreground">
289
+ ETH price at checkout: ${(ethCheckout.priceCents / 100).toFixed(2)}. Only send ETH
290
+ on the {ethCheckout.chain} network.
291
+ </p>
292
+ </div>
293
+ <Button variant="ghost" size="sm" onClick={handleReset}>
294
+ Cancel
295
+ </Button>
296
+ </motion.div>
297
+ ) : stablecoinCheckout ? (
298
+ <StablecoinDeposit checkout={stablecoinCheckout} onReset={handleReset} />
299
+ ) : (
300
+ <>
301
+ {method === "stablecoin" && (
302
+ <div className="flex gap-2">
303
+ {STABLECOIN_TOKENS.map((t) => (
304
+ <button
305
+ key={`${t.token}:${t.chain}`}
306
+ type="button"
307
+ onClick={() => setSelectedToken(t)}
308
+ className={cn(
309
+ "rounded-md border px-3 py-1.5 text-xs font-medium transition-colors",
310
+ selectedToken.token === t.token && selectedToken.chain === t.chain
311
+ ? "border-primary bg-primary/5 text-primary"
312
+ : "border-border text-muted-foreground hover:text-foreground",
313
+ )}
314
+ >
315
+ {t.label}
316
+ </button>
317
+ ))}
318
+ </div>
319
+ )}
320
+
321
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
322
+ {CRYPTO_AMOUNTS.map((amt) => (
323
+ <motion.button
324
+ key={amt.value}
325
+ type="button"
326
+ onClick={() => setSelected(amt.value)}
327
+ whileHover={{ scale: 1.03 }}
328
+ whileTap={{ scale: 0.98 }}
329
+ transition={{ type: "spring", stiffness: 400, damping: 25 }}
330
+ className={cn(
331
+ "flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
332
+ selected === amt.value
333
+ ? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
334
+ : "border-border",
335
+ )}
336
+ >
337
+ <span className="text-lg font-bold">{amt.label}</span>
338
+ </motion.button>
339
+ ))}
340
+ </div>
341
+
342
+ {error && <p className="text-sm text-destructive">{error}</p>}
343
+
344
+ <Button
345
+ onClick={
346
+ method === "btc"
347
+ ? handleBtcCheckout
348
+ : method === "eth"
349
+ ? handleEthCheckout
350
+ : handleStablecoinCheckout
351
+ }
352
+ disabled={selected === null || loading}
353
+ variant="outline"
354
+ className="w-full sm:w-auto"
78
355
  >
79
- <span className="text-lg font-bold">{amt.label}</span>
80
- </motion.button>
81
- ))}
82
- </div>
83
- {error && <p className="text-sm text-destructive">{error}</p>}
84
- <Button
85
- onClick={handleCheckout}
86
- disabled={selected === null || loading}
87
- variant="outline"
88
- className="w-full sm:w-auto"
89
- >
90
- {loading ? "Opening checkout..." : "Pay with crypto"}
91
- </Button>
356
+ {loading
357
+ ? "Creating checkout..."
358
+ : method === "btc"
359
+ ? "Pay with BTC"
360
+ : method === "eth"
361
+ ? "Pay with ETH"
362
+ : `Pay with ${selectedToken.label}`}
363
+ </Button>
364
+ </>
365
+ )}
92
366
  </CardContent>
93
367
  </Card>
94
368
  </motion.div>
package/src/lib/api.ts CHANGED
@@ -1336,6 +1336,39 @@ 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
+
1356
+ export interface EthCheckoutResult {
1357
+ depositAddress: string;
1358
+ expectedWei: string;
1359
+ amountUsd: number;
1360
+ priceCents: number;
1361
+ chain: string;
1362
+ referenceId: string;
1363
+ }
1364
+
1365
+ export async function createEthCheckout(
1366
+ amountUsd: number,
1367
+ chain: string,
1368
+ ): Promise<EthCheckoutResult> {
1369
+ return trpcVanilla.billing.ethCheckout.mutate({ amountUsd, chain });
1370
+ }
1371
+
1339
1372
  // --- Dividend types ---
1340
1373
 
1341
1374
  export interface DividendWalletStats {
@@ -113,6 +113,8 @@ type AppRouterRecord = {
113
113
  affiliateStats: AnyTRPCQueryProcedure;
114
114
  affiliateReferrals: AnyTRPCQueryProcedure;
115
115
  cryptoCheckout: AnyTRPCMutationProcedure;
116
+ stablecoinCheckout: AnyTRPCMutationProcedure;
117
+ ethCheckout: AnyTRPCMutationProcedure;
116
118
  autoTopupSettings: AnyTRPCQueryProcedure;
117
119
  updateAutoTopupSettings: AnyTRPCMutationProcedure;
118
120
  accountStatus: AnyTRPCQueryProcedure;