"use client"; import { useEffect, useState } from "react"; import { Button } from "@kandev/ui/button"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Spinner } from "@kandev/ui/spinner"; import { useToast } from "@/components/toast-provider"; import type { useGitHubAppRegistrations } from "@/hooks/domains/github/use-github-app-registrations"; import type { GitHubAppOwnerType, PrepareGitHubAppImportResponse } from "@/lib/types/github"; import { appRegistrationError, defaultVisibility, githubAppSettingsURL, normalizePublicBaseUrl, validateImportSecrets, validateRegistrationBasics, type AppSetupErrors, } from "./github-app-onboarding-model"; import { GitHubAppImportGuide } from "./github-app-import-guide"; import { GitHubAppImportIdentityFields, GitHubAppImportSecretFields, type GitHubAppImportValues, } from "./github-app-import-fields"; import { GitHubAppVisibilityField } from "./github-app-visibility-field"; type RegistrationHook = ReturnType; export function GitHubAppImportForm({ workspaceId, registrations, onImported, }: { workspaceId: string; registrations: RegistrationHook; onImported: (registrationId: string) => void; }) { const [preparation, setPreparation] = useState(null); const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const { toast } = useToast(); useEffect(() => { setPreparation(null); setValues(initialValues); setErrors({}); }, [workspaceId]); const update = (name: keyof typeof values, value: string) => setValues((current) => ({ ...current, [name]: value })); async function prepare(event: React.FormEvent) { event.preventDefault(); const nextErrors: AppSetupErrors = {}; const publicBaseUrl = normalizePublicBaseUrl(values.publicBaseUrl, nextErrors); setErrors(nextErrors); if (!publicBaseUrl) return; try { setPreparation(await registrations.prepareImport({ public_base_url: publicBaseUrl })); } catch (error) { showError(error, "Import setup could not start", toast); } } async function submit(event: React.FormEvent) { event.preventDefault(); if (!preparation) return; const ownerType = values.ownerType as GitHubAppOwnerType; const basics = validateRegistrationBasics({ ...values, ownerType }); const secrets = validateImportSecrets(values); const nextErrors = { ...basics.errors, ...secrets.errors }; setErrors(nextErrors); if (!basics.publicBaseUrl || Object.keys(nextErrors).length) return; try { const imported = await registrations.importRegistration({ registration_id: preparation.registration_id, display_name: basics.displayName, github_host: "github.com", app_id: secrets.appId, client_id: values.clientId.trim(), client_secret: values.clientSecret, private_key: values.privateKey.trim(), webhook_secret: values.webhookSecret, slug: values.slug.trim(), owner_login: basics.ownerLogin, owner_type: ownerType, visibility: values.visibility, public_base_url: basics.publicBaseUrl, }); setValues(clearImportSecrets); toast({ description: "GitHub App imported. It is ready to install.", variant: "success", }); onImported(imported.id); } catch (error) { showError(error, "GitHub App could not be imported", toast); } } if (!preparation) { return ( update("publicBaseUrl", value)} onSubmit={prepare} /> ); } const settingsUrl = importSettingsUrl(values); const startOver = () => { setPreparation(null); setErrors({}); setValues(clearImportSecrets); }; return ( setValues((current) => ({ ...current, visibility }))} onStartOver={startOver} onSubmit={submit} /> ); } function PrepareImportForm(props: { publicBaseUrl: string; error?: string; mutating: boolean; onPublicBaseUrl: (value: string) => void; onSubmit: (event: React.FormEvent) => void; }) { return (

Add an existing GitHub App

Kandev first reserves exact callback and webhook URLs, then guides you through GitHub's App settings. Existing workspace access is unchanged until you install the imported App.

props.onPublicBaseUrl(event.target.value)} />
); } type PreparedImportProps = { preparation: PrepareGitHubAppImportResponse; settingsUrl?: string; values: GitHubAppImportValues; errors: AppSetupErrors; mutating: boolean; update: (name: keyof GitHubAppImportValues, value: string) => void; onVisibility: (value: "private" | "public") => void; onStartOver: () => void; onSubmit: (event: React.FormEvent) => void; }; function PreparedImportForm(props: PreparedImportProps) { return (
); } const initialValues: GitHubAppImportValues = { displayName: "", ownerLogin: "", ownerType: "Organization" as GitHubAppOwnerType, publicBaseUrl: "", visibility: defaultVisibility, appId: "", clientId: "", clientSecret: "", privateKey: "", webhookSecret: "", slug: "", }; function clearImportSecrets(values: GitHubAppImportValues): GitHubAppImportValues { return { ...values, clientSecret: "", privateKey: "", webhookSecret: "" }; } function importSettingsUrl(values: GitHubAppImportValues) { if (!values.ownerLogin.trim() || !values.slug.trim()) return undefined; return githubAppSettingsURL(values.ownerType, values.ownerLogin, values.slug); } function showError(error: unknown, fallback: string, toast: ReturnType["toast"]) { const detail = appRegistrationError(error); toast({ description: detail?.error ?? fallback, variant: "error" }); } function Field({ label, error, children, }: { label: string; error?: string; children: React.ReactNode; }) { return ( ); }