"use client"; import { useEffect, useState } from "react"; import { IconExternalLink } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group"; import { Spinner } from "@kandev/ui/spinner"; import { useToast } from "@/components/toast-provider"; import type { StartGitHubAppManifestResponse } from "@/lib/types/github"; import { appRegistrationError, defaultVisibility, submitManifestToGitHub, validateRegistrationBasics, type AppSetupErrors, } from "./github-app-onboarding-model"; import { GitHubAppPolicyDialog } from "./github-app-policy-dialog"; import { GitHubAppVisibilityField } from "./github-app-visibility-field"; import type { useGitHubAppRegistrations } from "@/hooks/domains/github/use-github-app-registrations"; type RegistrationHook = ReturnType; export function GitHubAppCreateForm({ workspaceId, registrations, }: { workspaceId: string; registrations: RegistrationHook; }) { const [displayName, setDisplayName] = useState(""); const [ownerType, setOwnerType] = useState<"user" | "organization">("organization"); const [ownerLogin, setOwnerLogin] = useState(""); const [publicBaseUrl, setPublicBaseUrl] = useState(""); const [visibility, setVisibility] = useState(defaultVisibility); const [errors, setErrors] = useState({}); const [handoff, setHandoff] = useState(null); const { toast } = useToast(); useEffect(() => { setDisplayName(""); setOwnerType("organization"); setOwnerLogin(""); setPublicBaseUrl(""); setVisibility(defaultVisibility); setErrors({}); setHandoff(null); }, [workspaceId]); async function prepare(event: React.FormEvent) { event.preventDefault(); const basics = validateRegistrationBasics({ displayName, ownerType, ownerLogin, publicBaseUrl, }); setErrors(basics.errors); if (!basics.publicBaseUrl || Object.keys(basics.errors).length) return; try { setHandoff( await registrations.startManifest({ display_name: basics.displayName, owner_type: ownerType, owner_login: basics.ownerLogin, visibility, public_base_url: basics.publicBaseUrl, }), ); } catch (error) { const detail = appRegistrationError(error); toast({ description: detail?.error ?? "GitHub App setup could not start", variant: "error" }); } } if (handoff) { return setHandoff(null)} />; } return ( ); } function ManifestHandoff({ handoff, onEdit, }: { handoff: StartGitHubAppManifestResponse; onEdit: () => void; }) { return (

GitHub is ready to create the App

GitHub will show the generated permissions and ask you to confirm. Returning from GitHub adds the registration; installing it for this workspace remains an explicit step.

); } type CreateFieldsProps = { displayName: string; ownerType: "user" | "organization"; ownerLogin: string; publicBaseUrl: string; visibility: "private" | "public"; errors: AppSetupErrors; mutating: boolean; onDisplayName: (value: string) => void; onOwnerType: (value: "user" | "organization") => void; onOwnerLogin: (value: string) => void; onPublicBaseUrl: (value: string) => void; onVisibility: (value: "private" | "public") => void; onSubmit: (event: React.FormEvent) => void; }; function CreateAppFields(props: CreateFieldsProps) { return (

Create a GitHub App

Kandev generates the exact App policy. You confirm creation on GitHub; Kandev then stores this registration so it can be selected by a workspace.

props.onDisplayName(event.target.value)} /> props.onOwnerLogin(event.target.value)} />
props.onOwnerType(value as CreateFieldsProps["ownerType"])} className="flex min-h-11 flex-wrap items-center gap-5" > props.onPublicBaseUrl(event.target.value)} />

GitHub must reach this HTTPS origin for signed webhooks and callbacks.

); } function Field({ label, error, children, }: { label: string; error?: string; children: React.ReactNode; }) { return ( ); }