Skip to content

feat(dashboard): flat-tier pricing UX — owner-as-container checkout funnel + named confirmation - #28

Open
NicolasRitouet wants to merge 3 commits into
mainfrom
feat/dashboard-flat-pricing
Open

feat(dashboard): flat-tier pricing UX — owner-as-container checkout funnel + named confirmation#28
NicolasRitouet wants to merge 3 commits into
mainfrom
feat/dashboard-flat-pricing

Conversation

@NicolasRitouet

@NicolasRitouet NicolasRitouet commented Jul 15, 2026

Copy link
Copy Markdown
Member

What

Decision update (2026-07-15): owner-as-container. Any GitHub account — personal or organization — can subscribe to the same flat Team/Business plans. This PR turns /upgrade into a complete checkout funnel and cleans up all stale plan references.

  • /upgrade rework: three flat cards (Free / Team / Business) with per-account pricing from the API (all-or-nothing fallback so live Stripe amounts are never mixed with hardcoded ones). Each paid card offers Monthly/Yearly → an account picker (your personal account + your orgs; owners first, members and already-subscribed orgs disabled with the reason) → a named confirmation spelling out which account, which plan, at what price → Stripe Checkout, directly from the page (personal via /v1/billing/create-checkout-session, org via /v1/organizations/:org/billing/checkout). Fixes two broken links inherited from the old page (/dashboard and /dashboard/settings are not routes).
  • Named checkout confirmation on the org billing page too, with the upgrade_click analytics event restored at real checkout start.
  • Org-aware Exposure access: the Security tab gated on the personal plan, hiding reports from members of a Business organization. It now unlocks when the personal plan or any of the user's orgs is on Business, defaulting the scope to that org (backend still enforces entitlements per request).
  • Backend companion fix: GET /v1/orgs now includes the requesting user's role — the client always declared it, the serializer never sent it; the account picker is the first UI to depend on it.
  • Stale plan cleanup: UserPlan loses 'pro' and is reused by API clients; PLAN_LIMITS copies claiming 1 private repo on Free (it's 10, paid tiers unlimited); hardcoded €9/€19/€39 badges in Settings; 'Upgrade to Pro' CTAs; shared currency/amount formatting.

Merge order

Independent of #26 (which is now pure billing hardening — TTL + origin allowlist). Merge in any order.

Follow-ups (deliberate non-goals)

  • Stripe product descriptions still say « per organization » (Test+Live copy tweak, with the landing pass)
  • Unify the marketing feature lists between /upgrade and org billing
  • Migrate /upgrade data fetching to React Query

Tests

  • Dashboard: 785 green, incl. new coverage — account picker states (personal / owner / member / already-paid), named confirmation for personal and org, interval carried through the funnel, cancel aborts, fallback prices with contact CTA
  • Backend: 949 green (role field)
  • E2E in Chrome against local backend + Stripe Test: personal checkout AND org checkout both reach Stripe with the right price

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Upgrades now support purchasing for your personal account or an eligible organization (owners), with an account picker followed by a named checkout confirmation step.
    • Organization billing shows a clear subscription summary (org, plan, currency, formatted price) before checkout starts.
    • Security exposure reporting now adapts to personal access vs Business-owned organization ownership, including auto-selecting the right scope.
  • Bug Fixes

    • Removed Pro-plan wording across billing, sidebar, security, settings, and upgrade flows.
    • Standardized Free-tier messaging/limits to 10 private vaults or repositories and updated the plan-limit prompt to “Upgrade to Team.”

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
keyway-dashboard Ready Ready Preview, Comment Jul 16, 2026 5:20pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d21e0511-aab3-4d0d-a3c0-d9748e1a9a9a

📥 Commits

Reviewing files that changed from the base of the PR and between d816c89 and f810902.

📒 Files selected for processing (9)
  • packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsx
  • packages/dashboard/app/(dashboard)/settings/page.tsx
  • packages/dashboard/app/(dashboard)/upgrade/layout.tsx
  • packages/dashboard/app/(dashboard)/upgrade/page.tsx
  • packages/dashboard/lib/analytics.ts
  • packages/dashboard/next-env.d.ts
  • packages/dashboard/tests/components/SecurityExposureTab.test.tsx
  • packages/dashboard/tests/pages/UpgradePage.test.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/dashboard/tests/pages/UpgradePage.test.tsx
  • packages/dashboard/app/(dashboard)/settings/page.tsx
  • packages/dashboard/lib/analytics.ts
  • packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsx
  • packages/dashboard/app/(dashboard)/upgrade/page.tsx

📝 Walkthrough

Walkthrough

The dashboard retires the Pro tier, standardizes billing contracts and price formatting, adds personal or organization checkout confirmation flows, updates Free-tier limits, and grants exposure access through Business organizations. Organization listings now include the requesting user’s role.

Changes

Plan contracts and exposure access

Layer / File(s) Summary
Plan and organization contracts
packages/backend/src/services/organization.service.ts, packages/dashboard/lib/*, packages/dashboard/app/(dashboard)/security/...
Plan types now cover Free, Team, and Business; billing responses use shared contracts and helpers; organization listings include member roles; exposure entitlement inputs use organization ownership and Business plans.
Organization-scoped exposure access
packages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsx, packages/dashboard/tests/components/SecurityExposureTab.test.tsx
Exposure access loads organizations, selects eligible owned Business organizations, avoids invalid personal requests, and covers owner and member cases.

Personal and organization checkout flows

Layer / File(s) Summary
Personal and organization checkout flows
packages/dashboard/app/(dashboard)/upgrade/page.tsx, packages/dashboard/tests/pages/UpgradePage.test.tsx
The upgrade page selects an account, confirms the tier and interval, then creates the appropriate personal or organization checkout session.
Organization billing confirmation
packages/dashboard/app/(dashboard)/orgs/.../billing/page.tsx, packages/dashboard/lib/analytics.ts, packages/dashboard/tests/pages/OrgBillingPage.test.tsx
Organization billing uses shared price formatting and a named confirmation dialog before checkout, with checkout-start analytics tracking.

Plan labels and Free-tier limits

Layer / File(s) Summary
Plan labels and Free-tier limits
packages/dashboard/app/(dashboard)/page.tsx, packages/dashboard/app/(dashboard)/settings/page.tsx, packages/dashboard/app/components/dashboard/*, packages/dashboard/tests/pages/*, packages/dashboard/tests/Sidebar.test.tsx, packages/dashboard/tests/api/*
Pro labels and dynamic limits are removed, Free-tier limits become 10 private repositories or vaults, and paid-plan pricing badges are removed from settings UI.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UpgradePage
  participant BillingAPI
  participant Stripe
  User->>UpgradePage: choose Team or Business tier
  UpgradePage->>BillingAPI: fetch prices and organizations
  BillingAPI-->>UpgradePage: return prices and eligible accounts
  UpgradePage-->>User: show account picker and confirmation
  User->>UpgradePage: confirm checkout
  UpgradePage->>BillingAPI: create personal or organization session
  BillingAPI->>Stripe: create checkout session
  Stripe-->>UpgradePage: return checkout URL
Loading

Possibly related PRs

  • keywaysh/keyway#22: Overlaps with the plan model, Stripe price resolution, and Business exposure access changes.
  • keywaysh/keyway#23: Also updates billing contracts and upgrade handling for the retired Pro pricing data.
  • keywaysh/keyway#26: Relates to organization-scoped checkout and changes to personal checkout behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main dashboard pricing and checkout-flow changes, including flat-tier pricing, owner-based checkout, and named confirmation.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-flat-pricing

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

NicolasRitouet and others added 2 commits July 15, 2026 11:32
…kout confirmation

The /upgrade page now reflects flat-tier pricing: three cards (Free, Team,
Business) with flat per-organization prices resolved from the API (an
all-or-nothing fallback so live Stripe amounts are never mixed with
hardcoded ones), and org-first CTAs — paid plans are subscribed from the
organization billing page, so the personal checkout flow and its client
method are removed. Users with several orgs get a picker (owners first,
members flagged); users without any are pointed at org connection; legacy
personal subscriptions keep a Settings banner. Also fixes two broken links
inherited from the old page (/dashboard and /dashboard/settings are not
routes).

The org billing page now asks for a named confirmation before redirecting
to Stripe: it spells out which organization is being subscribed, to which
plan, at what price and interval — and checkout starts emit the
upgrade_click analytics event again.

Exposure access is now org-aware: the Security tab used to gate on the
personal plan only, hiding reports from members of a Business organization
— the buyers of the feature. It now unlocks when any of the user's orgs is
on Business and defaults the scope to that org (the backend still enforces
entitlements per request).

Stale plan references are cleaned up across the dashboard: UserPlan loses
'pro' (and is now reused by the API clients instead of inline unions),
PLAN_LIMITS copies claiming 1 private repo on Free (it is 10, and paid
tiers are unlimited), hardcoded €9/€19/€39 badges in Settings, 'Upgrade to
Pro' CTAs, and shared currency/amount formatting replaces per-page copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard client has always declared a role field on the org list
response, but the serializer never included it — every consumer reading
org.role got undefined. The account picker on the upgrade page is the first
UI to depend on it (owners can subscribe an org, members can't).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NicolasRitouet
NicolasRitouet force-pushed the feat/dashboard-flat-pricing branch from 7b0959b to d816c89 Compare July 15, 2026 09:34
@NicolasRitouet NicolasRitouet changed the title feat(dashboard): flat-tier pricing UX — org-first upgrade + named checkout confirmation feat(dashboard): flat-tier pricing UX — owner-as-container checkout funnel + named confirmation Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
packages/dashboard/app/(dashboard)/settings/page.tsx (1)

19-45: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Import SubscriptionData instead of redeclaring it locally.

lib/api/billing.ts now exports a SubscriptionData type with this exact shape. Redeclaring it here (and separately in SettingsPage.test.tsx) means the pro-removal change already had to be applied in multiple places by hand — importing the shared type would make future contract changes single-sourced.

♻️ Suggested fix
-type UsageData = {
-  plan: 'free' | 'team' | 'business'
-  limits: {
-    maxPublicRepos: string | number
-    maxPrivateRepos: string | number
-    maxProviders: string | number
-    maxEnvironmentsPerVault: string | number
-    maxSecretsPerPrivateVault: string | number
-  }
-  usage: {
-    public: number
-    private: number
-    providers: number
-  }
-}
-
-type SubscriptionData = {
-  subscription: {
-    id: string
-    status: string
-    currentPeriodEnd: string
-    cancelAtPeriodEnd: boolean
-  } | null
-  plan: 'free' | 'team' | 'business'
-  billingStatus: 'active' | 'past_due' | 'canceled' | 'trialing'
-  stripeCustomerId: string | null
-}
+import type { SubscriptionData } from '`@/lib/api/billing`'
+
+type UsageData = {
+  plan: 'free' | 'team' | 'business'
+  limits: {
+    maxPublicRepos: string | number
+    maxPrivateRepos: string | number
+    maxProviders: string | number
+    maxEnvironmentsPerVault: string | number
+    maxSecretsPerPrivateVault: string | number
+  }
+  usage: {
+    public: number
+    private: number
+    providers: number
+  }
+}
🤖 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/dashboard/app/`(dashboard)/settings/page.tsx around lines 19 - 45,
Remove the local SubscriptionData declaration in the settings page and import
the shared SubscriptionData type from lib/api/billing.ts. Update any related
test usage such as SettingsPage.test.tsx to reuse that exported type rather than
redeclaring it, while preserving the existing shape and behavior.
packages/dashboard/tests/api/users.test.ts (1)

78-85: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align this Team fixture with the canonical Team limits.

The fixture still assigns finite private-repository, provider, and environment limits while the changed Team contract advertises those resources as unlimited. Update these values so this test does not preserve stale Pro-era limits.

🤖 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/dashboard/tests/api/users.test.ts` around lines 78 - 85, Update the
Team fixture’s limits object in the users test so maxPrivateRepos, maxProviders,
and maxEnvironmentsPerVault use the canonical unlimited values instead of finite
Pro-era limits. Leave maxPublicRepos and maxSecretsPerPrivateVault unchanged.
packages/dashboard/app/(dashboard)/upgrade/page.tsx (1)

92-110: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use useAuth as the single authentication-state source.

The one-time cookie check can become stale and disagree with user, enabling the checkout UI after authentication changes. Destructure isLoading and isAuthenticated from useAuth instead.

As per coding guidelines, “Use the useAuth hook from '@/lib/auth' to access authentication state (user, isLoading, isAuthenticated).”

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 92 - 110,
The UpgradePage authentication state should come exclusively from useAuth rather
than a one-time cookie check. Destructure isLoading and isAuthenticated
alongside user, remove the isLoggedIn state and its cookie-reading useEffect,
and update checkout/loading conditions to use these hook values consistently.

Source: Coding guidelines

🧹 Nitpick comments (5)
packages/dashboard/app/(dashboard)/page.tsx (1)

32-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Free-plan private-vault limit ("10") is defined independently in multiple files. packages/dashboard/app/(dashboard)/page.tsx defines a local FREE_PRIVATE_VAULT_LIMIT = 10 constant while packages/dashboard/app/(dashboard)/settings/page.tsx hardcodes the same value as a literal string, and (per referenced context) VaultDetailHeader.tsx defines its own separate copy of the same constant — three independent sources of truth for one business rule that this very PR had to update in lockstep (1 → 10).

  • packages/dashboard/app/(dashboard)/page.tsx#L32-L34: export FREE_PRIVATE_VAULT_LIMIT from a shared module (e.g. lib/types.ts or a new lib/plan-limits.ts) instead of a local const, so all consumers import one value.
  • packages/dashboard/app/(dashboard)/settings/page.tsx#L233-L247: replace the hardcoded "10 private repos" string with the shared constant (or better, the already-fetched usageData.limits.maxPrivateRepos) so the Billing section text can't drift from the Usage section's live limit.
🤖 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/dashboard/app/`(dashboard)/page.tsx around lines 32 - 34, Move
FREE_PRIVATE_VAULT_LIMIT from the dashboard page into a shared limits module and
update its consumers, including VaultDetailHeader.tsx, to import that single
value. In packages/dashboard/app/(dashboard)/settings/page.tsx#L233-L247,
replace the hardcoded “10 private repos” text with the shared constant or the
fetched usageData.limits.maxPrivateRepos value so the Billing and Usage sections
remain synchronized.
packages/dashboard/tests/pages/OrgBillingPage.test.tsx (1)

145-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the restored checkout analytics event.

Mock @/lib/analytics and verify UPGRADE_CLICK fires once, with the organization, plan, and interval, only after confirmation. This behavior is an explicit PR objective but currently has no regression coverage.

🤖 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/dashboard/tests/pages/OrgBillingPage.test.tsx` around lines 145 -
168, Add coverage in the “should start checkout only after confirmation” test
for the restored checkout analytics event: mock "`@/lib/analytics`" and assert
UPGRADE_CLICK is called exactly once with the organization, selected plan, and
yearly interval after clicking “Continue to checkout,” while preserving the
existing checkout-session assertion.
packages/dashboard/app/(dashboard)/upgrade/page.tsx (3)

439-442: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace space-y-2 with a flex gap.

Use flex flex-col gap-2 for the account-option container.

As per coding guidelines, “Always use gap utilities for internal spacing in flex and grid layouts instead of space-x-* or space-y-*.”

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 439 - 442,
Update the account-option container wrapping the personal target button in the
upgrade page to use flex column layout with gap spacing: replace the space-y-2
utility on its className with flex flex-col gap-2, preserving all other classes
and behavior.

Source: Coding guidelines


319-321: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use size-* for the changed square icons.

  • packages/dashboard/app/(dashboard)/upgrade/page.tsx#L319-L321: replace w-8 h-8 with size-8.
  • packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx#L111-L113: replace h-4 w-4 with size-4.

As per coding guidelines, “Prefer size-* utilities over separate w-* and h-* when setting equal dimensions.”

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 319 - 321,
Replace the equal-dimension utilities on the Loader2 icons with size utilities:
use size-8 in packages/dashboard/app/(dashboard)/upgrade/page.tsx lines 319-321
and size-4 in packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx
lines 111-113, preserving the existing icon styling.

Source: Coding guidelines


119-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move these client-side requests to React Query.

The added organizations request extends the manual loading/error state and silently converts failures into an empty organization list. Separate useQuery calls can preserve error state and caching.

As per coding guidelines, “Use client-side data fetching with React Query (useQuery hooks).”

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 119 - 146,
Replace the manual fetchData useEffect flow with separate React Query useQuery
hooks for prices, subscription, and organizations. Preserve the existing
unauthorized subscription handling while retaining each request’s loading,
error, caching, and data states; derive the page’s loading and displayed values
from the query results instead of setLoading, setPrices, setSubscription, and
setOrgs.

Source: Coding guidelines

🤖 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/dashboard/app/`(dashboard)/security/_components/SecurityExposureTab.tsx:
- Around line 295-304: Refactor the organizations data-fetching flow in
SecurityExposureTab to use React Query useQuery hooks instead of useEffect and
manual loading state such as isLoadingOrgs. Move organization loading, caching,
dependency management, and related derived state into the query-based flow,
while preserving the existing fallback that selects the first business
organization when personal access is unavailable and selectedOrg is "all".

In `@packages/dashboard/app/`(dashboard)/upgrade/page.tsx:
- Around line 158-178: The upgrade_click event is emitted prematurely and
duplicated for completed checkouts. Remove the trackEvent call from choosePlan,
keeping the existing event in startCheckout as the sole upgrade_click emission
when checkout begins.
- Around line 440-463: The personal checkout button in the target-selection UI
must be disabled when hasPersonalSubscription is true. Match the existing
paid-organization handling by preventing chooseTarget({ kind: 'personal' }) and
directing the user to Settings instead, while preserving normal checkout
behavior for unsubscribed accounts.

---

Outside diff comments:
In `@packages/dashboard/app/`(dashboard)/settings/page.tsx:
- Around line 19-45: Remove the local SubscriptionData declaration in the
settings page and import the shared SubscriptionData type from
lib/api/billing.ts. Update any related test usage such as SettingsPage.test.tsx
to reuse that exported type rather than redeclaring it, while preserving the
existing shape and behavior.

In `@packages/dashboard/app/`(dashboard)/upgrade/page.tsx:
- Around line 92-110: The UpgradePage authentication state should come
exclusively from useAuth rather than a one-time cookie check. Destructure
isLoading and isAuthenticated alongside user, remove the isLoggedIn state and
its cookie-reading useEffect, and update checkout/loading conditions to use
these hook values consistently.

In `@packages/dashboard/tests/api/users.test.ts`:
- Around line 78-85: Update the Team fixture’s limits object in the users test
so maxPrivateRepos, maxProviders, and maxEnvironmentsPerVault use the canonical
unlimited values instead of finite Pro-era limits. Leave maxPublicRepos and
maxSecretsPerPrivateVault unchanged.

---

Nitpick comments:
In `@packages/dashboard/app/`(dashboard)/page.tsx:
- Around line 32-34: Move FREE_PRIVATE_VAULT_LIMIT from the dashboard page into
a shared limits module and update its consumers, including
VaultDetailHeader.tsx, to import that single value. In
packages/dashboard/app/(dashboard)/settings/page.tsx#L233-L247, replace the
hardcoded “10 private repos” text with the shared constant or the fetched
usageData.limits.maxPrivateRepos value so the Billing and Usage sections remain
synchronized.

In `@packages/dashboard/app/`(dashboard)/upgrade/page.tsx:
- Around line 439-442: Update the account-option container wrapping the personal
target button in the upgrade page to use flex column layout with gap spacing:
replace the space-y-2 utility on its className with flex flex-col gap-2,
preserving all other classes and behavior.
- Around line 319-321: Replace the equal-dimension utilities on the Loader2
icons with size utilities: use size-8 in
packages/dashboard/app/(dashboard)/upgrade/page.tsx lines 319-321 and size-4 in
packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx lines 111-113,
preserving the existing icon styling.
- Around line 119-146: Replace the manual fetchData useEffect flow with separate
React Query useQuery hooks for prices, subscription, and organizations. Preserve
the existing unauthorized subscription handling while retaining each request’s
loading, error, caching, and data states; derive the page’s loading and
displayed values from the query results instead of setLoading, setPrices,
setSubscription, and setOrgs.

In `@packages/dashboard/tests/pages/OrgBillingPage.test.tsx`:
- Around line 145-168: Add coverage in the “should start checkout only after
confirmation” test for the restored checkout analytics event: mock
"`@/lib/analytics`" and assert UPGRADE_CLICK is called exactly once with the
organization, selected plan, and yearly interval after clicking “Continue to
checkout,” while preserving the existing checkout-session assertion.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37920956-9211-4053-8316-613eef5fb2e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6f99ed9 and d816c89.

📒 Files selected for processing (20)
  • packages/backend/src/services/organization.service.ts
  • packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx
  • packages/dashboard/app/(dashboard)/page.tsx
  • packages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsx
  • packages/dashboard/app/(dashboard)/settings/page.tsx
  • packages/dashboard/app/(dashboard)/upgrade/page.tsx
  • packages/dashboard/app/components/dashboard/Sidebar.tsx
  • packages/dashboard/app/components/dashboard/VaultDetailHeader.tsx
  • packages/dashboard/lib/analytics.ts
  • packages/dashboard/lib/api/billing.ts
  • packages/dashboard/lib/api/users.ts
  • packages/dashboard/lib/types.ts
  • packages/dashboard/tests/Sidebar.test.tsx
  • packages/dashboard/tests/VaultDetailHeader.test.tsx
  • packages/dashboard/tests/api/billing.test.ts
  • packages/dashboard/tests/api/users.test.ts
  • packages/dashboard/tests/components/SecurityExposureTab.test.tsx
  • packages/dashboard/tests/pages/OrgBillingPage.test.tsx
  • packages/dashboard/tests/pages/SettingsPage.test.tsx
  • packages/dashboard/tests/pages/UpgradePage.test.tsx
💤 Files with no reviewable changes (1)
  • packages/dashboard/lib/analytics.ts

Comment on lines +295 to +304
}, [])

// Without personal access, the default "all" scope (personal exposure)
// would 403 — start on the first Business organization instead
useEffect(() => {
if (!hasPersonalAccess && selectedOrg === 'all' && businessOrgs.length > 0) {
setSelectedOrg(businessOrgs[0].login)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organizations, hasPersonalAccess])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Migrate data fetching to React Query.

The organizations-related side effects and data fetching continue to use useEffect and local state. As per coding guidelines, client-side data fetching in the dashboard should use React Query (useQuery hooks) to manage loading states, dependencies, and caching automatically, rather than relying on useEffect and manual state flags like isLoadingOrgs.

🤖 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/dashboard/app/`(dashboard)/security/_components/SecurityExposureTab.tsx
around lines 295 - 304, Refactor the organizations data-fetching flow in
SecurityExposureTab to use React Query useQuery hooks instead of useEffect and
manual loading state such as isLoadingOrgs. Move organization loading, caching,
dependency management, and related derived state into the query-based flow,
while preserving the existing fallback that selects the first business
organization when personal access is unavailable and selectedOrg is "all".

Source: Coding guidelines

Comment on lines +158 to 178
const choosePlan = (tier: PaidTier, interval: BillingInterval) => {
trackEvent(AnalyticsEvents.UPGRADE_CLICK, { plan: tier, interval })
const price = apiPriceFor(tier, interval)
if (!price) return
setPicking({ tier, interval, price })
}

const chooseTarget = (target: CheckoutTarget) => {
if (!picking) return
setPendingCheckout({ ...picking, target })
setPicking(null)
}

const startCheckout = async () => {
if (!pendingCheckout) return
const { tier, interval, price, target } = pendingCheckout
trackEvent(AnalyticsEvents.UPGRADE_CLICK, {
plan,
plan: tier,
interval,
account: target.kind === 'org' ? target.org.login : 'personal',
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Emit upgrade_click only when checkout starts.

choosePlan records the event before account selection, then startCheckout records it again. Completed attempts are double-counted, while abandoned pickers are counted as checkout starts. Remove the first event or use a distinct plan-selection event.

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 158 - 178,
The upgrade_click event is emitted prematurely and duplicated for completed
checkouts. Remove the trackEvent call from choosePlan, keeping the existing
event in startCheckout as the sole upgrade_click emission when checkout begins.

Comment on lines +440 to +463
<button
onClick={() => chooseTarget({ kind: 'personal' })}
className="w-full flex items-center gap-3 p-3 rounded-lg border border-gray-800 hover:border-gray-700 hover:bg-gray-800 transition-colors text-left"
>
{user?.avatar_url ? (
<Image
src={user.avatar_url}
alt={user.github_username || 'you'}
width={32}
height={32}
className="rounded-full"
/>
) : (
<UserIcon className="size-8 text-gray-500" />
)}
<div className="flex-1">
<div className="text-sm font-medium text-white">
{user?.github_username || 'Personal account'}
</div>
<div className="text-xs text-gray-500">
Personal account · covers your own repos
</div>
</div>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Disable personal checkout for an already-subscribed account.

hasPersonalSubscription only renders a banner; the personal target still initiates another checkout. Disable this option and direct the user to Settings, matching the handling for paid organizations.

🤖 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/dashboard/app/`(dashboard)/upgrade/page.tsx around lines 440 - 463,
The personal checkout button in the target-selection UI must be disabled when
hasPersonalSubscription is true. Match the existing paid-organization handling
by preventing chooseTarget({ kind: 'personal' }) and directing the user to
Settings instead, while preserving normal checkout behavior for unsubscribed
accounts.

- Keep the confirmation dialog open through checkout: Radix closes on
  Action click by default, so the loading state never showed and errors
  landed after the funnel was torn down; errors now keep the dialog open
  for an in-context retry (both /upgrade and org billing)
- Disable the personal option in the account picker when a personal
  subscription is already active, matching the org rows
- Only a confirmed 'member' role disables an org row: role is missing on
  older backends and ownership is enforced server-side anyway
- Exposure: the backend serves org reports to owners only — gate the tab
  on owned Business orgs, don't fire the personal-scope request without
  personal access (its late 403 could overwrite valid data), and hide the
  'All organizations' scope when it would always fail
- Split analytics: UPGRADE_CLICK on plan selection, new checkout_start at
  confirmation on both pages (it was double-firing on /upgrade)
- The /upgrade SEO metadata still advertised the retired Pro tier
- aria-labels on Monthly/Yearly buttons (they were indistinguishable to
  screen readers, and forced order-dependent test selectors)
- Reuse the shared SubscriptionData type in settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant