1// Identity selection page (/auth/select-identity).
2//
3// Reached after a successful OAuth login when no existing git-bug identity
4// could be matched automatically (via provider metadata set by the bridge).
5// The user can either adopt an existing identity — which links it to their
6// OAuth account for future logins — or create a fresh one from their OAuth
7// profile.
8
9import { UserCircle, Plus, AlertCircle } from "lucide-react";
10import { useEffect, useState } from "react";
11
12import { Button } from "@/components/ui/button";
13import { Skeleton } from "@/components/ui/skeleton";
14
15interface IdentityItem {
16 repoSlug: string;
17 id: string;
18 humanId: string;
19 displayName: string;
20 login?: string;
21 avatarUrl?: string;
22}
23
24export function IdentitySelectPage() {
25 const [identities, setIdentities] = useState<IdentityItem[] | null>(null);
26 const [error, setError] = useState<string | null>(null);
27 const [working, setWorking] = useState(false);
28
29 useEffect(() => {
30 void fetch("/auth/identities", { credentials: "include" })
31 .then((res) => {
32 if (!res.ok) throw new Error(`unexpected status ${res.status}`);
33 return res.json() as Promise<IdentityItem[]>;
34 })
35 .then(setIdentities)
36 .catch((e) => setError(String(e)));
37 }, []);
38
39 async function adopt(identityId: string | null) {
40 setWorking(true);
41 try {
42 const res = await fetch("/auth/adopt", {
43 method: "POST",
44 credentials: "include",
45 headers: { "Content-Type": "application/json" },
46 body: JSON.stringify(identityId ? { identityId } : {}),
47 });
48 if (!res.ok) throw new Error(`adopt failed: ${res.status}`);
49 // Full page reload to reset Apollo cache and auth state cleanly.
50 window.location.assign("/");
51 } catch (e) {
52 setError(String(e));
53 setWorking(false);
54 }
55 }
56
57 return (
58 <div className="mx-auto max-w-lg py-12">
59 <div className="mb-2 flex items-center gap-3">
60 <UserCircle className="size-6 text-muted-foreground" />
61 <h1 className="text-xl font-semibold">Choose your identity</h1>
62 </div>
63 <p className="mb-8 text-sm text-muted-foreground">
64 No git-bug identity was found linked to your account. Select an existing identity to link
65 it, or create a new one from your profile.
66 </p>
67
68 {error && (
69 <div className="mb-4 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
70 <AlertCircle className="size-4 shrink-0" />
71 {error}
72 </div>
73 )}
74
75 {!identities && !error && (
76 <div className="space-y-2">
77 {Array.from({ length: 3 }).map((_, i) => (
78 <Skeleton key={i} className="h-14 w-full rounded-md" />
79 ))}
80 </div>
81 )}
82
83 <div className="divide-y divide-border rounded-md border border-border">
84 {identities?.map((id) => (
85 <div key={id.id} className="flex items-center gap-3 px-4 py-3">
86 <div className="min-w-0 flex-1">
87 <p className="font-medium">{id.displayName}</p>
88 <p className="text-xs text-muted-foreground">
89 {id.login ? `@${id.login} · ` : ""}
90 {id.repoSlug} · {id.humanId}
91 </p>
92 </div>
93 <Button
94 size="sm"
95 disabled={working}
96 onClick={() => {
97 void adopt(id.id);
98 }}
99 >
100 Adopt
101 </Button>
102 </div>
103 ))}
104
105 {/* Always offer to create a new identity */}
106 <div className="flex items-center gap-3 px-4 py-3">
107 <div className="min-w-0 flex-1">
108 <p className="font-medium">Create new identity</p>
109 <p className="text-xs text-muted-foreground">
110 A fresh git-bug identity will be created from your OAuth profile.
111 </p>
112 </div>
113 <Button
114 size="sm"
115 disabled={working}
116 onClick={() => {
117 void adopt(null);
118 }}
119 >
120 <Plus className="size-4" />
121 Create
122 </Button>
123 </div>
124 </div>
125 </div>
126 );
127}