diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a0526e0b..b796fe4c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import TokenInstaller from './components/App/TokenInstaller'; import { ConditionalCopilotKit, AuthenticatedCopilotWrapper } from './components/App/CopilotWrappers'; import Landing from './components/Landing/Landing'; import LazyLoadingFallback from './components/shared/LazyLoadingFallback'; +import FeatureRoute from './components/shared/FeatureRoute'; // ─── Lazy loaded route components ─────────────────────────────────────────── // Default exports @@ -187,62 +188,63 @@ const App: React.FC = () => { } /> )} } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> - } /> + } /> } /> } /> } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Auth callbacks — always accessible (needed for OAuth flow) */} } /> } /> } /> } /> - } /> + } /> diff --git a/frontend/src/components/shared/FeatureRoute.tsx b/frontend/src/components/shared/FeatureRoute.tsx new file mode 100644 index 00000000..e0e918c8 --- /dev/null +++ b/frontend/src/components/shared/FeatureRoute.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { isFeatureEnabled } from '../../utils/demoMode'; + +interface FeatureRouteProps { + feature: string; + children: React.ReactNode; + /** Where to redirect if feature is disabled (default: /dashboard) */ + redirectTo?: string; +} + +/** + * Route guard that checks if a feature is enabled via ALWRITY_ENABLED_FEATURES. + * If disabled, redirects to the fallback route and the lazy chunk never loads. + * + * Usage: + * + * + * + * } /> + */ +const FeatureRoute: React.FC = ({ + feature, + children, + redirectTo = '/dashboard' +}) => { + if (!isFeatureEnabled(feature)) { + return ; + } + return <>{children}; +}; + +export default FeatureRoute; diff --git a/frontend/src/utils/demoMode.ts b/frontend/src/utils/demoMode.ts index cb4ac4c2..a4f54f75 100644 --- a/frontend/src/utils/demoMode.ts +++ b/frontend/src/utils/demoMode.ts @@ -2,9 +2,36 @@ * Consolidated feature mode detection utilities. * * Primary env var: REACT_APP_ENABLED_FEATURES - * Format: "all" or comma-separated: "podcast,core" + * Format: "all" or comma-separated: "podcast,blog_writer" */ +/** + * Known feature keys for route gating with ALWRITY_ENABLED_FEATURES. + * These match the backend FEATURE_GROUPS in alwrity_utils/feature_registry.py. + * + * Usage: isFeatureEnabled(FEATURE_KEYS.PODCAST) → true/false + */ +export const FEATURE_KEYS = { + CORE: 'core', + SEO: 'seo', + CONTENT_PLANNING: 'content-planning', + SOCIAL: 'social', + BLOG_WRITER: 'blog_writer', + STORY: 'story', + YOUTUBE: 'youtube', + PODCAST: 'podcast', + VIDEO: 'video', + IMAGE: 'image', + CAMPAIGN: 'campaign', + SCHEDULER: 'scheduler', + RESEARCH: 'research', + WIX: 'wix', + BING: 'bing', + ASSET_LIBRARY: 'asset-library', +} as const; + +export type FeatureKey = typeof FEATURE_KEYS[keyof typeof FEATURE_KEYS]; + const PRIMARY_STORAGE_KEY = 'enabled_features'; const PRIMARY_ENV_KEY = 'REACT_APP_ENABLED_FEATURES'; @@ -16,12 +43,10 @@ let cachedFeatures: Set | null = null; * Returns a Set of enabled feature names. */ export function getEnabledFeatures(): Set { - // Return cached value if available if (cachedFeatures) { return cachedFeatures; } - // Check localStorage first const storageValue = localStorage.getItem(PRIMARY_STORAGE_KEY); if (storageValue) { const features = storageValue.toLowerCase().split(',').map(f => f.trim()); @@ -33,7 +58,6 @@ export function getEnabledFeatures(): Set { return cachedFeatures; } - // Check environment variable const envValue = process.env[PRIMARY_ENV_KEY]; if (envValue) { const features = envValue.toLowerCase().split(',').map(f => f.trim()); @@ -45,7 +69,6 @@ export function getEnabledFeatures(): Set { return cachedFeatures; } - // Default: all features enabled cachedFeatures = new Set(['all']); return cachedFeatures; } @@ -58,20 +81,55 @@ export function isFeatureEnabled(feature: string): boolean { return enabled.has('all') || enabled.has(feature); } +/** + * Check if running in feature-only mode (not "all"). + * Returns true when a specific subset of features is enabled. + */ +export function isFeatureOnlyMode(): boolean { + const enabled = getEnabledFeatures(); + return !enabled.has('all'); +} + /** * Check if podcast-only mode is enabled. - * Returns true when podcast is enabled but not "all". */ export function isPodcastOnlyDemoMode(): boolean { const enabled = getEnabledFeatures(); return enabled.has('podcast') && !enabled.has('all'); } +/** + * Get the single enabled feature name, or null if multiple or full mode. + */ +export function getSingleFeature(): string | null { + const enabled = getEnabledFeatures(); + if (enabled.has('all')) return null; + if (enabled.size === 1) return [...enabled][0]; + return null; +} + +/** + * Get the default landing route based on enabled features. + */ +export function getDefaultLandingRoute(): string { + const enabled = getEnabledFeatures(); + if (enabled.has('all')) return '/dashboard'; + const singleFeature = getSingleFeature(); + if (singleFeature) { + const routeMap: Record = { + 'podcast': '/podcast-maker', + 'blog_writer': '/blog-writer', + }; + return routeMap[singleFeature] || '/dashboard'; + } + return '/dashboard'; +} + /** * Check if the app should skip onboarding. - * Returns true in podcast-only mode. + * Returns true in feature-only mode. */ export function shouldSkipOnboarding(): boolean { const enabled = getEnabledFeatures(); - return enabled.has('podcast') || !enabled.has('all'); + return !enabled.has('all'); }