diff --git a/.changeset/electron-auto-allowed-redirect-protocols.md b/.changeset/electron-auto-allowed-redirect-protocols.md new file mode 100644 index 00000000000..2fea342cfed --- /dev/null +++ b/.changeset/electron-auto-allowed-redirect-protocols.md @@ -0,0 +1,7 @@ +--- +'@clerk/electron': patch +--- + +`` from `@clerk/electron/react` now allows the renderer's own custom scheme as a redirect protocol by default, so apps no longer need to set `allowedRedirectProtocols={[':']}` manually. + +This applies when the renderer is served from the custom scheme registered with `createClerkBridge({ renderer })`. Local `file:` renderers are not allowlisted automatically, and explicit `allowedRedirectProtocols` values are still respected. diff --git a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx index 096bb49097a..db19521fe50 100644 --- a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx +++ b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx @@ -40,6 +40,13 @@ vi.mock('@clerk/shared/loadClerkJsScript', async importOriginal => ({ loadClerkUIScript, })); +function stubWindowProtocol(protocol: string) { + vi.stubGlobal('window', { + ...window, + location: { protocol }, + }); +} + describe('Electron ClerkProvider', () => { const tokenCache = { clearToken: vi.fn(), @@ -92,6 +99,60 @@ describe('Electron ClerkProvider', () => { }); }); + it('defaults allowedRedirectProtocols to the renderer custom scheme', () => { + stubWindowProtocol('clerk:'); + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual(['clerk:']); + }); + + it('does not add standard web protocols to allowedRedirectProtocols', () => { + stubWindowProtocol('https:'); + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toBeUndefined(); + }); + + it('does not add file protocol to allowedRedirectProtocols', () => { + stubWindowProtocol('file:'); + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toBeUndefined(); + }); + + it('respects an explicit allowedRedirectProtocols value over the scheme default', () => { + stubWindowProtocol('clerk:'); + + renderToStaticMarkup( + + App + , + ); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual(['myscheme:']); + }); + + it('respects an explicit empty allowedRedirectProtocols array', () => { + stubWindowProtocol('clerk:'); + + renderToStaticMarkup( + + App + , + ); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual([]); + }); + it('registers an OAuth transport backed by the Electron bridge', async () => { oauthTransport.getRedirectUrl.mockResolvedValue('my-app://renderer/'); oauthTransport.open.mockResolvedValue({ callbackUrl: 'my-app://renderer/?code=123' }); diff --git a/packages/electron/src/react/index.tsx b/packages/electron/src/react/index.tsx index 4d8418d52f8..19a07449162 100644 --- a/packages/electron/src/react/index.tsx +++ b/packages/electron/src/react/index.tsx @@ -1,5 +1,6 @@ import type { ClerkProviderProps as ReactClerkProviderProps } from '@clerk/react'; import { InternalClerkProvider as ReactClerkProvider } from '@clerk/react/internal'; +import { ALLOWED_PROTOCOLS } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { loadClerkUIScript } from '@clerk/shared/loadClerkJsScript'; import type { ClerkUIConstructor } from '@clerk/shared/ui'; import type { ReactNode } from 'react'; @@ -78,7 +79,27 @@ function createOAuthTransport(): ClerkOAuthTransport | undefined { }; } -export function ClerkProvider({ children, publishableKey, passkeys, ...props }: ClerkProviderProps): JSX.Element { +/** + * Infer the custom renderer scheme registered with `createClerkBridge({ renderer })`. + * Built-in Clerk protocols and local file renderers are not inferred. + */ +function defaultAllowedRedirectProtocols(): string[] | undefined { + const protocol = typeof window !== 'undefined' ? window.location?.protocol : undefined; + + if (!protocol || ALLOWED_PROTOCOLS.includes(protocol) || protocol === 'file:') { + return undefined; + } + + return [protocol]; +} + +export function ClerkProvider({ + children, + publishableKey, + passkeys, + allowedRedirectProtocols, + ...props +}: ClerkProviderProps): JSX.Element { const clerk = createClerkInstance(publishableKey, passkeys); const oauthTransport = createOAuthTransport(); const clerkUI = loadClerkUI(publishableKey, props); @@ -88,6 +109,7 @@ export function ClerkProvider({ children, publishableKey, passkeys, ...props }: {...props} Clerk={clerk} __internal_oauthTransport={oauthTransport} + allowedRedirectProtocols={allowedRedirectProtocols ?? defaultAllowedRedirectProtocols()} publishableKey={publishableKey} standardBrowser={false} ui={{ ClerkUI: clerkUI }}