diff --git a/packages/template/src/components-page/oauth-callback.test.tsx b/packages/template/src/components-page/oauth-callback.test.tsx index 072b7fdf5..4b5e75dc5 100644 --- a/packages/template/src/components-page/oauth-callback.test.tsx +++ b/packages/template/src/components-page/oauth-callback.test.tsx @@ -5,6 +5,7 @@ import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { StackClientApp } from "../lib/hexclave-app/apps/interfaces/client-app"; +import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/common"; import { TranslationProviderClient } from "../providers/translation-provider-client"; import { OAuthCallback } from "./oauth-callback"; @@ -40,6 +41,9 @@ function createAppTestDouble(options: { callOAuthCallback: options.callOAuthCallback, redirectToSignIn: vi.fn(async () => {}), redirectToHome: vi.fn(async () => {}), + [hexclaveAppInternalsSymbol]: { + awaitPendingAuthResolutions: vi.fn(async () => {}), + }, }; // This test double intentionally implements only the StackClientApp surface diff --git a/packages/template/src/components-page/oauth-callback.tsx b/packages/template/src/components-page/oauth-callback.tsx index 7b19d635a..56d512e81 100644 --- a/packages/template/src/components-page/oauth-callback.tsx +++ b/packages/template/src/components-page/oauth-callback.tsx @@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react"; import { useStackApp } from ".."; import { MaybeFullPage } from "../components/elements/maybe-full-page"; import { StyledLink } from "../components/link"; +import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/common"; import { useTranslation } from "../lib/translations"; import { ErrorPage } from "./error-page"; @@ -22,6 +23,11 @@ export function OAuthCallback({ fullPage }: { fullPage?: boolean }) { if (called.current) return; called.current = true; try { + // The startup handler in StackClientApp's constructor may have already consumed the + // one-time OAuth params (code + state cookie) via a microtask that fires before this + // macrotask-scheduled useEffect. Await its completion so we don't race: if it succeeds + // it will redirect and this page tears down; if it fails we fall through below. + await app[hexclaveAppInternalsSymbol].awaitPendingAuthResolutions(); const hasRedirected = await app.callOAuthCallback(); if (!hasRedirected) { await app.redirectToSignIn({ noRedirectBack: true }); diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts index c20bd6ff7..e7e4e7fb5 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts @@ -4204,6 +4204,9 @@ export class _HexclaveClientAppImplIncomplete { await this._signInToAccountWithTokens(tokens); }, + awaitPendingAuthResolutions: async () => { + await this._awaitPendingAuthResolutions(); + }, }; }; diff --git a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts index 4f15aa325..53d75d20e 100644 --- a/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts +++ b/packages/template/src/lib/hexclave-app/apps/interfaces/client-app.ts @@ -136,6 +136,7 @@ export type StackClientApp, redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise, signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise, + awaitPendingAuthResolutions(): Promise, }, } & AsyncStoreProperty<"project", [], Project, false>