@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-ui-core",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Brand-agnostic AI agent platform UI — deploy as any brand via env vars",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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, setConfirmed] = useState(false);
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
- {stablecoinCheckout ? (
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={method === "btc" ? handleBtcCheckout : handleStablecoinCheckout}
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
- : `Pay with ${selectedToken.label}`}
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 {
@@ -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;