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