Skip to content

Commit 9ba210b

Browse files
athul-rsclaudejohnyrahul
authored
UN-2781 [FEAT] Add admin custom Stripe product creation support (#1651)
* feat: Add admin custom Stripe product creation support 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]> * chore: Add NOSONAR comments for plugin loading pattern 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]> * style: Fix Prettier formatting for NOSONAR comments 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]> * fix: Correct NOSONAR comment placement for SonarQube 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]> * fix: Add NOSONAR comments to suppress empty catch warnings 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]> * path modified admin cusotm plans * UN-2861 [FEAT] Add RequireStaff permission for custom plans administration 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]> * Remove unused RequireStaff component 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]> * Remove RequireStaff dependency from OSS routes 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]> * Fix Prettier formatting for staff check return statement * Refactor inline CSS to classes in UnstractAdministrationPage - Create UnstractAdministrationPage.css for fallback UI styling - Replace inline styles with className in UnstractAdministrationPage.jsx - Improves code maintainability and follows project's CSS patterns * Fix React Rules of Hooks violation in useMainAppRoutes - 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. --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Rahul Johny <[email protected]>
1 parent 9cc47bd commit 9ba210b

File tree

4 files changed

+57
-0
lines changed

4 files changed

+57
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.administration-fallback {
2+
padding: 24px;
3+
text-align: center;
4+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import "./UnstractAdministrationPage.css";
2+
3+
let UnstractAdministration;
4+
5+
try {
6+
UnstractAdministration =
7+
require("../plugins/subscription-admin/components/UnstractAdministration.jsx").UnstractAdministration;
8+
} catch (err) {
9+
// NOSONAR
10+
// Cloud-only feature, not available in OSS
11+
}
12+
13+
function UnstractAdministrationPage() {
14+
if (!UnstractAdministration) {
15+
return (
16+
<div className="administration-fallback">
17+
<h2>Administration Panel</h2>
18+
<p>This feature is only available in Unstract Cloud.</p>
19+
</div>
20+
);
21+
}
22+
return <UnstractAdministration />;
23+
}
24+
25+
export { UnstractAdministrationPage };

frontend/src/routes/Router.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ try {
8686
// Do nothing, Not-found Page will be triggered.
8787
}
8888

89+
let LlmWhispererCustomCheckoutPage;
90+
try {
91+
LlmWhispererCustomCheckoutPage =
92+
require("../plugins/llm-whisperer/pages/LlmWhispererCustomCheckoutPage.jsx").LlmWhispererCustomCheckoutPage;
93+
} catch (err) {
94+
// NOSONAR
95+
// Do nothing, Not-found Page will be triggered.
96+
}
97+
8998
function Router() {
9099
const MainAppRoute = useMainAppRoutes();
91100
return (
@@ -147,6 +156,12 @@ function Router() {
147156
element={<CustomPlanCheckoutPage />}
148157
/>
149158
)}
159+
{LlmWhispererCustomCheckoutPage && (
160+
<Route
161+
path="/llm-whisperer/custom-checkout"
162+
element={<LlmWhispererCustomCheckoutPage />}
163+
/>
164+
)}
150165
<Route path="" element={<RequireAuth />}>
151166
{MainAppRoute}
152167
{llmWhispererRouter && (

frontend/src/routes/useMainAppRoutes.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ToolsSettingsPage } from "../pages/ToolsSettingsPage.jsx";
66
import { SettingsPage } from "../pages/SettingsPage.jsx";
77
import { PlatformSettings } from "../components/settings/platform/PlatformSettings.jsx";
88
import { RequireAdmin } from "../components/helpers/auth/RequireAdmin.js";
9+
import { useSessionStore } from "../store/session-store";
910
import { UsersPage } from "../pages/UsersPage.jsx";
1011
import { InviteEditUserPage } from "../pages/InviteEditUserPage.jsx";
1112
import { DefaultTriad } from "../components/settings/default-triad/DefaultTriad.jsx";
@@ -22,6 +23,7 @@ import { OutputAnalyzerPage } from "../pages/OutputAnalyzerPage.jsx";
2223
import { LogsPage } from "../pages/LogsPage.jsx";
2324
import { deploymentTypes } from "../helpers/GetStaticData.js";
2425
import ConnectorsPage from "../pages/ConnectorsPage.jsx";
26+
import { UnstractAdministrationPage } from "../pages/UnstractAdministrationPage.jsx";
2527

2628
let RequirePlatformAdmin;
2729
let PlatformAdminPage;
@@ -98,6 +100,11 @@ try {
98100
}
99101

100102
function useMainAppRoutes() {
103+
const { sessionDetails } = useSessionStore();
104+
const isStaff = sessionDetails?.isStaff || sessionDetails?.is_staff;
105+
const orgName = sessionDetails?.orgName;
106+
const isOpenSource = orgName === "mock_org";
107+
101108
const routes = (
102109
<>
103110
<Route path=":orgName" element={<FullPageLayout />}>
@@ -117,6 +124,12 @@ function useMainAppRoutes() {
117124
<Route path="pricing" element={<UnstractSubscriptionPage />} />
118125
</Route>
119126
)}
127+
{isStaff && !isOpenSource && (
128+
<Route
129+
path="admin/custom-plans"
130+
element={<UnstractAdministrationPage />}
131+
/>
132+
)}
120133
<Route path="profile" element={<ProfilePage />} />
121134
<Route
122135
path="api"

0 commit comments

Comments
 (0)