Skip to content

feat(ui): Support signUpIfMissing with Clerk <SignIn> component - #7928

Open
dmoerner wants to merge 23 commits into
mainfrom
daniel/user-4769-sign-up-if-missing-clerk-components
Open

feat(ui): Support signUpIfMissing with Clerk <SignIn> component#7928
dmoerner wants to merge 23 commits into
mainfrom
daniel/user-4769-sign-up-if-missing-clerk-components

Conversation

@dmoerner

@dmoerner dmoerner commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Description

The <SignIn> component can already be used in a sign-in-or-sign-up flow (CombinedFlow) under certain conditions. When strict enumeration protection is enabled, make that combined flow pass the signUpIfMissing parameter to the backend to allow an enumeration-safe combined flow. Previously, attempting to use a combined flow with strict enumeration protection enabled was silently broken.

Under the hood, the backend treats sign up if missing as an account transfer. We therefore add support for this account transfer logic when handling first factor verification in the combined sign in flow when strict enumeration protection is enabled.

Fixes USER-4769.

We should merge this in coordination with documentation and a Changelog release, since it will change existing behavior (although with a fix for something that is currently broken)

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

The `<SignIn>` component can already be used in a sign-in-or-sign-up flow
(`CombinedFlow`) under certain conditions. When strict enumeration
protection is enabled, make that combined flow pass the
`signUpIfMissing` parameter to the backend to allow an enumeration-safe
combined flow. Previously, attempting to use a combined flow with strict
enumeration protection enabled was silently broken.

Under the hood, the backend treats sign up if missing as an account
transfer. We therefore add support for this account transfer logic when
handling first factor verification in the combined sign in flow when
strict enumeration protection is enabled.
@changeset-bot

changeset-bot Bot commented Feb 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cbe26e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared Minor
@clerk/ui Minor
@clerk/localizations Minor
@clerk/chrome-extension Patch
@clerk/electron Patch
@clerk/expo Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Feb 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 29, 2026 6:03pm
swingset Ready Ready Preview, Comment Jul 29, 2026 6:03pm

Request Review

@github-actions github-actions Bot added the ui label Mar 2, 2026
@dmoerner
dmoerner marked this pull request as ready for review March 2, 2026 21:23
@dmoerner
dmoerner requested a review from wobsoriano March 2, 2026 21:23
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds transferable email-link verification support for the combined sign-in-or-sign-up flow. The changes update verification polling and redirect handling, add transferable UI rendering and localization keys, and extend tests and release metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: wobsoriano, jacekradko, alexcarpenter, nikosdouvlis, laurabeatris

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main UI change: adding signUpIfMissing support to Clerk's combined flow.
Description check ✅ Passed The description accurately explains the combined-flow signUpIfMissing change and transfer handling introduced by this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/clerk-js/src/core/resources/SignIn.ts`:
- Around line 311-312: SignInFuture.waitForEmailLinkVerification currently
treats only 'verified' and 'expired' as terminal statuses, which causes
transferable flows started by createEmailLinkFlow to poll indefinitely; update
the status check inside SignInFuture.waitForEmailLinkVerification to include
'transferable' alongside 'verified' and 'expired' so the polling loop calls
stop()/resolves when status === 'transferable', ensuring transferable email-link
flows terminate properly.

ℹ️ Review info

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd1973e and 721b518.

📒 Files selected for processing (15)
  • .changeset/fancy-candies-slide.md
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/clerk-js/src/core/resources/UserSettings.ts
  • packages/shared/src/errors/emailLinkError.ts
  • packages/shared/src/types/userSettings.ts
  • packages/ui/src/components/SignIn/SignInFactorOneCodeForm.tsx
  • packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx
  • packages/ui/src/components/SignIn/SignInStart.tsx
  • packages/ui/src/components/SignIn/__tests__/SignInFactorOneTransfer.test.tsx
  • packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
  • packages/ui/src/components/SignIn/__tests__/handleSignUpIfMissingTransfer.test.ts
  • packages/ui/src/components/SignIn/handleSignUpIfMissingTransfer.ts
  • packages/ui/src/test/fixture-helpers.ts
  • packages/ui/src/test/fixtures.ts

Comment thread packages/clerk-js/src/core/resources/SignIn.ts Outdated
This is connected to custom flows and was missed in the previous PRs
supporting custom flows. Let's add it now while we are here.
const res = await this.#resource.__internal_baseGet();
const status = res.firstFactorVerification.status;
if (status === 'verified' || status === 'expired') {
if (status === 'verified' || status === 'expired' || status === 'transferable') {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not necessary for Clerk builtin components but was missed in #7749. As suggested by CodeRabbit, let's just fix it now.

@pkg-pr-new

pkg-pr-new Bot commented Mar 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7928

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7928

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7928

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7928

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@7928

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@7928

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@7928

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7928

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@7928

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7928

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7928

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7928

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@7928

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7928

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7928

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7928

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7928

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7928

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7928

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7928

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7928

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7928

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7928

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7928

commit: 1cbe26e

@dmoerner
dmoerner marked this pull request as draft March 3, 2026 04:25
@dmoerner

dmoerner commented Mar 3, 2026

Copy link
Copy Markdown
Contributor Author

Depends on re-revert of #7962 with Enterprise SSO captcha transfer fix. That was now fixed.

@dmoerner
dmoerner marked this pull request as ready for review March 23, 2026 18:37
@dmoerner

dmoerner commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

Working scenarios I tested locally:

  • Password (falls back to normal strict enumeration protection sign in)
  • Restricted mode (falls back to normal strict enumeration protection sign in)
  • Email code sign in and sign up with and without additional requirements
  • Email code sign up with immutable attribute
  • Email link sign in and sign up with and without additional requirements
  • Email code sign in with 2FA enabled
  • Phone code sign in and sign up
  • Email link sign up transfer with require same device and browser.

Scenarios I didn't test locally:

  • Username (should fall back to normal strict enumeration protection sign in): Can you even sign in with a username with the Clerk components when you also have email or phone enabled (since you need those for enumeration protection)?

Under investigation:

  • Some sort of a race condition with email links. The polling tab sometimes does not update.

The link-click tab of a signUpIfMissing email-link flow previously showed
'Successfully signed in', but the user is not signed in yet - the email was
verified and the original tab continues the flow as a sign-up. Render a
dedicated 'Email verified' card (new signIn.emailLink.verifiedTransferable
localization keys) driven by the __clerk_status=transferable query param.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/clerk-js/src/core/clerk.ts (1)

3275-3293: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Abort the token refresh before falling back.

timeLimit only rejects the wrapper here; the underlying session.getToken() keeps running and can still emit events.TokenUpdate after the cookie fallback has been applied and polling resumes. Make the refresh cancelable or ignore late completions so a stale request can’t overwrite the fallback auth state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/clerk.ts` around lines 3275 - 3293, Update the
token refresh flow around session.getToken() and timeLimit() so a timeout
cancels the underlying refresh, or otherwise prevents its late completion from
applying state after the cookie fallback. Ensure the fallback
createClientFromJwt(jwt) result remains authoritative before
startPollingForToken() resumes, and prevent stale events.TokenUpdate emissions
from overwriting it.
🧹 Nitpick comments (1)
packages/clerk-js/src/core/clerk.ts (1)

2290-2297: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for __clerk_status=transferable.

Test that onVerifiedOnOtherDevice is invoked, setActive is not called, and no EmailLinkError is thrown. The existing test at packages/clerk-js/src/core/__tests__/clerk.test.ts:2875-2899 covers only verified.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/clerk.ts` around lines 2290 - 2297, The existing
clerk verification tests lack regression coverage for the transferable status
branch. Extend the relevant test near the existing verified case to exercise
__clerk_status=transferable, asserting onVerifiedOnOtherDevice is invoked,
setActive is not called, and no EmailLinkError is thrown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/clerk-js/src/core/clerk.ts`:
- Around line 3275-3293: Update the token refresh flow around session.getToken()
and timeLimit() so a timeout cancels the underlying refresh, or otherwise
prevents its late completion from applying state after the cookie fallback.
Ensure the fallback createClientFromJwt(jwt) result remains authoritative before
startPollingForToken() resumes, and prevent stale events.TokenUpdate emissions
from overwriting it.

---

Nitpick comments:
In `@packages/clerk-js/src/core/clerk.ts`:
- Around line 2290-2297: The existing clerk verification tests lack regression
coverage for the transferable status branch. Extend the relevant test near the
existing verified case to exercise __clerk_status=transferable, asserting
onVerifiedOnOtherDevice is invoked, setActive is not called, and no
EmailLinkError is thrown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 1acd0b4d-6e3a-4071-b846-0000ad8dd77d

📥 Commits

Reviewing files that changed from the base of the PR and between 77325fd and 99e9770.

📒 Files selected for processing (3)
  • packages/clerk-js/src/core/clerk.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
  • clerk/cli (auto-detected)
  • clerk/clerk-ios (auto-detected)
  • clerk/clerk-android (auto-detected)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/clerk-js/src/core/resources/tests/SignIn.test.ts
  • packages/clerk-js/src/core/resources/SignIn.ts

@wobsoriano wobsoriano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some minor comments.

Overall the approach makes sense to me, and to be clear on rollout, the current behavior is already broken for this population. A non-existent account hitting the combined flow with enumeration protection enabled dead-ends today.

We can ship as minor, and maybe with a changelog/docs entry.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate you moving this to a helper 🙏🏼

// stay at the call sites.
const signUpIfMissingEnabled =
isCombinedFlow &&
userSettings.attackProtection.enumeration_protection.enabled &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attackProtection is added to the resource in this PR, so older clerk-js runtimes don't populate it and this throws under ui/clerk-js version skew. Other userSettings reads are safe to deref because their fields exist in every runtime in the wild, this one isn't yet.

It's a small safe change

Suggested change
userSettings.attackProtection.enumeration_protection.enabled &&
Boolean(userSettings.attackProtection?.enumeration_protection?.enabled) &&

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, implemented!

throw new EmailLinkError(EmailLinkErrorCodeStatus.Expired);
} else if (verificationStatus === 'client_mismatch') {
throw new EmailLinkError(EmailLinkErrorCodeStatus.ClientMismatch);
} else if (verificationStatus === 'transferable') {

@wobsoriano wobsoriano Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since no session exists here yet, if the verification link replaces the polling tab or the original tab is gone, nothing runs the transfer and the user is told to return to a tab that no longer exists, correct? Should this branch complete the transfer on the current client and mirror the verified handling further down?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I went back and forth here on how to handle the polling tab or the new tab. I'm going to switch to using the other tab. Currently re-running my E2E tests with this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec83f7c

return clerk.__internal_navigateWithError('..', err.errors[0]);
}

if (signUpIfMissingEnabled && signIn.firstFactorVerification.status === 'transferable') {

@wobsoriano wobsoriano Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests covering this branch mock the rejection as form_identifier_not_found, but FAPI returns sign_up_if_missing_transfer for this case and marks firstFactorVerification.status as transferable, correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, fixed.

dmoerner and others added 3 commits July 29, 2026 12:27
`attackProtection` is introduced on the UserSettings resource in this same change, so an
app pinned to an older clerk-js runtime builds the resource without the field and the
combined SignIn crashes on the deref. Optional-chain it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FAPI answers attempt_first_factor with 404 sign_up_if_missing_transfer and marks
firstFactorVerification as transferable; the mocks used form_identifier_not_found. Behaviour
is unchanged because the component branches on the resource status rather than the code, but
the mocks should reflect what the backend actually returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A transferable email-link verification was always transferred by the polling tab, so the tab
the link opened in showed "return to the original tab" and dead-ended when that tab had been
closed or replaced. It also inverted the convention the non-transferable path already follows,
where the newly opened tab takes over and the polling tab points at it.

Check verifiedFromTheSameClient() before the transferable branch so the same-client case hands
off to the new tab, and give the SignIn verify route an onTransferable hook that runs the
transfer when this client holds the banked account transfer. Cross-device is unchanged and
still transfers from the polling tab, which is forced anyway: the transfer is banked on the
sign-in's client, so no other client has one to consume. Exactly one tab transfers in both
cases, so the tabs cannot race for it.

The polling tab's hand-off card reuses verifiedSwitchTab.subtitleNewTab rather than adding a
near-duplicate string to every locale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmoerner

Copy link
Copy Markdown
Contributor Author

@wobsoriano Thanks for the review. I made the changes you suggested. I also re-ran my E2E tests against staging covering the following flows:

  1. Email link: same client, new user -> sign up successful
  2. Email link: Cross-client, new user -> sign up successful
  3. Email link: Same client, existing user -> sign in successful
  4. Email link: link click without a transfer -> error as expected
  5. Email code: New user, no requirements -> sign up successful
  6. Email code: New user, name required -> sign up successful after continue
  7. Email link: New user, name required -> sign up successful after continue
  8. Phone code: New user -> sign up successful
  9. Existing user: Sign in successful
  10. Restricted mode: Blocked
  11. Waitlist mode: Blocked
  12. Password supplied: Falls back to existing behavior

@wobsoriano wobsoriano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for resolving the comments 🙏🏼

…n-or-up

On an instance with both password and strict enumeration protection enabled, a visitor without an
account is routed to the password screen and cannot get any further: enumeration protection makes
the sign-in advertise every first factor the instance supports, password is preferred by default,
and no sign-up transfer is possible from there. This predates the sign-up-if-missing work — the
combined flow was already a dead end for new users on that configuration.

The component cannot recover on its own, because a wrong password and a non-existent user are
deliberately indistinguishable to the client. Neither can the build: the flow is selected from
SDK options that are only observable in the browser bundle, and the instance settings only arrive
with the environment fetch, so there is no point where both are known. A development-mode warning
naming both settings is the most that can be detected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmoerner

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I also added a dev-only logger warning if the combined flow is enabled with password enabled and strict enumeration protection enabled, since that's the path that's still not covered by this fix.

Going to work on docs and changelog now so we can get this all out together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants