Skip to content

Conversation

@athul-rs
Copy link
Contributor

What

  • Added admin routing and infrastructure for custom Stripe product creation feature
  • Enables platform admins to create custom subscription plans with metered billing
  • Added public checkout route for LLM Whisperer custom plans

Why

  • Platform admins need ability to create custom subscription products with specific pricing
  • Required infrastructure to support admin UI for Stripe product/price creation
  • Consolidates routing changes from multiple PRs into unified implementation

How

Frontend Routes:

Router.jsx:

  • Added public route /llm-whisperer/custom-checkout for custom plan checkout flow
  • Dynamic import with try/catch for optional components

useMainAppRoutes.js:

  • Added admin-only /administration route
  • Protected with RequireAdmin wrapper

UnstractAdministrationPage.jsx (new):

  • Administration page with dynamic component loading
  • Graceful fallback if components not available

Backend (Already Committed):

Subscription Routes:

  • Unstract endpoint: /api/v1/unstract/admin/stripe/products/dynamic/
  • LLM Whisperer endpoint: /api/v1/llmwhisperer/admin/stripe/products/dynamic/portal/

Portal Proxy:

  • create_dynamic_stripe_product_portal_proxy() for LLM Whisperer multi-backend support
  • Both endpoints protected with IsAuthenticated + IsAdmin permissions

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

No, this PR will not break existing features:

  • Only adds new admin routes, does not modify existing routes
  • RequireAdmin wrapper prevents unauthorized access
  • Dynamic imports use try/catch to handle missing components gracefully
  • Backend endpoints require admin authentication
  • Public checkout route is additive, does not affect existing checkout flows

Database Migrations

  • None

Env Config

  • None (uses existing LLMWHISPERER_PORTAL_BASE_URL and LLMWHISPERER_PORTAL_KEY settings)

Relevant Docs

  • Admin users can access administration panel at /:orgName/administration
  • Custom plan checkout accessible at /llm-whisperer/custom-checkout?product_id=X

Related Issues or PRs

Dependencies Versions

  • None

Notes on Testing

  1. Navigate to /:orgName/administration as admin user - should see administration page
  2. Navigate to same route as non-admin - should get 403 or redirect
  3. Navigate to /llm-whisperer/custom-checkout?product_id=X - should load checkout page (public route)
  4. Verify no console errors when components are not available

Screenshots

N/A - Backend routing infrastructure

Checklist

I have read and understood the Contribution Guidelines.

This commit adds comprehensive support for admins to create custom Stripe
subscription products for both Unstract and LLM Whisperer platforms.

## Frontend Changes

### Router Updates (Router.jsx)
- Added dynamic import for LlmWhispererCustomCheckoutPage plugin component
- Added public route `/llm-whisperer/custom-checkout` for LLM Whisperer
  custom plan checkout flow
- Follows existing pattern with try/catch for Cloud-only features

### Main App Routes (useMainAppRoutes.js)
- Added Unstract administration route at `/administration`
- Protected with RequireAdmin wrapper (admin-only access)
- Imported UnstractAdministrationPage component

### New Administration Page (UnstractAdministrationPage.jsx)
- Plugin-loader pattern with try/catch for Cloud-only component
- Falls back to "feature not available" message in OSS builds
- Dynamically imports UnstractAdministration from Cloud plugins

## Backend Changes (Already Committed)

The backend infrastructure was already added in previous commits:

### Subscription Routes (cloud_urls_v2.py)
- Unstract admin endpoint: `/api/v1/unstract/admin/stripe/products/dynamic/`
- LLM Whisperer admin endpoint: `/api/v1/llmwhisperer/admin/stripe/products/dynamic/portal/`

### Portal Proxy (subscription_v2/views.py)
- `create_dynamic_stripe_product_portal_proxy()` method forwards LLM Whisperer
  admin requests to Portal service
- Follows existing LLMWhispererService pattern for multi-backend architecture
- Includes proper error handling and logging

### URL Patterns (subscription_v2/urls.py)
- Added `admin_stripe_product_portal_proxy` view and URL pattern
- Both endpoints protected with IsAuthenticated + IsAdmin permissions

## Architecture

This implementation follows Unstract's plugin-based architecture:
- OSS repo contains base routing and plugin-loader patterns
- Cloud repo contains actual admin UI components (subscription-admin plugin)
- Multi-backend support: Unstract uses local backend, LLM Whisperer proxies
  to Portal service
- Graceful degradation for OSS builds (shows "Cloud-only feature" message)

## Related
- Supersedes PR #1087 (monolithic admin UI)
- Integrates checkout functionality from PR #1016
- Consolidates router changes from PR #1558

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

Summary by CodeRabbit

  • New Features
    • Added Administration page for staff members in cloud deployments, accessible through the new admin/custom-plans route.
    • Added LLM Whisperer custom checkout page route for payment workflows.
    • Implemented graceful fallback behavior for open source deployments, automatically displaying placeholder UI when premium features are unavailable.

Summary by CodeRabbit

  • New Features
    • Added Unstract Administration page for managing custom plans (cloud deployments only)
    • Added LLM Whisperer custom checkout page with dynamic routing
    • Introduced /admin/custom-plans page accessible to staff users
    • Implemented graceful fallback UI for open-source deployments

Walkthrough

Adds UnstractAdministrationPage (with OSS fallback) and CSS, registers a conditional /admin/custom-plans route for staff in non-OSS contexts, and introduces an optional dynamic import and route for LlmWhisperer custom checkout in Router.jsx.

Changes

Cohort / File(s) Summary
New page component
frontend/src/pages/UnstractAdministrationPage.jsx
Adds UnstractAdministrationPage which attempts to load UnstractAdministration via a CommonJS require inside try/catch; renders a cloud-only placeholder when unavailable and the real component when available; exports the page.
Page styles
frontend/src/pages/UnstractAdministrationPage.css
Adds .administration-fallback class with padding: 24px; and text-align: center;.
Router optional import
frontend/src/routes/Router.jsx
Adds a try/catch guarded dynamic import/local declaration for LlmWhispererCustomCheckoutPage and conditionally registers a route for /llm-whisperer/custom-checkout only if the component loaded.
Main app routes update
frontend/src/routes/useMainAppRoutes.js
Imports useSessionStore and UnstractAdministrationPage; reads session details (isStaff, orgName, isOpenSource) and conditionally registers admin/custom-plans route rendering UnstractAdministrationPage when staff and non-OSS.

Sequence Diagram

sequenceDiagram
    participant User
    participant Router as Router.jsx
    participant Session as useSessionStore
    participant Page as UnstractAdministrationPage

    User->>Router: Navigate to /admin/custom-plans
    Router->>Session: read sessionDetails (isStaff, isOpenSource, orgName)
    Session-->>Router: returns session info
    alt isStaff AND NOT isOpenSource
        Router->>Page: instantiate UnstractAdministrationPage
        Page->>Page: try require('UnstractAdministration') (CJS)
        alt require succeeds
            Page-->>User: render UnstractAdministration component
        else require fails
            Page-->>User: render cloud-only placeholder
        end
    else
        Router-->>User: route not registered / access prevented
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review dynamic CommonJS require usage in frontend/src/pages/UnstractAdministrationPage.jsx for client-only/SSR assumptions and error handling.
  • Verify session-access checks (isStaff, isOpenSource) and route placement in frontend/src/routes/useMainAppRoutes.js.
  • Confirm Router.jsx optional import and conditional route do not introduce side effects.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: adding admin infrastructure for custom Stripe product creation support with metered billing.
Description check ✅ Passed The description is comprehensive and follows the template structure, covering all major sections with detailed information about frontend routes, backend endpoints, testing instructions, and breaking change analysis.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/custom-stripe-products-backend

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5c1d740 and 433b0b1.

📒 Files selected for processing (1)
  • frontend/src/routes/useMainAppRoutes.js (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/routes/useMainAppRoutes.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

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: 0

🧹 Nitpick comments (2)
frontend/src/routes/Router.jsx (1)

89-96: Optional: keep dynamic import style consistent or consider lazy() later.

The try/catch require matches existing patterns. If you move toward React.lazy in the future, this page can follow suit; not required now.

frontend/src/pages/UnstractAdministrationPage.jsx (1)

10-20: Clear Cloud‑only fallback.

Simple, accessible fallback; no runtime risk. Optionally factor a shared “CloudOnly” notice if reused elsewhere.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between dde2c9e and 39caa0d.

📒 Files selected for processing (3)
  • frontend/src/pages/UnstractAdministrationPage.jsx (1 hunks)
  • frontend/src/routes/Router.jsx (2 hunks)
  • frontend/src/routes/useMainAppRoutes.js (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
frontend/src/routes/useMainAppRoutes.js (2)

25-25: Import looks correct and scoped.

Static import is fine since the page handles its own dynamic fallback. No concerns.


121-123: Admin‑only route registered correctly.

Path nests under :orgName and is gated by RequireAdmin, matching the PR intent.

Please sanity‑check that “/orgXYZ/administration” renders the Cloud‑only placeholder in OSS builds and the full admin widget in Cloud builds.

frontend/src/routes/Router.jsx (1)

158-163: Public checkout route added correctly.

Absolute path and placement outside RequireAuth satisfy the “public route” requirement.

Confirm the page shows when the plugin exists and cleanly 404s (no route) when absent.

frontend/src/pages/UnstractAdministrationPage.jsx (1)

3-8: Graceful dynamic load.

try/catch keeps OSS builds clean without bundling errors. Good.

@athul-rs athul-rs changed the title UN-XXXX [FEAT] Add admin custom Stripe product creation support UN-2781 [FEAT] Add admin custom Stripe product creation support Nov 10, 2025
athul-rs and others added 4 commits November 10, 2025 19:03
Suppress SonarQube warnings for intentional empty catch blocks used in
plugin loading architecture. These catch blocks are part of the graceful
degradation pattern for Cloud-only features - when plugins are not available,
the component remains undefined and React Router triggers NotFound page.

This pattern is used throughout the codebase for optional plugin loading.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Move NOSONAR comments to separate lines to comply with Prettier formatting rules.
Format multi-line Route component in useMainAppRoutes.js.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Place NOSONAR comment inline with catch statement for proper recognition.
SonarQube requires the suppression comment on the same line as the issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add minimal NOSONAR comments to suppress SonarQube warnings for intentional
empty catch blocks in plugin loading pattern. These catch blocks are part of
the graceful degradation architecture for Cloud-only features.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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: 0

🧹 Nitpick comments (1)
frontend/src/routes/Router.jsx (1)

89-96: Consider moving the NOSONAR comment to follow standard convention.

The // NOSONAR comment on line 94 is placed inside the catch block. Standard practice is to place such suppression comments above the try block or on the same line as the construct being suppressed.

Apply this diff to align with convention:

 let LlmWhispererCustomCheckoutPage;
+// NOSONAR
 try {
   LlmWhispererCustomCheckoutPage =
     require("../plugins/llm-whisperer/pages/LlmWhispererCustomCheckoutPage.jsx").LlmWhispererCustomCheckoutPage;
 } catch (err) {
-  // NOSONAR
   // Do nothing, Not-found Page will be triggered.
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa0f53 and f824c5c.

📒 Files selected for processing (2)
  • frontend/src/pages/UnstractAdministrationPage.jsx (1 hunks)
  • frontend/src/routes/Router.jsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/UnstractAdministrationPage.jsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
frontend/src/routes/Router.jsx (1)

159-164: LGTM! Route implementation follows existing patterns correctly.

The conditional route registration for the LLM Whisperer custom checkout page is well-implemented:

  • Follows the same pattern as other dynamic routes in the file
  • Correctly placed as a public route (outside RequireAuth) to support the checkout flow
  • Gracefully handles missing component with conditional rendering

Copy link
Contributor

@jaseemjaskp jaseemjaskp left a comment

Choose a reason for hiding this comment

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

LGTM

…ation

Add platform staff-only access control for custom Unstract subscription plans:
- Add RequireStaff component to enforce platform staff access
- Update custom plans route to use RequireStaff instead of RequireAdmin
- Block access for open-source deployments (mock_org)

Frontend Changes:
- New RequireStaff.js component with isStaff and orgName validation
- Update useMainAppRoutes.js to use RequireStaff for /admin/custom-plans route
- Shows Unauthorized for non-staff users, NotFound for OSS deployments

This complements the backend IsStaff permission class to ensure only platform
staff can create custom Stripe products with dual pricing structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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: 0

🧹 Nitpick comments (1)
frontend/src/components/helpers/auth/RequireStaff.js (1)

11-11: Consider extracting magic string to a constant.

The hardcoded string "mock_org" for open source detection would be more maintainable as a named constant, especially if used in multiple places.

Example refactor:

+const OPEN_SOURCE_ORG_NAME = "mock_org";
+
 const RequireStaff = () => {
   const { sessionDetails } = useSessionStore();
   const isStaff = sessionDetails?.isStaff || sessionDetails?.is_staff;
   const orgName = sessionDetails?.orgName;
-  const isOpenSource = orgName === "mock_org";
+  const isOpenSource = orgName === OPEN_SOURCE_ORG_NAME;

Alternatively, if this identifier is used across the codebase, consider moving it to a shared constants file.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between dfc3f5a and 3f8cb53.

📒 Files selected for processing (2)
  • frontend/src/components/helpers/auth/RequireStaff.js (1 hunks)
  • frontend/src/routes/useMainAppRoutes.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/routes/useMainAppRoutes.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
frontend/src/components/helpers/auth/RequireStaff.js (4)

1-5: LGTM! Imports are appropriate.

All necessary dependencies are imported correctly for implementing the route guard.


7-21: LGTM! Guard logic correctly implements two-level access control.

The component correctly enforces:

  1. Staff-only access (returns Unauthorized if not staff)
  2. Cloud-only feature availability (returns NotFound for open source deployments)
  3. Nested route rendering via Outlet for authorized users

The guard order is appropriate for protecting admin routes that are only available in cloud deployments.


8-11: Verify session loading behavior for appropriate UX.

When sessionDetails is undefined (e.g., during initial load), the component will immediately render <Unauthorized /> since !isStaff evaluates to true.

Please verify:

  • Does the session store load synchronously before routes render?
  • Or should there be a loading state while sessionDetails is being fetched?

If the session loads asynchronously, consider adding a loading state:

 const RequireStaff = () => {
   const { sessionDetails } = useSessionStore();
+  
+  if (!sessionDetails) {
+    return <div>Loading...</div>; // or a proper loading component
+  }
+  
   const isStaff = sessionDetails?.isStaff || sessionDetails?.is_staff;

9-9: Simplify redundant field check; backend normalization already ensures camelCase format.

The dual field check is unnecessary. GetSessionData.js normalizes backend's snake_case is_staff to camelCase isStaff before storing in sessionDetails via setSessionDetails. Since all session updates flow through this normalization, sessionDetails will always contain isStaff in camelCase, never is_staff.

Simplify line 9 to:

const isStaff = sessionDetails?.isStaff;

athul-rs and others added 2 commits November 13, 2025 01:51
Remove RequireStaff component as it's been replaced with inline staff
checking pattern in route files. This avoids component dependency issues
and follows the established Verticals pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace RequireStaff component wrapper with inline staff check using
useSessionStore in an IIFE. This matches the pattern used in Cloud
plugin routes and fixes the build error where RequireStaff component
was deleted but still referenced.

The inline check ensures:
- Staff-only access to admin custom plans route
- Hidden in open-source deployments (orgName === "mock_org")
- No component import dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8cb53 and 51e1a92.

📒 Files selected for processing (1)
  • frontend/src/routes/useMainAppRoutes.js (3 hunks)
🧰 Additional context used
🪛 Biome (2.1.2)
frontend/src/routes/useMainAppRoutes.js

[error] 123-123: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
frontend/src/routes/useMainAppRoutes.js (1)

9-9: LGTM on imports.

The new imports are necessary for the admin route functionality and follow the existing patterns in the file.

Also applies to: 26-26

athul-rs and others added 3 commits November 13, 2025 10:47
- Create UnstractAdministrationPage.css for fallback UI styling
- Replace inline styles with className in UnstractAdministrationPage.jsx
- Improves code maintainability and follows project's CSS patterns
- Move useSessionStore hook call to top level of function
- Remove IIFE pattern that violated hooks rules
- Simplify conditional rendering logic

Fixes CodeRabbit review comment about hooks being called inside nested function.
@sonarqubecloud
Copy link

@johnyrahul johnyrahul merged commit 9ba210b into main Nov 13, 2025
5 checks passed
@johnyrahul johnyrahul deleted the feature/custom-stripe-products-backend branch November 13, 2025 06:10
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.

5 participants