From 5a0683282f2b437b6a46d4ad9ad5396c21a7a9b4 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Mon, 4 May 2026 12:18:24 +0100 Subject: [PATCH] feat(code): add inline GitHub disconnect to onboarding step --- apps/code/src/renderer/api/posthogClient.ts | 15 ++++ .../hooks/useGithubUserConnect.ts | 35 +++++---- .../components/GitIntegrationStep.tsx | 77 ++++++++++++++++++- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index b2003f1149..8f64e50377 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -622,6 +622,21 @@ export class PostHogAPIClient { return data.results ?? []; } + async disconnectGithubUserIntegration(installationId: string): Promise { + const urlPath = `/api/users/@me/integrations/github/${encodeURIComponent(installationId)}/`; + const url = new URL(`${this.api.baseUrl}${urlPath}`); + const response = await this.api.fetcher.fetch({ + method: "delete", + url, + path: urlPath, + }); + if (!response.ok && response.status !== 404) { + throw new Error( + `Failed to disconnect GitHub integration: ${response.statusText}`, + ); + } + } + async switchOrganization(orgId: string): Promise { await this.api.patch("/api/users/{uuid}/", { path: { uuid: "@me" }, diff --git a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts index 6023efe3fd..5eeeb2945a 100644 --- a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts +++ b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts @@ -3,7 +3,7 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { useGitHubIntegrationCallback } from "@features/integrations/hooks/useGitHubIntegrationCallback"; import { trpcClient } from "@renderer/trpc/client"; import { IS_DEV } from "@shared/constants/environment"; -import { useQueryClient } from "@tanstack/react-query"; +import { type QueryClient, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; const POLL_INTERVAL_MS = 3_000; @@ -68,6 +68,24 @@ interface Result { reset: () => void; } +export function invalidateGithubQueries( + queryClient: QueryClient, + projectId: number | null = null, +): void { + if (projectId !== null) { + void queryClient.invalidateQueries({ + queryKey: ["integrations", projectId], + }); + } + void queryClient.invalidateQueries({ + queryKey: ["integrations", "list"], + }); + void queryClient.invalidateQueries({ + queryKey: ["user-github-integrations"], + }); + void queryClient.invalidateQueries({ queryKey: ["github_login"] }); +} + async function openUrlInBrowser(url: string): Promise { try { await trpcClient.os.openExternal.mutate({ url }); @@ -99,20 +117,7 @@ export function useGithubUserConnect({ projectId }: Options): Result { }, []); const invalidate = useCallback( - (pid: number | null) => { - if (pid !== null) { - void queryClient.invalidateQueries({ - queryKey: ["integrations", pid], - }); - } - void queryClient.invalidateQueries({ - queryKey: ["integrations", "list"], - }); - void queryClient.invalidateQueries({ - queryKey: ["user-github-integrations"], - }); - void queryClient.invalidateQueries({ queryKey: ["github_login"] }); - }, + (pid: number | null) => invalidateGithubQueries(queryClient, pid), [queryClient], ); diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx index d09600ee97..36263396df 100644 --- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx @@ -1,8 +1,10 @@ +import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useSelectProjectMutation } from "@features/auth/hooks/authMutations"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; import { describeGithubConnectError, + invalidateGithubQueries, useGithubUserConnect, } from "@features/integrations/hooks/useGithubUserConnect"; import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; @@ -22,18 +24,21 @@ import { GitBranch, } from "@phosphor-icons/react"; import { + AlertDialog, Box, Button, DropdownMenu, Flex, Skeleton, + Spinner, Text, } from "@radix-ui/themes"; import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png"; import { trpcClient } from "@renderer/trpc/client"; -import { useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AnimatePresence, motion } from "framer-motion"; import { useMemo, useState } from "react"; +import { toast } from "sonner"; import type { DetectedRepo } from "../hooks/useOnboardingFlow"; import { useProjectsWithIntegrations } from "../hooks/useProjectsWithIntegrations"; import { OnboardingHogTip } from "./OnboardingHogTip"; @@ -141,6 +146,28 @@ export function GitIntegrationStep({ ); }, [detectedRepo, repositories]); + const apiClient = useOptionalAuthenticatedClient(); + const [disconnectConfirmOpen, setDisconnectConfirmOpen] = useState(false); + const disconnectMutation = useMutation({ + mutationFn: async () => { + const installationId = githubIntegration?.installation_id; + if (!apiClient || !installationId) { + throw new Error("No GitHub integration to disconnect"); + } + await apiClient.disconnectGithubUserIntegration(installationId); + }, + onSuccess: () => { + setDisconnectConfirmOpen(false); + invalidateGithubQueries(queryClient, selectedProjectId); + toast.success("GitHub disconnected."); + }, + onError: (e) => { + toast.error( + e instanceof Error ? e.message : "Failed to disconnect GitHub.", + ); + }, + }); + const handleContinue = () => { if (selectedProjectId && selectedProjectId !== currentProjectId) { selectProjectMutation.mutate(selectedProjectId); @@ -369,6 +396,15 @@ export function GitIntegrationStep({ Refresh + ) : !isLoading && !githubUserIntegrationsLoading ? ( @@ -529,6 +565,45 @@ export function GitIntegrationStep({ + + { + if (!next && !disconnectMutation.isPending) { + setDisconnectConfirmOpen(false); + } + }} + > + + Disconnect GitHub + + This removes your personal GitHub authorization from PostHog. You + can reconnect at any time. The GitHub App itself stays installed + in your org — uninstall it on GitHub if you want to remove that + too. + + + + + + + + + );