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