diff --git a/.changeset/fix-signin-factor-two-spinner.md b/.changeset/fix-signin-factor-two-spinner.md
new file mode 100644
index 00000000000..a55b2122ffd
--- /dev/null
+++ b/.changeset/fix-signin-factor-two-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..4cced739797 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,18 @@ function SignInFactorTwoInternal(): JSX.Element {
toggleAllStrategies,
} = useSecondFactorSelection(signIn.supportedSecondFactors);
+ React.useEffect(() => {
+ if (__internal_setActiveInProgress) {
+ return;
+ }
+
+ // If the sign-in doesn't require second factor verification,
+ // redirect back to the start of the sign-in flow
+ if (signIn.status !== 'needs_second_factor') {
+ void router.navigate('../');
+ }
+ }, [__internal_setActiveInProgress, signIn.status, router]);
+
if (!currentFactor) {
return ;
}
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx
index 3d944954df9..63f9e05cf80 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx
@@ -18,8 +18,17 @@ describe('SignInFactorTwo', () => {
});
describe('Navigation', () => {
- //This isn't yet implemented in the component
- it.todo('navigates to SignInStart component if user lands on SignInFactorTwo page but they should not');
+ it('navigates to SignInStart component if sign-in status is not needs_second_factor', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withEmailAddress();
+ // Note: We do NOT call f.startSignInFactorTwo() here, so the sign-in status
+ // will be null/needs_identifier, not 'needs_second_factor'
+ });
+ render(, { wrapper });
+ await waitFor(() => {
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('../');
+ });
+ });
});
describe('Submitting', () => {