From 1c50dd5c84bad693ff43b6f68afb16e8d8c554c3 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Fri, 21 Nov 2025 14:33:11 -0600 Subject: [PATCH] Implement three-state feature flag system with upgrade badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/react/components/ComingSoonBadge.tsx | 22 ++++ src/react/components/UpgradeBadge.tsx | 35 ++++++ src/react/contexts/FeatureProvider.tsx | 18 +-- src/react/hooks/useFeatures.ts | 18 ++- src/react/pageHeader.tsx | 76 ++++++++--- src/react/pages/createDiagramModal.tsx | 58 +++++++-- src/react/pages/vrExperience.tsx | 133 ++++++++++++-------- src/util/featureConfig.ts | 154 +++++++++++++---------- src/vrApp.ts | 2 +- 9 files changed, 355 insertions(+), 161 deletions(-) create mode 100644 src/react/components/ComingSoonBadge.tsx create mode 100644 src/react/components/UpgradeBadge.tsx diff --git a/src/react/components/ComingSoonBadge.tsx b/src/react/components/ComingSoonBadge.tsx new file mode 100644 index 0000000..6d59cbe --- /dev/null +++ b/src/react/components/ComingSoonBadge.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Badge } from '@mantine/core'; + +interface ComingSoonBadgeProps { + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +} + +/** + * Small pill badge to indicate a feature or page is coming soon + */ +export default function ComingSoonBadge({ size = 'sm' }: ComingSoonBadgeProps) { + return ( + + Coming Soon! + + ); +} diff --git a/src/react/components/UpgradeBadge.tsx b/src/react/components/UpgradeBadge.tsx new file mode 100644 index 0000000..a198762 --- /dev/null +++ b/src/react/components/UpgradeBadge.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Badge } from '@mantine/core'; +import { IconStar } from '@tabler/icons-react'; + +interface UpgradeBadgeProps { + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + tier?: 'basic' | 'pro'; + onClick?: () => void; +} + +/** + * Small pill badge to indicate a feature requires sign up or upgrade + */ +export default function UpgradeBadge({ size = 'sm', tier, onClick }: UpgradeBadgeProps) { + const tierLabel = tier === 'basic' ? 'Sign Up for Free' : tier === 'pro' ? 'Upgrade to Pro' : 'Upgrade'; + const gradient = tier === 'pro' + ? { from: 'yellow', to: 'orange', deg: 90 } + : { from: 'indigo', to: 'grape', deg: 90 }; + + return ( + : undefined} + onClick={onClick} + > + {tierLabel} + + ); +} diff --git a/src/react/contexts/FeatureProvider.tsx b/src/react/contexts/FeatureProvider.tsx index e3e0a66..d4dcf15 100644 --- a/src/react/contexts/FeatureProvider.tsx +++ b/src/react/contexts/FeatureProvider.tsx @@ -1,7 +1,7 @@ import { ReactNode, useCallback, useEffect, useState } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { FeatureContext } from './FeatureContext'; -import { FeatureConfig, DEFAULT_FEATURE_CONFIG, GUEST_FEATURE_CONFIG } from '../../util/featureConfig'; +import { FeatureConfig, DEFAULT_FEATURE_CONFIG } from '../../util/featureConfig'; import log from 'loglevel'; const logger = log.getLogger('FeatureProvider'); @@ -31,7 +31,7 @@ async function fetchFeatureConfig(accessToken: string | undefined): Promise(GUEST_FEATURE_CONFIG); // Start with guest config + const [config, setConfig] = useState(DEFAULT_FEATURE_CONFIG); // Start with default (guest) config const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -67,10 +67,10 @@ export function FeatureProvider({ children }: FeatureProviderProps) { } } - // If not authenticated, use guest config + // If not authenticated, use default (guest) config if (!isAuthenticated) { - logger.info('User not authenticated, using guest config'); - setConfig(GUEST_FEATURE_CONFIG); + logger.info('User not authenticated, using default (guest) config'); + setConfig(DEFAULT_FEATURE_CONFIG); setIsLoading(false); return; } @@ -80,9 +80,9 @@ export function FeatureProvider({ children }: FeatureProviderProps) { } catch (err) { const error = err instanceof Error ? err : new Error('Unknown error fetching features'); setError(error); - // On error, fallback to guest config for better UX - logger.warn('Error loading features, falling back to guest config'); - setConfig(GUEST_FEATURE_CONFIG); + // On error, fallback to default (guest) config for better UX + logger.warn('Error loading features, falling back to default (guest) config'); + setConfig(DEFAULT_FEATURE_CONFIG); } finally { setIsLoading(false); } diff --git a/src/react/hooks/useFeatures.ts b/src/react/hooks/useFeatures.ts index 438bc08..5d23bd0 100644 --- a/src/react/hooks/useFeatures.ts +++ b/src/react/hooks/useFeatures.ts @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { FeatureContext } from '../contexts/FeatureContext'; -import { FeatureFlags, PageFlags, LimitFlags } from '../../util/featureConfig'; +import { FeatureFlags, FeatureState, LimitFlags, PageFlags } from '../../util/featureConfig'; /** * Hook to access the full feature configuration context @@ -16,21 +16,29 @@ export function useFeatures() { } /** - * Hook to check if a specific page is enabled + * Hook to get the state of a specific page */ -export function useIsPageEnabled(page: keyof PageFlags): boolean { +export function usePageState(page: keyof PageFlags): FeatureState { const { config } = useFeatures(); return config.pages[page]; } /** - * Hook to check if a specific feature is enabled + * Hook to get the state of a specific feature */ -export function useIsFeatureEnabled(feature: keyof FeatureFlags): boolean { +export function useFeatureState(feature: keyof FeatureFlags): FeatureState { const { config } = useFeatures(); return config.features[feature]; } +/** + * Hook to check if a specific page is enabled (on) + */ +export function useIsPageEnabled(page: keyof PageFlags): boolean { + const { config } = useFeatures(); + return config.pages[page] === 'on'; +} + /** * Hook to get a specific limit value */ diff --git a/src/react/pageHeader.tsx b/src/react/pageHeader.tsx index f2e7681..33dbdc2 100644 --- a/src/react/pageHeader.tsx +++ b/src/react/pageHeader.tsx @@ -2,14 +2,15 @@ import {Anchor, AppShell, Box, Burger, Button, Group, Image, Menu, Stack} from " import React from "react"; import {Link} from "react-router-dom"; import {useAuth0} from "@auth0/auth0-react"; -import {useIsPageEnabled} from "./hooks/useFeatures"; +import {usePageState} from "./hooks/useFeatures"; +import ComingSoonBadge from "./components/ComingSoonBadge"; export default function PageHeader() { const {user, isAuthenticated, loginWithRedirect, logout} = useAuth0(); - const examplesEnabled = useIsPageEnabled('examples'); - const documentationEnabled = useIsPageEnabled('documentation'); - const pricingEnabled = useIsPageEnabled('pricing'); - const vrExperienceEnabled = useIsPageEnabled('vrExperience'); + const examplesState = usePageState('examples'); + const documentationState = usePageState('documentation'); + const pricingState = usePageState('pricing'); + const vrExperienceState = usePageState('vrExperience'); const picture = () => { if (user.picture) { @@ -29,32 +30,69 @@ export default function PageHeader() { } } - // Define all possible menu items + // Define all possible menu items with their states const allItems = [ - {name: 'Examples', href: '/examples', key: 'examples', enabled: examplesEnabled}, - {name: 'About', href: '/', key: 'about', enabled: true}, // About (home) is always visible - {name: 'Documentation', href: '/documentation', key: 'documentation', enabled: documentationEnabled}, - {name: 'Pricing', href: '/pricing', key: 'pricing', enabled: pricingEnabled}, - {name: 'VR Experience', href: '/db/public/local', key: 'vrexperience', enabled: vrExperienceEnabled} + {name: 'Examples', href: '/examples', key: 'examples', state: examplesState}, + {name: 'About', href: '/', key: 'about', state: 'on' as const}, // About (home) is always visible + {name: 'Documentation', href: '/documentation', key: 'documentation', state: documentationState}, + {name: 'Pricing', href: '/pricing', key: 'pricing', state: pricingState}, + {name: 'VR Experience', href: '/db/public/local', key: 'vrexperience', state: vrExperienceState} ]; - // Filter to only enabled items - const items = allItems.filter(item => item.enabled) + // Filter to only 'on' and 'coming-soon' items (hide 'off' items) + const items = allItems.filter(item => item.state !== 'off') const mainMenu = function () { return items.map((item) => { + const isComingSoon = item.state === 'coming-soon'; return ( - {item.name} + + + {item.name} + + {isComingSoon && } + + ) }) } const miniMenu = function () { return items.map((item) => { + const isComingSoon = item.state === 'coming-soon'; return ( - {item.name} + + + + {item.name} + + {isComingSoon && } + + ) }) } diff --git a/src/react/pages/createDiagramModal.tsx b/src/react/pages/createDiagramModal.tsx index cabc885..4a699cf 100644 --- a/src/react/pages/createDiagramModal.tsx +++ b/src/react/pages/createDiagramModal.tsx @@ -3,16 +3,30 @@ import {usePouch} from "use-pouchdb"; import {useState} from "react"; import {v4} from "uuid"; import log from "loglevel"; -import {useIsFeatureEnabled} from "../hooks/useFeatures"; +import {useFeatureState} from "../hooks/useFeatures"; +import ComingSoonBadge from "../components/ComingSoonBadge"; +import UpgradeBadge from "../components/UpgradeBadge"; +import {useAuth0} from "@auth0/auth0-react"; export default function CreateDiagramModal({createOpened, closeCreate}) { const logger = log.getLogger('createDiagramModal'); const db = usePouch(); + const { loginWithRedirect } = useAuth0(); // Feature flags - const privateDesignsEnabled = useIsFeatureEnabled('privateDesigns'); - const encryptedDesignsEnabled = useIsFeatureEnabled('encryptedDesigns'); - const shareCollaborateEnabled = useIsFeatureEnabled('shareCollaborate'); + const privateDesignsState = useFeatureState('privateDesigns'); + const encryptedDesignsState = useFeatureState('encryptedDesigns'); + const shareCollaborateState = useFeatureState('shareCollaborate'); + + const privateDesignsEnabled = privateDesignsState === 'on'; + const encryptedDesignsEnabled = encryptedDesignsState === 'on'; + const shareCollaborateEnabled = shareCollaborateState === 'on'; + + const handleSignUp = () => { + loginWithRedirect({ + appState: { returnTo: window.location.pathname } + }); + }; const [diagram, setDiagram] = useState({ name: '', @@ -68,10 +82,16 @@ export default function CreateDiagramModal({createOpened, closeCreate}) { label="Private" checked={diagram.private} onChange={(e) => { - setDiagram({...diagram, private: e.currentTarget.checked}) + if (privateDesignsState === 'basic') { + handleSignUp(); + } else { + setDiagram({...diagram, private: e.currentTarget.checked}) + } }} - disabled={!privateDesignsEnabled}/> - {!privateDesignsEnabled && Basic} + disabled={!privateDesignsEnabled && privateDesignsState !== 'basic'}/> + {privateDesignsState === 'coming-soon' && } + {privateDesignsState === 'basic' && } + {privateDesignsState === 'pro' && } { - setDiagram({...diagram, encrypted: e.currentTarget.checked}) + if (encryptedDesignsState === 'basic') { + handleSignUp(); + } else { + setDiagram({...diagram, encrypted: e.currentTarget.checked}) + } }} - disabled={!encryptedDesignsEnabled}/> - {!encryptedDesignsEnabled && Pro} + disabled={!encryptedDesignsEnabled && encryptedDesignsState !== 'basic'}/> + {encryptedDesignsState === 'coming-soon' && } + {encryptedDesignsState === 'basic' && } + {encryptedDesignsState === 'pro' && } { - setDiagram({...diagram, invite: e.currentTarget.checked}) + if (shareCollaborateState === 'basic') { + handleSignUp(); + } else { + setDiagram({...diagram, invite: e.currentTarget.checked}) + } }} - disabled={!shareCollaborateEnabled}/> - {!shareCollaborateEnabled && Pro} + disabled={!shareCollaborateEnabled && shareCollaborateState !== 'basic'}/> + {shareCollaborateState === 'coming-soon' && } + {shareCollaborateState === 'basic' && } + {shareCollaborateState === 'pro' && } diff --git a/src/react/pages/vrExperience.tsx b/src/react/pages/vrExperience.tsx index 0bfc4bf..7191cf8 100644 --- a/src/react/pages/vrExperience.tsx +++ b/src/react/pages/vrExperience.tsx @@ -10,36 +10,57 @@ import {useNavigate, useParams} from "react-router-dom"; import {useDisclosure} from "@mantine/hooks"; import ConfigModal from "./configModal"; import log from "loglevel"; -import {useIsFeatureEnabled, useUserTier} from "../hooks/useFeatures"; +import {useFeatureState, useUserTier} from "../hooks/useFeatures"; import {useAuth0} from "@auth0/auth0-react"; import {GUEST_MODE_BANNER} from "../../content/upgradeCopy"; import {exportDiagramAsJSON} from "../../util/functions/exportDiagramAsJSON"; import {isMobileVRDevice} from "../../util/deviceDetection"; import {DefaultScene} from "../../defaultScene"; import VREntryPrompt from "../components/VREntryPrompt"; +import ComingSoonBadge from "../components/ComingSoonBadge"; +import UpgradeBadge from "../components/UpgradeBadge"; let vrApp: VrApp = null; const defaultCreate = window.localStorage.getItem('createOpened') === 'true'; const defaultConfig = window.localStorage.getItem('configOpened') === 'true'; const defaultManage = window.localStorage.getItem('manageOpened') === 'true'; +const defaultMenuOpened = window.localStorage.getItem('menuOpened') !== 'false'; // Default to true (open) + export default function VrExperience() { const logger = log.getLogger('vrExperience'); const params = useParams(); const { isAuthenticated, loginWithRedirect } = useAuth0(); const [guestBannerDismissed, setGuestBannerDismissed] = useState(false); + const [menuOpened, setMenuOpened] = useState(defaultMenuOpened); - // Feature flags - const createDiagramEnabled = useIsFeatureEnabled('createDiagram'); - const createFromTemplateEnabled = useIsFeatureEnabled('createFromTemplate'); - const manageDiagramsEnabled = useIsFeatureEnabled('manageDiagrams'); - const shareCollaborateEnabled = useIsFeatureEnabled('shareCollaborate'); - const editDataEnabled = useIsFeatureEnabled('editData'); - const configEnabled = useIsFeatureEnabled('config'); - const enterImmersiveEnabled = useIsFeatureEnabled('enterImmersive'); - const launchMetaQuestEnabled = useIsFeatureEnabled('launchMetaQuest'); + // Feature flags - get states instead of just enabled boolean + const createDiagramState = useFeatureState('createDiagram'); + const createFromTemplateState = useFeatureState('createFromTemplate'); + const manageDiagramsState = useFeatureState('manageDiagrams'); + const shareCollaborateState = useFeatureState('shareCollaborate'); + const editDataState = useFeatureState('editData'); + const configState = useFeatureState('config'); + const enterImmersiveState = useFeatureState('enterImmersive'); + const launchMetaQuestState = useFeatureState('launchMetaQuest'); const userTier = useUserTier(); + // Helper to check if feature should be shown (not 'off') + const shouldShow = (state) => state !== 'off'; + const isEnabled = (state) => state === 'on'; + + // Get the appropriate click handler based on feature state + const getClickHandler = (state, enabledHandler) => { + if (state === 'on') { + return enabledHandler; // Feature is enabled, use the normal handler + } + if (state === 'basic') { + return handleSignUp; // Feature requires sign up, trigger auth + } + // For 'coming-soon', 'pro', or other states, no click handler + return null; + }; + const handleSignUp = () => { loginWithRedirect({ appState: { returnTo: window.location.pathname } @@ -59,6 +80,12 @@ export default function VrExperience() { logger.debug('saving', key, value) window.localStorage.setItem(key, value ? 'true' : 'false'); } + + const toggleMenu = () => { + const newState = !menuOpened; + setMenuOpened(newState); + window.localStorage.setItem('menuOpened', newState ? 'true' : 'false'); + }; const [createOpened, {open: openCreate, close: closeCreate}] = useDisclosure(defaultCreate, { @@ -140,21 +167,27 @@ export default function VrExperience() { const [immersiveDisabled, setImmersiveDisabled] = useState(true); const navigate = useNavigate(); - // Tier indicator functions - now using the actual user tier - const getTierIndicator = (requiredTier: 'free' | 'basic' | 'pro') => { - if (requiredTier === 'free' || userTier === requiredTier || - (userTier === 'pro' && (requiredTier === 'basic' || requiredTier === 'free')) || - (userTier === 'basic' && requiredTier === 'free')) { - return null; // User has access, no indicator needed + // Get the appropriate indicator for a feature based on its state + const getFeatureIndicator = (featureState) => { + // 'off' - don't show at all (handled by shouldShow) + // 'on' - no badge needed, feature is fully accessible + // 'coming-soon' - show Coming Soon badge (visible to all) + // 'basic' - show Sign Up badge (requires basic tier) - clickable to sign up + // 'pro' - show Upgrade to Pro badge (requires pro tier) + + if (featureState === 'coming-soon') { + return ; } - // Show tier requirement - if (requiredTier === 'basic') { - return Basic; + if (featureState === 'basic') { + return ; } - if (requiredTier === 'pro') { - return Pro!; + + if (featureState === 'pro') { + return ; } + + // 'on' state - no indicator needed return null; } @@ -214,9 +247,9 @@ export default function VrExperience() { {createModal()} {manageModal()} - + - + {/* Home is always visible */} @@ -228,59 +261,59 @@ export default function VrExperience() { label="Home" availableIcon={null}/> - {enterImmersiveEnabled && ( + {shouldShow(enterImmersiveState) && ( + availableIcon={getFeatureIndicator(enterImmersiveState)}/> )} - {launchMetaQuestEnabled && ( + {shouldShow(launchMetaQuestState) && ( { + onClick={getClickHandler(launchMetaQuestState, () => { window.open('https://www.oculus.com/open_url/?url=' + window.location.href, 'launchQuest', 'popup') - }} + })} label="Launch On Meta Quest" - availableIcon={getTierIndicator('free')}/> + availableIcon={getFeatureIndicator(launchMetaQuestState)}/> )} - {editDataEnabled && ( + {shouldShow(editDataState) && ( <> + onClick={getClickHandler(editDataState, null)} + availableIcon={getFeatureIndicator(editDataState)}/> )} - {(createDiagramEnabled || createFromTemplateEnabled || manageDiagramsEnabled) && } + {(shouldShow(createDiagramState) || shouldShow(createFromTemplateState) || shouldShow(manageDiagramsState)) && } - {createDiagramEnabled && ( + {shouldShow(createDiagramState) && ( + onClick={getClickHandler(createDiagramState, openCreate)} + availableIcon={getFeatureIndicator(createDiagramState)}/> )} - {createFromTemplateEnabled && ( + {shouldShow(createFromTemplateState) && ( + onClick={getClickHandler(createFromTemplateState, null)} + availableIcon={getFeatureIndicator(createFromTemplateState)}/> )} - {manageDiagramsEnabled && ( + {shouldShow(manageDiagramsState) && ( + onClick={getClickHandler(manageDiagramsState, openManage)} + availableIcon={getFeatureIndicator(manageDiagramsState)}/> )} {/* Export JSON - Always available for creating templates */} @@ -290,22 +323,22 @@ export default function VrExperience() { onClick={handleExportJSON} availableIcon={null}/> - {(shareCollaborateEnabled || configEnabled) && } + {(shouldShow(shareCollaborateState) || shouldShow(configState)) && } - {shareCollaborateEnabled && ( + {shouldShow(shareCollaborateState) && ( + onClick={getClickHandler(shareCollaborateState, null)} + availableIcon={getFeatureIndicator(shareCollaborateState)}/> )} - {configEnabled && ( + {shouldShow(configState) && ( + onClick={getClickHandler(configState, openConfig)} + availableIcon={getFeatureIndicator(configState)}/> )} diff --git a/src/util/featureConfig.ts b/src/util/featureConfig.ts index 852347b..9bf4c13 100644 --- a/src/util/featureConfig.ts +++ b/src/util/featureConfig.ts @@ -5,24 +5,26 @@ export type UserTier = 'none' | 'free' | 'basic' | 'pro'; +export type FeatureState = 'on' | 'coming-soon' | 'basic' | 'pro' | 'off'; + export interface PageFlags { - examples: boolean; - documentation: boolean; - pricing: boolean; - vrExperience: boolean; + examples: FeatureState; + documentation: FeatureState; + pricing: FeatureState; + vrExperience: FeatureState; } export interface FeatureFlags { - createDiagram: boolean; - createFromTemplate: boolean; - manageDiagrams: boolean; - shareCollaborate: boolean; - privateDesigns: boolean; - encryptedDesigns: boolean; - editData: boolean; - config: boolean; - enterImmersive: boolean; - launchMetaQuest: boolean; + createDiagram: FeatureState; + createFromTemplate: FeatureState; + manageDiagrams: FeatureState; + shareCollaborate: FeatureState; + privateDesigns: FeatureState; + encryptedDesigns: FeatureState; + editData: FeatureState; + config: FeatureState; + enterImmersive: FeatureState; + launchMetaQuest: FeatureState; } export interface LimitFlags { @@ -39,28 +41,55 @@ export interface FeatureConfig { } /** - * Default configuration for unauthenticated users or when API fetch fails. - * Everything is disabled except the home page. + * 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: false, - documentation: false, - pricing: false, - vrExperience: false, + examples: 'coming-soon', + documentation: 'coming-soon', + pricing: 'coming-soon', + vrExperience: 'on', // Allow VR experience for guests }, features: { - createDiagram: false, - createFromTemplate: false, - manageDiagrams: false, - shareCollaborate: false, - privateDesigns: false, - encryptedDesigns: false, - editData: false, - config: false, - enterImmersive: false, - launchMetaQuest: false, + 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, @@ -69,37 +98,6 @@ export const DEFAULT_FEATURE_CONFIG: FeatureConfig = { }, }; -/** - * Guest mode configuration for unauthenticated users. - * Allows limited access with local storage only (no sync/collaboration). - */ -export const GUEST_FEATURE_CONFIG: FeatureConfig = { - tier: 'none', - pages: { - examples: false, - documentation: false, - pricing: false, - vrExperience: true, // Allow VR experience for guests - }, - features: { - createDiagram: true, // Guests can create diagrams - createFromTemplate: false, // No templates for guests - manageDiagrams: true, // Guests can manage their local diagrams - shareCollaborate: false, // No sharing/collaboration for guests - privateDesigns: false, // No private designs (local only anyway) - encryptedDesigns: false, // No encryption for guests - editData: true, // Guests can edit data - config: true, // Guests can access settings - enterImmersive: true, // Guests can enter immersive mode - launchMetaQuest: true, // 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 - }, -}; - /** * Type guard to check if a page name is valid */ @@ -122,16 +120,44 @@ export function isValidLimit(limit: string): limit is keyof LimitFlags { } /** - * Helper to check if a page is enabled in the config + * 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 check if a feature is enabled in the config + * Helper to get the state of a feature */ -export function isFeatureEnabled(config: FeatureConfig, feature: keyof FeatureFlags): boolean { +export function getFeatureState(config: FeatureConfig, feature: keyof FeatureFlags): FeatureState { return config.features[feature]; } diff --git a/src/vrApp.ts b/src/vrApp.ts index d0c2160..073f995 100644 --- a/src/vrApp.ts +++ b/src/vrApp.ts @@ -108,7 +108,7 @@ export default class VrApp { private setMainCamera(scene: Scene) { const CAMERA_NAME = 'Main Camera'; const camera: FreeCamera = new FreeCamera(CAMERA_NAME, - new Vector3(0, 1.6, 0), scene); + new Vector3(0, 1.6, -5), scene); // Match VR rig Z position to prevent flip scene.setActiveCameraByName(CAMERA_NAME); /* if (!this._mobileCamera) {