Skip to content

Conversation

@themavik
Copy link

@themavik themavik commented Feb 9, 2026

Summary

Fixes #815.

Root cause: The layout's billing check at packages/web/src/app/[domain]/layout.tsx wraps ALL users in UpgradeGuard when the subscription is expired or inactive. This redirects every user — including non-owners — to the upgrade page. Non-owner users cannot perform billing actions, so showing them the upgrade prompt is misleading.

Fix: Check the user's organization role before redirecting to the upgrade page. Only org owners see the upgrade redirect. Non-owners see a static message asking them to contact their organization owner.

Changes

  • packages/web/src/app/[domain]/layout.tsx:
    • Hoisted membership variable from inside the if (session) block to the outer scope, making it available for the billing check.
    • Added role check in the billing guard: membership?.role === 'OWNER' determines whether to show the upgrade page or the "contact your owner" message.
    • Added fallback UI for non-owners: a centered card with "Subscription Expired" heading and instructions to contact the org owner.

Root Cause Analysis

The membership variable was scoped inside the if (session) block, making it inaccessible to the billing check further down in the layout. The billing guard had no way to distinguish between owners and regular users, so it treated all authenticated members equally.

Risk Assessment

  • Low risk: The change only affects the billing guard path — all other layout logic remains unchanged.
  • Anonymous users also see the "contact owner" message instead of the upgrade page, which is correct behavior.
  • Existing UpgradeGuard behavior is preserved for owners.
  • Type safety: The hoisted membership variable uses explicit Prisma type annotation to maintain type safety.

Summary by CodeRabbit

  • New Features
    • Role-based subscription expiry handling. Workspace owners now see upgrade prompts when subscriptions expire, allowing immediate action. Non-owner members receive an in-page notification directing them to contact their workspace owner to renew the subscription.

…ebot-dev#815)

Root cause: The layout's billing check wraps ALL users in UpgradeGuard
when the subscription is expired, redirecting everyone to the upgrade
page. Non-owner users cannot perform billing actions, so showing them
the upgrade page is confusing.

Fix: Hoist the membership variable from the auth block and check the
user's role at the billing guard. Only org owners are redirected to
the upgrade page. Non-owners see a message asking them to contact
their organization owner.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Walkthrough

This change modifies subscription expiry handling in the domain layout component to restrict upgrade prompts to organization owners only. Non-owners now receive an in-page "Subscription Expired" message instead of the upgrade guard popup, while unauthenticated and other flows remain unchanged.

Changes

Cohort / File(s) Summary
Subscription Expiry Handling
packages/web/src/app/[domain]/layout.tsx
Refactored billing guard logic to perform role-based filtering. Membership variable is now hoisted and checked earlier; UpgradeGuard renders only for owners (membership?.role === 'OWNER'), while non-owners receive an in-page "Subscription Expired" message with guidance to contact the org owner.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 describes the main change: restricting the upgrade prompt to organization owners only, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The implementation addresses all coding requirements from issue #815: membership variable hoisting enables role checks, UpgradeGuard is now restricted to owners, and non-owners receive alternative UI.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the billing/upgrade guard behavior in the layout file as required by issue #815; no unrelated modifications are present.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/web/src/app/`[domain]/layout.tsx:
- Around line 188-201: The current expired-subscription UI renders
LogoutEscapeHatch and an owner-contact message even for anonymous visitors;
update the else branch in layout.tsx to (1) only render <LogoutEscapeHatch> when
session is present (guard it with a session check) and (2) change the copy based
on whether session/membership is null—e.g., show "This organization's
subscription has expired." or other non-actionable text for anonymous users and
keep the "contact your organization owner" text for authenticated members;
locate the logic around session, membership and LogoutEscapeHatch to apply these
conditional checks and text variants.
🧹 Nitpick comments (1)
packages/web/src/app/[domain]/layout.tsx (1)

66-70: Consider simplifying the type annotation.

This Awaited<ReturnType<typeof prisma.userToOrg.findUnique<{…}>>> encoding is fragile—if the query shape changes, the standalone type must be updated in lockstep. Using Prisma's generated types (e.g., Prisma.UserToOrgGetPayload<{ include: { user: true } }> | null) or simply letting TypeScript infer the type with a two-step pattern would be cleaner:

♻️ Suggested simplification
-    // Hoist membership so it's available for the billing owner check below (`#815`)
-    let membership: Awaited<ReturnType<typeof prisma.userToOrg.findUnique<{
-        where: { orgId_userId: { orgId: string; userId: string } };
-        include: { user: true };
-    }>>> = null;
+    // Hoist membership so it's available for the billing owner check below (`#815`)
+    let membership: Prisma.UserToOrgGetPayload<{ include: { user: true } }> | null = null;

This requires importing Prisma from @prisma/client. Alternatively, just use the inferred type from the query result if the include shape is stable.

#!/bin/bash
# Check what Prisma types are available for UserToOrg
rg -n "UserToOrg" --type=ts -g '!node_modules/**' -l

Comment on lines +188 to +201
} else {
return (
<div className="min-h-screen flex items-center justify-center p-6">
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
<div className="text-center max-w-md">
<h2 className="text-xl font-semibold mb-2">Subscription Expired</h2>
<p className="text-muted-foreground">
Your organization&apos;s subscription has expired or is inactive.
Please contact your organization owner to renew the subscription.
</p>
</div>
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Anonymous users with expired subscriptions see a logout escape hatch and an owner-contact message.

When anonymous access is enabled and the subscription is expired, unauthenticated visitors (session === null, membership === null) land in this else branch. They'll see the LogoutEscapeHatch (which may be a no-op if it checks session internally—worth verifying) and a message to "contact your organization owner," which isn't actionable for anonymous visitors.

Consider either:

  1. Guarding LogoutEscapeHatch with session here, and
  2. Adjusting the copy for unauthenticated users (e.g., "This organization's subscription has expired.").
#!/bin/bash
# Check if LogoutEscapeHatch renders conditionally based on session
fd "logoutEscapeHatch" --type f --exec cat {}
🤖 Prompt for AI Agents
In `@packages/web/src/app/`[domain]/layout.tsx around lines 188 - 201, The current
expired-subscription UI renders LogoutEscapeHatch and an owner-contact message
even for anonymous visitors; update the else branch in layout.tsx to (1) only
render <LogoutEscapeHatch> when session is present (guard it with a session
check) and (2) change the copy based on whether session/membership is null—e.g.,
show "This organization's subscription has expired." or other non-actionable
text for anonymous users and keep the "contact your organization owner" text for
authenticated members; locate the logic around session, membership and
LogoutEscapeHatch to apply these conditional checks and text variants.

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.

[FR] Upgrade pop-up should only be shown to current owner, not users

1 participant