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