feat(01-code-splitting): add feature gating with ALWRITY_ENABLED_FEATURES

- Create FeatureRoute.tsx wrapper component for route-level feature gating
- Add FEATURE_KEYS constant map to demoMode.ts for type-safe feature references
- Wrap 47 feature-specific routes with <FeatureRoute> in App.tsx
- Core routes (dashboard, billing, pricing, auth callbacks) remain ungated
- Disabled features redirect to /dashboard and never load their lazy chunks
- Main bundle: +259 bytes (FeatureRoute is a lightweight component)

Closes Phase 1 Plan 01-02
This commit is contained in:
ajaysi
2026-05-08 11:31:01 +05:30
parent 8ee042bd2c
commit ab827e9ab9
3 changed files with 148 additions and 54 deletions

View File

@@ -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 = () => {
<Route path="/error-test" element={<ErrorBoundaryTest />} />
)}
<Route path="/dashboard" element={<ProtectedRoute><MainDashboard /></ProtectedRoute>} />
<Route path="/seo" element={<ProtectedRoute><SEODashboard /></ProtectedRoute>} />
<Route path="/seo-dashboard" element={<ProtectedRoute><SEODashboard /></ProtectedRoute>} />
<Route path="/content-planning" element={<ProtectedRoute><ContentPlanningDashboard /></ProtectedRoute>} />
<Route path="/facebook-writer" element={<ProtectedRoute><FacebookWriter /></ProtectedRoute>} />
<Route path="/linkedin-writer" element={<ProtectedRoute><LinkedInWriter /></ProtectedRoute>} />
<Route path="/blog-writer" element={<ProtectedRoute><BlogWriter /></ProtectedRoute>} />
<Route path="/story-writer" element={<ProtectedRoute><StoryWriter /></ProtectedRoute>} />
<Route path="/story-projects" element={<ProtectedRoute><StoryProjectList /></ProtectedRoute>} />
<Route path="/youtube-creator" element={<ProtectedRoute><YouTubeCreator /></ProtectedRoute>} />
<Route path="/podcast-maker" element={<ProtectedRoute><PodcastDashboard /></ProtectedRoute>} />
<Route path="/image-studio" element={<ProtectedRoute><ImageStudioDashboard /></ProtectedRoute>} />
<Route path="/video-studio" element={<ProtectedRoute><VideoStudioDashboard /></ProtectedRoute>} />
<Route path="/video-studio/create" element={<ProtectedRoute><CreateVideo /></ProtectedRoute>} />
<Route path="/video-studio/avatar" element={<ProtectedRoute><AvatarVideo /></ProtectedRoute>} />
<Route path="/video-studio/enhance" element={<ProtectedRoute><EnhanceVideo /></ProtectedRoute>} />
<Route path="/video-studio/extend" element={<ProtectedRoute><ExtendVideo /></ProtectedRoute>} />
<Route path="/video-studio/edit" element={<ProtectedRoute><EditVideo /></ProtectedRoute>} />
<Route path="/video-studio/transform" element={<ProtectedRoute><TransformVideo /></ProtectedRoute>} />
<Route path="/video-studio/social" element={<ProtectedRoute><SocialVideo /></ProtectedRoute>} />
<Route path="/video-studio/face-swap" element={<ProtectedRoute><FaceSwap /></ProtectedRoute>} />
<Route path="/video-studio/video-translate" element={<ProtectedRoute><VideoTranslate /></ProtectedRoute>} />
<Route path="/video-studio/video-background-remover" element={<ProtectedRoute><VideoBackgroundRemover /></ProtectedRoute>} />
<Route path="/video-studio/add-audio-to-video" element={<ProtectedRoute><AddAudioToVideo /></ProtectedRoute>} />
<Route path="/video-studio/library" element={<ProtectedRoute><LibraryVideo /></ProtectedRoute>} />
<Route path="/image-generator" element={<ProtectedRoute><CreateStudio /></ProtectedRoute>} />
<Route path="/image-editor" element={<ProtectedRoute><EditStudio /></ProtectedRoute>} />
<Route path="/image-upscale" element={<ProtectedRoute><UpscaleStudio /></ProtectedRoute>} />
<Route path="/image-control" element={<ProtectedRoute><ControlStudio /></ProtectedRoute>} />
<Route path="/image-studio/face-swap" element={<ProtectedRoute><FaceSwapStudio /></ProtectedRoute>} />
<Route path="/image-studio/compress" element={<ProtectedRoute><CompressionStudio /></ProtectedRoute>} />
<Route path="/image-studio/processing" element={<ProtectedRoute><ImageProcessingStudio /></ProtectedRoute>} />
<Route path="/image-studio/social-optimizer" element={<ProtectedRoute><SocialOptimizer /></ProtectedRoute>} />
<Route path="/asset-library" element={<ProtectedRoute><AssetLibrary /></ProtectedRoute>} />
<Route path="/campaign-creator" element={<ProtectedRoute><ProductMarketingDashboard /></ProtectedRoute>} />
<Route path="/campaign-creator/photoshoot" element={<ProtectedRoute><ProductPhotoshootStudio /></ProtectedRoute>} />
<Route path="/campaign-creator/animation" element={<ProtectedRoute><ProductAnimationStudio /></ProtectedRoute>} />
<Route path="/campaign-creator/video" element={<ProtectedRoute><ProductVideoStudio /></ProtectedRoute>} />
<Route path="/campaign-creator/avatar" element={<ProtectedRoute><ProductAvatarStudio /></ProtectedRoute>} />
<Route path="/seo" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/seo-dashboard" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/content-planning" element={<ProtectedRoute><FeatureRoute feature="content-planning"><ContentPlanningDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/facebook-writer" element={<ProtectedRoute><FeatureRoute feature="social"><FacebookWriter /></FeatureRoute></ProtectedRoute>} />
<Route path="/linkedin-writer" element={<ProtectedRoute><FeatureRoute feature="social"><LinkedInWriter /></FeatureRoute></ProtectedRoute>} />
<Route path="/blog-writer" element={<ProtectedRoute><FeatureRoute feature="blog_writer"><BlogWriter /></FeatureRoute></ProtectedRoute>} />
<Route path="/story-writer" element={<ProtectedRoute><FeatureRoute feature="story"><StoryWriter /></FeatureRoute></ProtectedRoute>} />
<Route path="/story-projects" element={<ProtectedRoute><FeatureRoute feature="story"><StoryProjectList /></FeatureRoute></ProtectedRoute>} />
<Route path="/youtube-creator" element={<ProtectedRoute><FeatureRoute feature="youtube"><YouTubeCreator /></FeatureRoute></ProtectedRoute>} />
<Route path="/podcast-maker" element={<ProtectedRoute><FeatureRoute feature="podcast"><PodcastDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-studio" element={<ProtectedRoute><FeatureRoute feature="image"><ImageStudioDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio" element={<ProtectedRoute><FeatureRoute feature="video"><VideoStudioDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/create" element={<ProtectedRoute><FeatureRoute feature="video"><CreateVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/avatar" element={<ProtectedRoute><FeatureRoute feature="video"><AvatarVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/enhance" element={<ProtectedRoute><FeatureRoute feature="video"><EnhanceVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/extend" element={<ProtectedRoute><FeatureRoute feature="video"><ExtendVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/edit" element={<ProtectedRoute><FeatureRoute feature="video"><EditVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/transform" element={<ProtectedRoute><FeatureRoute feature="video"><TransformVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/social" element={<ProtectedRoute><FeatureRoute feature="video"><SocialVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/face-swap" element={<ProtectedRoute><FeatureRoute feature="video"><FaceSwap /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/video-translate" element={<ProtectedRoute><FeatureRoute feature="video"><VideoTranslate /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/video-background-remover" element={<ProtectedRoute><FeatureRoute feature="video"><VideoBackgroundRemover /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/add-audio-to-video" element={<ProtectedRoute><FeatureRoute feature="video"><AddAudioToVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/video-studio/library" element={<ProtectedRoute><FeatureRoute feature="video"><LibraryVideo /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-generator" element={<ProtectedRoute><FeatureRoute feature="image"><CreateStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-editor" element={<ProtectedRoute><FeatureRoute feature="image"><EditStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-upscale" element={<ProtectedRoute><FeatureRoute feature="image"><UpscaleStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-control" element={<ProtectedRoute><FeatureRoute feature="image"><ControlStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-studio/face-swap" element={<ProtectedRoute><FeatureRoute feature="image"><FaceSwapStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-studio/compress" element={<ProtectedRoute><FeatureRoute feature="image"><CompressionStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-studio/processing" element={<ProtectedRoute><FeatureRoute feature="image"><ImageProcessingStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/image-studio/social-optimizer" element={<ProtectedRoute><FeatureRoute feature="image"><SocialOptimizer /></FeatureRoute></ProtectedRoute>} />
<Route path="/asset-library" element={<ProtectedRoute><FeatureRoute feature="asset-library"><AssetLibrary /></FeatureRoute></ProtectedRoute>} />
<Route path="/campaign-creator" element={<ProtectedRoute><FeatureRoute feature="campaign"><ProductMarketingDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/campaign-creator/photoshoot" element={<ProtectedRoute><FeatureRoute feature="campaign"><ProductPhotoshootStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/campaign-creator/animation" element={<ProtectedRoute><FeatureRoute feature="campaign"><ProductAnimationStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/campaign-creator/video" element={<ProtectedRoute><FeatureRoute feature="campaign"><ProductVideoStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/campaign-creator/avatar" element={<ProtectedRoute><FeatureRoute feature="campaign"><ProductAvatarStudio /></FeatureRoute></ProtectedRoute>} />
<Route path="/product-marketing" element={<Navigate to="/campaign-creator" replace />} />
<Route path="/scheduler-dashboard" element={<ProtectedRoute><SchedulerDashboard /></ProtectedRoute>} />
<Route path="/scheduler-dashboard" element={<ProtectedRoute><FeatureRoute feature="scheduler"><SchedulerDashboard /></FeatureRoute></ProtectedRoute>} />
<Route path="/billing" element={<ProtectedRoute><BillingPage /></ProtectedRoute>} />
<Route path="/approvals" element={<ProtectedRoute><ApprovalsPage /></ProtectedRoute>} />
<Route path="/team-activity" element={<ProtectedRoute><TeamActivityPage /></ProtectedRoute>} />
<Route path="/stripe-disputes" element={<ProtectedRoute><StripeDisputesDashboard /></ProtectedRoute>} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/research-test" element={<ResearchDashboard />} />
<Route path="/research-dashboard" element={<ResearchDashboard />} />
<Route path="/alwrity-researcher" element={<ResearchDashboard />} />
<Route path="/intent-research" element={<IntentResearchTest />} />
<Route path="/wix-test" element={<WixTestPage />} />
<Route path="/wix-test-direct" element={<WixTestPage />} />
<Route path="/research-test" element={<FeatureRoute feature="research"><ResearchDashboard /></FeatureRoute>} />
<Route path="/research-dashboard" element={<FeatureRoute feature="research"><ResearchDashboard /></FeatureRoute>} />
<Route path="/alwrity-researcher" element={<FeatureRoute feature="research"><ResearchDashboard /></FeatureRoute>} />
<Route path="/intent-research" element={<FeatureRoute feature="research"><IntentResearchTest /></FeatureRoute>} />
<Route path="/wix-test" element={<FeatureRoute feature="wix"><WixTestPage /></FeatureRoute>} />
<Route path="/wix-test-direct" element={<FeatureRoute feature="wix"><WixTestPage /></FeatureRoute>} />
{/* Auth callbacks — always accessible (needed for OAuth flow) */}
<Route path="/wix/callback" element={<WixCallbackPage />} />
<Route path="/wp/callback" element={<WordPressCallbackPage />} />
<Route path="/gsc/callback" element={<GSCAuthCallback />} />
<Route path="/bing/callback" element={<BingCallbackPage />} />
<Route path="/bing-analytics-storage" element={<ProtectedRoute><BingAnalyticsStorage /></ProtectedRoute>} />
<Route path="/bing-analytics-storage" element={<ProtectedRoute><FeatureRoute feature="bing"><BingAnalyticsStorage /></FeatureRoute></ProtectedRoute>} />
</Routes>
</Suspense>
</ConditionalCopilotKit>

View File

@@ -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:
* <Route path="/blog-writer" element={
* <ProtectedRoute>
* <FeatureRoute feature="blog_writer"><BlogWriter /></FeatureRoute>
* </ProtectedRoute>
* } />
*/
const FeatureRoute: React.FC<FeatureRouteProps> = ({
feature,
children,
redirectTo = '/dashboard'
}) => {
if (!isFeatureEnabled(feature)) {
return <Navigate to={redirectTo} replace />;
}
return <>{children}</>;
};
export default FeatureRoute;

View File

@@ -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<string> | null = null;
* Returns a Set of enabled feature names.
*/
export function getEnabledFeatures(): Set<string> {
// 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<string> {
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<string> {
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<string, string> = {
'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');
}