From 0be7dbb03149445735f248d3b71e974d3236fa88 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:32:58 -0400 Subject: [PATCH 1/2] Improve GitHub App authorization status UX --- .../github/GitHubAppInstallPanel.test.ts | 47 ++++++++++++ .../github/GitHubAppInstallPanel.tsx | 72 +++++++++++++++++-- .../onboarding-and-settings/README.md | 10 ++- 3 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.test.ts diff --git a/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.test.ts b/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.test.ts new file mode 100644 index 000000000..36b3a1204 --- /dev/null +++ b/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import type { GitHubAppInstallationStatus } from "../../../shared/types"; +import { isGitHubAppRepoAccessPending, statusView } from "./GitHubAppInstallPanel"; + +function makeStatus(overrides: Partial = {}): GitHubAppInstallationStatus { + return { + repo: { owner: "arul28", name: "ADE" }, + appName: "ADE", + appSlug: "ade-for-github", + installUrl: "https://github.com/apps/ade-for-github/installations/new", + manageUrl: "https://github.com/settings/installations", + relayConfigured: true, + installed: false, + state: "error", + installationId: null, + repositorySelection: null, + lastSeenAt: null, + webhookEvents: [], + missingWebhookEvents: [], + webhookState: "unknown", + webhookLastSeenAt: null, + checkedAt: "2026-07-02T18:39:42.000Z", + error: "Not Found", + ...overrides, + }; +} + +describe("GitHubAppInstallPanel status helpers", () => { + it("treats post-authorization GitHub repo 404s as pending repo access", () => { + const status = makeStatus(); + const view = statusView(status, false, true); + + expect(isGitHubAppRepoAccessPending(status)).toBe(true); + expect(view.label).toBe("Checking access"); + expect(view.description("arul28/ADE")).toContain("GitHub accepted authorization"); + expect(view.description("arul28/ADE")).toContain("select this repo in GitHub"); + }); + + it("keeps non-propagation relay failures as check failures after authorization", () => { + const status = makeStatus({ error: "GitHub App relay status check failed (500)" }); + const view = statusView(status, false, true); + + expect(isGitHubAppRepoAccessPending(status)).toBe(false); + expect(view.label).toBe("Check failed"); + expect(view.description("arul28/ADE")).toBe("GitHub App relay status check failed (500)"); + }); +}); diff --git a/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx b/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx index 8044adff2..4d6b537ee 100644 --- a/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx +++ b/apps/desktop/src/renderer/components/github/GitHubAppInstallPanel.tsx @@ -12,6 +12,7 @@ import { COLORS, MONO_FONT, SANS_FONT, cardStyle, inlineBadge, outlineButton, pr const ADE_GITHUB_APP_NAME = "ADE"; const ADE_GITHUB_APP_INSTALL_URL = "https://github.com/apps/ade-for-github/installations/new"; const GITHUB_APP_INSTALLATIONS_URL = "https://github.com/settings/installations"; +const POST_AUTH_STATUS_RETRY_DELAYS_MS = [1_500, 3_000, 6_000] as const; type GitHubAppInstallPanelProps = { variant?: "settings" | "onboarding"; @@ -29,14 +30,35 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall const autoRenewCountRef = useRef(0); const copyFeedbackTimeoutRef = useRef(null); const appAuthRef = useRef(null); + const statusRequestSeqRef = useRef(0); + const mountedRef = useRef(true); appAuthRef.current = appAuth; - const loadStatus = useCallback(async (forceRefresh = false) => { + const loadStatus = useCallback(async (forceRefresh = false, opts: { retryAfterAuthorization?: boolean } = {}) => { if (!window.ade?.github?.getAppInstallationStatus) return; + const requestSeq = statusRequestSeqRef.current + 1; + statusRequestSeqRef.current = requestSeq; setLoading(true); + let latestStatus: GitHubAppInstallationStatus | null = null; try { - setStatus(await window.ade.github.getAppInstallationStatus({ forceRefresh })); + const attemptCount = opts.retryAfterAuthorization + ? POST_AUTH_STATUS_RETRY_DELAYS_MS.length + 1 + : 1; + for (let attempt = 0; attempt < attemptCount; attempt += 1) { + latestStatus = await window.ade.github.getAppInstallationStatus({ + forceRefresh: forceRefresh || attempt > 0, + }); + if (!mountedRef.current || statusRequestSeqRef.current !== requestSeq) return; + setStatus(latestStatus); + if (!opts.retryAfterAuthorization || !isGitHubAppRepoAccessPending(latestStatus)) break; + const retryDelay = POST_AUTH_STATUS_RETRY_DELAYS_MS[attempt]; + if (retryDelay == null) break; + setDeviceMessage("GitHub authorization is complete. Waiting for repository access to appear..."); + await sleepMs(retryDelay); + if (!mountedRef.current || statusRequestSeqRef.current !== requestSeq) return; + } } catch (error) { + if (!mountedRef.current || statusRequestSeqRef.current !== requestSeq) return; setStatus({ repo: null, appName: ADE_GITHUB_APP_NAME, @@ -57,11 +79,20 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall error: error instanceof Error ? error.message : String(error), }); } finally { + if (!mountedRef.current || statusRequestSeqRef.current !== requestSeq) return; // Read auth state AFTER the status call (success or failure): an // expired stored token can be cleared during the status check, and the // panel must reflect that immediately. const authStatus = await window.ade.github.getAppUserAuthStatus?.().catch(() => null); + if (!mountedRef.current || statusRequestSeqRef.current !== requestSeq) return; setAppAuth(authStatus ?? null); + if (opts.retryAfterAuthorization) { + setDeviceMessage( + latestStatus && isGitHubAppRepoAccessPending(latestStatus) + ? "GitHub authorization is complete. Repository access is still warming up; use Refresh again in a moment." + : null, + ); + } setLoading(false); } }, []); @@ -145,7 +176,8 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall setDeviceMessage(result.message); if (result.status === "authorized") { autoRenewCountRef.current = 0; - await loadStatus(true); + setDeviceMessage("GitHub authorization is complete. Checking repository access..."); + await loadStatus(true, { retryAfterAuthorization: true }); } }, Math.max(1, deviceSession.intervalSec) * 1000); return () => { @@ -159,7 +191,10 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall }, [deviceSession?.sessionId]); useEffect(() => { + mountedRef.current = true; return () => { + mountedRef.current = false; + statusRequestSeqRef.current += 1; if (copyFeedbackTimeoutRef.current != null) { window.clearTimeout(copyFeedbackTimeoutRef.current); } @@ -171,6 +206,7 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall }, [loadStatus]); const appAuthorized = appAuth?.tokenStored === true; + const repoAccessPending = appAuthorized && isGitHubAppRepoAccessPending(status); const view = statusView(status, loading, appAuthorized); const repoLabel = status?.repo ? `${status.repo.owner}/${status.repo.name}` : null; @@ -221,7 +257,7 @@ export function GitHubAppInstallPanel({ variant = "settings" }: GitHubAppInstall {!appAuthorized && !deviceSession ?

One-time GitHub sign-off that lets ADE verify your repo access for instant PR updates.

: null}
- {!appAuthorized || status?.state === "error" ? ( + {!appAuthorized || (status?.state === "error" && !repoAccessPending) ? (