@wopr-network/platform-ui-core 1.4.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.4.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
+ }