@wopr-network/platform-ui-core 1.3.0 → 1.5.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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BuildingIcon, CheckCircleIcon, Loader2Icon, XCircleIcon } from "lucide-react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { use, useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import { useSession } from "@/lib/auth-client";
|
|
8
|
+
import { productName } from "@/lib/brand-config";
|
|
9
|
+
import { acceptInvite } from "@/lib/org-api";
|
|
10
|
+
|
|
11
|
+
type InviteState = "loading" | "accepting" | "success" | "error" | "login-required";
|
|
12
|
+
|
|
13
|
+
export default function InviteAcceptPage({ params }: { params: Promise<{ token: string }> }) {
|
|
14
|
+
const { token } = use(params);
|
|
15
|
+
const { data: session, isPending: sessionLoading } = useSession();
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const [state, setState] = useState<InviteState>("loading");
|
|
18
|
+
const [orgName, setOrgName] = useState<string>("");
|
|
19
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
20
|
+
const acceptedRef = useRef(false);
|
|
21
|
+
|
|
22
|
+
const doAccept = useCallback(async () => {
|
|
23
|
+
if (acceptedRef.current) return;
|
|
24
|
+
acceptedRef.current = true;
|
|
25
|
+
|
|
26
|
+
setState("accepting");
|
|
27
|
+
try {
|
|
28
|
+
const result = await acceptInvite(token);
|
|
29
|
+
setOrgName(result.orgName ?? "your organization");
|
|
30
|
+
setState("success");
|
|
31
|
+
// Redirect to dashboard after brief success message
|
|
32
|
+
setTimeout(() => router.push("/"), 2000);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : "Failed to accept invite";
|
|
35
|
+
// Surface specific error messages from the backend
|
|
36
|
+
if (msg.includes("already a member")) {
|
|
37
|
+
setErrorMessage("You are already a member of this organization.");
|
|
38
|
+
} else if (msg.includes("expired")) {
|
|
39
|
+
setErrorMessage("This invite has expired. Please ask the admin to send a new one.");
|
|
40
|
+
} else if (msg.includes("revoked")) {
|
|
41
|
+
setErrorMessage("This invite has been revoked.");
|
|
42
|
+
} else if (msg.includes("not found")) {
|
|
43
|
+
setErrorMessage("Invalid invite link. It may have already been used.");
|
|
44
|
+
} else {
|
|
45
|
+
setErrorMessage(msg);
|
|
46
|
+
}
|
|
47
|
+
setState("error");
|
|
48
|
+
acceptedRef.current = false;
|
|
49
|
+
}
|
|
50
|
+
}, [token, router]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (sessionLoading) return;
|
|
54
|
+
|
|
55
|
+
if (!session?.user) {
|
|
56
|
+
// Not logged in — redirect to login with callback
|
|
57
|
+
setState("login-required");
|
|
58
|
+
const callbackUrl = encodeURIComponent(`/invite/${token}`);
|
|
59
|
+
router.push(`/login?callbackUrl=${callbackUrl}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// User is logged in — accept the invite
|
|
64
|
+
doAccept();
|
|
65
|
+
}, [session, sessionLoading, token, router, doAccept]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card className="border-terminal/20 bg-card/80 backdrop-blur-sm">
|
|
69
|
+
<CardHeader className="text-center">
|
|
70
|
+
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-terminal/10">
|
|
71
|
+
<BuildingIcon className="h-6 w-6 text-terminal" />
|
|
72
|
+
</div>
|
|
73
|
+
<CardTitle className="text-lg font-semibold tracking-tight">Organization Invite</CardTitle>
|
|
74
|
+
<CardDescription>
|
|
75
|
+
{state === "loading" && "Checking your session..."}
|
|
76
|
+
{state === "login-required" && "Redirecting to sign in..."}
|
|
77
|
+
{state === "accepting" && `Joining organization on ${productName()}...`}
|
|
78
|
+
{state === "success" && `Welcome to ${orgName}!`}
|
|
79
|
+
{state === "error" && "Something went wrong"}
|
|
80
|
+
</CardDescription>
|
|
81
|
+
</CardHeader>
|
|
82
|
+
<CardContent className="flex flex-col items-center gap-4 pb-8">
|
|
83
|
+
{(state === "loading" || state === "login-required" || state === "accepting") && (
|
|
84
|
+
<Loader2Icon className="h-8 w-8 animate-spin text-terminal/60" />
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{state === "success" && (
|
|
88
|
+
<>
|
|
89
|
+
<CheckCircleIcon className="h-8 w-8 text-emerald-500" />
|
|
90
|
+
<p className="text-sm text-muted-foreground">Redirecting to your dashboard...</p>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{state === "error" && (
|
|
95
|
+
<>
|
|
96
|
+
<XCircleIcon className="h-8 w-8 text-destructive" />
|
|
97
|
+
<p className="text-sm text-muted-foreground text-center">{errorMessage}</p>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={() => router.push("/")}
|
|
101
|
+
className="mt-2 text-sm font-medium text-terminal hover:text-terminal/80 underline underline-offset-4"
|
|
102
|
+
>
|
|
103
|
+
Go to dashboard
|
|
104
|
+
</button>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -8,7 +8,9 @@ import { Button } from "@/components/ui/button";
|
|
|
8
8
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
9
9
|
import {
|
|
10
10
|
createCryptoCheckout,
|
|
11
|
+
createEthCheckout,
|
|
11
12
|
createStablecoinCheckout,
|
|
13
|
+
type EthCheckoutResult,
|
|
12
14
|
type StablecoinCheckoutResult,
|
|
13
15
|
} from "@/lib/api";
|
|
14
16
|
import { cn } from "@/lib/utils";
|
|
@@ -27,7 +29,17 @@ const STABLECOIN_TOKENS = [
|
|
|
27
29
|
{ token: "DAI", label: "DAI", chain: "base", chainLabel: "Base" },
|
|
28
30
|
];
|
|
29
31
|
|
|
30
|
-
type PaymentMethod = "btc" | "stablecoin";
|
|
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
|
+
}
|
|
31
43
|
|
|
32
44
|
function CopyButton({ text }: { text: string }) {
|
|
33
45
|
const [copied, setCopied] = useState(false);
|
|
@@ -52,7 +64,7 @@ function StablecoinDeposit({
|
|
|
52
64
|
checkout: StablecoinCheckoutResult;
|
|
53
65
|
onReset: () => void;
|
|
54
66
|
}) {
|
|
55
|
-
const [confirmed,
|
|
67
|
+
const [confirmed, _setConfirmed] = useState(false);
|
|
56
68
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
57
69
|
|
|
58
70
|
useEffect(() => {
|
|
@@ -122,6 +134,21 @@ export function BuyCryptoCreditPanel() {
|
|
|
122
134
|
const [stablecoinCheckout, setStablecoinCheckout] = useState<StablecoinCheckoutResult | null>(
|
|
123
135
|
null,
|
|
124
136
|
);
|
|
137
|
+
const [ethCheckout, setEthCheckout] = useState<EthCheckoutResult | null>(null);
|
|
138
|
+
|
|
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
|
+
}
|
|
125
152
|
|
|
126
153
|
async function handleBtcCheckout() {
|
|
127
154
|
if (selected === null) return;
|
|
@@ -161,6 +188,7 @@ export function BuyCryptoCreditPanel() {
|
|
|
161
188
|
|
|
162
189
|
function handleReset() {
|
|
163
190
|
setStablecoinCheckout(null);
|
|
191
|
+
setEthCheckout(null);
|
|
164
192
|
setSelected(null);
|
|
165
193
|
setError(null);
|
|
166
194
|
}
|
|
@@ -176,6 +204,8 @@ export function BuyCryptoCreditPanel() {
|
|
|
176
204
|
<CardTitle className="flex items-center gap-2">
|
|
177
205
|
{method === "btc" ? (
|
|
178
206
|
<Bitcoin className="h-4 w-4 text-amber-500" />
|
|
207
|
+
) : method === "eth" ? (
|
|
208
|
+
<CircleDollarSign className="h-4 w-4 text-indigo-500" />
|
|
179
209
|
) : (
|
|
180
210
|
<CircleDollarSign className="h-4 w-4 text-blue-500" />
|
|
181
211
|
)}
|
|
@@ -197,6 +227,21 @@ export function BuyCryptoCreditPanel() {
|
|
|
197
227
|
>
|
|
198
228
|
Stablecoin
|
|
199
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>
|
|
200
245
|
<button
|
|
201
246
|
type="button"
|
|
202
247
|
onClick={() => {
|
|
@@ -215,7 +260,41 @@ export function BuyCryptoCreditPanel() {
|
|
|
215
260
|
</div>
|
|
216
261
|
</CardHeader>
|
|
217
262
|
<CardContent className="space-y-4">
|
|
218
|
-
{
|
|
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 ? (
|
|
219
298
|
<StablecoinDeposit checkout={stablecoinCheckout} onReset={handleReset} />
|
|
220
299
|
) : (
|
|
221
300
|
<>
|
|
@@ -263,7 +342,13 @@ export function BuyCryptoCreditPanel() {
|
|
|
263
342
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
264
343
|
|
|
265
344
|
<Button
|
|
266
|
-
onClick={
|
|
345
|
+
onClick={
|
|
346
|
+
method === "btc"
|
|
347
|
+
? handleBtcCheckout
|
|
348
|
+
: method === "eth"
|
|
349
|
+
? handleEthCheckout
|
|
350
|
+
: handleStablecoinCheckout
|
|
351
|
+
}
|
|
267
352
|
disabled={selected === null || loading}
|
|
268
353
|
variant="outline"
|
|
269
354
|
className="w-full sm:w-auto"
|
|
@@ -272,7 +357,9 @@ export function BuyCryptoCreditPanel() {
|
|
|
272
357
|
? "Creating checkout..."
|
|
273
358
|
: method === "btc"
|
|
274
359
|
? "Pay with BTC"
|
|
275
|
-
:
|
|
360
|
+
: method === "eth"
|
|
361
|
+
? "Pay with ETH"
|
|
362
|
+
: `Pay with ${selectedToken.label}`}
|
|
276
363
|
</Button>
|
|
277
364
|
</>
|
|
278
365
|
)}
|
package/src/lib/api.ts
CHANGED
|
@@ -1353,6 +1353,22 @@ export async function createStablecoinCheckout(
|
|
|
1353
1353
|
return trpcVanilla.billing.stablecoinCheckout.mutate({ amountUsd, token, chain });
|
|
1354
1354
|
}
|
|
1355
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
|
+
|
|
1356
1372
|
// --- Dividend types ---
|
|
1357
1373
|
|
|
1358
1374
|
export interface DividendWalletStats {
|
package/src/lib/trpc-types.ts
CHANGED
|
@@ -114,6 +114,7 @@ type AppRouterRecord = {
|
|
|
114
114
|
affiliateReferrals: AnyTRPCQueryProcedure;
|
|
115
115
|
cryptoCheckout: AnyTRPCMutationProcedure;
|
|
116
116
|
stablecoinCheckout: AnyTRPCMutationProcedure;
|
|
117
|
+
ethCheckout: AnyTRPCMutationProcedure;
|
|
117
118
|
autoTopupSettings: AnyTRPCQueryProcedure;
|
|
118
119
|
updateAutoTopupSettings: AnyTRPCMutationProcedure;
|
|
119
120
|
accountStatus: AnyTRPCQueryProcedure;
|