') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(ui): Automatically send invites after checkout by dstaley · Pull Request #8869 · clerk/javascript · GitHub
Skip to content

feat(ui): Automatically send invites after checkout - #8869

Merged
dstaley merged 2 commits into
mainfrom
ds.feat/auto-invite-members
Jun 16, 2026
Merged

feat(ui): Automatically send invites after checkout#8869
dstaley merged 2 commits into
mainfrom
ds.feat/auto-invite-members

Conversation

@dstaley

@dstaleydstaley commented Jun 15, 2026

Copy link
Copy Markdown
Member

Description

This PR updates the invite members flow to automatically send invitations on successful checkout if the checkout was triggered from an insufficient_seats API error. It will only trigger checkout once; if insufficient_seats is returned again after checkout the components will render an error instead of attempting to checkout again.

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:

Summary by CodeRabbit

  • New Features

    • Organization member invitations requiring additional seat purchases now send automatically once checkout completes successfully, removing the need for manual follow-up steps.
  • Bug Fixes

    • Strengthened error handling and email validation in the invitation workflow for improved reliability.
  • Tests

    • Integration tests updated to verify automatic invitation delivery following successful checkout.

@changeset-bot

changeset-botBot commented Jun 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba6a595

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

This PR includes changesets to release 3 packages
NameType
@clerk/uiPatch
@clerk/chrome-extensionPatch
@clerk/swingsetPatch

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

vercelBot commented Jun 15, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 16, 2026 4:21pm
swingsetReadyReadyPreview, CommentJun 16, 2026 4:21pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

HandleSelectPlanProps gains an optional onSubscriptionComplete callback that is forwarded into the checkout handler after revalidateAll. InviteMembersForm is refactored to centralize invite logic into inviteMembers, inviteMembersAfterCheckout, and handleInviteMembersError helpers, using the new callback to automatically resume pending invitations after a successful seat-purchase checkout completes.

Changes

Invite-after-checkout flow

Layer / File(s)Summary
HandleSelectPlanProps onSubscriptionComplete extension
packages/ui/src/contexts/components/Plans.tsx
Adds optional onSubscriptionComplete?: () => void to HandleSelectPlanProps, destructures it in handleSelectPlan, and calls it after revalidateAll() inside the checkout completion handler.
InviteMembersForm invite-after-checkout refactor
packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx
Introduces InviteMembersParams type and splits submission into inviteMembers, inviteMembersAfterCheckout, and handleInviteMembersError helpers. The insufficient_seats path conditionally opens checkout and resumes invites via the onSubscriptionComplete callback. removeInvalidEmails accepts an explicit email list.
Integration test and changeset
integration/tests/per-seat-pricing.test.ts, .changeset/invite-checkout-complete.md
fillInviteForm removes the role-selection UI steps. Post-checkout assertion replaces UI tab checks with a 15s backend poll via getOrganizationInvitationList for a pending invitation matching INVITEE_EMAIL. Changeset records the patch.

Sequence Diagram(s)

sequenceDiagram
participant User
participant InviteMembersForm
participant handleSelectPlan
participant Checkout
participant ClerkAPI
User->>InviteMembersForm: submit invite form
InviteMembersForm->>ClerkAPI: inviteMembers(params)
ClerkAPI-->>InviteMembersForm: insufficient_seats error
InviteMembersForm->>handleSelectPlan: open checkout with onSubscriptionComplete
handleSelectPlan->>Checkout: clerk.__internal_openCheckout
User->>Checkout: complete purchase
Checkout-->>handleSelectPlan: subscription complete
handleSelectPlan->>handleSelectPlan: revalidateAll()
handleSelectPlan->>InviteMembersForm: onSubscriptionComplete()
InviteMembersForm->>ClerkAPI: inviteMembersAfterCheckout(params)
ClerkAPI-->>InviteMembersForm: invitations sent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop, hop, the checkout's done,
No more waiting — invites run!
Seats are bought, the queue resumes,
Emails fly through virtual rooms.
One smooth flow from start to end,
A bouncy path around each bend! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(ui): Automatically send invites after checkout' accurately summarizes the main feature being implemented across all changed files, which add functionality to automatically send organization member invitations after successful checkout completion.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@pkg-pr-new

pkg-pr-newBot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: ba6a595

@github-actions

github-actionsBot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-16T16:22:30.251Z

Summary

MetricCount
Packages analyzed19
Packages with changes0
🔴 Breaking changes0
🟡 Non-breaking changes0
🟢 Additions0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on ba6a595.

@coderabbitaicoderabbitaiBot 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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx`:
- Around line 151-158: The error handling logic in the InviteMembersForm.tsx
file has a risk of creating unhandled promise rejections when used in
fire-and-forget callback paths. In the error handler that checks for
ClerkAPIResponseError, when the error is not a ClerkAPIResponseError and not an
Error instance, the code currently rethrows the unknown value. Instead of
rethrowing err directly in the else branch after the `if (err instanceof Error)`
check, wrap the unknown value as an Error object or call handleError with it to
ensure all error paths result in surfaced UI errors rather than unhandled
promise rejections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro

Run ID: eccc2d46-d8e6-4977-8438-42cc72051a83

📥 Commits

Reviewing files that changed from the base of the PR and between 045d93f and dc68843.

📒 Files selected for processing (4)
  • .changeset/invite-checkout-complete.md
  • integration/tests/per-seat-pricing.test.ts
  • packages/ui/src/components/OrganizationProfile/InviteMembersForm.tsx
  • packages/ui/src/contexts/components/Plans.tsx

Comment on lines +151 to +158
if (!isClerkAPIResponseError(err)) {
if (err instanceof Error) {
handleError(err, [], card.setError);
return;
}

throw err;
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid rethrowing unknown values from this shared error handler.

This helper is also used by the fire-and-forget checkout callback path; rethrowing non-Error values can become an unhandled promise rejection instead of a surfaced UI error.

Suggested fix
 if (!isClerkAPIResponseError(err)) {
- if (err instanceof Error) {- handleError(err, [], card.setError);- return;- }-- throw err;+ const normalizedError = err instanceof Error ? err : new Error('Unknown invite error');+ handleError(normalizedError, [], card.setError);+ return;
}
🤖 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/ui/src/components/OrganizationProfile/InviteMembersForm.tsx` around
lines 151 - 158, The error handling logic in the InviteMembersForm.tsx file has
a risk of creating unhandled promise rejections when used in fire-and-forget
callback paths. In the error handler that checks for ClerkAPIResponseError, when
the error is not a ClerkAPIResponseError and not an Error instance, the code
currently rethrows the unknown value. Instead of rethrowing err directly in the
else branch after the `if (err instanceof Error)` check, wrap the unknown value
as an Error object or call handleError with it to ensure all error paths result
in surfaced UI errors rather than unhandled promise rejections.

@dstaley
dstaley merged commit a938d74 into mainJun 16, 2026
47 checks passed
@dstaley
dstaley deleted the ds.feat/auto-invite-members branch June 16, 2026 17:03
Sign up for freeto 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.

3 participants

@dstaley@mauricioabreu@wobsoriano