diff --git a/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx
index 67c9225753..fc9e687159 100644
--- a/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx
+++ b/apps/code/src/renderer/features/inbox/components/list/GitHubConnectionBanner.tsx
@@ -1,6 +1,9 @@
import { Button } from "@components/ui/Button";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
-import { useGithubUserConnect } from "@features/integrations/hooks/useGithubUserConnect";
+import {
+ describeGithubConnectError,
+ useGithubUserConnect,
+} from "@features/integrations/hooks/useGithubUserConnect";
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
import { useUserRepositoryIntegration } from "@hooks/useIntegrations";
import {
@@ -21,8 +24,9 @@ export function GitHubConnectionBanner() {
const projectId = useAuthStateValue((s) => s.projectId);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
- const { state, connect } = useGithubUserConnect({ projectId });
+ const { state, error, connect, reset } = useGithubUserConnect({ projectId });
const connecting = state === "connecting";
+ const hasConnectError = state === "error";
const canConnectCloud = projectId != null && cloudRegion != null;
if (loginLoading) {
@@ -37,11 +41,13 @@ export function GitHubConnectionBanner() {
return null;
}
- const label = connecting
- ? "Waiting for GitHub connection to complete in browser…"
- : hasGithubForProject
- ? "Connect your GitHub profile to highlight what's relevant to you"
- : "Connect your GitHub repo(s) to highlight what's relevant to you";
+ const label = hasConnectError
+ ? describeGithubConnectError(error)
+ : connecting
+ ? "Waiting for GitHub connection to complete in browser…"
+ : hasGithubForProject
+ ? "Connect your GitHub profile to highlight what's relevant to you"
+ : "Connect your GitHub repo(s) to highlight what's relevant to you";
return (
@@ -77,6 +83,7 @@ export function GitHubConnectionBanner() {
}
onClick={() => {
if (!canConnectCloud) return;
+ if (hasConnectError) reset();
void connect();
}}
>
diff --git a/apps/code/src/renderer/features/integrations/hooks/useGitHubIntegrationCallback.ts b/apps/code/src/renderer/features/integrations/hooks/useGitHubIntegrationCallback.ts
index 7c9910af49..c0cb20a935 100644
--- a/apps/code/src/renderer/features/integrations/hooks/useGitHubIntegrationCallback.ts
+++ b/apps/code/src/renderer/features/integrations/hooks/useGitHubIntegrationCallback.ts
@@ -8,9 +8,14 @@ const log = logger.scope("github-integration-callback-hook");
const DEFAULT_ERROR_MESSAGE =
"GitHub install failed. Please try connecting again.";
+export interface IntegrationCallbackError {
+ message: string;
+ code: string | null;
+}
+
interface Options {
onSuccess: (projectId: number | null) => void;
- onError: (message: string) => void;
+ onError: (error: IntegrationCallbackError) => void;
onTimedOut?: () => void;
}
@@ -34,7 +39,10 @@ export function useGitHubIntegrationCallback({
onData: (data) => {
log.info("Received integration deep link callback", data);
if (data.status === "error") {
- optsRef.current.onError(data.errorMessage ?? DEFAULT_ERROR_MESSAGE);
+ optsRef.current.onError({
+ message: data.errorMessage ?? DEFAULT_ERROR_MESSAGE,
+ code: data.errorCode,
+ });
return;
}
optsRef.current.onSuccess(data.projectId);
@@ -61,9 +69,10 @@ export function useGitHubIntegrationCallback({
if (!pending) return;
log.info("Consumed pending integration callback on mount", pending);
if (pending.status === "error") {
- optsRef.current.onError(
- pending.errorMessage ?? DEFAULT_ERROR_MESSAGE,
- );
+ optsRef.current.onError({
+ message: pending.errorMessage ?? DEFAULT_ERROR_MESSAGE,
+ code: pending.errorCode,
+ });
return;
}
optsRef.current.onSuccess(pending.projectId);
diff --git a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts
index 7314d63f1b..6023efe3fd 100644
--- a/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts
+++ b/apps/code/src/renderer/features/integrations/hooks/useGithubUserConnect.ts
@@ -5,12 +5,57 @@ import { trpcClient } from "@renderer/trpc/client";
import { IS_DEV } from "@shared/constants/environment";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
-import { toast } from "sonner";
const POLL_INTERVAL_MS = 3_000;
const POLL_TIMEOUT_MS = 300_000;
-export type GithubUserConnectState = "idle" | "connecting" | "timed-out";
+export type GithubUserConnectState =
+ | "idle"
+ | "connecting"
+ | "timed-out"
+ | "error";
+
+export interface GithubUserConnectError {
+ message: string;
+ code: string | null;
+}
+
+const ERROR_MESSAGES: Record
= {
+ access_denied:
+ "You declined access on GitHub. Try again to grant the permissions PostHog Code needs.",
+ github_oauth_error: "GitHub returned an error during sign-in. Please retry.",
+ missing_params: "GitHub returned an incomplete response. Please retry.",
+ invalid_state:
+ "The connection link expired before you finished. Please retry.",
+ invalid_installation:
+ "This GitHub installation isn't reachable from your account. Try a different account or org.",
+ invalid_team:
+ "Your project access changed during sign-in. Please retry from the current project.",
+ invalid_installation_id:
+ "GitHub returned an invalid installation. Please retry.",
+ exchange_failed:
+ "Couldn't exchange the GitHub authorization code. Please retry.",
+ installation_verify_failed:
+ "Couldn't verify your access to this GitHub installation. Please retry.",
+ installation_not_authorized:
+ "Your GitHub account isn't authorized for this installation. Ask the org admin to grant access, or sign in with a different GitHub account.",
+ installation_fetch_failed:
+ "Couldn't fetch installation details from GitHub. Please retry.",
+ installation_token_failed:
+ "Couldn't get an access token from GitHub. Please retry.",
+ integration_create_failed:
+ "Couldn't save the GitHub connection. Please retry.",
+};
+
+export function describeGithubConnectError(
+ error: GithubUserConnectError | null,
+): string {
+ if (!error) return "";
+ if (error.code && ERROR_MESSAGES[error.code]) {
+ return ERROR_MESSAGES[error.code];
+ }
+ return error.message;
+}
interface Options {
projectId: number | null;
@@ -18,6 +63,7 @@ interface Options {
interface Result {
state: GithubUserConnectState;
+ error: GithubUserConnectError | null;
connect: () => Promise;
reset: () => void;
}
@@ -35,6 +81,7 @@ export function useGithubUserConnect({ projectId }: Options): Result {
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const queryClient = useQueryClient();
const [state, setState] = useState("idle");
+ const [error, setError] = useState(null);
const stateRef = useRef(state);
stateRef.current = state;
const pollTimerRef = useRef | null>(null);
@@ -84,12 +131,13 @@ export function useGithubUserConnect({ projectId }: Options): Result {
onSuccess: (callbackProjectId) => {
stopPolling();
setState("idle");
+ setError(null);
invalidate(callbackProjectId ?? projectId);
},
- onError: (message) => {
+ onError: (cbError) => {
stopPolling();
- setState("idle");
- toast.error(message);
+ setState("error");
+ setError(cbError);
},
onTimedOut: () => {
stopPolling();
@@ -99,9 +147,10 @@ export function useGithubUserConnect({ projectId }: Options): Result {
});
const connect = useCallback(async () => {
- if (stateRef.current !== "idle") return;
+ if (stateRef.current === "connecting") return;
if (!cloudRegion || projectId === null || !client) return;
stopPolling();
+ setError(null);
setState("connecting");
try {
const res = await client.startGithubUserIntegrationConnect(projectId);
@@ -122,20 +171,21 @@ export function useGithubUserConnect({ projectId }: Options): Result {
stopPolling();
setState("timed-out");
}, POLL_TIMEOUT_MS);
- } catch (error) {
- setState("idle");
- toast.error(
- error instanceof Error
- ? error.message
- : "Failed to start GitHub connection",
- );
+ } catch (e) {
+ setState("error");
+ setError({
+ message:
+ e instanceof Error ? e.message : "Failed to start GitHub connection",
+ code: null,
+ });
}
}, [client, cloudRegion, projectId, invalidate, stopPolling]);
const reset = useCallback(() => {
stopPolling();
+ setError(null);
setState("idle");
}, [stopPolling]);
- return { state, connect, reset };
+ return { state, error, connect, reset };
}
diff --git a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
index ed5b787089..d09600ee97 100644
--- a/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
+++ b/apps/code/src/renderer/features/onboarding/components/GitIntegrationStep.tsx
@@ -1,7 +1,10 @@
import { useSelectProjectMutation } from "@features/auth/hooks/authMutations";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
-import { useGithubUserConnect } from "@features/integrations/hooks/useGithubUserConnect";
+import {
+ describeGithubConnectError,
+ useGithubUserConnect,
+} from "@features/integrations/hooks/useGithubUserConnect";
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import {
useUserGithubIntegrations,
@@ -74,11 +77,23 @@ export function GitIntegrationStep({
return currentProjectId ?? projects[0]?.id ?? null;
}, [manuallySelectedProjectId, currentProjectId, projects]);
- const { state: connectState, connect: handleConnectGitHub } =
- useGithubUserConnect({ projectId: selectedProjectId });
+ const {
+ state: connectState,
+ error: connectError,
+ connect: handleConnectGitHub,
+ reset: resetConnect,
+ } = useGithubUserConnect({ projectId: selectedProjectId });
const isConnecting = connectState === "connecting";
const timedOut = connectState === "timed-out";
- const canTakeAction = !isConnecting && !timedOut;
+ const hasConnectError = connectState === "error";
+ const canTakeAction = !isConnecting && !timedOut && !hasConnectError;
+ const defaultPanelMessage = hasConnectError
+ ? describeGithubConnectError(connectError)
+ : timedOut
+ ? "We didn't hear back from GitHub. If the browser tab was closed, click Connect again."
+ : isConnecting
+ ? "Waiting for GitHub..."
+ : "Optional. Unlocks cloud agents and pull request workflows.";
const selectedProject = useMemo(
() => projects.find((p) => p.id === selectedProjectId),
@@ -446,25 +461,43 @@ export function GitIntegrationStep({
) : (
-
- {timedOut
- ? "We didn't hear back from GitHub. If the browser tab was closed, click Connect again."
- : isConnecting
- ? "Waiting for GitHub..."
- : "Optional. Unlocks cloud agents and pull request workflows."}
-
-
+ {defaultPanelMessage}
+
+
+
+ {hasConnectError && (
+
+ )}
+
)
) : null}
diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx
index 7b498abbd3..93639605c7 100644
--- a/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx
+++ b/apps/code/src/renderer/features/settings/components/sections/GitHubIntegrationSection.tsx
@@ -74,7 +74,7 @@ export function GitHubIntegrationSection({
setConnecting(false);
invalidateIntegrations();
},
- onError: (message) => {
+ onError: ({ message }) => {
stopPolling();
setConnecting(false);
toast.error(message);