Surface team seat mismatch UX#506
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds seat-count utilities and messaging, an openBillingPortal helper, a TeamSeatMismatchAlert component, and UI changes (AccountMenu, TeamDashboard, TeamInviteDialog, app) to detect and surface when team members exceed purchased seats. ChangesTeam Seat Mismatch Detection & Billing Integration
Sequence DiagramsequenceDiagram
participant App as App
participant Alert as TeamSeatMismatchAlert
participant BillingService as BillingService
participant PortalHelper as openBillingPortal
participant Browser as ExternalBrowser
participant Dashboard as TeamDashboard
App->>Alert: render component
Alert->>BillingService: getTeamStatus()
BillingService-->>Alert: teamStatus
Alert->>Alert: compute mismatch (getTeamSeatMismatch)
alt Mismatch detected
Alert->>App: render banner + "Manage" button
end
Dashboard->>PortalHelper: handleOpenBilling()
PortalHelper->>BillingService: getPortalUrl()
BillingService-->>PortalHelper: portalUrl
PortalHelper->>Browser: openExternalUrl(portalUrl)
Browser-->>Dashboard: user manages subscription externally
Dashboard->>BillingService: invalidate billingStatus & teamStatus
BillingService-->>Dashboard: refreshed data
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Deploying maple with
|
| Latest commit: |
e43eb88
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://71a3af4d.maple-ca8.pages.dev |
| Branch Preview URL: | https://team-seat-mismatch-ux.maple-ca8.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/components/team/TeamInviteDialog.tsx (1)
188-197:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMark the billing CTA as
type="button".This button lives inside the invite form, so clicking it also triggers
handleInvite()today.
That can surface a bogus validation error while the user is trying to open billing.💡 Suggested fix
{canOpenBillingPortal && ( <Button + type="button" variant="outline" size="sm" className="w-full" onClick={handleManageSubscription} disabled={isPortalLoading}🤖 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 `@frontend/src/components/team/TeamInviteDialog.tsx` around lines 188 - 197, The billing CTA Button inside the invite form is missing an explicit type so clicking it triggers the form submit (invoking handleInvite); update the Button component rendering for the billing action (the instance that calls handleManageSubscription and uses isPortalLoading/ CreditCard) to include type="button" to prevent form submission and avoid spurious validation errors.
🧹 Nitpick comments (2)
frontend/src/components/team/TeamSeatMismatchAlert.tsx (1)
9-9: ⚡ Quick winUse the repo alias for this import.
Keeping new TSX imports on
@/components/...avoids path churn when the component tree moves.♻️ Suggested change
-import { TeamManagementDialog } from "./TeamManagementDialog"; +import { TeamManagementDialog } from "@/components/team/TeamManagementDialog";As per coding guidelines, Use path aliases (
@/*maps to./src/*) for imports in TypeScript/React files.🤖 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 `@frontend/src/components/team/TeamSeatMismatchAlert.tsx` at line 9, The import in TeamSeatMismatchAlert.tsx uses a relative path; update the import of TeamManagementDialog to use the repo path alias (rooted at `@/`) so it follows the TypeScript/React guideline—replace the current import of TeamManagementDialog with the alias-based import (e.g., import from "@/components/team/TeamManagementDialog") so future file moves won't break paths.frontend/src/app.tsx (1)
18-18: ⚡ Quick winSwitch this new import to the
@/alias.That keeps the app entrypoint aligned with the repo's TS import convention and avoids brittle
relative paths.♻️ Suggested change
-import { TeamSeatMismatchAlert } from "./components/team/TeamSeatMismatchAlert"; +import { TeamSeatMismatchAlert } from "@/components/team/TeamSeatMismatchAlert";As per coding guidelines, Use path aliases (
@/*maps to./src/*) for imports in TypeScript/React files.🤖 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 `@frontend/src/app.tsx` at line 18, Replace the relative import of TeamSeatMismatchAlert in frontend/src/app.tsx with the repository path-alias form; locate the import statement importing TeamSeatMismatchAlert and change it to use the "@/..." alias (which maps to ./src/) so the module is imported via the alias rather than a relative path.
🤖 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 `@frontend/src/components/team/TeamInviteDialog.tsx`:
- Around line 35-38: The code currently sets seatsAvailable using only
teamStatus.seats_available and defaults missing data to 0, causing false "no
seats" when the API supplies billed_seat_count or team_member_count instead;
update the normalization logic used to compute seatsAvailable (the variable
computed from teamStatus) to derive available seats from the available fields in
order of preference: use teamStatus.seats_available if present, else compute
seats from teamStatus.billed_seat_count minus teamStatus.team_member_count (or
other business-rule delta) and clamp to Math.max(0, ...), and keep existing
usage sites (e.g., canOpenBillingPortal) unchanged so UI enables invites when
seats remain.
---
Outside diff comments:
In `@frontend/src/components/team/TeamInviteDialog.tsx`:
- Around line 188-197: The billing CTA Button inside the invite form is missing
an explicit type so clicking it triggers the form submit (invoking
handleInvite); update the Button component rendering for the billing action (the
instance that calls handleManageSubscription and uses isPortalLoading/
CreditCard) to include type="button" to prevent form submission and avoid
spurious validation errors.
---
Nitpick comments:
In `@frontend/src/app.tsx`:
- Line 18: Replace the relative import of TeamSeatMismatchAlert in
frontend/src/app.tsx with the repository path-alias form; locate the import
statement importing TeamSeatMismatchAlert and change it to use the "@/..." alias
(which maps to ./src/) so the module is imported via the alias rather than a
relative path.
In `@frontend/src/components/team/TeamSeatMismatchAlert.tsx`:
- Line 9: The import in TeamSeatMismatchAlert.tsx uses a relative path; update
the import of TeamManagementDialog to use the repo path alias (rooted at `@/`) so
it follows the TypeScript/React guideline—replace the current import of
TeamManagementDialog with the alias-based import (e.g., import from
"@/components/team/TeamManagementDialog") so future file moves won't break
paths.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b47fd06f-0741-41dc-94c1-2ea0da651265
📒 Files selected for processing (8)
frontend/src/app.tsxfrontend/src/billing/billingPortal.tsfrontend/src/components/AccountMenu.tsxfrontend/src/components/team/TeamDashboard.tsxfrontend/src/components/team/TeamInviteDialog.tsxfrontend/src/components/team/TeamSeatMismatchAlert.tsxfrontend/src/types/team.tsfrontend/src/utils/teamSeats.ts
| const seatsAvailable = Math.max(0, teamStatus?.seats_available || 0); | ||
| const canOpenBillingPortal = billingStatus | ||
| ? !!billingStatus.stripe_customer_id | ||
| : !!teamStatus?.has_team_subscription; |
There was a problem hiding this comment.
Normalize seat availability instead of defaulting missing data to 0.
This dialog still reads only teamStatus.seats_available. If the API returns the new
billed_seat_count / team_member_count fields but omits seats_available, this will render
0 available seats, disable invites, and push admins into the billing flow even when seats remain.
💡 Suggested fix
import { getBillingService } from "@/billing/billingService";
import { openBillingPortal } from "@/billing/billingPortal";
import { useLocalState } from "@/state/useLocalState";
import type { TeamStatus } from "@/types/team";
+import { getTeamSeatCounts } from "@/utils/teamSeats";
@@
- const seatsAvailable = Math.max(0, teamStatus?.seats_available || 0);
+ const seatCounts = getTeamSeatCounts(teamStatus);
+ const seatsAvailable =
+ seatCounts.seatsAvailable ??
+ (seatCounts.memberCount !== null && seatCounts.billedSeatCount !== null
+ ? Math.max(0, seatCounts.billedSeatCount - seatCounts.memberCount)
+ : 0);🤖 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 `@frontend/src/components/team/TeamInviteDialog.tsx` around lines 35 - 38, The
code currently sets seatsAvailable using only teamStatus.seats_available and
defaults missing data to 0, causing false "no seats" when the API supplies
billed_seat_count or team_member_count instead; update the normalization logic
used to compute seatsAvailable (the variable computed from teamStatus) to derive
available seats from the available fields in order of preference: use
teamStatus.seats_available if present, else compute seats from
teamStatus.billed_seat_count minus teamStatus.team_member_count (or other
business-rule delta) and clamp to Math.max(0, ...), and keep existing usage
sites (e.g., canOpenBillingPortal) unchanged so UI enables invites when seats
remain.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/utils/teamSeats.ts (1)
57-78:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse a neutral fallback when exact counts do not prove an overage.
If
seat_limit_exceededistruebut the exact counts are present andmemberCount <= billedSeatCount, this now skips the detailed branch and still falls through to a message claiming the team has more members than paid seats. That leaves the edge case misleading; the fallback should avoid asserting the cause unless the counts confirm it.💡 Suggested change
return audience === "admin" - ? "This team has more members than paid seats. Team usage is paused until seats are added or members are removed." - : "This team has more members than paid seats. Contact your team admin to add paid seats or remove members."; + ? "This team's seat usage needs attention. Team usage is paused until seats are added or members are removed." + : "This team's seat usage needs attention. Contact your team admin to review seats and membership.";🤖 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 `@frontend/src/utils/teamSeats.ts` around lines 57 - 78, The fallback message wrongly asserts "more members than paid seats" even when mismatch.hasExactCounts is true and the counts (mismatch.memberCount and mismatch.billedSeatCount) do not indicate an overage; update the logic in the function that returns the team seats message so if mismatch.hasExactCounts is true but memberCount <= billedSeatCount it returns a neutral fallback (based on audience) rather than the assertive overage string — locate the branch using mismatch.hasExactCounts, mismatch.memberCount, mismatch.billedSeatCount and audience and add a conditional to return a neutral message when exact counts contradict an overage.
🤖 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 `@frontend/src/utils/teamSeats.ts`:
- Around line 57-78: The fallback message wrongly asserts "more members than
paid seats" even when mismatch.hasExactCounts is true and the counts
(mismatch.memberCount and mismatch.billedSeatCount) do not indicate an overage;
update the logic in the function that returns the team seats message so if
mismatch.hasExactCounts is true but memberCount <= billedSeatCount it returns a
neutral fallback (based on audience) rather than the assertive overage string —
locate the branch using mismatch.hasExactCounts, mismatch.memberCount,
mismatch.billedSeatCount and audience and add a conditional to return a neutral
message when exact counts contradict an overage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 93529ee0-f452-4c15-96d2-f42d86723eda
📒 Files selected for processing (2)
frontend/src/components/team/TeamInviteDialog.tsxfrontend/src/utils/teamSeats.ts
Co-Authored-By: tony@opensecret.cloud <TonyGiorgio@protonmail.com>
Co-Authored-By: tony@opensecret.cloud <TonyGiorgio@protonmail.com>
0188cfc to
0c72fe3
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
frontend/src/components/team/TeamInviteDialog.tsx (1)
35-38:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize seat availability from all supported seat fields, not just
seats_available.Lines 35-38 can still produce false
0available seats whenseats_availableis missing but exact billed/member counts are present, which incorrectly blocks invites.🐛 Proposed fix
import { openBillingPortal } from "`@/billing/billingPortal`"; import { useLocalState } from "`@/state/useLocalState`"; import type { TeamStatus } from "`@/types/team`"; +import { getTeamSeatCounts } from "`@/utils/teamSeats`"; @@ - const seatsAvailable = Math.max(0, teamStatus?.seats_available || 0); + const seatCounts = getTeamSeatCounts(teamStatus); + const seatsAvailable = + seatCounts.seatsAvailable ?? + (seatCounts.memberCount !== null && seatCounts.billedSeatCount !== null + ? Math.max(0, seatCounts.billedSeatCount - seatCounts.memberCount) + : 0);🤖 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 `@frontend/src/components/team/TeamInviteDialog.tsx` around lines 35 - 38, Fix seatsAvailable calculation to consider all supported seat fields on teamStatus instead of only seats_available: when computing seatsAvailable (the const currently set to Math.max(0, teamStatus?.seats_available || 0)), check fallbacks like billed_seats, seats, total_seats (or similarly named exact billed/member count fields available on teamStatus) and, if member counts exist (member_count or users_count), subtract current members to derive available seats, then clamp with Math.max(0, ...). Update the seatsAvailable binding to use this normalized logic so missing seats_available doesn't yield a false zero while leaving canOpenBillingPortal logic unchanged.
🤖 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 `@frontend/src/app.tsx`:
- Line 18: The import for TeamSeatMismatchAlert in app.tsx uses a relative path;
update the import to use the project path alias by replacing the current
relative import with the alias form (import { TeamSeatMismatchAlert } from
"`@/components/team/TeamSeatMismatchAlert`";) so it follows the "`@/`..." mapping
used across the TypeScript/React codebase and matches the coding guidelines.
In `@frontend/src/components/team/TeamSeatMismatchAlert.tsx`:
- Line 9: The import in TeamSeatMismatchAlert.tsx currently uses a relative path
for TeamManagementDialog; change it to use the project path alias by replacing
the relative import with the alias-based import (e.g., import {
TeamManagementDialog } from "`@/components/team/TeamManagementDialog`") so it
follows the coding guideline that `@/*` maps to `./src/*` and keeps imports
consistent; update the import statement where TeamManagementDialog is
referenced.
---
Duplicate comments:
In `@frontend/src/components/team/TeamInviteDialog.tsx`:
- Around line 35-38: Fix seatsAvailable calculation to consider all supported
seat fields on teamStatus instead of only seats_available: when computing
seatsAvailable (the const currently set to Math.max(0,
teamStatus?.seats_available || 0)), check fallbacks like billed_seats, seats,
total_seats (or similarly named exact billed/member count fields available on
teamStatus) and, if member counts exist (member_count or users_count), subtract
current members to derive available seats, then clamp with Math.max(0, ...).
Update the seatsAvailable binding to use this normalized logic so missing
seats_available doesn't yield a false zero while leaving canOpenBillingPortal
logic unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cb91ab5a-5ecd-4da4-9c3d-44674c444a28
📒 Files selected for processing (8)
frontend/src/app.tsxfrontend/src/billing/billingPortal.tsfrontend/src/components/AccountMenu.tsxfrontend/src/components/team/TeamDashboard.tsxfrontend/src/components/team/TeamInviteDialog.tsxfrontend/src/components/team/TeamSeatMismatchAlert.tsxfrontend/src/types/team.tsfrontend/src/utils/teamSeats.ts
| import { ThemeProvider } from "./contexts/ThemeContext"; | ||
| import { ProxyEventListener } from "./components/ProxyEventListener"; | ||
| import { UpdateEventListener } from "./components/UpdateEventListener"; | ||
| import { TeamSeatMismatchAlert } from "./components/team/TeamSeatMismatchAlert"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Switch the new TeamSeatMismatchAlert import to the @/ path alias.
Line 18 should use the project alias instead of a relative import.
♻️ Proposed fix
-import { TeamSeatMismatchAlert } from "./components/team/TeamSeatMismatchAlert";
+import { TeamSeatMismatchAlert } from "`@/components/team/TeamSeatMismatchAlert`";As per coding guidelines "Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { TeamSeatMismatchAlert } from "./components/team/TeamSeatMismatchAlert"; | |
| import { TeamSeatMismatchAlert } from "`@/components/team/TeamSeatMismatchAlert`"; |
🤖 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 `@frontend/src/app.tsx` at line 18, The import for TeamSeatMismatchAlert in
app.tsx uses a relative path; update the import to use the project path alias by
replacing the current relative import with the alias form (import {
TeamSeatMismatchAlert } from "`@/components/team/TeamSeatMismatchAlert`";) so it
follows the "`@/`..." mapping used across the TypeScript/React codebase and
matches the coding guidelines.
| import { getBillingService } from "@/billing/billingService"; | ||
| import type { TeamStatus } from "@/types/team"; | ||
| import { formatTeamSeatMismatchMessage, getTeamSeatMismatch } from "@/utils/teamSeats"; | ||
| import { TeamManagementDialog } from "./TeamManagementDialog"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use path alias for the TeamManagementDialog import.
Line 9 should use the @/ alias instead of a relative import.
♻️ Proposed fix
-import { TeamManagementDialog } from "./TeamManagementDialog";
+import { TeamManagementDialog } from "`@/components/team/TeamManagementDialog`";As per coding guidelines "Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { TeamManagementDialog } from "./TeamManagementDialog"; | |
| import { TeamManagementDialog } from "`@/components/team/TeamManagementDialog`"; |
🤖 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 `@frontend/src/components/team/TeamSeatMismatchAlert.tsx` at line 9, The import
in TeamSeatMismatchAlert.tsx currently uses a relative path for
TeamManagementDialog; change it to use the project path alias by replacing the
relative import with the alias-based import (e.g., import { TeamManagementDialog
} from "`@/components/team/TeamManagementDialog`") so it follows the coding
guideline that `@/*` maps to `./src/*` and keeps imports consistent; update the
import statement where TeamManagementDialog is referenced.
Co-Authored-By: tony@opensecret.cloud <TonyGiorgio@protonmail.com>
Summary
Adds persistent team seat mismatch detection and alerting across the app when team members exceed purchased seats.
New files:
utils/teamSeats.ts— Pure helpers (getTeamSeatCounts,getTeamSeatMismatch,formatTeamSeatMismatchMessage) that normalize seat counts from multiple API fields and detect overage via client-side count comparison OR the server'sseat_limit_exceededflag.billing/billingPortal.ts— SharedopenBillingPortal()helper (replaces duplicated platform-specific logic inTeamInviteDialog).components/team/TeamSeatMismatchAlert.tsx— Fixed-position toast alert rendered at app root; shows admin/member-appropriate messaging with a "Manage Team" / "Team Info" CTA.Modified files:
AccountMenu— Badge changes from warning (Setup Required) to destructive (Paused) when seat mismatch detected.TeamDashboard— Admin view gets mismatch banner with "Manage Members" and "Add Seats" buttons; member view gets read-only warning. Seat usage bar turns destructive on mismatch. Invite button disabled during mismatch.TeamInviteDialog— Billing portal logic replaced with shared helper;type="button"added to prevent form submission;canOpenBillingPortalcheck tightened.types/team.ts—TeamStatusextended withbilled_seat_countandteam_member_count.app.tsx— Renders<TeamSeatMismatchAlert />at app root.Bug fixes from review:
memberCount > billedSeatCountguard to bothformatTeamSeatMismatchMessageand the alert summary to prevent contradictory display (e.g. "3 members, 5 paid seats" alongside "more members than paid seats") whenseat_limit_exceededis stale.type="button"to billing CTA in invite dialog to prevent it from also submitting the invite form.Review & Testing Checklist for Human
seat_limit_exceededis true orteam_member_count > billed_seat_count. Confirm the fixed-position alert appears, shows correct counts, and the "Manage Team" button opens the team dialog.billingStatus+teamStatusqueries refresh afterward.canOpenBillingPortaledge cases — The check changed fromstripe_customer_id !== nullto!!stripe_customer_idwith fallback tohas_team_subscription. Confirm users without Stripe accounts don't see billing buttons.Recommended test plan: Use a team account, temporarily set
seat_limit_exceeded: trueserver-side (or add more members than seats), and walk through all surfaces: app-level alert, account menu badge, team dashboard, and invite dialog.Notes
memberCount > billedSeatCount) OR the server'sseat_limit_exceededflag. When the server flag fires but exact counts aren't available or don't confirm overage, a generic message is shown without specific numbers.Link to Devin session: https://app.devin.ai/sessions/3297c6fa90564002a802713f48b3e22c
Requested by: @AnthonyRonning