@wopr-network/platform-ui-core 1.25.0 → 1.27.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.
@@ -1,304 +1,3 @@
1
1
  "use client";
2
2
 
3
- import { motion } from "framer-motion";
4
- import { Check, CircleDollarSign, Copy } from "lucide-react";
5
- import { useCallback, useEffect, useState } from "react";
6
- import { Badge } from "@/components/ui/badge";
7
- import { Button } from "@/components/ui/button";
8
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9
- import {
10
- type CheckoutResult,
11
- createCheckout,
12
- getChargeStatus,
13
- getSupportedPaymentMethods,
14
- type SupportedPaymentMethod,
15
- } from "@/lib/api";
16
- import { cn } from "@/lib/utils";
17
-
18
- type PaymentProgress = {
19
- status: "waiting" | "partial" | "confirming" | "credited" | "expired" | "failed";
20
- amountExpectedCents: number;
21
- amountReceivedCents: number;
22
- confirmations: number;
23
- confirmationsRequired: number;
24
- };
25
-
26
- const AMOUNTS = [
27
- { value: 10, label: "$10" },
28
- { value: 25, label: "$25" },
29
- { value: 50, label: "$50" },
30
- { value: 100, label: "$100" },
31
- ];
32
-
33
- function CopyButton({ text }: { text: string }) {
34
- const [copied, setCopied] = useState(false);
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
- export function BuyCryptoCreditPanel() {
49
- const [methods, setMethods] = useState<SupportedPaymentMethod[]>([]);
50
- const [selectedMethod, setSelectedMethod] = useState<SupportedPaymentMethod | null>(null);
51
- const [selectedAmount, setSelectedAmount] = useState<number | null>(null);
52
- const [loading, setLoading] = useState(false);
53
- const [error, setError] = useState<string | null>(null);
54
- const [checkout, setCheckout] = useState<CheckoutResult | null>(null);
55
- const [paymentProgress, setPaymentProgress] = useState<PaymentProgress | null>(null);
56
-
57
- // Poll charge status after checkout
58
- useEffect(() => {
59
- if (!checkout?.referenceId) {
60
- setPaymentProgress(null);
61
- return;
62
- }
63
- setPaymentProgress({
64
- status: "waiting",
65
- amountExpectedCents: 0,
66
- amountReceivedCents: 0,
67
- confirmations: 0,
68
- confirmationsRequired: 0,
69
- });
70
- const interval = setInterval(async () => {
71
- try {
72
- const res = await getChargeStatus(checkout.referenceId);
73
- let status: PaymentProgress["status"] = "waiting";
74
- if (res.credited) {
75
- status = "credited";
76
- } else if (res.status === "expired" || res.status === "failed") {
77
- status = res.status;
78
- } else if (
79
- res.amountReceivedCents >= res.amountExpectedCents &&
80
- res.amountReceivedCents > 0
81
- ) {
82
- status = "confirming";
83
- } else if (res.amountReceivedCents > 0) {
84
- status = "partial";
85
- }
86
- setPaymentProgress({
87
- status,
88
- amountExpectedCents: res.amountExpectedCents,
89
- amountReceivedCents: res.amountReceivedCents,
90
- confirmations: res.confirmations,
91
- confirmationsRequired: res.confirmationsRequired,
92
- });
93
- if (status === "credited" || status === "expired" || status === "failed") {
94
- clearInterval(interval);
95
- }
96
- } catch {
97
- // Ignore poll errors
98
- }
99
- }, 5000);
100
- return () => clearInterval(interval);
101
- }, [checkout?.referenceId]);
102
-
103
- // Fetch available payment methods from backend on mount
104
- useEffect(() => {
105
- getSupportedPaymentMethods()
106
- .then((m) => {
107
- if (m.length > 0) {
108
- setMethods(m);
109
- setSelectedMethod(m[0]);
110
- }
111
- })
112
- .catch(() => {
113
- // Backend unavailable — panel stays empty
114
- });
115
- }, []);
116
-
117
- async function handleCheckout() {
118
- if (selectedAmount === null || !selectedMethod) return;
119
- setLoading(true);
120
- setError(null);
121
- try {
122
- const result = await createCheckout(selectedMethod.id, selectedAmount);
123
- setCheckout(result);
124
- } catch {
125
- setError("Checkout failed. Please try again.");
126
- } finally {
127
- setLoading(false);
128
- }
129
- }
130
-
131
- function handleReset() {
132
- setCheckout(null);
133
- setSelectedAmount(null);
134
- setError(null);
135
- }
136
-
137
- if (methods.length === 0) return null;
138
-
139
- return (
140
- <motion.div
141
- initial={{ opacity: 0, y: 8 }}
142
- animate={{ opacity: 1, y: 0 }}
143
- transition={{ duration: 0.4, ease: "easeOut", delay: 0.1 }}
144
- >
145
- <Card>
146
- <CardHeader>
147
- <CardTitle className="flex items-center gap-2">
148
- <CircleDollarSign className="h-4 w-4 text-primary" />
149
- Pay with Crypto
150
- </CardTitle>
151
- <div className="flex flex-wrap gap-2 pt-1">
152
- {methods.map((m) => (
153
- <button
154
- key={m.id}
155
- type="button"
156
- onClick={() => {
157
- setSelectedMethod(m);
158
- handleReset();
159
- }}
160
- className={cn(
161
- "flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors",
162
- selectedMethod?.id === m.id
163
- ? "bg-primary/10 text-primary"
164
- : "text-muted-foreground hover:text-foreground",
165
- )}
166
- >
167
- {/* biome-ignore lint/performance/noImgElement: external dynamic URLs from backend, not static assets */}
168
- <img
169
- src={m.iconUrl}
170
- alt={m.token}
171
- className="h-4 w-4"
172
- loading="lazy"
173
- onError={(e) => {
174
- e.currentTarget.style.display = "none";
175
- }}
176
- />
177
- {m.token}
178
- </button>
179
- ))}
180
- </div>
181
- </CardHeader>
182
- <CardContent className="space-y-4">
183
- {checkout ? (
184
- <motion.div
185
- initial={{ opacity: 0, y: 8 }}
186
- animate={{ opacity: 1, y: 0 }}
187
- className="space-y-4"
188
- >
189
- <div className="rounded-md border p-4 space-y-3">
190
- <div className="flex items-center justify-between">
191
- <p className="text-sm text-muted-foreground">
192
- Send{" "}
193
- <span className="font-mono font-bold text-foreground">
194
- {checkout.displayAmount}
195
- </span>{" "}
196
- to:
197
- </p>
198
- <Badge variant="outline" className="text-xs">
199
- {checkout.token} on {checkout.chain}
200
- </Badge>
201
- </div>
202
- <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
203
- <code className="flex-1 text-xs font-mono break-all text-foreground">
204
- {checkout.depositAddress}
205
- </code>
206
- <CopyButton text={checkout.depositAddress} />
207
- </div>
208
- {checkout.priceCents && (
209
- <p className="text-xs text-muted-foreground">
210
- Price at checkout: ${(checkout.priceCents / 100).toFixed(2)} per{" "}
211
- {checkout.token}
212
- </p>
213
- )}
214
- <p className="text-xs text-muted-foreground">
215
- Only send {checkout.token} on the {checkout.chain} network.
216
- </p>
217
- {paymentProgress?.status === "waiting" && (
218
- <p className="text-xs text-yellow-500 animate-pulse">Waiting for payment...</p>
219
- )}
220
- {paymentProgress?.status === "partial" && (
221
- <p className="text-xs text-blue-500">
222
- Received ${(paymentProgress.amountReceivedCents / 100).toFixed(2)} of $
223
- {(paymentProgress.amountExpectedCents / 100).toFixed(2)} — send $
224
- {(
225
- (paymentProgress.amountExpectedCents - paymentProgress.amountReceivedCents) /
226
- 100
227
- ).toFixed(2)}{" "}
228
- more
229
- </p>
230
- )}
231
- {paymentProgress?.status === "confirming" && (
232
- <p className="text-xs text-blue-500">
233
- Payment received. Confirming ({paymentProgress.confirmations} of{" "}
234
- {paymentProgress.confirmationsRequired})...
235
- </p>
236
- )}
237
- {paymentProgress?.status === "credited" && (
238
- <p className="text-xs text-green-500 font-medium">
239
- Payment confirmed! Credits added.
240
- </p>
241
- )}
242
- {paymentProgress?.status === "expired" && (
243
- <p className="text-xs text-red-500">Payment expired.</p>
244
- )}
245
- {paymentProgress?.status === "failed" && (
246
- <p className="text-xs text-red-500">Payment failed.</p>
247
- )}
248
- </div>
249
- {paymentProgress?.status === "credited" ? (
250
- <Button variant="outline" size="sm" onClick={handleReset}>
251
- Done
252
- </Button>
253
- ) : paymentProgress?.status === "expired" || paymentProgress?.status === "failed" ? (
254
- <div className="flex gap-2">
255
- <Button variant="outline" size="sm" onClick={handleReset}>
256
- Try Again
257
- </Button>
258
- </div>
259
- ) : (
260
- <Button variant="ghost" size="sm" onClick={handleReset}>
261
- Cancel
262
- </Button>
263
- )}
264
- </motion.div>
265
- ) : (
266
- <>
267
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
268
- {AMOUNTS.map((amt) => (
269
- <motion.button
270
- key={amt.value}
271
- type="button"
272
- onClick={() => setSelectedAmount(amt.value)}
273
- whileHover={{ scale: 1.03 }}
274
- whileTap={{ scale: 0.98 }}
275
- transition={{ type: "spring", stiffness: 400, damping: 25 }}
276
- className={cn(
277
- "flex flex-col items-center gap-1 rounded-md border p-3 text-sm font-medium transition-colors hover:bg-accent",
278
- selectedAmount === amt.value
279
- ? "border-primary bg-primary/5 ring-1 ring-primary shadow-[0_0_15px_rgba(0,255,65,0.3)]"
280
- : "border-border",
281
- )}
282
- >
283
- <span className="text-lg font-bold">{amt.label}</span>
284
- </motion.button>
285
- ))}
286
- </div>
287
-
288
- {error && <p className="text-sm text-destructive">{error}</p>}
289
-
290
- <Button
291
- onClick={handleCheckout}
292
- disabled={selectedAmount === null || !selectedMethod || loading}
293
- variant="outline"
294
- className="w-full sm:w-auto"
295
- >
296
- {loading ? "Creating checkout..." : `Pay with ${selectedMethod?.token ?? "Crypto"}`}
297
- </Button>
298
- </>
299
- )}
300
- </CardContent>
301
- </Card>
302
- </motion.div>
303
- );
304
- }
3
+ export { CryptoCheckout as BuyCryptoCreditPanel } from "./crypto-checkout";
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import { Check } from "lucide-react";
4
+
5
+ interface ConfirmationTrackerProps {
6
+ confirmations: number;
7
+ confirmationsRequired: number;
8
+ displayAmount: string;
9
+ credited: boolean;
10
+ txHash?: string;
11
+ }
12
+
13
+ export function ConfirmationTracker({
14
+ confirmations,
15
+ confirmationsRequired,
16
+ displayAmount,
17
+ credited,
18
+ txHash,
19
+ }: ConfirmationTrackerProps) {
20
+ const pct =
21
+ confirmationsRequired > 0
22
+ ? Math.min(100, Math.round((confirmations / confirmationsRequired) * 100))
23
+ : 0;
24
+ const detected = confirmations > 0 || credited;
25
+
26
+ return (
27
+ <div className="space-y-4 text-center">
28
+ <p className="text-sm text-muted-foreground">
29
+ {credited ? "Payment complete!" : "Payment received!"}
30
+ </p>
31
+ <p className="text-xl font-semibold">{displayAmount}</p>
32
+
33
+ <div className="rounded-lg border border-border p-3 space-y-2">
34
+ <div className="flex justify-between text-xs">
35
+ <span className="text-muted-foreground">Confirmations</span>
36
+ <span>
37
+ {confirmations} / {confirmationsRequired}
38
+ </span>
39
+ </div>
40
+ <div
41
+ className="h-1.5 rounded-full bg-muted overflow-hidden"
42
+ role="progressbar"
43
+ aria-valuenow={pct}
44
+ aria-valuemin={0}
45
+ aria-valuemax={100}
46
+ >
47
+ <div
48
+ className="h-full rounded-full bg-primary transition-all duration-500"
49
+ style={{ width: `${pct}%` }}
50
+ />
51
+ </div>
52
+ </div>
53
+
54
+ <div className="space-y-2 text-left">
55
+ <div className="flex items-center gap-2">
56
+ <div
57
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${detected ? "bg-green-500 text-white" : "bg-muted"}`}
58
+ >
59
+ {detected && <Check className="h-2.5 w-2.5" />}
60
+ </div>
61
+ <span
62
+ className={`text-xs ${detected ? "text-muted-foreground" : "text-muted-foreground/50"}`}
63
+ >
64
+ Payment detected
65
+ </span>
66
+ </div>
67
+ <div className="flex items-center gap-2">
68
+ <div
69
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : detected ? "bg-primary text-white animate-pulse" : "bg-muted"}`}
70
+ >
71
+ {credited ? <Check className="h-2.5 w-2.5" /> : detected ? <span>&middot;</span> : null}
72
+ </div>
73
+ <span className={`text-xs ${detected ? "text-foreground" : "text-muted-foreground/50"}`}>
74
+ {credited ? "Confirmed" : "Confirming on chain"}
75
+ </span>
76
+ </div>
77
+ <div className="flex items-center gap-2">
78
+ <div
79
+ className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] ${credited ? "bg-green-500 text-white" : "bg-muted"}`}
80
+ >
81
+ {credited && <Check className="h-2.5 w-2.5" />}
82
+ </div>
83
+ <span className={`text-xs ${credited ? "text-foreground" : "text-muted-foreground/50"}`}>
84
+ Credits applied
85
+ </span>
86
+ </div>
87
+ </div>
88
+
89
+ {txHash && <p className="text-xs text-muted-foreground font-mono truncate">tx: {txHash}</p>}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,185 @@
1
+ "use client";
2
+
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+ import { CircleDollarSign } from "lucide-react";
5
+ import { useCallback, useEffect, useState } from "react";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ type CheckoutResult,
9
+ createCheckout,
10
+ getChargeStatus,
11
+ getSupportedPaymentMethods,
12
+ type SupportedPaymentMethod,
13
+ } from "@/lib/api";
14
+ import { AmountSelector } from "./amount-selector";
15
+ import { ConfirmationTracker } from "./confirmation-tracker";
16
+ import { DepositView } from "./deposit-view";
17
+ import { PaymentMethodPicker } from "./payment-method-picker";
18
+
19
+ type Step = "amount" | "method" | "deposit" | "confirming";
20
+ type PaymentStatus = "waiting" | "partial" | "confirming" | "credited" | "expired" | "failed";
21
+
22
+ export function CryptoCheckout() {
23
+ const [step, setStep] = useState<Step>("amount");
24
+ const [methods, setMethods] = useState<SupportedPaymentMethod[]>([]);
25
+ const [amountUsd, setAmountUsd] = useState(0);
26
+ const [checkout, setCheckout] = useState<CheckoutResult | null>(null);
27
+ const [status, setStatus] = useState<PaymentStatus>("waiting");
28
+ const [confirmations, setConfirmations] = useState(0);
29
+ const [confirmationsRequired, setConfirmationsRequired] = useState(0);
30
+ const [loading, setLoading] = useState(false);
31
+
32
+ useEffect(() => {
33
+ getSupportedPaymentMethods()
34
+ .then(setMethods)
35
+ .catch(() => {
36
+ // silently fall back to empty methods list
37
+ });
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ if (!checkout?.referenceId) return;
42
+ const interval = setInterval(async () => {
43
+ try {
44
+ const res = await getChargeStatus(checkout.referenceId);
45
+ setConfirmations(res.confirmations);
46
+ setConfirmationsRequired(res.confirmationsRequired);
47
+ if (res.credited) {
48
+ setStatus("credited");
49
+ setStep("confirming");
50
+ clearInterval(interval);
51
+ } else if (res.status === "expired" || res.status === "failed") {
52
+ setStatus(res.status as PaymentStatus);
53
+ clearInterval(interval);
54
+ } else if (
55
+ res.amountReceivedCents > 0 &&
56
+ res.amountReceivedCents >= res.amountExpectedCents
57
+ ) {
58
+ setStatus("confirming");
59
+ setStep("confirming");
60
+ } else if (res.amountReceivedCents > 0) {
61
+ setStatus("partial");
62
+ }
63
+ } catch {
64
+ /* ignore poll errors */
65
+ }
66
+ }, 5000);
67
+ return () => clearInterval(interval);
68
+ }, [checkout?.referenceId]);
69
+
70
+ const handleAmount = useCallback((amt: number) => {
71
+ setAmountUsd(amt);
72
+ setStep("method");
73
+ }, []);
74
+
75
+ const handleMethod = useCallback(
76
+ async (method: SupportedPaymentMethod) => {
77
+ setLoading(true);
78
+ try {
79
+ const result = await createCheckout(method.id, amountUsd);
80
+ setCheckout(result);
81
+ setStatus("waiting");
82
+ setStep("deposit");
83
+ } catch {
84
+ // Stay on method step
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ },
89
+ [amountUsd],
90
+ );
91
+
92
+ const handleReset = useCallback(() => {
93
+ setStep("amount");
94
+ setCheckout(null);
95
+ setStatus("waiting");
96
+ setAmountUsd(0);
97
+ setConfirmations(0);
98
+ }, []);
99
+
100
+ if (methods.length === 0) return null;
101
+
102
+ return (
103
+ <motion.div
104
+ initial={{ opacity: 0, y: 8 }}
105
+ animate={{ opacity: 1, y: 0 }}
106
+ transition={{ duration: 0.3 }}
107
+ >
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle className="flex items-center gap-2">
111
+ <CircleDollarSign className="h-4 w-4 text-primary" />
112
+ Pay with Crypto
113
+ </CardTitle>
114
+ </CardHeader>
115
+ <CardContent>
116
+ <AnimatePresence mode="wait">
117
+ {step === "amount" && (
118
+ <motion.div
119
+ key="amount"
120
+ initial={{ opacity: 0, x: -20 }}
121
+ animate={{ opacity: 1, x: 0 }}
122
+ exit={{ opacity: 0, x: 20 }}
123
+ >
124
+ <AmountSelector onSelect={handleAmount} />
125
+ </motion.div>
126
+ )}
127
+ {step === "method" && (
128
+ <motion.div
129
+ key="method"
130
+ initial={{ opacity: 0, x: -20 }}
131
+ animate={{ opacity: 1, x: 0 }}
132
+ exit={{ opacity: 0, x: 20 }}
133
+ >
134
+ <PaymentMethodPicker
135
+ methods={methods}
136
+ onSelect={handleMethod}
137
+ onBack={() => setStep("amount")}
138
+ />
139
+ {loading && (
140
+ <p className="mt-2 text-xs text-muted-foreground animate-pulse">
141
+ Creating checkout...
142
+ </p>
143
+ )}
144
+ </motion.div>
145
+ )}
146
+ {step === "deposit" && checkout && (
147
+ <motion.div
148
+ key="deposit"
149
+ initial={{ opacity: 0, x: -20 }}
150
+ animate={{ opacity: 1, x: 0 }}
151
+ exit={{ opacity: 0, x: 20 }}
152
+ >
153
+ <DepositView checkout={checkout} status={status} onBack={() => setStep("method")} />
154
+ </motion.div>
155
+ )}
156
+ {step === "confirming" && checkout && (
157
+ <motion.div
158
+ key="confirming"
159
+ initial={{ opacity: 0, x: -20 }}
160
+ animate={{ opacity: 1, x: 0 }}
161
+ exit={{ opacity: 0, x: 20 }}
162
+ >
163
+ <ConfirmationTracker
164
+ confirmations={confirmations}
165
+ confirmationsRequired={confirmationsRequired}
166
+ displayAmount={checkout.displayAmount}
167
+ credited={status === "credited"}
168
+ />
169
+ {status === "credited" && (
170
+ <button
171
+ type="button"
172
+ onClick={handleReset}
173
+ className="mt-4 text-sm text-primary hover:underline"
174
+ >
175
+ Done — buy more credits
176
+ </button>
177
+ )}
178
+ </motion.div>
179
+ )}
180
+ </AnimatePresence>
181
+ </CardContent>
182
+ </Card>
183
+ </motion.div>
184
+ );
185
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import { Check, Copy } from "lucide-react";
4
+ import { QRCodeSVG } from "qrcode.react";
5
+ import { useCallback, useEffect, useState } from "react";
6
+ import { Button } from "@/components/ui/button";
7
+ import type { CheckoutResult } from "@/lib/api";
8
+
9
+ interface DepositViewProps {
10
+ checkout: CheckoutResult;
11
+ status: "waiting" | "partial" | "confirming" | "credited" | "expired" | "failed";
12
+ onBack: () => void;
13
+ }
14
+
15
+ export function DepositView({ checkout, status, onBack }: DepositViewProps) {
16
+ const [copied, setCopied] = useState(false);
17
+ const [timeLeft, setTimeLeft] = useState(30 * 60);
18
+
19
+ const handleCopy = useCallback(() => {
20
+ navigator.clipboard.writeText(checkout.depositAddress);
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 2000);
23
+ }, [checkout.depositAddress]);
24
+
25
+ useEffect(() => {
26
+ if (status !== "waiting") return;
27
+ const timer = setInterval(() => setTimeLeft((t) => Math.max(0, t - 1)), 1000);
28
+ return () => clearInterval(timer);
29
+ }, [status]);
30
+
31
+ const mins = Math.floor(timeLeft / 60);
32
+ const secs = timeLeft % 60;
33
+
34
+ return (
35
+ <div className="space-y-4 text-center">
36
+ <button
37
+ type="button"
38
+ onClick={onBack}
39
+ className="text-sm text-muted-foreground hover:text-foreground self-start"
40
+ >
41
+ &larr; Back
42
+ </button>
43
+ <p className="text-sm text-muted-foreground">Send exactly</p>
44
+ <p className="text-2xl font-semibold">{checkout.displayAmount}</p>
45
+ <p className="text-xs text-muted-foreground">
46
+ on {checkout.chain} &middot; ${checkout.amountUsd.toFixed(2)} USD
47
+ </p>
48
+ <div
49
+ className="mx-auto w-fit rounded-lg border border-border bg-background p-3"
50
+ aria-hidden="true"
51
+ >
52
+ <QRCodeSVG
53
+ value={checkout.depositAddress}
54
+ size={140}
55
+ bgColor="hsl(var(--background))"
56
+ fgColor="hsl(var(--foreground))"
57
+ />
58
+ </div>
59
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-muted/50 px-3 py-2">
60
+ <code className="flex-1 truncate text-xs font-mono">{checkout.depositAddress}</code>
61
+ <Button variant="ghost" size="sm" onClick={handleCopy} aria-label="Copy address">
62
+ {copied ? (
63
+ <Check className="h-3.5 w-3.5 text-primary" />
64
+ ) : (
65
+ <Copy className="h-3.5 w-3.5" />
66
+ )}
67
+ </Button>
68
+ </div>
69
+ <div className="flex items-center justify-center gap-2 rounded-lg border border-border p-2">
70
+ {status === "waiting" && (
71
+ <>
72
+ <span className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
73
+ <span className="text-xs text-yellow-500">Waiting for payment...</span>
74
+ <span className="text-xs text-muted-foreground">
75
+ &middot; {mins}:{secs.toString().padStart(2, "0")}
76
+ </span>
77
+ </>
78
+ )}
79
+ {status === "partial" && (
80
+ <>
81
+ <span className="h-2 w-2 rounded-full bg-blue-500" />
82
+ <span className="text-xs text-blue-500">Partial payment received</span>
83
+ </>
84
+ )}
85
+ {status === "expired" && <span className="text-xs text-destructive">Payment expired</span>}
86
+ {status === "failed" && <span className="text-xs text-destructive">Payment failed</span>}
87
+ </div>
88
+ </div>
89
+ );
90
+ }