Skip to content

<SignIn> sends a new email OTP on every mount/refresh while a pending verification already exists #8463

@marekabaffy

Description

@marekabaffy

Reproduction

  1. Render <SignIn withSignUp /> on a route.
  2. Submit an email address. Clerk sends an email_code OTP. The OTP screen appears with a "Resend" button on a 30s cooldown.
  3. Hard-refresh the page (or, on a mobile WebView, background the app long enough for the OS to kill it, then re-open).
  4. The OTP screen re-renders. A second OTP email arrives within seconds.
  5. Refreshing again sends a third. And so on — the 30s cooldown is never respected across mounts.

Network panel shows a fresh POST .../sign_ins/{id}/prepare_first_factor request firing on every mount, even though client.signIn?.status === 'needs_first_factor' is already true at mount time with a valid pending verification.

Expected behavior

On mount, when client.signIn?.status === 'needs_first_factor' (or the equivalent for signUp) and email_code is the active strategy, <SignIn> should resume on the existing verification rather than calling prepareFirstFactor again. A new code should only be sent when the user explicitly clicks "Resend" (and only after the cooldown window).

The client.signIn resource already persists this state server-side per client — the prebuilt component just isn't reading it before re-preparing.

Why this matters in practice

Web impact is mild: most users don't refresh during OTP entry. The painful case is mobile apps that embed <SignIn> in a WebView:

  1. User is shown the OTP screen.
  2. User backgrounds the app to open their email client.
  3. The OS kills the WebView host process to reclaim memory.
  4. User returns; the app cold-starts; <SignIn> mounts fresh; a new OTP is sent.
  5. By the time the user reads the first email (the one they actually went to fetch), it's already superseded.

This makes WebView-embedded sign-in unusable on memory-constrained devices.

Environment

  • @clerk/clerk-react: 5.61.5
  • @clerk/react-router: 1.10.2
  • @clerk/shared: 3.47.4
  • @clerk/types: 4.101.22
  • React: 19, Vite 5
  • Reproduces on a pk_test_* instance with email_code + Google + Apple enabled, withSignUp mode
  • Reproduces in Chrome (desktop) and in iOS/Android WKWebView/WebView shells

Suggested fix

Inside <SignIn>'s mount effect for the verification step, branch on the existing client.signIn state before calling prepareFirstFactor:

if (
  client.signIn?.status === 'needs_first_factor' &&
  client.signIn.firstFactorVerification?.strategy === 'email_code' &&
  client.signIn.firstFactorVerification?.status !== 'expired'
) {
  // resume — do NOT call prepareFirstFactor
  return;
}

Same logic for <SignUp> reading client.signUp.verifications.emailAddress.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions