Reproduction
- Render
<SignIn withSignUp /> on a route.
- Submit an email address. Clerk sends an email_code OTP. The OTP screen appears with a "Resend" button on a 30s cooldown.
- Hard-refresh the page (or, on a mobile WebView, background the app long enough for the OS to kill it, then re-open).
- The OTP screen re-renders. A second OTP email arrives within seconds.
- 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:
- User is shown the OTP screen.
- User backgrounds the app to open their email client.
- The OS kills the WebView host process to reclaim memory.
- User returns; the app cold-starts;
<SignIn> mounts fresh; a new OTP is sent.
- 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.
Reproduction
<SignIn withSignUp />on a route.Network panel shows a fresh
POST .../sign_ins/{id}/prepare_first_factorrequest firing on every mount, even thoughclient.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 forsignUp) andemail_codeis the active strategy,<SignIn>should resume on the existing verification rather than callingprepareFirstFactoragain. A new code should only be sent when the user explicitly clicks "Resend" (and only after the cooldown window).The
client.signInresource 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:<SignIn>mounts fresh; a new OTP is sent.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.22pk_test_*instance with email_code + Google + Apple enabled,withSignUpmodeSuggested fix
Inside
<SignIn>'s mount effect for the verification step, branch on the existingclient.signInstate before callingprepareFirstFactor:Same logic for
<SignUp>readingclient.signUp.verifications.emailAddress.