"use client"; import { useEffect, useState } from "react"; import { IconExternalLink, IconPlus } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Spinner } from "@kandev/ui/spinner"; import { useToast } from "@/components/toast-provider"; import { useGitHubAppRegistrations } from "@/hooks/domains/github/use-github-app-registrations"; import { GitHubAppCreateForm } from "./github-app-create-form"; import { GitHubAppImportForm } from "./github-app-import-form"; import { GitHubAppRegistrationList } from "./github-app-registration-list"; type AppView = "choose" | "import" | "create"; type RegistrationHook = ReturnType; export function GitHubAppConnectionPanel({ workspaceId }: { workspaceId: string }) { const registrations = useGitHubAppRegistrations(workspaceId); const [view, setView] = useState("choose"); const { selectedId, setSelectedId, selectedRegistration } = useAppRegistrationSelection( workspaceId, registrations, ); const { toast } = useToast(); useEffect(() => { setView("choose"); }, [workspaceId]); async function install() { if (!selectedRegistration || selectedRegistration.status !== "active") return; try { const response = await registrations.startInstall(selectedRegistration.id); const url = response.url ?? response.URL; if (!url) throw new Error("GitHub did not return an installation URL"); window.location.assign(url); } catch (error) { toast({ description: error instanceof Error ? error.message : "App installation failed", variant: "error", }); } } if (view === "import") { return (
setView("choose")} /> { setSelectedId(registrationId); setView("choose"); }} />
); } if (view === "create") { return (
setView("choose")} />
); } return (

Choose a GitHub App

Use an App when automation needs its own identity, short-lived tokens, and repository-level installation control. Setup is more involved and requires a publicly reachable HTTPS URL.

{registrations.loading ? (
Loading registered Apps...
) : ( )} {registrations.error &&

{registrations.error}

}

A registration can be reused across workspaces, or you can keep work and personal Apps separate. Each workspace selects and installs its own credential.

); } function useAppRegistrationSelection(workspaceId: string, registrations: RegistrationHook) { const [selectedId, setSelectedId] = useState(""); useEffect(() => setSelectedId(""), [workspaceId]); useEffect(() => { if (!registrations.loaded) return; setSelectedId((current) => { const currentRegistration = registrations.registrations.find(({ id }) => id === current); if (currentRegistration?.status === "active") return current; if (registrations.selected?.status === "active") return registrations.selected.id; return registrations.registrations.find(({ status }) => status === "active")?.id ?? ""; }); }, [registrations.loaded, registrations.registrations, registrations.selected]); const selectedRegistration = registrations.registrations.find(({ id }) => id === selectedId); return { selectedId, setSelectedId, selectedRegistration }; } function BackButton({ onClick }: { onClick: () => void }) { return ( ); }