From affb643d9591499e00d6dcadbadb8016f8ccb8fa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 11 Jun 2026 14:56:03 -0700 Subject: [PATCH 1/2] Keep desktop app available when Clerk UI fails Co-authored-by: codex --- apps/web/src/cloud/desktopClerk.tsx | 40 +++------ .../clerk/DesktopClerkProvider.browser.tsx | 81 +++++++++++++++++++ 2 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx diff --git a/apps/web/src/cloud/desktopClerk.tsx b/apps/web/src/cloud/desktopClerk.tsx index 68179f5cf03..2b9e0501559 100644 --- a/apps/web/src/cloud/desktopClerk.tsx +++ b/apps/web/src/cloud/desktopClerk.tsx @@ -9,7 +9,7 @@ import { clerkFrontendApiHostnameFromPublishableKey, isAllowedClerkFrontendApiHostname, } from "@t3tools/shared/relayAuth"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo } from "react"; import { makeDesktopClerkExternalAccountAdapter, @@ -276,36 +276,16 @@ function getDesktopClerkInstance(publishableKey: string): Clerk { } export function DesktopClerkProvider({ children, publishableKey }: DesktopClerkProviderProps) { - const [clerkUiCtor, setClerkUiCtor] = useState( - () => window.__internal_ClerkUICtor, + // Clerk accepts a thenable UI constructor, so auth context can mount before the optional UI bundle. + const clerkUiLoad = useMemo( + () => Promise.resolve().then(() => loadDesktopClerkUi(publishableKey)), + [publishableKey], ); - const [clerkUiError, setClerkUiError] = useState(null); - useEffect(() => { - let isCurrent = true; - void loadDesktopClerkUi(publishableKey).then( - (ClerkUI) => { - if (isCurrent) { - setClerkUiCtor(() => ClerkUI); - } - }, - (error: unknown) => { - if (isCurrent) { - setClerkUiError(error); - } - }, - ); - return () => { - isCurrent = false; - }; - }, [publishableKey]); - - if (!clerkUiCtor) { - if (clerkUiError) { - console.error("Failed to load Clerk UI for desktop auth.", clerkUiError); - } - return null; - } + void clerkUiLoad.catch((error: unknown) => { + console.error("Failed to load Clerk UI for desktop auth.", error); + }); + }, [clerkUiLoad]); const clerk = getDesktopClerkInstance(publishableKey); return ( @@ -313,7 +293,7 @@ export function DesktopClerkProvider({ children, publishableKey }: DesktopClerkP key={publishableKey} publishableKey={publishableKey} Clerk={clerk as ClerkProviderProps["Clerk"]} - ui={{ ClerkUI: clerkUiCtor }} + ui={{ ClerkUI: clerkUiLoad as unknown as DesktopClerkUiCtor }} standardBrowser={false} > {children} diff --git a/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx b/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx new file mode 100644 index 00000000000..f71111b858a --- /dev/null +++ b/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx @@ -0,0 +1,81 @@ +import { page } from "vite-plus/test/browser"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import { render } from "vitest-browser-react"; + +const internalProviderProps = vi.hoisted(() => vi.fn()); + +vi.mock("@clerk/clerk-js", () => ({ + Clerk: class { + readonly publishableKey: string; + + constructor(publishableKey: string) { + this.publishableKey = publishableKey; + } + + addListener() { + return () => undefined; + } + + __internal_onBeforeRequest() {} + + __internal_onAfterResponse() {} + }, +})); + +vi.mock("@clerk/react/internal", async () => { + const React = await import("react"); + return { + buildClerkUIScriptAttributes: () => ({}), + clerkUIScriptUrl: () => "https://clerk.example.test/npm/@clerk/ui/dist/ui.browser.js", + InternalClerkProvider: ({ children, ...props }: { readonly children: React.ReactNode }) => { + internalProviderProps(props); + return React.createElement(React.Fragment, null, children); + }, + }; +}); + +import { DesktopClerkProvider } from "../../cloud/desktopClerk"; + +const publishableKey = `pk_test_${btoa("clerk.example.test$")}`; + +describe("DesktopClerkProvider", () => { + afterEach(() => { + document.querySelector("script[data-clerk-ui-script]")?.remove(); + Reflect.deleteProperty(window, "__internal_ClerkUICtor"); + internalProviderProps.mockClear(); + }); + + it("keeps rendering children when the remote Clerk UI bundle is unavailable", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + await render( + +
Application content
+
, + ); + + await expect.element(page.getByText("Application content")).toBeVisible(); + + await vi.waitFor(() => { + expect(document.querySelector("script[data-clerk-ui-script]")).not.toBeNull(); + }); + const script = document.querySelector("script[data-clerk-ui-script]"); + expect(script).not.toBeNull(); + expect(internalProviderProps).toHaveBeenCalledWith( + expect.objectContaining({ + ui: { + ClerkUI: expect.any(Promise), + }, + }), + ); + + script?.dispatchEvent(new Event("error")); + + await vi.waitFor(() => { + expect(consoleError).toHaveBeenCalledWith( + "Failed to load Clerk UI for desktop auth.", + expect.any(Error), + ); + }); + await expect.element(page.getByText("Application content")).toBeVisible(); + }); +}); From f44f1a572b38e0451d5cd966b130bbfa7ca260ef Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 11 Jun 2026 15:00:08 -0700 Subject: [PATCH 2/2] Delete apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx --- .../clerk/DesktopClerkProvider.browser.tsx | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx diff --git a/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx b/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx deleted file mode 100644 index f71111b858a..00000000000 --- a/apps/web/src/components/clerk/DesktopClerkProvider.browser.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { page } from "vite-plus/test/browser"; -import { afterEach, describe, expect, it, vi } from "vite-plus/test"; -import { render } from "vitest-browser-react"; - -const internalProviderProps = vi.hoisted(() => vi.fn()); - -vi.mock("@clerk/clerk-js", () => ({ - Clerk: class { - readonly publishableKey: string; - - constructor(publishableKey: string) { - this.publishableKey = publishableKey; - } - - addListener() { - return () => undefined; - } - - __internal_onBeforeRequest() {} - - __internal_onAfterResponse() {} - }, -})); - -vi.mock("@clerk/react/internal", async () => { - const React = await import("react"); - return { - buildClerkUIScriptAttributes: () => ({}), - clerkUIScriptUrl: () => "https://clerk.example.test/npm/@clerk/ui/dist/ui.browser.js", - InternalClerkProvider: ({ children, ...props }: { readonly children: React.ReactNode }) => { - internalProviderProps(props); - return React.createElement(React.Fragment, null, children); - }, - }; -}); - -import { DesktopClerkProvider } from "../../cloud/desktopClerk"; - -const publishableKey = `pk_test_${btoa("clerk.example.test$")}`; - -describe("DesktopClerkProvider", () => { - afterEach(() => { - document.querySelector("script[data-clerk-ui-script]")?.remove(); - Reflect.deleteProperty(window, "__internal_ClerkUICtor"); - internalProviderProps.mockClear(); - }); - - it("keeps rendering children when the remote Clerk UI bundle is unavailable", async () => { - const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); - await render( - -
Application content
-
, - ); - - await expect.element(page.getByText("Application content")).toBeVisible(); - - await vi.waitFor(() => { - expect(document.querySelector("script[data-clerk-ui-script]")).not.toBeNull(); - }); - const script = document.querySelector("script[data-clerk-ui-script]"); - expect(script).not.toBeNull(); - expect(internalProviderProps).toHaveBeenCalledWith( - expect.objectContaining({ - ui: { - ClerkUI: expect.any(Promise), - }, - }), - ); - - script?.dispatchEvent(new Event("error")); - - await vi.waitFor(() => { - expect(consoleError).toHaveBeenCalledWith( - "Failed to load Clerk UI for desktop auth.", - expect.any(Error), - ); - }); - await expect.element(page.getByText("Application content")).toBeVisible(); - }); -});