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

No one assigned

    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