feat(dashboard): flat-tier pricing UX — owner-as-container checkout funnel + named confirmation - #28
feat(dashboard): flat-tier pricing UX — owner-as-container checkout funnel + named confirmation#28NicolasRitouet wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughThe 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. ChangesPlan contracts and exposure access
Personal and organization checkout flows
Plan labels and Free-tier limits
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
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. Comment |
…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>
7b0959b to
d816c89
Compare
There was a problem hiding this comment.
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 winImport
SubscriptionDatainstead of redeclaring it locally.
lib/api/billing.tsnow exports aSubscriptionDatatype with this exact shape. Redeclaring it here (and separately inSettingsPage.test.tsx) means thepro-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 winAlign 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 winUse
useAuthas the single authentication-state source.The one-time cookie check can become stale and disagree with
user, enabling the checkout UI after authentication changes. DestructureisLoadingandisAuthenticatedfromuseAuthinstead.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 winFree-plan private-vault limit ("10") is defined independently in multiple files.
packages/dashboard/app/(dashboard)/page.tsxdefines a localFREE_PRIVATE_VAULT_LIMIT = 10constant whilepackages/dashboard/app/(dashboard)/settings/page.tsxhardcodes the same value as a literal string, and (per referenced context)VaultDetailHeader.tsxdefines 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: exportFREE_PRIVATE_VAULT_LIMITfrom a shared module (e.g.lib/types.tsor a newlib/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-fetchedusageData.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 winAdd an assertion for the restored checkout analytics event.
Mock
@/lib/analyticsand verifyUPGRADE_CLICKfires 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 winReplace
space-y-2with a flex gap.Use
flex flex-col gap-2for the account-option container.As per coding guidelines, “Always use
gaputilities for internal spacing in flex and grid layouts instead ofspace-x-*orspace-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 winUse
size-*for the changed square icons.
packages/dashboard/app/(dashboard)/upgrade/page.tsx#L319-L321: replacew-8 h-8withsize-8.packages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsx#L111-L113: replaceh-4 w-4withsize-4.As per coding guidelines, “Prefer
size-*utilities over separatew-*andh-*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 liftMove 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
useQuerycalls 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
📒 Files selected for processing (20)
packages/backend/src/services/organization.service.tspackages/dashboard/app/(dashboard)/orgs/[org]/billing/page.tsxpackages/dashboard/app/(dashboard)/page.tsxpackages/dashboard/app/(dashboard)/security/_components/SecurityExposureTab.tsxpackages/dashboard/app/(dashboard)/settings/page.tsxpackages/dashboard/app/(dashboard)/upgrade/page.tsxpackages/dashboard/app/components/dashboard/Sidebar.tsxpackages/dashboard/app/components/dashboard/VaultDetailHeader.tsxpackages/dashboard/lib/analytics.tspackages/dashboard/lib/api/billing.tspackages/dashboard/lib/api/users.tspackages/dashboard/lib/types.tspackages/dashboard/tests/Sidebar.test.tsxpackages/dashboard/tests/VaultDetailHeader.test.tsxpackages/dashboard/tests/api/billing.test.tspackages/dashboard/tests/api/users.test.tspackages/dashboard/tests/components/SecurityExposureTab.test.tsxpackages/dashboard/tests/pages/OrgBillingPage.test.tsxpackages/dashboard/tests/pages/SettingsPage.test.tsxpackages/dashboard/tests/pages/UpgradePage.test.tsx
💤 Files with no reviewable changes (1)
- packages/dashboard/lib/analytics.ts
| }, []) | ||
|
|
||
| // 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]) |
There was a problem hiding this comment.
📐 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
| 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', | ||
| }) |
There was a problem hiding this comment.
🗄️ 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.
| <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> |
There was a problem hiding this comment.
🗄️ 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>
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.
/v1/billing/create-checkout-session, org via/v1/organizations/:org/billing/checkout). Fixes two broken links inherited from the old page (/dashboardand/dashboard/settingsare not routes).upgrade_clickanalytics event restored at real checkout start.GET /v1/orgsnow includes the requesting user'srole— the client always declared it, the serializer never sent it; the account picker is the first UI to depend on it.UserPlanloses'pro'and is reused by API clients;PLAN_LIMITScopies 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)
Tests
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes