From 6b510afe28e2da91d0a05eb6c162a9632e6aeebb Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 5 Feb 2026 18:10:47 +0200 Subject: [PATCH 1/4] fix(ui): prevent infinite spinner on factor-two without active 2FA session Why: Users navigating directly to /sign-in#/factor-two without an active sign-in requiring 2FA would see an infinite loading spinner because there was no status check to redirect them back to sign-in start. What changed: Added useEffect to SignInFactorTwo that redirects to sign-in start when signIn.status is null, needs_identifier, or needs_first_factor. Matches the existing pattern in SignInFactorOne. Key detail: dependency array only includes __internal_setActiveInProgress (not signIn.status) to avoid triggering redirect during valid sign-in completion when status changes to null. --- .changeset/fix-factor-two-infinite-spinner.md | 5 +++++ .../src/components/SignIn/SignInFactorTwo.tsx | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .changeset/fix-factor-two-infinite-spinner.md diff --git a/.changeset/fix-factor-two-infinite-spinner.md b/.changeset/fix-factor-two-infinite-spinner.md new file mode 100644 index 00000000000..a55b2122ffd --- /dev/null +++ b/.changeset/fix-factor-two-infinite-spinner.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fix infinite loading spinner when navigating to factor-two sign-in route without an active 2FA session diff --git a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx index caa771ed0e3..b3c2688f97c 100644 --- a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx @@ -1,8 +1,12 @@ +import { useClerk } from '@clerk/shared/react'; +import React from 'react'; + import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCard } from '@/ui/elements/LoadingCard'; import { withRedirectToAfterSignIn, withRedirectToSignInTask } from '../../common'; import { useCoreSignIn } from '../../contexts'; +import { useRouter } from '../../router'; import { SignInFactorTwoAlternativeMethods } from './SignInFactorTwoAlternativeMethods'; import { SignInFactorTwoBackupCodeCard } from './SignInFactorTwoBackupCodeCard'; import { SignInFactorTwoEmailCodeCard } from './SignInFactorTwoEmailCodeCard'; @@ -12,7 +16,9 @@ import { SignInFactorTwoTOTPCard } from './SignInFactorTwoTOTPCard'; import { useSecondFactorSelection } from './useSecondFactorSelection'; function SignInFactorTwoInternal(): JSX.Element { + const { __internal_setActiveInProgress } = useClerk(); const signIn = useCoreSignIn(); + const router = useRouter(); const { currentFactor, factorAlreadyPrepared, @@ -22,6 +28,19 @@ function SignInFactorTwoInternal(): JSX.Element { toggleAllStrategies, } = useSecondFactorSelection(signIn.supportedSecondFactors); + React.useEffect(() => { + if (__internal_setActiveInProgress) { + return; + } + + // If the sign-in was reset or doesn't exist, redirect back to the start. + // Don't redirect for 'complete' status - setActive will handle navigation. + if (signIn.status === null || signIn.status === 'needs_identifier' || signIn.status === 'needs_first_factor') { + void router.navigate('../'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- Match SignInFactorOne pattern: only run on mount and when setActiveInProgress changes + }, [__internal_setActiveInProgress]); + if (!currentFactor) { return ; } From aecc1b5e3bbaf73b320fa0635052f9c04649d9d8 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Fri, 6 Feb 2026 11:15:43 +0200 Subject: [PATCH 2/4] chore(repo): downgrade subject-case commitlint rule to warning Acronyms like 2FA, SSO, JWT in commit subjects trigger false positives with the strict case check. --- commitlint.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitlint.config.ts b/commitlint.config.ts index 5448a7e00d0..58cd2776348 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -31,7 +31,7 @@ const Configuration = { 'body-max-line-length': [1, 'always', '150'], 'scope-empty': [2, 'never'], 'scope-enum': [2, 'always', [...getPackageNames(), 'repo', 'release', 'e2e', '*', 'ci']], - 'subject-case': [2, 'always', ['camel-case', 'lower-case', 'sentence-case']], + 'subject-case': [1, 'always', ['camel-case', 'lower-case', 'sentence-case']], }, }; From 69d2d181fa8d83f741350707ded97a05477767c9 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Fri, 6 Feb 2026 14:13:42 +0200 Subject: [PATCH 3/4] fix(ui): redirect signed-in users forward from factor-two When a signed-in user lands on factor-two without a pending 2FA (e.g. page reload after successful verification in a multi-session app), redirect to afterSignInUrl instead of back to sign-in start. --- .../src/components/SignIn/SignInFactorTwo.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx index b3c2688f97c..90bfddd6df7 100644 --- a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx @@ -5,7 +5,7 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCard } from '@/ui/elements/LoadingCard'; import { withRedirectToAfterSignIn, withRedirectToSignInTask } from '../../common'; -import { useCoreSignIn } from '../../contexts'; +import { useCoreSignIn, useSignInContext } from '../../contexts'; import { useRouter } from '../../router'; import { SignInFactorTwoAlternativeMethods } from './SignInFactorTwoAlternativeMethods'; import { SignInFactorTwoBackupCodeCard } from './SignInFactorTwoBackupCodeCard'; @@ -16,9 +16,10 @@ import { SignInFactorTwoTOTPCard } from './SignInFactorTwoTOTPCard'; import { useSecondFactorSelection } from './useSecondFactorSelection'; function SignInFactorTwoInternal(): JSX.Element { - const { __internal_setActiveInProgress } = useClerk(); + const clerk = useClerk(); const signIn = useCoreSignIn(); const router = useRouter(); + const { afterSignInUrl } = useSignInContext(); const { currentFactor, factorAlreadyPrepared, @@ -29,17 +30,24 @@ function SignInFactorTwoInternal(): JSX.Element { } = useSecondFactorSelection(signIn.supportedSecondFactors); React.useEffect(() => { - if (__internal_setActiveInProgress) { + if (clerk.__internal_setActiveInProgress) { return; } - // If the sign-in was reset or doesn't exist, redirect back to the start. + // If the sign-in doesn't need second factor verification, redirect away. // Don't redirect for 'complete' status - setActive will handle navigation. if (signIn.status === null || signIn.status === 'needs_identifier' || signIn.status === 'needs_first_factor') { - void router.navigate('../'); + // If the user is already signed in (e.g. multi-session app, page reload after + // successful verification), redirect forward to afterSignInUrl instead of + // back to sign-in start. + if (clerk.isSignedIn) { + void router.navigate(afterSignInUrl); + } else { + void router.navigate('../'); + } } - // eslint-disable-next-line react-hooks/exhaustive-deps -- Match SignInFactorOne pattern: only run on mount and when setActiveInProgress changes - }, [__internal_setActiveInProgress]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- Only run on mount and when setActiveInProgress changes + }, [clerk.__internal_setActiveInProgress]); if (!currentFactor) { return ; From b1bbe999afa18efa9f8bc75ef4d7f90165b58e9c Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Fri, 6 Feb 2026 14:13:55 +0200 Subject: [PATCH 4/4] fix(ui): add changeset for factor-two redirect enhancement --- .changeset/factor-two-redirect-signed-in-forward.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/factor-two-redirect-signed-in-forward.md diff --git a/.changeset/factor-two-redirect-signed-in-forward.md b/.changeset/factor-two-redirect-signed-in-forward.md new file mode 100644 index 00000000000..cde1a41ef36 --- /dev/null +++ b/.changeset/factor-two-redirect-signed-in-forward.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Redirect signed-in users forward to afterSignInUrl when landing on factor-two without a pending 2FA session, instead of redirecting back to sign-in start