From 417166caffdbe86c77c759a34895976000dc90a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 02:13:03 +0000 Subject: [PATCH 1/2] fix: Add another account button now navigates to sign-in page - Fixed premature redirect-to-home when authenticated user visits /auth/login?switchAccount=1, caused by searchParams being momentarily null during Next.js client-side navigation - Added event.preventDefault/stopPropagation to addAnotherAccount handler, consistent with other interactive buttons in the dropdown - Added informational banner on login page when ?switchAccount=1 is present - Extracted shared hasSearchParam() utility to avoid duplicating the searchParams null-fallback logic Agent-Logs-Url: https://github.com/WeWriteApp/WeWrite/sessions/a0477169-9657-4fc2-8de4-c3efe4606891 Co-authored-by: sirjamesgray <16139439+sirjamesgray@users.noreply.github.com> --- app/auth/login/page.tsx | 11 +++++++++++ app/components/forms/login-form.tsx | 10 +++++++--- app/components/layout/MobileAccountMenu.tsx | 6 ++++-- app/utils/searchParams.ts | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 app/utils/searchParams.ts diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index e136e9e6..84ad4fc6 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation'; import { LoginForm, DevQuickLogin } from "../../components/forms/login-form" import { ModernAuthLayout } from "../../components/layout/modern-auth-layout" import { Alert, AlertDescription } from "../../components/ui/alert"; +import { hasSearchParam } from "../../utils/searchParams"; function LoginContent() { const searchParams = useSearchParams(); @@ -13,9 +14,19 @@ function LoginContent() { const message = searchParams?.get('message'); const isPostingReply = action === 'posting_reply'; const isSessionRevoked = message === 'session_revoked'; + const isAddingAccount = hasSearchParam(searchParams, 'switchAccount'); return ( }> + {isAddingAccount && ( + + + + Sign in with another account to add it to your account switcher. + + + )} + {isPostingReply && ( diff --git a/app/components/forms/login-form.tsx b/app/components/forms/login-form.tsx index f62bf060..e9d09abf 100644 --- a/app/components/forms/login-form.tsx +++ b/app/components/forms/login-form.tsx @@ -13,6 +13,7 @@ import Link from 'next/link'; import { getEnvironmentType } from '../../utils/environmentConfig'; import { looksLikeEmail } from '@/utils/validationPatterns'; import { ChallengeWrapper, useChallengeToken } from '../auth/ChallengeWrapper'; +import { hasSearchParam } from '../../utils/searchParams'; // Constants for rate limiting const MAX_ATTEMPTS_BEFORE_WARNING = 3; @@ -139,10 +140,13 @@ export function LoginForm() { setWarning(''); }, []); - // Redirect if already authenticated + // Redirect if already authenticated (but not when adding another account) useEffect(() => { - const isAddingAccount = searchParams?.has('switchAccount'); - if (!authLoading && isAuthenticated && !isAddingAccount) { + // searchParams can be null before Next.js resolves the URL; hasSearchParam + // also checks window.location as a fallback so we never accidentally redirect + // away when the user is in the middle of an add-another-account flow. + const urlHasSwitchAccount = hasSearchParam(searchParams, 'switchAccount'); + if (!authLoading && isAuthenticated && !urlHasSwitchAccount) { router.push('/home'); } }, [isAuthenticated, authLoading, router, searchParams]); diff --git a/app/components/layout/MobileAccountMenu.tsx b/app/components/layout/MobileAccountMenu.tsx index fc97d982..05bb23c0 100644 --- a/app/components/layout/MobileAccountMenu.tsx +++ b/app/components/layout/MobileAccountMenu.tsx @@ -127,7 +127,9 @@ export default function MobileAccountMenu() { } }; - const addAnotherAccount = () => { + const addAnotherAccount = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); setIsOpen(false); router.push("/auth/login?switchAccount=1"); }; @@ -322,7 +324,7 @@ export default function MobileAccountMenu() { variant="outline" className="mt-2 w-full justify-center" disabled={savedAccountLimitReached || isSwitchingAccounts} - onClick={addAnotherAccount} + onClick={(e) => addAnotherAccount(e)} > Add another account diff --git a/app/utils/searchParams.ts b/app/utils/searchParams.ts new file mode 100644 index 00000000..1d9da596 --- /dev/null +++ b/app/utils/searchParams.ts @@ -0,0 +1,20 @@ +/** + * Returns true if the given key exists in the provided Next.js searchParams object + * OR in the current browser URL. + * + * `useSearchParams()` can momentarily return `null` before Next.js has resolved + * the URL during client-side navigation. Falling back to `window.location.search` + * ensures we never miss a param that is already in the browser's address bar. + */ +export function hasSearchParam( + searchParams: { has(key: string): boolean } | null | undefined, + key: string +): boolean { + if (searchParams != null) { + return searchParams.has(key); + } + if (typeof window !== 'undefined') { + return new URLSearchParams(window.location.search).has(key); + } + return false; +} From cc96cc9b10aa26e7defc78365c3fc5b229ca176f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 02:29:41 +0000 Subject: [PATCH 2/2] fix: use window.location.href for add-another-account navigation on PWA - Changed addAnotherAccount to use window.location.href (full page reload) instead of router.push (client-side nav) to prevent premature redirect. With router.push the AuthProvider stays settled (isLoading=false) so the login form's redirect guard fires before useSearchParams has the new URL. - Updated hasSearchParam to always check window.location.search first (as the authoritative source), then fall back to the searchParams object. Previously only fell back to window.location when searchParams was null, missing the stale-but-non-null case during client-side navigation. Agent-Logs-Url: https://github.com/WeWriteApp/WeWrite/sessions/39dca12d-9ffe-4242-9469-01ba643071e8 Co-authored-by: sirjamesgray <16139439+sirjamesgray@users.noreply.github.com> --- app/components/layout/MobileAccountMenu.tsx | 11 +++++++++-- app/utils/searchParams.ts | 20 ++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/components/layout/MobileAccountMenu.tsx b/app/components/layout/MobileAccountMenu.tsx index 05bb23c0..96910852 100644 --- a/app/components/layout/MobileAccountMenu.tsx +++ b/app/components/layout/MobileAccountMenu.tsx @@ -131,7 +131,14 @@ export default function MobileAccountMenu() { event.preventDefault(); event.stopPropagation(); setIsOpen(false); - router.push("/auth/login?switchAccount=1"); + // Use window.location.href instead of router.push so the page performs a + // full reload. With router.push (client-side nav) the AuthProvider is + // already settled (isLoading=false, isAuthenticated=true) and the login + // form's redirect guard can fire before useSearchParams reflects the new + // URL, bouncing the user back immediately. A full reload reinitialises the + // AuthProvider with isLoading=true, keeping the guard silent while the + // params are read correctly from the address bar. + window.location.href = "/auth/login?switchAccount=1"; }; const forgetAccount = async (uid: string) => { @@ -324,7 +331,7 @@ export default function MobileAccountMenu() { variant="outline" className="mt-2 w-full justify-center" disabled={savedAccountLimitReached || isSwitchingAccounts} - onClick={(e) => addAnotherAccount(e)} + onClick={addAnotherAccount} > Add another account diff --git a/app/utils/searchParams.ts b/app/utils/searchParams.ts index 1d9da596..48adf594 100644 --- a/app/utils/searchParams.ts +++ b/app/utils/searchParams.ts @@ -2,19 +2,27 @@ * Returns true if the given key exists in the provided Next.js searchParams object * OR in the current browser URL. * - * `useSearchParams()` can momentarily return `null` before Next.js has resolved - * the URL during client-side navigation. Falling back to `window.location.search` - * ensures we never miss a param that is already in the browser's address bar. + * Two failure modes are handled: + * 1. `useSearchParams()` returns null before Next.js has resolved the URL during + * client-side navigation — the window.location fallback covers this case. + * 2. `useSearchParams()` returns a non-null but *stale* object (from the previous + * page) when the component mounts before the router has flushed the new params — + * the window.location fallback is always checked regardless. */ export function hasSearchParam( searchParams: { has(key: string): boolean } | null | undefined, key: string ): boolean { + // Always check window.location first — it reflects the address bar immediately + // and is immune to any stale searchParams object from the previous render. + if (typeof window !== 'undefined') { + if (new URLSearchParams(window.location.search).has(key)) { + return true; + } + } + // Fall back to the searchParams object (may be null/stale on client-side nav) if (searchParams != null) { return searchParams.has(key); } - if (typeof window !== 'undefined') { - return new URLSearchParams(window.location.search).has(key); - } return false; }