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 { 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}