immersive2/src/util/featureConfig.ts
Michael Mainguy 1c50dd5c84 Implement three-state feature flag system with upgrade badges
Feature States:
- 'on': Feature fully accessible
- 'off': Feature hidden from menus
- 'coming-soon': Visible with "Coming Soon!" badge, not clickable
- 'basic': Visible with "Sign Up for Free" badge, triggers Auth0 login
- 'pro': Visible with "Upgrade to Pro" badge (for future upgrade flow)

Changes:
- Update FeatureState type to support 5 states (on/off/coming-soon/basic/pro)
- Consolidate GUEST_FEATURE_CONFIG as DEFAULT_FEATURE_CONFIG
- Create ComingSoonBadge component for coming-soon features
- Create UpgradeBadge component for basic/pro tier requirements
- Update VR Experience hamburger menu to maintain open/closed state
- Make menu default to open, persist state in localStorage
- Make 'basic' features clickable to trigger Auth0 sign-in
- Update createDiagramModal to show appropriate badges
- Fix camera initial position to match VR rig (prevent flip on load)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 14:33:11 -06:00

170 lines
4.9 KiB
TypeScript

/**
* Feature configuration system for controlling access to pages, features, and limits
* based on user tier/subscription level.
*/
export type UserTier = 'none' | 'free' | 'basic' | 'pro';
export type FeatureState = 'on' | 'coming-soon' | 'basic' | 'pro' | 'off';
export interface PageFlags {
examples: FeatureState;
documentation: FeatureState;
pricing: FeatureState;
vrExperience: FeatureState;
}
export interface FeatureFlags {
createDiagram: FeatureState;
createFromTemplate: FeatureState;
manageDiagrams: FeatureState;
shareCollaborate: FeatureState;
privateDesigns: FeatureState;
encryptedDesigns: FeatureState;
editData: FeatureState;
config: FeatureState;
enterImmersive: FeatureState;
launchMetaQuest: FeatureState;
}
export interface LimitFlags {
maxDiagrams: number;
maxCollaborators: number;
storageQuotaMB: number;
}
export interface FeatureConfig {
tier: UserTier;
pages: PageFlags;
features: FeatureFlags;
limits: LimitFlags;
}
/**
* Default configuration for unauthenticated users (guest mode).
* Allows limited access with local storage only (no sync/collaboration).
*/
export const DEFAULT_FEATURE_CONFIG: FeatureConfig = {
tier: 'none',
pages: {
examples: 'coming-soon',
documentation: 'coming-soon',
pricing: 'coming-soon',
vrExperience: 'on', // Allow VR experience for guests
},
features: {
createDiagram: 'basic', // Guests can create diagrams
createFromTemplate: 'coming-soon', // Coming soon for guests
manageDiagrams: 'basic', // Guests can manage their local diagrams
shareCollaborate: 'coming-soon', // Coming soon for guests
privateDesigns: 'coming-soon', // Coming soon for guests
encryptedDesigns: 'pro', // No encryption for guests
editData: 'coming-soon', // Guests can edit data
config: 'on', // Guests can access settings
enterImmersive: 'on', // Guests can enter immersive mode
launchMetaQuest: 'on', // Guests can launch on Meta Quest
},
limits: {
maxDiagrams: 3, // Guests limited to 3 diagrams
maxCollaborators: 0, // No collaboration for guests
storageQuotaMB: 50, // 50MB local storage for guests
},
};
export const BASIC_FEATURE_CONFIG: FeatureConfig = {
tier: 'basic',
pages: {
examples: 'off',
documentation: 'off',
pricing: 'coming-soon',
vrExperience: 'on',
},
features: {
createDiagram: 'on',
createFromTemplate: 'off',
manageDiagrams: 'off',
shareCollaborate: 'off',
privateDesigns: 'off',
encryptedDesigns: 'off',
editData: 'off',
config: 'off',
enterImmersive: 'off',
launchMetaQuest: 'off',
},
limits: {
maxDiagrams: 0,
maxCollaborators: 0,
storageQuotaMB: 0,
},
};
/**
* Type guard to check if a page name is valid
*/
export function isValidPage(page: string): page is keyof PageFlags {
return page in DEFAULT_FEATURE_CONFIG.pages;
}
/**
* Type guard to check if a feature name is valid
*/
export function isValidFeature(feature: string): feature is keyof FeatureFlags {
return feature in DEFAULT_FEATURE_CONFIG.features;
}
/**
* Type guard to check if a limit name is valid
*/
export function isValidLimit(limit: string): limit is keyof LimitFlags {
return limit in DEFAULT_FEATURE_CONFIG.limits;
}
/**
* Helper to check if a page is enabled (on) in the config
*/
export function isPageEnabled(config: FeatureConfig, page: keyof PageFlags): boolean {
return config.pages[page] === 'on';
}
/**
* Helper to check if a feature is enabled (on) in the config
*/
export function isFeatureEnabled(config: FeatureConfig, feature: keyof FeatureFlags): boolean {
return config.features[feature] === 'on';
}
/**
* Helper to check if a page or feature should be visible (not 'off')
*/
export function shouldShowPage(config: FeatureConfig, page: keyof PageFlags): boolean {
return config.pages[page] !== 'off';
}
/**
* Helper to check if a feature should be visible (not 'off')
*/
export function shouldShowFeature(config: FeatureConfig, feature: keyof FeatureFlags): boolean {
return config.features[feature] !== 'off';
}
/**
* Helper to get the state of a page
*/
export function getPageState(config: FeatureConfig, page: keyof PageFlags): FeatureState {
return config.pages[page];
}
/**
* Helper to get the state of a feature
*/
export function getFeatureState(config: FeatureConfig, feature: keyof FeatureFlags): FeatureState {
return config.features[feature];
}
/**
* Helper to get a limit value from the config
*/
export function getFeatureLimit(config: FeatureConfig, limit: keyof LimitFlags): number {
return config.limits[limit];
}