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