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()}
-