From b475c797aa780b0944673c40d66701fae8438146 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:25:16 +1100 Subject: [PATCH 1/3] Allow sign-in after provider discovery times out --- .../web/src/components/CloudLibrary.tsx | 7 +++-- src/components/auth/RecordlySignInDialog.tsx | 8 ++--- src/components/auth/RecordlySignInFields.tsx | 5 +-- tests/ui/login.spec.ts | 31 +++++++++++++++++++ tests/ui/onboarding-startup.spec.ts | 5 ++- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx index df9aff8cd..a2fa58aad 100644 --- a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx +++ b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx @@ -5,6 +5,7 @@ import LibraryPage from "./LibraryPage"; import { CenteredCard } from "./ShareUI"; import { RecordlySignInFields, + SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS, type SignInProvider, } from "../../../../../../src/components/auth/RecordlySignInFields"; import { RecordlyPasswordForm } from "../../../../../../src/components/auth/RecordlyPasswordForm"; @@ -82,10 +83,10 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) unsubscribe(); return; } - // Discover only enabled providers; email stays usable if discovery fails. + // Disable only providers that Auth confirms are unavailable. void fetch(`${config.url}/auth/v1/settings`, { headers: { apikey: config.publishableKey }, - signal: AbortSignal.timeout(10000), + signal: AbortSignal.timeout(SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS), }) .then(async (response) => { if (!response.ok) throw new Error("Provider discovery unavailable"); @@ -100,7 +101,7 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) } }) .catch(() => { - /* Email remains available when providers cannot be discovered. */ + /* Leave provider availability unknown so users can still try signing in. */ }) .finally(() => { if (active) setLoadingProviders(false); diff --git a/src/components/auth/RecordlySignInDialog.tsx b/src/components/auth/RecordlySignInDialog.tsx index 90e08f42e..54ffe787e 100644 --- a/src/components/auth/RecordlySignInDialog.tsx +++ b/src/components/auth/RecordlySignInDialog.tsx @@ -18,7 +18,7 @@ import { signInWithSocial, signOutRecordly, } from "@/lib/auth/recordlyAuth"; -import { RecordlySignInFields } from "./RecordlySignInFields"; +import { RecordlySignInFields, SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS } from "./RecordlySignInFields"; import { FirstBootMusic } from "./FirstBootMusic"; import { OnboardingBody, @@ -130,14 +130,14 @@ export function RecordlySignInDialog({ const [reportDiagnostic, setReportDiagnostic] = useState(); const [linkSent, setLinkSent] = useState(false); const [providers, setProviders] = useState([]); - const [loadingProviders, setLoadingProviders] = useState(false); + const [loadingProviders, setLoadingProviders] = useState(configured); const [providersKnown, setProvidersKnown] = useState(false); useEffect(() => { if (!open || !configured || user) return; let disposed = false; const controller = new AbortController(); - const timeout = window.setTimeout(() => controller.abort(), 10000); + const timeout = window.setTimeout(() => controller.abort(), SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS); setLoadingProviders(true); setProvidersKnown(false); setProviders([]); @@ -149,7 +149,7 @@ export function RecordlySignInDialog({ } }) .catch((error) => { - // Email remains available to retry when provider discovery cannot reach Auth. + // Allow sign-in attempts when provider availability could not be checked. if (!controller.signal.aborted) captureAuthFailure("provider-discovery", error); }) .finally(() => { diff --git a/src/components/auth/RecordlySignInFields.tsx b/src/components/auth/RecordlySignInFields.tsx index 9c86ffa08..1891b4c4a 100644 --- a/src/components/auth/RecordlySignInFields.tsx +++ b/src/components/auth/RecordlySignInFields.tsx @@ -16,6 +16,8 @@ import { RecordlyBotChallenge } from "./RecordlyBotChallenge"; export type SignInProvider = "google" | "azure" | "apple"; +export const SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS = 5_000; + /** The same primary sign-in controls for desktop and the hosted cloud library. */ export function RecordlySignInFields({ email, @@ -191,8 +193,7 @@ export function RecordlySignInFields({ disabled || !configured || loadingProviders || - !providersKnown || - !providers.includes(provider) + (providersKnown && !providers.includes(provider)) } aria-describedby={ providersKnown && !providers.includes(provider) diff --git a/tests/ui/login.spec.ts b/tests/ui/login.spec.ts index 65c4efa6b..354f2402d 100644 --- a/tests/ui/login.spec.ts +++ b/tests/ui/login.spec.ts @@ -87,6 +87,37 @@ test("sign-in keeps all providers visible and marks unavailable providers disabl }); }); +test("production onboarding allows OAuth when provider discovery times out after five seconds", async ({ + page, +}) => { + await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false }); + await page.addInitScript(() => { + window.electronAPI.openExternalUrl = async (url) => { + document.documentElement.dataset.authProvider = + new URL(url).searchParams.get("provider") ?? ""; + return { success: true }; + }; + }); + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.route("https://auth.recordly.test/auth/v1/settings", () => { + // Leave the request pending until the app's timeout aborts it. + }); + const discovery = page.waitForRequest("https://auth.recordly.test/auth/v1/settings"); + await page.goto("/?windowType=onboarding"); + await discovery; + const login = page.getByRole("dialog", { name: "Welcome to Recordly" }); + const google = login.getByRole("button", { name: "Continue with Google" }); + + await expect(google).toBeDisabled(); + await expect(google).toBeEnabled({ timeout: 7_000 }); + for (const provider of ["Google", "Microsoft", "Apple"]) { + await expect(login.getByRole("button", { name: `Continue with ${provider}` })).toBeEnabled(); + } + await google.click(); + await expect(page.locator("html")).toHaveAttribute("data-auth-provider", "google"); + await expect(login.getByRole("button", { name: "Permissions", exact: true })).toBeDisabled(); +}); + test("email sign-in reports localized failures and allows retry without granting access", async ({ page, }) => { diff --git a/tests/ui/onboarding-startup.spec.ts b/tests/ui/onboarding-startup.spec.ts index 947f9df1a..12e2e7149 100644 --- a/tests/ui/onboarding-startup.spec.ts +++ b/tests/ui/onboarding-startup.spec.ts @@ -89,15 +89,14 @@ for (const signedIn of [false, true]) { }); } -test("provider discovery failure leaves the real email form usable", async ({ page }) => { +test("provider discovery failure leaves social and email sign-in usable", async ({ page }) => { await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false }); await page.route("**/auth/v1/settings", (route) => route.abort()); await page.goto("/?windowType=onboarding"); const tour = page.getByRole("dialog", { name: "Welcome to Recordly" }); await tour.getByPlaceholder("you@example.com").fill("person@example.com"); await expect(tour.getByRole("button", { name: "Email me a code", exact: true })).toBeEnabled(); - await expect(tour.getByRole("button", { name: "Continue with Google" })).toBeVisible(); - await expect(tour.getByRole("button", { name: "Continue with Google" })).toBeDisabled(); + await expect(tour.getByRole("button", { name: "Continue with Google" })).toBeEnabled(); await tour.getByRole("button", { name: "Close onboarding window" }).click(); await expect(page.locator("html")).toHaveAttribute("data-onboarding-closed", "true"); }); From 6353bbf0e81f089c3a07f07edd9b22b6e82c935f Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:32:50 +1100 Subject: [PATCH 2/3] Allow hosted OAuth when provider status is unknown --- .../recordly-share/worker/web/src/components/CloudLibrary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx index a2fa58aad..83cfc6a31 100644 --- a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx +++ b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx @@ -201,7 +201,7 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) } async function signInWithProvider(provider: SignInProvider) { - if (!client || pending || !providers.includes(provider)) return; + if (!client || pending || (providersKnown && !providers.includes(provider))) return; setPending(true); setAction(provider); setError(""); From b978e8c6248c65e115c925e548603feef544a6f3 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:38:57 +1100 Subject: [PATCH 3/3] Let users start sign-in while provider discovery runs --- .../worker/web/src/components/CloudLibrary.tsx | 5 ----- src/components/auth/RecordlySignInDialog.tsx | 7 ------- src/components/auth/RecordlySignInFields.tsx | 11 +---------- tests/ui/login.spec.ts | 9 ++------- tests/ui/onboarding-startup.spec.ts | 6 +++--- 5 files changed, 6 insertions(+), 32 deletions(-) diff --git a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx index 83cfc6a31..0fc14ce9b 100644 --- a/services/recordly-share/worker/web/src/components/CloudLibrary.tsx +++ b/services/recordly-share/worker/web/src/components/CloudLibrary.tsx @@ -46,7 +46,6 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) const [action, setAction] = useState(); const [providers, setProviders] = useState([]); const [providersKnown, setProvidersKnown] = useState(false); - const [loadingProviders, setLoadingProviders] = useState(true); // biome-ignore lint/correctness/useExhaustiveDependencies: The parent loads config before mounting; keep one Supabase client for this mount. useEffect(() => { let active = true; @@ -102,9 +101,6 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) }) .catch(() => { /* Leave provider availability unknown so users can still try signing in. */ - }) - .finally(() => { - if (active) setLoadingProviders(false); }); // Keep the client available even if a stale callback needs a fresh link. setClient(auth); @@ -395,7 +391,6 @@ export default function CloudLibrary({ authConfig, commentAccount }: Props = {}) onProvider={(provider) => void signInWithProvider(provider)} providers={providers} providersKnown={providersKnown} - loadingProviders={loadingProviders} configured={Boolean(client)} busy={action} labels={{ diff --git a/src/components/auth/RecordlySignInDialog.tsx b/src/components/auth/RecordlySignInDialog.tsx index 54ffe787e..09b6b2d37 100644 --- a/src/components/auth/RecordlySignInDialog.tsx +++ b/src/components/auth/RecordlySignInDialog.tsx @@ -130,15 +130,12 @@ export function RecordlySignInDialog({ const [reportDiagnostic, setReportDiagnostic] = useState(); const [linkSent, setLinkSent] = useState(false); const [providers, setProviders] = useState([]); - const [loadingProviders, setLoadingProviders] = useState(configured); const [providersKnown, setProvidersKnown] = useState(false); useEffect(() => { if (!open || !configured || user) return; - let disposed = false; const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), SIGN_IN_PROVIDER_CHECK_TIMEOUT_MS); - setLoadingProviders(true); setProvidersKnown(false); setProviders([]); void getEnabledSignInProviders(controller.signal) @@ -154,10 +151,8 @@ export function RecordlySignInDialog({ }) .finally(() => { window.clearTimeout(timeout); - if (!disposed) setLoadingProviders(false); }); return () => { - disposed = true; window.clearTimeout(timeout); controller.abort(); }; @@ -344,14 +339,12 @@ export function RecordlySignInDialog({ void run(provider, () => signInWithSocial(provider, consent)) } providers={providers} - loadingProviders={loadingProviders} providersKnown={providersKnown} configured={configured} busy={busy} compact={onboarding} assetBase={import.meta.env.BASE_URL} labels={{ - loadingProviders: ot("loadingProviders"), checkInbox: ot("checkInbox"), code: ot("code"), verifyCode: ot("verifyCode"), diff --git a/src/components/auth/RecordlySignInFields.tsx b/src/components/auth/RecordlySignInFields.tsx index 1891b4c4a..915b02b89 100644 --- a/src/components/auth/RecordlySignInFields.tsx +++ b/src/components/auth/RecordlySignInFields.tsx @@ -27,7 +27,6 @@ export function RecordlySignInFields({ onPasswordSignIn, onProvider, providers, - loadingProviders, providersKnown, configured, busy, @@ -43,7 +42,6 @@ export function RecordlySignInFields({ onPasswordSignIn: (password: string, captchaToken?: string) => Promise; onProvider: (provider: SignInProvider) => void; providers: SignInProvider[]; - loadingProviders: boolean; providersKnown: boolean; configured: boolean; busy?: string; @@ -51,7 +49,6 @@ export function RecordlySignInFields({ assetBase?: string; authProtectionConfigUrl?: string; labels: { - loadingProviders: string; continueWith: (provider: string) => string; providerDisabled: (provider: string) => string; continueEmail: string; @@ -167,12 +164,7 @@ export function RecordlySignInFields({ className="space-y-4 @container" > {!sentTo && !passwordMode && challengeVersion === 0 && ( -
+
{( [ { provider: "google", label: "Google", image: "google-logo.png" }, @@ -192,7 +184,6 @@ export function RecordlySignInFields({ isDisabled={ disabled || !configured || - loadingProviders || (providersKnown && !providers.includes(provider)) } aria-describedby={ diff --git a/tests/ui/login.spec.ts b/tests/ui/login.spec.ts index 354f2402d..0720117e7 100644 --- a/tests/ui/login.spec.ts +++ b/tests/ui/login.spec.ts @@ -87,7 +87,7 @@ test("sign-in keeps all providers visible and marks unavailable providers disabl }); }); -test("production onboarding allows OAuth when provider discovery times out after five seconds", async ({ +test("production onboarding allows OAuth before provider discovery finishes", async ({ page, }) => { await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false }); @@ -108,12 +108,7 @@ test("production onboarding allows OAuth when provider discovery times out after const login = page.getByRole("dialog", { name: "Welcome to Recordly" }); const google = login.getByRole("button", { name: "Continue with Google" }); - await expect(google).toBeDisabled(); - await expect(google).toBeEnabled({ timeout: 7_000 }); - for (const provider of ["Google", "Microsoft", "Apple"]) { - await expect(login.getByRole("button", { name: `Continue with ${provider}` })).toBeEnabled(); - } - await google.click(); + await google.click({ timeout: 3_500 }); await expect(page.locator("html")).toHaveAttribute("data-auth-provider", "google"); await expect(login.getByRole("button", { name: "Permissions", exact: true })).toBeDisabled(); }); diff --git a/tests/ui/onboarding-startup.spec.ts b/tests/ui/onboarding-startup.spec.ts index 12e2e7149..168825be3 100644 --- a/tests/ui/onboarding-startup.spec.ts +++ b/tests/ui/onboarding-startup.spec.ts @@ -42,7 +42,7 @@ for (const signedIn of [false, true]) { await page.route("**/auth/v1/settings", async (route) => { await providersReady; await route.fulfill({ - json: { external: { google: true, azure: true, apple: true } }, + json: { external: { google: true, azure: true, apple: false } }, }); }); // Even unavailable artwork cannot gate the form. @@ -70,7 +70,7 @@ for (const signedIn of [false, true]) { ).toBeVisible(); await expect( tour.getByRole("button", { name: `Continue with ${provider}` }), - ).toBeDisabled(); + ).toBeEnabled(); } const before = await email.boundingBox(); if (!signedIn) @@ -78,7 +78,7 @@ for (const signedIn of [false, true]) { path: test.info().outputPath("pending-session.png"), }); releaseProviders(); - await expect(tour.getByRole("button", { name: "Continue with Google" })).toBeEnabled(); + await expect(tour.getByRole("button", { name: "Continue with Apple" })).toBeDisabled(); await expect(email).toHaveValue("person@example.com"); expect(await email.boundingBox()).toEqual(before); await expect(email).toHaveValue("person@example.com");