IdentitySelectPage.tsx

  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}