From 77b3e86b50e6125f954964e48f14875aaa78362a Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 5 May 2026 10:24:01 -0300 Subject: [PATCH] feat(frontend): /pricing route placeholder + UserResponse subscription fields Two upstream additions to support private overlays implementing paid tiers without forking client code: - store/useAuthStore.ts: UserResponse extended with optional is_paid_subscriber, subscription_status, subscription_period_end. The backend now returns these in /api/auth/me; the persist middleware serialises them automatically. - pages/PricingPlaceholder.tsx (NEW): the /pricing route. Renders a polite "this image is fully free" message for self-hosters plus a data-velxio-slot="pricing-page" target where private overlays can portal-inject a real pricing page. - App.tsx: register the /pricing route after /about. Self-hosted OSS image: /pricing shows the placeholder, no behavioural change anywhere else. Production with a private overlay: /pricing shows the overlay's full pricing UI. Frontend build verified. --- frontend/src/App.tsx | 3 + frontend/src/pages/PricingPlaceholder.tsx | 69 +++++++++++++++++++++++ frontend/src/store/useAuthStore.ts | 6 ++ 3 files changed, 78 insertions(+) create mode 100644 frontend/src/pages/PricingPlaceholder.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 384c3e73..c10c3e5c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ import { RaspberryPiSimulatorPage } from './pages/RaspberryPiSimulatorPage'; import { Velxio2Page } from './pages/Velxio2Page'; import { Velxio25Page } from './pages/Velxio25Page'; import { AboutPage } from './pages/AboutPage'; +import { PricingPlaceholder } from './pages/PricingPlaceholder'; import { useAuthStore } from './store/useAuthStore'; import './App.css'; @@ -68,6 +69,8 @@ function App() { } /> } /> } /> + {/* Pricing — placeholder by default; private overlays portal-inject the real page */} + } /> {/* Canonical project URL by ID */} } /> {/* Legacy slug route — redirects to /project/:id */} diff --git a/frontend/src/pages/PricingPlaceholder.tsx b/frontend/src/pages/PricingPlaceholder.tsx new file mode 100644 index 00000000..a5e33556 --- /dev/null +++ b/frontend/src/pages/PricingPlaceholder.tsx @@ -0,0 +1,69 @@ +/** + * PricingPlaceholder — the route at /pricing. + * + * In the open-source build this renders a minimal heading + slot div. It + * exists so that: + * 1. Private overlays can portal-inject a real PricingPage into the slot + * (data-velxio-slot="pricing-page") without forking this file. + * 2. Self-hosters who navigate to /pricing get a polite "this image + * doesn't sell anything" page rather than a 404. + * + * The route is registered in App.tsx. The slot pattern is the same as + * data-velxio-slot="user-menu" / "admin-tabs" / "admin-tab-content". + */ + +import { useEffect } from 'react'; +import { AppHeader } from '../components/layout/AppHeader'; + +export const PricingPlaceholder = () => { + useEffect(() => { + document.title = 'Pricing — Velxio'; + }, []); + + return ( +
+ +
+ {/* Default content — only visible if no overlay is mounted (i.e. + self-hosted OSS image without a private pricing overlay). */} +
+

Pricing

+

+ This Velxio instance is self-hosted from the open-source image — + all features are free for everyone using it. +

+

+ The hosted version at{' '} + + velxio.dev + {' '} + offers an optional Pro tier that helps fund development. +

+

+ Source code is{' '} + + MIT-licensed on GitHub + + . +

+
+
+
+ ); +}; + +export default PricingPlaceholder; diff --git a/frontend/src/store/useAuthStore.ts b/frontend/src/store/useAuthStore.ts index a346e23f..ee3e30b6 100644 --- a/frontend/src/store/useAuthStore.ts +++ b/frontend/src/store/useAuthStore.ts @@ -9,6 +9,12 @@ export interface UserResponse { avatar_url: string | null; is_admin: boolean; created_at: string; + // Subscription state — populated by deployments that wire an external + // billing system (e.g. velxio.dev). Self-hosted OSS images leave these + // at the safe defaults (no paid features unlock). + is_paid_subscriber?: boolean; + subscription_status?: string | null; + subscription_period_end?: string | null; } interface AuthState {