From 1c54fb51e8721efb199a3d45cbcda04bb2d4fc8a Mon Sep 17 00:00:00 2001 From: gnustella-lab <277467474+gnustella-lab@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:08:04 -0300 Subject: [PATCH 1/3] fix(web): retry legacy Connect auth backfill --- apps/web/src/cloud/connectAuth.tsx | 6 ++-- apps/web/src/cloud/connectAuthState.test.ts | 32 +++++++++++++++++++++ apps/web/src/cloud/connectAuthState.ts | 10 +++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/cloud/connectAuthState.test.ts create mode 100644 apps/web/src/cloud/connectAuthState.ts diff --git a/apps/web/src/cloud/connectAuth.tsx b/apps/web/src/cloud/connectAuth.tsx index 5ad0fb911248..b8f342451bae 100644 --- a/apps/web/src/cloud/connectAuth.tsx +++ b/apps/web/src/cloud/connectAuth.tsx @@ -20,6 +20,7 @@ import { resolveClerkSignInProps } from "../components/clerk/authRedirect"; import { PrimaryEnvironmentHttpClient } from "../environments/primary/httpClient"; import { runPrimaryHttp } from "../lib/runtime"; import { resolveRelayClerkTokenOptions } from "./publicConfig"; +import { shouldRetryDesktopConnectAuthState } from "./connectAuthState"; /** * One T3 Connect session surface across auth backends. The hosted web app @@ -191,11 +192,12 @@ export function DesktopConnectAuthProvider({ children }: { readonly children: Re // The bundled server may still be starting when the app mounts; retry until // the first state read lands. const isLoaded = state !== null; + const shouldRetryAuthState = shouldRetryDesktopConnectAuthState(state); useEffect(() => { - if (isLoaded) return; + if (!shouldRetryAuthState) return; const interval = setInterval(() => void refresh(), 3_000); return () => clearInterval(interval); - }, [isLoaded, refresh]); + }, [refresh, shouldRetryAuthState]); // The credential is shared with `t3 connect`, so a CLI sign-in or logout // can change it while the app is open; re-read when the window regains diff --git a/apps/web/src/cloud/connectAuthState.test.ts b/apps/web/src/cloud/connectAuthState.test.ts new file mode 100644 index 000000000000..fa5c44328620 --- /dev/null +++ b/apps/web/src/cloud/connectAuthState.test.ts @@ -0,0 +1,32 @@ +import type { EnvironmentConnectAuthState } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { shouldRetryDesktopConnectAuthState } from "./connectAuthState"; + +const authState = ( + overrides: Partial = {}, +): EnvironmentConnectAuthState => ({ + authorized: false, + pendingLogin: false, + authorizationUrl: null, + accountId: null, + identity: null, + ...overrides, +}); + +describe("desktop Connect auth state retry", () => { + it("retries before the first auth state response arrives", () => { + expect(shouldRetryDesktopConnectAuthState(null)).toBe(true); + }); + + it("retries an authorized legacy credential until its account id is backfilled", () => { + expect(shouldRetryDesktopConnectAuthState(authState({ authorized: true }))).toBe(true); + }); + + it("stops retrying once the state is stable", () => { + expect(shouldRetryDesktopConnectAuthState(authState())).toBe(false); + expect( + shouldRetryDesktopConnectAuthState(authState({ authorized: true, accountId: "user-123" })), + ).toBe(false); + }); +}); diff --git a/apps/web/src/cloud/connectAuthState.ts b/apps/web/src/cloud/connectAuthState.ts new file mode 100644 index 000000000000..d99fb656fa68 --- /dev/null +++ b/apps/web/src/cloud/connectAuthState.ts @@ -0,0 +1,10 @@ +import type { EnvironmentConnectAuthState } from "@t3tools/contracts"; + +export function shouldRetryDesktopConnectAuthState( + state: EnvironmentConnectAuthState | null, +): boolean { + // Legacy CLI credentials are authorized before the server lazily backfills + // their Clerk account id. Keep polling so the desktop can reach a usable + // signed-in state without requiring a focus change or another login. + return state === null || (state.authorized && state.accountId === null); +} From 2e129edacb22645c2ea722d34004a962f4b6d13b Mon Sep 17 00:00:00 2001 From: gnustella-lab <277467474+gnustella-lab@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:55:51 -0300 Subject: [PATCH 2/3] fix(web): avoid onboarding during identity backfill --- apps/web/src/cloud/connectAuth.tsx | 10 +++++++++- apps/web/src/cloud/connectAuthState.test.ts | 13 ++++++++++++- apps/web/src/cloud/connectAuthState.ts | 8 +++++++- .../components/cloud/ConnectOnboardingDialog.tsx | 8 +++++--- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/web/src/cloud/connectAuth.tsx b/apps/web/src/cloud/connectAuth.tsx index b8f342451bae..0f21fcf6729c 100644 --- a/apps/web/src/cloud/connectAuth.tsx +++ b/apps/web/src/cloud/connectAuth.tsx @@ -20,7 +20,10 @@ import { resolveClerkSignInProps } from "../components/clerk/authRedirect"; import { PrimaryEnvironmentHttpClient } from "../environments/primary/httpClient"; import { runPrimaryHttp } from "../lib/runtime"; import { resolveRelayClerkTokenOptions } from "./publicConfig"; -import { shouldRetryDesktopConnectAuthState } from "./connectAuthState"; +import { + isDesktopConnectAuthIdentityPending, + shouldRetryDesktopConnectAuthState, +} from "./connectAuthState"; /** * One T3 Connect session surface across auth backends. The hosted web app @@ -30,6 +33,8 @@ import { shouldRetryDesktopConnectAuthState } from "./connectAuthState"; */ export interface T3ConnectAuth { readonly isLoaded: boolean; + /** A legacy desktop credential is waiting for its account id backfill. */ + readonly isIdentityPending: boolean; readonly isSignedIn: boolean; readonly userId: string | null; /** Account label for display (desktop; web renders Clerk's UserButton). */ @@ -53,6 +58,7 @@ export interface T3ConnectAuth { // gates itself on hasCloudPublicConfig, so this is just a safe floor. const signedOutAuth: T3ConnectAuth = { isLoaded: true, + isIdentityPending: false, isSignedIn: false, userId: null, identity: null, @@ -78,6 +84,7 @@ export function ClerkConnectAuthProvider({ children }: { readonly children: Reac const value = useMemo( () => ({ isLoaded, + isIdentityPending: false, isSignedIn: isSignedIn === true, userId: userId ?? null, identity: null, @@ -268,6 +275,7 @@ export function DesktopConnectAuthProvider({ children }: { readonly children: Re const value = useMemo( () => ({ isLoaded, + isIdentityPending: isDesktopConnectAuthIdentityPending(state), // accountId can lag behind authorization for legacy `t3 connect` // credentials while the server backfills it; relay features need the // account id, so hold "signed in" until it resolves. diff --git a/apps/web/src/cloud/connectAuthState.test.ts b/apps/web/src/cloud/connectAuthState.test.ts index fa5c44328620..d5640c2939a0 100644 --- a/apps/web/src/cloud/connectAuthState.test.ts +++ b/apps/web/src/cloud/connectAuthState.test.ts @@ -1,7 +1,10 @@ import type { EnvironmentConnectAuthState } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; -import { shouldRetryDesktopConnectAuthState } from "./connectAuthState"; +import { + isDesktopConnectAuthIdentityPending, + shouldRetryDesktopConnectAuthState, +} from "./connectAuthState"; const authState = ( overrides: Partial = {}, @@ -23,6 +26,14 @@ describe("desktop Connect auth state retry", () => { expect(shouldRetryDesktopConnectAuthState(authState({ authorized: true }))).toBe(true); }); + it("marks legacy identity backfill without changing stable load states", () => { + expect(isDesktopConnectAuthIdentityPending(authState({ authorized: true }))).toBe(true); + expect(isDesktopConnectAuthIdentityPending(authState())).toBe(false); + expect( + isDesktopConnectAuthIdentityPending(authState({ authorized: true, accountId: "user-123" })), + ).toBe(false); + }); + it("stops retrying once the state is stable", () => { expect(shouldRetryDesktopConnectAuthState(authState())).toBe(false); expect( diff --git a/apps/web/src/cloud/connectAuthState.ts b/apps/web/src/cloud/connectAuthState.ts index d99fb656fa68..c4933b7b56ed 100644 --- a/apps/web/src/cloud/connectAuthState.ts +++ b/apps/web/src/cloud/connectAuthState.ts @@ -1,10 +1,16 @@ import type { EnvironmentConnectAuthState } from "@t3tools/contracts"; +export function isDesktopConnectAuthIdentityPending( + state: EnvironmentConnectAuthState | null, +): boolean { + return state?.authorized === true && state.accountId === null; +} + export function shouldRetryDesktopConnectAuthState( state: EnvironmentConnectAuthState | null, ): boolean { // Legacy CLI credentials are authorized before the server lazily backfills // their Clerk account id. Keep polling so the desktop can reach a usable // signed-in state without requiring a focus change or another login. - return state === null || (state.authorized && state.accountId === null); + return state === null || isDesktopConnectAuthIdentityPending(state); } diff --git a/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx b/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx index 2c348c6af727..333999c5d47b 100644 --- a/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx +++ b/apps/web/src/components/cloud/ConnectOnboardingDialog.tsx @@ -47,7 +47,7 @@ export function ConnectOnboardingDialog() { type OnboardingStep = "publish" | "devices"; function ConfiguredConnectOnboardingDialog() { - const { isLoaded, isSignedIn, userId } = useT3ConnectAuth(); + const { isIdentityPending, isLoaded, isSignedIn, userId } = useT3ConnectAuth(); const [optOutState, setOptOutState] = useLocalStorage( CONNECT_ONBOARDING_OPT_OUT_STORAGE_KEY, EMPTY_CONNECT_ONBOARDING_OPT_OUT_STATE, @@ -94,7 +94,9 @@ function ConfiguredConnectOnboardingDialog() { // environments, so each new session starts with no devices to reach. A cold // load observes undefined → account and must not re-prompt. useEffect(() => { - if (!isLoaded) return; + // A legacy credential is authorized before its account id is backfilled; + // do not record that incomplete snapshot as a signed-out cold load. + if (!isLoaded || isIdentityPending) return; // A loaded-but-incomplete snapshot (signed in, user id not yet populated) // must not be recorded as signed-out — the next render would then look // like a fresh sign-in on a cold load. @@ -105,7 +107,7 @@ function ConfiguredConnectOnboardingDialog() { if (previousAccount !== undefined && previousAccount !== nextAccount && nextAccount !== null) { setRequestedAccount(nextAccount); } - }, [isLoaded, isSignedIn, userId]); + }, [isIdentityPending, isLoaded, isSignedIn, userId]); // A manageable session implies a primary environment, so when the scopes // allow publishing, wait for the connection target too — otherwise the From 6998c9e9d108318e19a3ffc69e26a686ba08a460 Mon Sep 17 00:00:00 2001 From: gnustella-lab <277467474+gnustella-lab@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:16:09 -0300 Subject: [PATCH 3/3] fix(web): serialize Connect auth polling --- apps/web/src/cloud/connectAuth.tsx | 7 ++-- apps/web/src/cloud/connectAuthState.test.ts | 37 ++++++++++++++++++++- apps/web/src/cloud/connectAuthState.ts | 20 +++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/apps/web/src/cloud/connectAuth.tsx b/apps/web/src/cloud/connectAuth.tsx index 0f21fcf6729c..9c74b80d64df 100644 --- a/apps/web/src/cloud/connectAuth.tsx +++ b/apps/web/src/cloud/connectAuth.tsx @@ -23,6 +23,7 @@ import { resolveRelayClerkTokenOptions } from "./publicConfig"; import { isDesktopConnectAuthIdentityPending, shouldRetryDesktopConnectAuthState, + startSettledPolling, } from "./connectAuthState"; /** @@ -202,8 +203,7 @@ export function DesktopConnectAuthProvider({ children }: { readonly children: Re const shouldRetryAuthState = shouldRetryDesktopConnectAuthState(state); useEffect(() => { if (!shouldRetryAuthState) return; - const interval = setInterval(() => void refresh(), 3_000); - return () => clearInterval(interval); + return startSettledPolling(refresh, 3_000); }, [refresh, shouldRetryAuthState]); // The credential is shared with `t3 connect`, so a CLI sign-in or logout @@ -220,8 +220,7 @@ export function DesktopConnectAuthProvider({ children }: { readonly children: Re const pendingLogin = state?.pendingLogin ?? false; useEffect(() => { if (!pendingLogin) return; - const interval = setInterval(() => void refresh(), LOGIN_WATCH_INTERVAL_MS); - return () => clearInterval(interval); + return startSettledPolling(refresh, LOGIN_WATCH_INTERVAL_MS); }, [pendingLogin, refresh]); const getToken = useCallback(async () => { diff --git a/apps/web/src/cloud/connectAuthState.test.ts b/apps/web/src/cloud/connectAuthState.test.ts index d5640c2939a0..31fa87081ad4 100644 --- a/apps/web/src/cloud/connectAuthState.test.ts +++ b/apps/web/src/cloud/connectAuthState.test.ts @@ -1,9 +1,10 @@ import type { EnvironmentConnectAuthState } from "@t3tools/contracts"; -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { isDesktopConnectAuthIdentityPending, shouldRetryDesktopConnectAuthState, + startSettledPolling, } from "./connectAuthState"; const authState = ( @@ -34,6 +35,40 @@ describe("desktop Connect auth state retry", () => { ).toBe(false); }); + it("waits for each refresh to settle before scheduling another poll", async () => { + vi.useFakeTimers(); + try { + let releaseFirst: (() => void) | undefined; + let refreshCount = 0; + const refresh = vi.fn(() => { + refreshCount += 1; + if (refreshCount === 1) { + return new Promise((resolve) => { + releaseFirst = resolve; + }); + } + return Promise.resolve(); + }); + const stop = startSettledPolling(refresh, 3_000); + + await vi.advanceTimersByTimeAsync(3_000); + expect(refresh).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(3_000); + expect(refresh).toHaveBeenCalledTimes(1); + + releaseFirst?.(); + await vi.advanceTimersByTimeAsync(0); + await vi.advanceTimersByTimeAsync(2_999); + expect(refresh).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + expect(refresh).toHaveBeenCalledTimes(2); + + stop(); + } finally { + vi.useRealTimers(); + } + }); + it("stops retrying once the state is stable", () => { expect(shouldRetryDesktopConnectAuthState(authState())).toBe(false); expect( diff --git a/apps/web/src/cloud/connectAuthState.ts b/apps/web/src/cloud/connectAuthState.ts index c4933b7b56ed..fff66aebdf5e 100644 --- a/apps/web/src/cloud/connectAuthState.ts +++ b/apps/web/src/cloud/connectAuthState.ts @@ -6,6 +6,26 @@ export function isDesktopConnectAuthIdentityPending( return state?.authorized === true && state.accountId === null; } +export function startSettledPolling(task: () => Promise, intervalMs: number): () => void { + let cancelled = false; + let timeout: ReturnType | undefined; + + const poll = async () => { + await task(); + if (!cancelled) { + timeout = setTimeout(poll, intervalMs); + } + }; + + timeout = setTimeout(poll, intervalMs); + return () => { + cancelled = true; + if (timeout !== undefined) { + clearTimeout(timeout); + } + }; +} + export function shouldRetryDesktopConnectAuthState( state: EnvironmentConnectAuthState | null, ): boolean {