From 9b3ff97ba67349e68b7001d89e80b4c20a7309cf Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 07:06:27 +0000 Subject: [PATCH 1/4] fix: preserve auth subtree and share OneTap initialization Follow-up to #88 and #89; SaaS Maker #244. Preserve pending auth and callback intents. Keep real OneTap failures visible and normal Google sign-in available. Synthetic regressions block external browser traffic. --- src/components/AuthProvider.test.tsx | 86 ++++++++++++++++++++++++++++ src/components/AuthProvider.tsx | 28 +++++---- src/components/LoginClient.tsx | 15 ++++- src/lib/auth-client.test.ts | 55 ++++++++++++++++++ src/lib/auth-client.ts | 13 ++++- tests/one-tap.spec.ts | 77 +++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 src/components/AuthProvider.test.tsx create mode 100644 src/lib/auth-client.test.ts create mode 100644 tests/one-tap.spec.ts diff --git a/src/components/AuthProvider.test.tsx b/src/components/AuthProvider.test.tsx new file mode 100644 index 0000000..b3b84b0 --- /dev/null +++ b/src/components/AuthProvider.test.tsx @@ -0,0 +1,86 @@ +import { act, useEffect, type ReactNode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +const mocks = vi.hoisted(() => ({ + session: { data: null as null | { user: { id: string; email: string } }, isPending: true }, + social: vi.fn(), + signOut: vi.fn(), + oneTap: vi.fn(), +})); +vi.mock('@/lib/auth-client', () => ({ + useSession: () => mocks.session, + signIn: { social: mocks.social }, + signOut: mocks.signOut, + startGoogleOneTap: mocks.oneTap, +})); +vi.mock('@/lib/analytics', () => ({ trackReturnedOnce: vi.fn(), trackSignupOnce: vi.fn() })); +vi.mock('@/lib/foundry-monitoring', () => ({ captureAuthFailure: vi.fn() })); +import { AuthProvider, useAuth } from './AuthProvider'; +import LoginClient from './LoginClient'; +let host: HTMLDivElement; +let root: ReturnType; +beforeEach(() => { + mocks.session = { data: null, isPending: true }; + mocks.oneTap.mockReset().mockResolvedValue(undefined); + mocks.social.mockReset().mockResolvedValue({}); + host = document.createElement('div'); + document.body.append(host); + root = createRoot(host); +}); +afterEach(async () => { + await act(async () => root.unmount()); + host.remove(); +}); +async function render(child: ReactNode) { + await act(async () => { + root.render({child}); + }); +} +describe('auth loading preserves its subtree', () => { + it('retains DOM and local state through pending, signed-out and account changes', async () => { + let mounts = 0; + function Child() { + const { user, loading } = useAuth(); + useEffect(() => { + mounts += 1; + }, []); + return ( + <> + + {loading ? 'pending' : (user?.id ?? 'signed-out')} + + ); + } + await render(); + const input = host.querySelector('input')!; + input.value = 'retained draft'; + expect(host.querySelector('output')?.textContent).toBe('pending'); + mocks.session = { data: null, isPending: false }; + await render(); + expect(host.querySelector('output')?.textContent).toBe('signed-out'); + mocks.session = { + data: { user: { id: 'synthetic-account', email: 'audit@example.invalid' } }, + isPending: false, + }; + await render(); + expect(host.querySelector('output')?.textContent).toBe('synthetic-account'); + expect(host.querySelector('input')).toBe(input); + expect(input.value).toBe('retained draft'); + expect(mounts).toBe(1); + }); + it('initializes Login once and shows NetworkError while keeping standard sign-in', async () => { + mocks.oneTap.mockRejectedValue( + new DOMException('Synthetic FedCM network failure', 'NetworkError') + ); + await render(); + expect(mocks.oneTap).toHaveBeenCalledTimes(1); + expect(mocks.oneTap).toHaveBeenCalledWith('/library'); + expect(host.querySelector('[role=alert]')?.textContent).toBe( + 'NetworkError: Synthetic FedCM network failure' + ); + const button = host.querySelector('button')!; + expect(button.disabled).toBe(false); + await act(async () => button.click()); + expect(mocks.social).toHaveBeenCalledWith({ provider: 'google', callbackURL: '/' }); + }); +}); diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx index 8e1e712..5ae49e6 100644 --- a/src/components/AuthProvider.tsx +++ b/src/components/AuthProvider.tsx @@ -34,7 +34,13 @@ function AnalyticsTracker({ useSession }: { useSession: AuthClientModule['useSes return null; } -function AuthProviderReady({ auth, children }: { auth: AuthClientModule; children: ReactNode }) { +function AuthProviderReady({ + auth, + onValue, +}: { + auth: AuthClientModule; + onValue: (value: AuthContextValue) => void; +}) { const { data: session, isPending } = auth.useSession(); const sessionUser = session?.user; @@ -63,12 +69,9 @@ function AuthProviderReady({ auth, children }: { auth: AuthClientModule; childre [auth, isPending, sessionUser] ); - return ( - - - {children} - - ); + useEffect(() => onValue(value), [onValue, value]); + + return ; } export function AuthProvider({ children }: { children: ReactNode }) { @@ -101,11 +104,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { [] ); - if (!auth) { - return {children}; - } + const [value, setValue] = useState(bootValue); - return {children}; + return ( + + {auth && } + {children} + + ); } export function useAuth(): AuthContextValue { diff --git a/src/components/LoginClient.tsx b/src/components/LoginClient.tsx index 6df8cd1..3b0e9e2 100644 --- a/src/components/LoginClient.tsx +++ b/src/components/LoginClient.tsx @@ -13,7 +13,13 @@ export default function LoginClient() { const [redirecting, setRedirecting] = useState(false); useEffect(() => { + let active = true; void startGoogleOneTap('/library').catch((error) => { + if (active) { + setError( + error instanceof Error ? `${error.name}: ${error.message}` : 'Google One Tap failed' + ); + } captureAuthFailure({ provider: 'google', stage: 'one-tap', @@ -21,6 +27,9 @@ export default function LoginClient() { source: 'login-client', }); }); + return () => { + active = false; + }; }, []); const handleSignIn = async () => { @@ -94,7 +103,11 @@ export default function LoginClient() { )} - {error &&

{error}

} + {error && ( +

+ {error} +

+ )} ); diff --git a/src/lib/auth-client.test.ts b/src/lib/auth-client.test.ts new file mode 100644 index 0000000..1351438 --- /dev/null +++ b/src/lib/auth-client.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +const mocks = vi.hoisted(() => ({ oneTap: vi.fn(), vanilla: vi.fn() })); +vi.mock('better-auth/react', () => ({ + createAuthClient: () => ({ useSession: vi.fn(), signIn: {}, signOut: vi.fn() }), +})); +vi.mock('better-auth/client', () => ({ createAuthClient: mocks.vanilla })); +vi.mock('better-auth/client/plugins', () => ({ oneTapClient: vi.fn() })); +afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); + vi.clearAllMocks(); +}); +describe('concurrent One Tap initialization', () => { + it('shares initialization, propagates failure, then permits retry', async () => { + let reject!: (error: Error) => void; + mocks.oneTap.mockImplementationOnce( + () => + new Promise((_, fail) => { + reject = fail; + }) + ); + mocks.oneTap.mockResolvedValue(undefined); + mocks.vanilla.mockReturnValue({ oneTap: mocks.oneTap }); + const fetch = vi + .fn() + .mockImplementation( + async () => new Response(JSON.stringify({ googleClientId: 'synthetic-only' })) + ); + vi.stubGlobal('fetch', fetch); + const { startGoogleOneTap } = await import('./auth-client'); + const first = startGoogleOneTap('/library'); + const second = startGoogleOneTap('/library'); + expect(second).toBe(first); + const results = Promise.allSettled([first, second]); + await vi.waitFor(() => expect(mocks.oneTap).toHaveBeenCalledTimes(1)); + const error = new DOMException('Synthetic network failure', 'NetworkError'); + reject(error); + expect(await results).toEqual([ + { status: 'rejected', reason: error }, + { status: 'rejected', reason: error }, + ]); + expect(fetch).toHaveBeenCalledTimes(1); + await startGoogleOneTap('/library'); + expect(mocks.oneTap).toHaveBeenCalledTimes(2); + }); + it('uses null config without invoking a provider', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response(JSON.stringify({ googleClientId: null }))) + ); + const { startGoogleOneTap } = await import('./auth-client'); + await startGoogleOneTap('/library'); + expect(mocks.vanilla).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 4f4c78e..83d16c8 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -15,7 +15,18 @@ type AuthClientConfig = { googleClientId: string | null; }; -export async function startGoogleOneTap(callbackPath = '/library'): Promise { +let oneTapInFlight: Promise | null = null; + +export function startGoogleOneTap(callbackPath = '/library'): Promise { + if (oneTapInFlight) return oneTapInFlight; + const pending = initializeGoogleOneTap(callbackPath).finally(() => { + if (oneTapInFlight === pending) oneTapInFlight = null; + }); + oneTapInFlight = pending; + return pending; +} + +async function initializeGoogleOneTap(callbackPath: string): Promise { const response = await fetch('/api/auth/client-config', { credentials: 'same-origin', cache: 'no-store', diff --git a/tests/one-tap.spec.ts b/tests/one-tap.spec.ts new file mode 100644 index 0000000..98b3696 --- /dev/null +++ b/tests/one-tap.spec.ts @@ -0,0 +1,77 @@ +import { expect, test } from './fixtures'; + +test('one synthetic initialization across auth loading; errors retain standard sign-in', async ({ + page, +}) => { + test.setTimeout(60000); + const cdp = await page.context().newCDPSession(page); + await cdp.send('Network.enable'); + await cdp.send('Network.emulateNetworkConditions', { + offline: false, + latency: 150, + downloadThroughput: 1_600_000 / 8, + uploadThroughput: 750_000 / 8, + }); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); + let configRequests = 0; + await page.addInitScript(() => { + Object.assign(window, { + googleScriptInitialized: true, + syntheticOneTapInitializations: 0, + google: { + accounts: { + id: { + initialize: () => { + const state = window as unknown as { syntheticOneTapInitializations: number }; + state.syntheticOneTapInitializations += 1; + throw new DOMException('Synthetic FedCM network failure', 'NetworkError'); + }, + }, + }, + }, + }); + }); + await page.route('**/api/auth/get-session', async (route) => { + await new Promise((resolve) => setTimeout(resolve, 500)); + await route.fulfill({ json: null }); + }); + await page.route('**/api/auth/client-config', (route) => { + configRequests += 1; + return route.fulfill({ json: { googleClientId: 'synthetic-local-only' } }); + }); + const consoleErrors: string[] = []; + page.on('console', (message) => { + if (message.type() === 'error') consoleErrors.push(message.text()); + }); + const sessionResponse = page.waitForResponse((response) => + response.url().includes('/api/auth/get-session') + ); + await page.goto('/login'); + await expect(page.getByRole('alert')).toHaveText('NetworkError: Synthetic FedCM network failure'); + await sessionResponse; + expect(configRequests).toBe(1); + expect( + await page.evaluate( + () => + (window as unknown as { syntheticOneTapInitializations: number }) + .syntheticOneTapInitializations + ) + ).toBe(1); + expect( + consoleErrors.some((message) => message.includes('Error during Google One Tap flow')) + ).toBe(true); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + await page.screenshot({ path: test.info().outputPath('one-tap-error.png'), fullPage: true }); + await page.route('**/api/auth/sign-in/social', async (route) => { + expect(route.request().postDataJSON()).toMatchObject({ provider: 'google', callbackURL: '/' }); + await route.fulfill({ + status: 503, + json: { message: 'Synthetic standard sign-in unavailable' }, + }); + }); + await page.getByRole('button', { name: 'Sign in with Google', exact: true }).click(); + await expect(page.getByRole('alert')).toHaveText('Failed to sign in. Please try again.'); + await expect( + page.getByRole('button', { name: 'Sign in with Google', exact: true }) + ).toBeEnabled(); +}); From 5740f4e976130994b4d4361b5c7cca2bfce3f158 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 13:29:28 +0530 Subject: [PATCH 2/4] test: run One Tap browser replay on WebKit without CDP --- tests/one-tap.spec.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/one-tap.spec.ts b/tests/one-tap.spec.ts index 98b3696..28207e5 100644 --- a/tests/one-tap.spec.ts +++ b/tests/one-tap.spec.ts @@ -2,17 +2,20 @@ import { expect, test } from './fixtures'; test('one synthetic initialization across auth loading; errors retain standard sign-in', async ({ page, + browserName, }) => { test.setTimeout(60000); - const cdp = await page.context().newCDPSession(page); - await cdp.send('Network.enable'); - await cdp.send('Network.emulateNetworkConditions', { - offline: false, - latency: 150, - downloadThroughput: 1_600_000 / 8, - uploadThroughput: 750_000 / 8, - }); - await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); + if (browserName === 'chromium') { + const cdp = await page.context().newCDPSession(page); + await cdp.send('Network.enable'); + await cdp.send('Network.emulateNetworkConditions', { + offline: false, + latency: 150, + downloadThroughput: 1_600_000 / 8, + uploadThroughput: 750_000 / 8, + }); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 }); + } let configRequests = 0; await page.addInitScript(() => { Object.assign(window, { From f772fe68ce7093969adf0bfa2807df5a3586b048 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 13:32:13 +0530 Subject: [PATCH 3/4] fix(login): explain One Tap fallback in product language --- src/components/LoginClient.tsx | 4 +--- tests/one-tap.spec.ts | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/LoginClient.tsx b/src/components/LoginClient.tsx index 3b0e9e2..1a75f9a 100644 --- a/src/components/LoginClient.tsx +++ b/src/components/LoginClient.tsx @@ -16,9 +16,7 @@ export default function LoginClient() { let active = true; void startGoogleOneTap('/library').catch((error) => { if (active) { - setError( - error instanceof Error ? `${error.name}: ${error.message}` : 'Google One Tap failed' - ); + setError('One-tap sign-in is unavailable. Sign in with Google to continue.'); } captureAuthFailure({ provider: 'google', diff --git a/tests/one-tap.spec.ts b/tests/one-tap.spec.ts index 28207e5..1524469 100644 --- a/tests/one-tap.spec.ts +++ b/tests/one-tap.spec.ts @@ -50,7 +50,9 @@ test('one synthetic initialization across auth loading; errors retain standard s response.url().includes('/api/auth/get-session') ); await page.goto('/login'); - await expect(page.getByRole('alert')).toHaveText('NetworkError: Synthetic FedCM network failure'); + await expect(page.getByRole('alert')).toHaveText( + 'One-tap sign-in is unavailable. Sign in with Google to continue.' + ); await sessionResponse; expect(configRequests).toBe(1); expect( From ebc42618eca8b6206232c361af52ca7c6c5935ad Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 13:34:18 +0530 Subject: [PATCH 4/4] test: assert the friendly One Tap fallback message --- src/components/AuthProvider.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AuthProvider.test.tsx b/src/components/AuthProvider.test.tsx index b3b84b0..49e31ac 100644 --- a/src/components/AuthProvider.test.tsx +++ b/src/components/AuthProvider.test.tsx @@ -76,7 +76,7 @@ describe('auth loading preserves its subtree', () => { expect(mocks.oneTap).toHaveBeenCalledTimes(1); expect(mocks.oneTap).toHaveBeenCalledWith('/library'); expect(host.querySelector('[role=alert]')?.textContent).toBe( - 'NetworkError: Synthetic FedCM network failure' + 'One-tap sign-in is unavailable. Sign in with Google to continue.' ); const button = host.querySelector('button')!; expect(button.disabled).toBe(false);