feat: Implement license-aware UI for revenue optimization (Phase 3A)

Business Objective: Transform WHOOSH from license-unaware to comprehensive
license-integrated experience that drives upgrade conversions and maximizes
customer lifetime value through usage visibility.

Implementation Summary:

1. SECURE BACKEND PROXY INTEGRATION:
   - License API proxy endpoints (/api/license/status, /api/license/quotas)
   - Server-side license ID resolution (no frontend exposure)
   - Mock data support for development and testing
   - Intelligent upgrade suggestion algorithms

2. COMPREHENSIVE FRONTEND LICENSE INTEGRATION:
   - License API Client with caching and error handling
   - Global License Context for state management
   - License Status Header for always-visible tier information
   - Feature Gate Component for conditional rendering
   - License Dashboard with quotas, features, upgrade suggestions
   - Upgrade Prompt Components for revenue optimization

3. APPLICATION-WIDE INTEGRATION:
   - License Provider integrated into App context hierarchy
   - License status header in main navigation
   - License dashboard route at /license
   - Example feature gates in Analytics page
   - Version bump: → 1.2.0

Key Business Benefits:
 Revenue Optimization: Strategic feature gating drives conversions
 User Trust: Transparent license information builds confidence
 Proactive Upgrades: Usage-based suggestions with ROI estimates
 Self-Service: Clear upgrade paths reduce sales friction

Security-First Design:
🔒 All license operations server-side via proxy
🔒 No sensitive license data exposed to frontend
🔒 Feature enforcement at API level prevents bypass
🔒 Graceful degradation for license API failures

Technical Implementation:
- React 18+ with TypeScript and modern hooks
- Context API for license state management
- Tailwind CSS following existing patterns
- Backend proxy pattern for security compliance
- Comprehensive error handling and loading states

Files Created/Modified:
Backend:
- /backend/app/api/license.py - Complete license proxy API
- /backend/app/main.py - Router integration

Frontend:
- /frontend/src/services/licenseApi.ts - API client with caching
- /frontend/src/contexts/LicenseContext.tsx - Global license state
- /frontend/src/hooks/useLicenseFeatures.ts - Feature checking logic
- /frontend/src/components/license/* - Complete license UI components
- /frontend/src/App.tsx - Context integration and routing
- /frontend/package.json - Version bump to 1.2.0

This Phase 3A implementation provides the complete foundation for
license-aware user experiences, driving revenue optimization through
intelligent feature gating and upgrade suggestions while maintaining
excellent UX and security best practices.

Ready for KACHING integration and Phase 3B advanced features.

🤖 Generated with Claude Code (claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-01 16:20:24 +10:00
parent 268214d971
commit a880b26951
15 changed files with 4511 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "whoosh-frontend",
"version": "1.1.0",
"version": "1.2.0",
"description": "WHOOSH Distributed AI Orchestration Platform - Frontend",
"private": true,
"scripts": {

View File

@@ -5,6 +5,7 @@ import Layout from './components/Layout'
import { SocketIOProvider } from './contexts/SocketIOContext'
import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import { LicenseProvider } from './contexts/LicenseContext'
import ProtectedRoute from './components/auth/ProtectedRoute'
import ClusterDetector from './components/setup/ClusterDetector'
import Login from './pages/Login'
@@ -25,6 +26,7 @@ import BzzzChat from './pages/BzzzChat'
import BzzzTeam from './pages/BzzzTeam'
import AIModels from './pages/AIModels'
import GitRepositories from './pages/GitRepositories'
import LicenseDashboard from './components/license/LicenseDashboard'
function App() {
// Check for connection issues and provide fallback
@@ -208,6 +210,15 @@ function App() {
</ProtectedRoute>
} />
{/* License Dashboard */}
<Route path="/license" element={
<ProtectedRoute>
<Layout>
<LicenseDashboard />
</Layout>
</ProtectedRoute>
} />
{/* Redirect unknown routes to dashboard */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
@@ -218,15 +229,17 @@ function App() {
<ThemeProvider>
<ClusterDetector>
<AuthProvider>
<ReactFlowProvider>
{socketIOEnabled ? (
<SocketIOProvider>
<LicenseProvider>
<ReactFlowProvider>
{socketIOEnabled ? (
<SocketIOProvider>
<AppContent />
</SocketIOProvider>
) : (
<AppContent />
</SocketIOProvider>
) : (
<AppContent />
)}
</ReactFlowProvider>
)}
</ReactFlowProvider>
</LicenseProvider>
</AuthProvider>
</ClusterDetector>
</ThemeProvider>

View File

@@ -14,12 +14,14 @@ import {
ChevronDownIcon,
AdjustmentsHorizontalIcon,
ChatBubbleLeftRightIcon,
CpuChipIcon
CpuChipIcon,
ShieldCheckIcon
} from '@heroicons/react/24/outline';
import { GitBranch } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import UserProfile from './auth/UserProfile';
import { ThemeToggle } from './ThemeToggle';
import { LicenseStatusHeader } from './license/LicenseStatusHeader';
import WHOOSHLogo from '../assets/WHOOSH_symbol.png';
interface NavigationItem {
@@ -41,6 +43,7 @@ const navigation: NavigationItem[] = [
{ name: 'Bzzz Chat', href: '/bzzz-chat', icon: ChatBubbleLeftRightIcon },
{ name: 'Bzzz Team', href: '/bzzz-team', icon: UserGroupIcon },
{ name: 'Analytics', href: '/analytics', icon: ChartBarIcon },
{ name: 'License', href: '/license', icon: ShieldCheckIcon },
{ name: 'Settings', href: '/settings', icon: AdjustmentsHorizontalIcon },
];
@@ -174,8 +177,9 @@ export default function Layout({ children }: LayoutProps) {
</div>
</div>
{/* Theme toggle and User menu */}
{/* License Status, Theme toggle and User menu */}
<div className="flex items-center space-x-3">
<LicenseStatusHeader compact={true} className="hidden sm:block" />
<ThemeToggle />
<div className="relative" ref={userMenuRef}>
<button

View File

@@ -0,0 +1,403 @@
/**
* FeatureGate Component
*
* This component implements license-based feature gating throughout the WHOOSH application.
* It conditionally renders content based on license tier and feature availability.
*
* Key Features:
* - Server-side feature validation for security
* - Customizable fallback content for restricted features
* - Intelligent upgrade prompts with ROI messaging
* - Graceful degradation for license API failures
* - Loading state handling during feature checks
*
* Business Logic:
* - Prevents access to premium features for lower tiers
* - Converts feature restrictions into upgrade opportunities
* - Provides clear value proposition for restricted features
* - Tracks feature gate interactions for business intelligence
*
* Security Model:
* - All feature validation happens server-side
* - Client-side gates are UX enhancement only
* - Backend APIs enforce feature restrictions independently
* - No sensitive license data exposed to frontend
*/
import React, { useState, useEffect, ReactNode } from 'react';
import { LockClosedIcon, SparklesIcon, ArrowUpIcon } from '@heroicons/react/24/outline';
import { useLicense } from '../../contexts/LicenseContext';
import { FeatureAvailability } from '../../services/licenseApi';
/**
* Props interface for FeatureGate component
*/
interface FeatureGateProps {
/** The feature name to check against license */
feature: string;
/** Content to render when feature is available */
children: ReactNode;
/** Custom fallback content when feature is not available */
fallback?: ReactNode;
/** Whether to show upgrade prompt for restricted features */
showUpgradePrompt?: boolean;
/** Custom upgrade prompt message */
upgradeMessage?: string;
/** Custom upgrade benefits list */
upgradeBenefits?: string[];
/** Loading placeholder while checking feature availability */
loadingFallback?: ReactNode;
/** Custom CSS classes */
className?: string;
/** Callback when upgrade is clicked */
onUpgradeClick?: () => void;
/** Callback when feature is restricted (for analytics) */
onFeatureRestricted?: (feature: string, tierRequired?: string) => void;
/** Force server-side validation (bypasses client cache) */
forceServerValidation?: boolean;
/** Silent mode - don't show any UI for restricted features */
silent?: boolean;
}
/**
* Default upgrade benefits for common features
*/
const DEFAULT_FEATURE_BENEFITS = {
'advanced-search': [
'Complex search operators and filters',
'Date range and metadata filtering',
'Saved search queries',
'Export search results'
],
'analytics': [
'Detailed usage analytics and trends',
'Custom dashboards and reports',
'Performance metrics tracking',
'Historical data analysis'
],
'workflows': [
'Multi-agent workflow orchestration',
'Custom workflow templates',
'Automated task scheduling',
'Workflow performance monitoring'
],
'bulk-operations': [
'Batch processing capabilities',
'Large dataset operations',
'Automated bulk actions',
'Enterprise-scale processing'
],
'api-access': [
'Full REST API access',
'Webhook integrations',
'Custom automation tools',
'Third-party integrations'
],
'enterprise-support': [
'Priority technical support',
'Dedicated account management',
'SLA guarantees',
'Custom feature development'
]
};
/**
* UpgradePrompt sub-component
* Renders upgrade messaging for restricted features
*/
interface UpgradePromptProps {
feature: string;
tierRequired?: string;
reason?: string;
benefits?: string[];
customMessage?: string;
onUpgradeClick?: () => void;
className?: string;
}
const UpgradePrompt: React.FC<UpgradePromptProps> = ({
feature,
tierRequired,
reason,
benefits,
customMessage,
onUpgradeClick,
className = ''
}) => {
const featureBenefits = benefits || DEFAULT_FEATURE_BENEFITS[feature as keyof typeof DEFAULT_FEATURE_BENEFITS] || [];
const handleUpgradeClick = () => {
if (onUpgradeClick) {
onUpgradeClick();
} else {
// Default behavior - could open upgrade modal
console.log('Upgrade needed for feature:', feature);
}
};
return (
<div className={`bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6 ${className}`}>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<LockClosedIcon className="h-6 w-6 text-blue-600" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<SparklesIcon className="h-5 w-5 text-blue-600" />
<h3 className="text-lg font-semibold text-gray-900">
Unlock {feature.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h3>
</div>
<p className="text-gray-600 mb-4">
{customMessage || reason || `This feature requires ${tierRequired || 'a higher'} tier license.`}
</p>
{featureBenefits.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-900 mb-2">What you'll get:</h4>
<ul className="space-y-1">
{featureBenefits.map((benefit, index) => (
<li key={index} className="flex items-center text-sm text-gray-700">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full mr-3 flex-shrink-0"></div>
{benefit}
</li>
))}
</ul>
</div>
)}
<div className="flex items-center space-x-3">
<button
onClick={handleUpgradeClick}
className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<ArrowUpIcon className="h-4 w-4" />
<span>Upgrade Now</span>
</button>
<button
onClick={() => {/* Could open tier comparison */}}
className="text-blue-600 hover:text-blue-700 font-medium text-sm"
>
Compare Plans
</button>
</div>
</div>
</div>
</div>
);
};
/**
* FeatureGate Component
*
* Main component that handles feature gating logic and renders appropriate content
* based on license status and feature availability.
*/
export const FeatureGate: React.FC<FeatureGateProps> = ({
feature,
children,
fallback,
showUpgradePrompt = true,
upgradeMessage,
upgradeBenefits,
loadingFallback,
className = '',
onUpgradeClick,
onFeatureRestricted,
forceServerValidation = false,
silent = false,
}) => {
const { hasFeature, checkFeature, isLoading: licenseLoading } = useLicense();
// Local state for server-side feature validation
const [serverFeatureCheck, setServerFeatureCheck] = useState<FeatureAvailability | null>(null);
const [isCheckingServer, setIsCheckingServer] = useState(false);
const [checkError, setCheckError] = useState<string | null>(null);
/**
* Perform server-side feature validation
* Used when forceServerValidation is true or for sensitive features
*/
useEffect(() => {
if (forceServerValidation) {
const performServerCheck = async () => {
setIsCheckingServer(true);
setCheckError(null);
try {
const result = await checkFeature(feature);
setServerFeatureCheck(result);
// Notify about feature restriction for analytics
if (result && !result.available && onFeatureRestricted) {
onFeatureRestricted(feature, result.tier_required);
}
} catch (error) {
console.error(`Server feature check failed for ${feature}:`, error);
setCheckError('Unable to verify feature access');
} finally {
setIsCheckingServer(false);
}
};
performServerCheck();
}
}, [feature, forceServerValidation, checkFeature, onFeatureRestricted]);
/**
* Determine feature availability
* Uses server validation if available, falls back to client check
*/
const getFeatureAvailability = (): {
available: boolean;
tierRequired?: string;
reason?: string;
} => {
if (forceServerValidation && serverFeatureCheck) {
return {
available: serverFeatureCheck.available,
tierRequired: serverFeatureCheck.tier_required,
reason: serverFeatureCheck.reason,
};
}
// Fallback to client-side check
return {
available: hasFeature(feature),
reason: hasFeature(feature) ? undefined : 'Feature not available in current tier',
};
};
/**
* Determine loading state
*/
const isLoading = licenseLoading || (forceServerValidation && isCheckingServer);
/**
* Handle loading state
*/
if (isLoading) {
if (loadingFallback) {
return <>{loadingFallback}</>;
}
// Default loading state
return (
<div className={`animate-pulse ${className}`}>
<div className="bg-gray-200 rounded h-20 w-full"></div>
</div>
);
}
/**
* Handle server check errors
*/
if (checkError && forceServerValidation) {
if (silent) return null;
return (
<div className={`bg-yellow-50 border border-yellow-200 rounded-lg p-4 ${className}`}>
<div className="text-yellow-800">
<p className="font-medium">Feature Check Error</p>
<p className="text-sm mt-1">{checkError}</p>
</div>
</div>
);
}
/**
* Check feature availability and render accordingly
*/
const { available, tierRequired, reason } = getFeatureAvailability();
/**
* Feature is available - render children
*/
if (available) {
return <div className={className}>{children}</div>;
}
/**
* Feature is not available - handle fallback
*/
// Silent mode - render nothing
if (silent) {
return null;
}
// Custom fallback provided
if (fallback) {
return <div className={className}>{fallback}</div>;
}
// Show upgrade prompt (default behavior)
if (showUpgradePrompt) {
return (
<UpgradePrompt
feature={feature}
tierRequired={tierRequired}
reason={reason}
benefits={upgradeBenefits}
customMessage={upgradeMessage}
onUpgradeClick={onUpgradeClick}
className={className}
/>
);
}
// No fallback - render nothing
return null;
};
/**
* Higher-order component for feature gating
* Provides a wrapper pattern for components that need feature gating
*/
export const withFeatureGate = <P extends object>(
WrappedComponent: React.ComponentType<P>,
feature: string,
gateProps?: Partial<FeatureGateProps>
) => {
const FeatureGatedComponent: React.FC<P> = (props) => (
<FeatureGate feature={feature} {...gateProps}>
<WrappedComponent {...props} />
</FeatureGate>
);
FeatureGatedComponent.displayName = `withFeatureGate(${WrappedComponent.displayName || WrappedComponent.name})`;
return FeatureGatedComponent;
};
/**
* Utility hook for feature gating in functional components
* Returns a render function that handles feature gating
*/
export const useFeatureGate = (feature: string) => {
const { hasFeature } = useLicense();
return {
isAvailable: hasFeature(feature),
renderIfAvailable: (content: ReactNode) => hasFeature(feature) ? content : null,
renderIfRestricted: (content: ReactNode) => !hasFeature(feature) ? content : null,
};
};
export default FeatureGate;

View File

@@ -0,0 +1,492 @@
/**
* License Dashboard Component
*
* This component provides a comprehensive view of license status, quotas, and upgrade opportunities.
* It serves as the central hub for license management and upgrade conversion.
*
* Key Features:
* - Complete license status overview with tier information
* - Real-time quota usage monitoring with visual indicators
* - Intelligent upgrade suggestions based on usage patterns
* - Feature availability matrix for tier comparison
* - Expiration tracking and renewal reminders
*
* Business Logic:
* - Maximizes upgrade conversion through strategic messaging
* - Provides transparent usage information to build trust
* - Shows clear value proposition for higher tiers
* - Enables self-service upgrade workflows
*
* Revenue Optimization:
* - Usage-based upgrade recommendations
* - ROI calculations for upgrade justification
* - Urgency indicators for time-sensitive upgrades
* - Clear tier comparison for informed decisions
*/
import React, { useState } from 'react';
import {
ChartBarIcon,
ClockIcon,
ExclamationTriangleIcon,
SparklesIcon,
CheckCircleIcon,
XCircleIcon,
ArrowUpIcon,
InformationCircleIcon
} from '@heroicons/react/24/outline';
import { useLicense } from '../../contexts/LicenseContext';
import { useLicenseFeatures } from '../../hooks/useLicenseFeatures';
import { LicenseQuotas, UpgradeSuggestion } from '../../services/licenseApi';
/**
* Props for the License Dashboard component
*/
interface LicenseDashboardProps {
className?: string;
showUpgradeActions?: boolean;
onUpgradeClick?: (suggestion: UpgradeSuggestion) => void;
onRenewClick?: () => void;
}
/**
* Quota Card sub-component
* Displays individual quota usage with visual progress indicators
*/
interface QuotaCardProps {
quotaType: keyof LicenseQuotas;
quota: LicenseQuotas[keyof LicenseQuotas];
title: string;
icon: React.ComponentType<any>;
unit?: string;
}
const QuotaCard: React.FC<QuotaCardProps> = ({
quotaType,
quota,
title,
icon: Icon,
unit = ''
}) => {
const getStatusColor = (percentage: number) => {
if (percentage >= 95) return 'text-red-600 bg-red-50 border-red-200';
if (percentage >= 80) return 'text-yellow-600 bg-yellow-50 border-yellow-200';
return 'text-green-600 bg-green-50 border-green-200';
};
const getProgressColor = (percentage: number) => {
if (percentage >= 95) return 'bg-red-500';
if (percentage >= 80) return 'bg-yellow-500';
return 'bg-green-500';
};
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num.toString();
};
return (
<div className={`border rounded-lg p-6 ${getStatusColor(quota.percentage)}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<Icon className="h-6 w-6" />
<h3 className="font-semibold">{title}</h3>
</div>
<span className="text-sm font-medium">
{quota.percentage}%
</span>
</div>
<div className="mb-3">
<div className="flex justify-between text-sm mb-2">
<span>{formatNumber(quota.used)} {unit}</span>
<span className="text-gray-500">
{quota.limit === -1 ? 'Unlimited' : `${formatNumber(quota.limit)} ${unit}`}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-500 ${getProgressColor(quota.percentage)}`}
style={{ width: `${Math.min(quota.percentage, 100)}%` }}
/>
</div>
</div>
{quota.percentage >= 80 && (
<div className="text-xs font-medium">
{quota.percentage >= 95
? '⚠️ Quota exceeded - upgrade needed'
: '⚠️ Approaching limit'
}
</div>
)}
</div>
);
};
/**
* Upgrade Suggestion Card sub-component
* Displays intelligent upgrade recommendations with ROI information
*/
interface UpgradeSuggestionCardProps {
suggestion: UpgradeSuggestion;
onUpgradeClick?: (suggestion: UpgradeSuggestion) => void;
}
const UpgradeSuggestionCard: React.FC<UpgradeSuggestionCardProps> = ({
suggestion,
onUpgradeClick
}) => {
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'high': return 'border-red-500 bg-red-50';
case 'medium': return 'border-yellow-500 bg-yellow-50';
default: return 'border-blue-500 bg-blue-50';
}
};
const getUrgencyIcon = (urgency: string) => {
switch (urgency) {
case 'high': return <ExclamationTriangleIcon className="h-5 w-5 text-red-600" />;
case 'medium': return <ClockIcon className="h-5 w-5 text-yellow-600" />;
default: return <SparklesIcon className="h-5 w-5 text-blue-600" />;
}
};
return (
<div className={`border-l-4 rounded-lg p-6 ${getUrgencyColor(suggestion.urgency)}`}>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
{getUrgencyIcon(suggestion.urgency)}
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{suggestion.reason}</h4>
<p className="text-sm text-gray-600 mt-1">
Upgrade from {suggestion.current_tier} to {suggestion.suggested_tier}
</p>
{suggestion.roi_estimate && (
<div className="mt-2 text-sm font-medium text-green-600">
💡 {suggestion.roi_estimate}
</div>
)}
</div>
</div>
<button
onClick={() => onUpgradeClick?.(suggestion)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<ArrowUpIcon className="h-4 w-4" />
<span>Upgrade</span>
</button>
</div>
<div className="mt-4">
<h5 className="text-sm font-medium text-gray-900 mb-2">What you'll get:</h5>
<ul className="space-y-1">
{suggestion.benefits.map((benefit, idx) => (
<li key={idx} className="flex items-center text-sm text-gray-700">
<CheckCircleIcon className="h-4 w-4 text-green-500 mr-2 flex-shrink-0" />
{benefit}
</li>
))}
</ul>
</div>
</div>
);
};
/**
* Feature Matrix sub-component
* Shows available features for current tier vs what's available in higher tiers
*/
const FeatureMatrix: React.FC = () => {
const { licenseStatus, availableTiers } = useLicense();
if (!licenseStatus || !availableTiers) return null;
const currentTier = licenseStatus.tier;
const tierOrder = ['evaluation', 'standard', 'enterprise'];
const currentTierFeatures = licenseStatus.features;
return (
<div className="bg-white border rounded-lg p-6">
<h3 className="font-semibold text-gray-900 mb-4">Feature Availability</h3>
<div className="space-y-3">
{Array.from(new Set([
...currentTierFeatures,
...Object.values(availableTiers.tiers).flatMap(tier => tier.features)
])).map(feature => {
const isAvailable = currentTierFeatures.includes(feature);
const featureName = feature.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
return (
<div key={feature} className="flex items-center justify-between py-2">
<span className="text-sm text-gray-700">{featureName}</span>
<div className="flex items-center">
{isAvailable ? (
<CheckCircleIcon className="h-5 w-5 text-green-500" />
) : (
<XCircleIcon className="h-5 w-5 text-gray-300" />
)}
</div>
</div>
);
})}
</div>
</div>
);
};
/**
* License Status Overview sub-component
*/
const LicenseStatusOverview: React.FC<{ onRenewClick?: () => void }> = ({ onRenewClick }) => {
const { licenseStatus } = useLicense();
if (!licenseStatus) return null;
const expirationDate = new Date(licenseStatus.expires_at);
const daysUntilExpiration = Math.ceil((expirationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
const isExpiringSoon = daysUntilExpiration <= 30;
const getStatusDisplay = (status: string) => {
switch (status) {
case 'active': return { text: 'Active', color: 'text-green-600 bg-green-50 border-green-200' };
case 'suspended': return { text: 'Suspended', color: 'text-red-600 bg-red-50 border-red-200' };
case 'expired': return { text: 'Expired', color: 'text-orange-600 bg-orange-50 border-orange-200' };
default: return { text: 'Unknown', color: 'text-gray-600 bg-gray-50 border-gray-200' };
}
};
const statusDisplay = getStatusDisplay(licenseStatus.status);
return (
<div className="bg-white border rounded-lg p-6">
<h3 className="font-semibold text-gray-900 mb-4">License Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-gray-500">Tier</label>
<div className={`mt-1 inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-${licenseStatus.tier_color}-50 text-${licenseStatus.tier_color}-700`}>
{licenseStatus.tier_display_name}
</div>
</div>
<div>
<label className="text-sm text-gray-500">Status</label>
<div className={`mt-1 inline-flex items-center px-3 py-1 rounded-full text-sm font-medium border ${statusDisplay.color}`}>
{statusDisplay.text}
</div>
</div>
<div>
<label className="text-sm text-gray-500">Max Nodes</label>
<p className="mt-1 text-sm font-medium">{licenseStatus.max_nodes}</p>
</div>
<div>
<label className="text-sm text-gray-500">Expires</label>
<div className="mt-1 flex items-center space-x-2">
<p className={`text-sm font-medium ${isExpiringSoon ? 'text-orange-600' : ''}`}>
{expirationDate.toLocaleDateString()}
</p>
{isExpiringSoon && (
<span className="text-xs bg-orange-100 text-orange-700 px-2 py-1 rounded-full">
{daysUntilExpiration} days left
</span>
)}
</div>
</div>
</div>
{(isExpiringSoon || licenseStatus.status !== 'active') && (
<div className="mt-4 p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-start space-x-2">
<ExclamationTriangleIcon className="h-5 w-5 text-orange-600 mt-0.5" />
<div className="flex-1">
<h4 className="text-sm font-medium text-orange-900">Action Required</h4>
<p className="text-sm text-orange-700 mt-1">
{licenseStatus.status !== 'active'
? 'Your license is not active. Contact support to resolve this issue.'
: `Your license expires in ${daysUntilExpiration} days. Renew now to avoid service interruption.`
}
</p>
<button
onClick={onRenewClick}
className="mt-2 bg-orange-600 text-white px-3 py-1 rounded text-sm font-medium hover:bg-orange-700 transition-colors"
>
{licenseStatus.status !== 'active' ? 'Contact Support' : 'Renew License'}
</button>
</div>
</div>
</div>
)}
</div>
);
};
/**
* Main License Dashboard Component
*/
export const LicenseDashboard: React.FC<LicenseDashboardProps> = ({
className = '',
showUpgradeActions = true,
onUpgradeClick,
onRenewClick
}) => {
const { quotas, upgradeSuggestions, isLoading, error } = useLicense();
const { getUsageWarnings } = useLicenseFeatures();
const [activeTab, setActiveTab] = useState<'overview' | 'quotas' | 'features'>('overview');
const usageWarnings = getUsageWarnings;
// Handle loading state
if (isLoading) {
return (
<div className={`space-y-6 animate-pulse ${className}`}>
<div className="h-64 bg-gray-200 rounded-lg"></div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="h-32 bg-gray-200 rounded-lg"></div>
<div className="h-32 bg-gray-200 rounded-lg"></div>
<div className="h-32 bg-gray-200 rounded-lg"></div>
</div>
</div>
);
}
// Handle error state
if (error) {
return (
<div className={`bg-red-50 border border-red-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center space-x-2">
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
<h3 className="font-semibold text-red-900">License Data Error</h3>
</div>
<p className="text-red-700 mt-2">{error}</p>
</div>
);
}
return (
<div className={`space-y-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">License Dashboard</h2>
<div className="flex space-x-2">
{usageWarnings.length > 0 && (
<div className="flex items-center space-x-2 text-sm text-orange-600">
<ExclamationTriangleIcon className="h-4 w-4" />
<span>{usageWarnings.length} warnings</span>
</div>
)}
</div>
</div>
{/* Tab Navigation */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
{[
{ key: 'overview', label: 'Overview', icon: InformationCircleIcon },
{ key: 'quotas', label: 'Usage & Quotas', icon: ChartBarIcon },
{ key: 'features', label: 'Features', icon: SparklesIcon }
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as any)}
className={`group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === tab.key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<tab.icon className="mr-2 h-5 w-5" />
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
<LicenseStatusOverview onRenewClick={onRenewClick} />
{upgradeSuggestions.length > 0 && showUpgradeActions && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Upgrade Recommendations</h3>
<div className="space-y-4">
{upgradeSuggestions.map((suggestion, index) => (
<UpgradeSuggestionCard
key={index}
suggestion={suggestion}
onUpgradeClick={onUpgradeClick}
/>
))}
</div>
</div>
)}
</div>
)}
{activeTab === 'quotas' && quotas && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<QuotaCard
quotaType="search_requests"
quota={quotas.search_requests}
title="Search Requests"
icon={ChartBarIcon}
unit="requests"
/>
<QuotaCard
quotaType="storage_gb"
quota={quotas.storage_gb}
title="Storage"
icon={ChartBarIcon}
unit="GB"
/>
<QuotaCard
quotaType="api_calls"
quota={quotas.api_calls}
title="API Calls"
icon={ChartBarIcon}
unit="calls"
/>
</div>
{usageWarnings.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold text-gray-900">Usage Warnings</h3>
{usageWarnings.map((warning, index) => (
<div
key={index}
className={`p-4 rounded-lg border-l-4 ${
warning.severity === 'critical'
? 'bg-red-50 border-red-500'
: 'bg-yellow-50 border-yellow-500'
}`}
>
<p className="text-sm font-medium">{warning.message}</p>
{warning.action && (
<button className="mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium">
{warning.action === 'upgrade' ? 'Upgrade Now' : 'Renew License'}
</button>
)}
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'features' && (
<FeatureMatrix />
)}
</div>
);
};
export default LicenseDashboard;

View File

@@ -0,0 +1,344 @@
/**
* License Status Header Component
*
* This component provides always-visible license information in the application header.
* It serves as the primary touchpoint for license awareness and upgrade discovery.
*
* Key Features:
* - Prominent tier display with color coding
* - Quick quota overview with visual indicators
* - Expiration warnings and renewal prompts
* - Direct upgrade call-to-action for limited tiers
* - Responsive design for all screen sizes
*
* Business Logic:
* - Builds license awareness by making tier info constantly visible
* - Drives upgrade conversions through strategic placement
* - Provides early warning for expiration and limit issues
* - Creates trust through transparent license information
*
* UX Considerations:
* - Non-intrusive but informative design
* - Color-coded status indicators for quick recognition
* - Hover tooltips for detailed information
* - Mobile-responsive layout
*/
import React, { useState } from 'react';
import { ChevronDownIcon, ExclamationTriangleIcon, ClockIcon } from '@heroicons/react/24/outline';
import { useLicense, useLicenseStatus } from '../../contexts/LicenseContext';
/**
* Props interface for the LicenseStatusHeader component
*/
interface LicenseStatusHeaderProps {
className?: string;
compact?: boolean; // For mobile/smaller displays
showQuotas?: boolean; // Show quota indicators
onUpgradeClick?: () => void; // Custom upgrade handler
}
/**
* Color mapping for license tiers
* Provides consistent visual identity across the application
*/
const TIER_COLORS = {
evaluation: {
bg: 'bg-gray-100',
text: 'text-gray-700',
border: 'border-gray-300',
dot: 'bg-gray-400',
},
standard: {
bg: 'bg-blue-50',
text: 'text-blue-700',
border: 'border-blue-200',
dot: 'bg-blue-500',
},
enterprise: {
bg: 'bg-purple-50',
text: 'text-purple-700',
border: 'border-purple-200',
dot: 'bg-purple-500',
},
};
/**
* Status indicators for license states
*/
const STATUS_INDICATORS = {
active: { color: 'text-green-600', icon: null },
suspended: { color: 'text-red-600', icon: ExclamationTriangleIcon },
expired: { color: 'text-orange-600', icon: ClockIcon },
cancelled: { color: 'text-red-600', icon: ExclamationTriangleIcon },
};
/**
* LicenseStatusHeader Component
*
* Displays compact license information in the application header.
* Designed to be always visible and provide quick license awareness.
*/
export const LicenseStatusHeader: React.FC<LicenseStatusHeaderProps> = ({
className = '',
compact = false,
showQuotas = true,
onUpgradeClick,
}) => {
const {
licenseStatus,
quotas,
isLoading,
error,
getQuotaUsage,
isApproachingLimit,
getUrgentSuggestions
} = useLicense();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// Handle loading state
if (isLoading) {
return (
<div className={`flex items-center space-x-2 ${className}`}>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-20"></div>
</div>
</div>
);
}
// Handle error state with graceful degradation
if (error || !licenseStatus) {
return (
<div className={`flex items-center space-x-2 text-gray-500 text-sm ${className}`}>
<ExclamationTriangleIcon className="h-4 w-4" />
<span>License info unavailable</span>
</div>
);
}
const tierColors = TIER_COLORS[licenseStatus.tier as keyof typeof TIER_COLORS] || TIER_COLORS.evaluation;
const statusInfo = STATUS_INDICATORS[licenseStatus.status as keyof typeof STATUS_INDICATORS] || STATUS_INDICATORS.active;
const urgentSuggestions = getUrgentSuggestions();
// Calculate days until expiration
const expirationDate = new Date(licenseStatus.expires_at);
const daysUntilExpiration = Math.ceil((expirationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
const isExpiringSoon = daysUntilExpiration <= 30;
// Check for quota warnings
const quotaWarnings = showQuotas && quotas ? Object.keys(quotas).filter(
(quotaType) => isApproachingLimit(quotaType as keyof typeof quotas, 85)
) : [];
/**
* Handle upgrade button click
* Either uses custom handler or default behavior
*/
const handleUpgradeClick = (e: React.MouseEvent) => {
e.preventDefault();
if (onUpgradeClick) {
onUpgradeClick();
} else {
// Default behavior - could open upgrade modal
console.log('Upgrade clicked for tier:', licenseStatus.tier);
}
};
/**
* Render compact version for mobile
*/
if (compact) {
return (
<div className={`flex items-center space-x-2 ${className}`}>
<div className={`flex items-center px-2 py-1 rounded-md text-xs font-medium ${tierColors.bg} ${tierColors.text} ${tierColors.border} border`}>
<div className={`w-2 h-2 rounded-full mr-2 ${tierColors.dot}`}></div>
{licenseStatus.tier_display_name}
</div>
{(urgentSuggestions.length > 0 || quotaWarnings.length > 0) && (
<button
onClick={handleUpgradeClick}
className="text-blue-600 hover:text-blue-700 text-xs font-medium"
>
Upgrade
</button>
)}
</div>
);
}
/**
* Render full version for desktop
*/
return (
<div className={`relative ${className}`}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center space-x-3 text-sm hover:bg-gray-50 px-3 py-2 rounded-md transition-colors"
>
{/* Tier Badge */}
<div className={`flex items-center px-3 py-1 rounded-md font-medium ${tierColors.bg} ${tierColors.text} ${tierColors.border} border`}>
<div className={`w-2 h-2 rounded-full mr-2 ${tierColors.dot}`}></div>
<span className="font-semibold">{licenseStatus.tier_display_name}</span>
{/* Status Icon */}
{statusInfo.icon && (
<statusInfo.icon className={`h-4 w-4 ml-2 ${statusInfo.color}`} />
)}
</div>
{/* Node Limit */}
<div className="text-gray-600 hidden sm:block">
<span className="font-medium">{licenseStatus.max_nodes}</span> nodes max
</div>
{/* Expiration Warning */}
{isExpiringSoon && (
<div className="flex items-center text-orange-600 text-xs">
<ClockIcon className="h-4 w-4 mr-1" />
<span>{daysUntilExpiration}d left</span>
</div>
)}
{/* Quota Warnings */}
{quotaWarnings.length > 0 && (
<div className="flex items-center text-amber-600 text-xs">
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
<span className="hidden sm:inline">Approaching limits</span>
<span className="sm:hidden">{quotaWarnings.length}</span>
</div>
)}
{/* Upgrade Prompt */}
{urgentSuggestions.length > 0 && licenseStatus.tier !== 'enterprise' && (
<button
onClick={handleUpgradeClick}
className="bg-blue-600 text-white px-3 py-1 rounded text-xs font-medium hover:bg-blue-700 transition-colors"
>
Upgrade Available
</button>
)}
<ChevronDownIcon className="h-4 w-4 text-gray-400" />
</button>
{/* Dropdown Panel */}
{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg border border-gray-200 z-50">
<div className="p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">License Status</h3>
<span className={`text-sm font-medium ${statusInfo.color}`}>
{licenseStatus.status.charAt(0).toUpperCase() + licenseStatus.status.slice(1)}
</span>
</div>
{/* License Info */}
<div className="space-y-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Tier:</span>
<span className="font-medium">{licenseStatus.tier_display_name}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Max Nodes:</span>
<span className="font-medium">{licenseStatus.max_nodes}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Expires:</span>
<span className={`font-medium ${isExpiringSoon ? 'text-orange-600' : ''}`}>
{expirationDate.toLocaleDateString()}
</span>
</div>
</div>
{/* Quota Summary */}
{showQuotas && quotas && (
<div className="mb-4">
<h4 className="font-medium text-gray-900 mb-2">Usage Overview</h4>
<div className="space-y-2">
{Object.entries(quotas).map(([key, quota]) => (
<div key={key} className="flex items-center justify-between">
<span className="text-sm text-gray-600 capitalize">
{key.replace('_', ' ')}
</span>
<div className="flex items-center space-x-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
quota.percentage >= 90
? 'bg-red-500'
: quota.percentage >= 75
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{ width: `${Math.min(quota.percentage, 100)}%` }}
/>
</div>
<span className="text-xs text-gray-500">
{quota.percentage}%
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Upgrade Suggestions */}
{urgentSuggestions.length > 0 && (
<div className="border-t pt-3">
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
<div className="flex items-start space-x-2">
<ExclamationTriangleIcon className="h-5 w-5 text-blue-600 mt-0.5" />
<div className="flex-1">
<h4 className="font-medium text-blue-900 text-sm">
Upgrade Recommended
</h4>
<p className="text-blue-700 text-xs mt-1">
{urgentSuggestions[0].reason}
</p>
<button
onClick={handleUpgradeClick}
className="mt-2 bg-blue-600 text-white px-3 py-1 rounded text-xs font-medium hover:bg-blue-700 transition-colors"
>
View Options
</button>
</div>
</div>
</div>
</div>
)}
{/* Features List */}
<div className="border-t pt-3 mt-4">
<h4 className="font-medium text-gray-900 mb-2">Available Features</h4>
<div className="flex flex-wrap gap-1">
{licenseStatus.features.map((feature) => (
<span
key={feature}
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-green-100 text-green-800"
>
{feature.replace('-', ' ')}
</span>
))}
</div>
</div>
</div>
</div>
)}
{/* Click overlay to close dropdown */}
{isDropdownOpen && (
<div
className="fixed inset-0 z-40"
onClick={() => setIsDropdownOpen(false)}
/>
)}
</div>
);
};
export default LicenseStatusHeader;

View File

@@ -0,0 +1,335 @@
/**
* Upgrade Prompt Component
*
* A reusable component for showing upgrade prompts throughout the application.
* This component is designed to be used standalone or as part of feature gates.
*
* Key Features:
* - Contextual upgrade messaging based on user's current tier
* - Clear value proposition with specific benefits
* - Call-to-action buttons for upgrade workflow initiation
* - Customizable styling and positioning
* - Analytics tracking for conversion optimization
*
* Business Logic:
* - Drives license upgrade conversions through strategic placement
* - Provides clear ROI messaging to justify upgrades
* - Shows progressive upgrade paths (evaluation → standard → enterprise)
* - Includes social proof and urgency indicators
*
* UX Considerations:
* - Non-intrusive but informative design
* - Clear hierarchy of information
* - Actionable next steps for users
* - Responsive design for all screen sizes
*/
import React from 'react';
import {
ArrowUpIcon,
SparklesIcon,
CheckCircleIcon,
StarIcon,
ClockIcon
} from '@heroicons/react/24/outline';
import { useLicense } from '../../contexts/LicenseContext';
import { UpgradeSuggestion } from '../../services/licenseApi';
/**
* Props interface for UpgradePrompt component
*/
interface UpgradePromptProps {
/** Target feature that triggered this upgrade prompt */
feature?: string;
/** Specific upgrade suggestion to display */
suggestion?: UpgradeSuggestion;
/** Custom title for the upgrade prompt */
title?: string;
/** Custom message explaining why upgrade is needed */
message?: string;
/** List of benefits for upgrading */
benefits?: string[];
/** Target tier for the upgrade */
targetTier?: string;
/** Urgency level affects styling and messaging */
urgency?: 'low' | 'medium' | 'high';
/** Show compact version of the prompt */
compact?: boolean;
/** Custom CSS classes */
className?: string;
/** Callback when upgrade button is clicked */
onUpgradeClick?: () => void;
/** Callback when "learn more" is clicked */
onLearnMoreClick?: () => void;
/** Show pricing information */
showPricing?: boolean;
/** Additional call-to-action text */
ctaText?: string;
}
/**
* Default benefits for different upgrade scenarios
*/
const DEFAULT_UPGRADE_BENEFITS = {
evaluation: {
standard: [
'20x more search results (1,000 vs 50)',
'Advanced search filters and operators',
'Workflow orchestration capabilities',
'10GB storage (vs 1GB)',
'Analytics dashboard access'
],
enterprise: [
'Unlimited search results and API calls',
'Bulk operations for large datasets',
'100GB storage capacity',
'Priority support with SLA',
'Advanced enterprise integrations'
]
},
standard: {
enterprise: [
'Unlimited search results and API calls',
'Bulk operations for large datasets',
'10x more storage (100GB vs 10GB)',
'Priority support with SLA',
'Advanced enterprise integrations',
'Custom feature development'
]
}
};
/**
* Upgrade Prompt Component
*/
export const UpgradePrompt: React.FC<UpgradePromptProps> = ({
feature,
suggestion,
title,
message,
benefits,
targetTier,
urgency = 'medium',
compact = false,
className = '',
onUpgradeClick,
onLearnMoreClick,
showPricing = false,
ctaText
}) => {
const { licenseStatus } = useLicense();
// Use suggestion data if provided, otherwise use props
const upgradeData = suggestion || {
reason: message || `Unlock ${feature || 'premium features'}`,
current_tier: licenseStatus?.tier_display_name || 'Current',
suggested_tier: targetTier || 'Standard',
benefits: benefits || [],
urgency: urgency,
};
// Get appropriate benefits list
const displayBenefits = upgradeData.benefits.length > 0
? upgradeData.benefits
: getDefaultBenefits(licenseStatus?.tier || 'evaluation', upgradeData.suggested_tier.toLowerCase());
// Urgency-based styling
const getUrgencyStyles = (urgencyLevel: string) => {
switch (urgencyLevel) {
case 'high':
return {
container: 'border-red-200 bg-gradient-to-r from-red-50 to-pink-50',
icon: 'text-red-600',
button: 'bg-red-600 hover:bg-red-700 text-white',
accent: 'text-red-600'
};
case 'low':
return {
container: 'border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50',
icon: 'text-blue-600',
button: 'bg-blue-600 hover:bg-blue-700 text-white',
accent: 'text-blue-600'
};
default: // medium
return {
container: 'border-orange-200 bg-gradient-to-r from-orange-50 to-yellow-50',
icon: 'text-orange-600',
button: 'bg-orange-600 hover:bg-orange-700 text-white',
accent: 'text-orange-600'
};
}
};
const styles = getUrgencyStyles(upgradeData.urgency || urgency);
/**
* Handle upgrade button click
*/
const handleUpgradeClick = () => {
// Track upgrade prompt interaction for analytics
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', 'upgrade_prompt_click', {
feature: feature || 'general',
current_tier: upgradeData.current_tier,
target_tier: upgradeData.suggested_tier,
urgency: upgradeData.urgency
});
}
if (onUpgradeClick) {
onUpgradeClick();
} else {
// Default behavior - could open upgrade modal or redirect
console.log('Upgrade clicked:', upgradeData);
}
};
/**
* Render compact version
*/
if (compact) {
return (
<div className={`border rounded-lg p-4 ${styles.container} ${className}`}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<SparklesIcon className={`h-5 w-5 ${styles.icon}`} />
<span className="font-medium text-gray-900">
Upgrade to {upgradeData.suggested_tier}
</span>
</div>
<button
onClick={handleUpgradeClick}
className={`px-3 py-1 rounded font-medium text-sm transition-colors ${styles.button}`}
>
{ctaText || 'Upgrade'}
</button>
</div>
<p className="text-sm text-gray-600 mt-2">{upgradeData.reason}</p>
</div>
);
}
/**
* Render full version
*/
return (
<div className={`border rounded-xl p-6 ${styles.container} ${className}`}>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-white shadow-sm`}>
<SparklesIcon className={`h-6 w-6 ${styles.icon}`} />
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{title || `Unlock ${upgradeData.suggested_tier} Features`}
</h3>
<p className="text-gray-600 mt-1">{upgradeData.reason}</p>
</div>
</div>
{/* Urgency indicator */}
{upgradeData.urgency === 'high' && (
<div className="flex items-center space-x-1 text-xs font-medium text-red-600 bg-red-100 px-2 py-1 rounded-full">
<ClockIcon className="h-3 w-3" />
<span>Urgent</span>
</div>
)}
</div>
{/* ROI Estimate */}
{suggestion?.roi_estimate && (
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center space-x-2">
<StarIcon className="h-5 w-5 text-green-600" />
<span className="font-medium text-green-900">ROI Estimate</span>
</div>
<p className="text-green-800 text-sm mt-1">{suggestion.roi_estimate}</p>
</div>
)}
{/* Benefits List */}
{displayBenefits.length > 0 && (
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">What you'll get:</h4>
<div className="grid grid-cols-1 gap-2">
{displayBenefits.map((benefit, index) => (
<div key={index} className="flex items-start space-x-2">
<CheckCircleIcon className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-700">{benefit}</span>
</div>
))}
</div>
</div>
)}
{/* Pricing Information */}
{showPricing && (
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
<div className="text-sm text-gray-600">
<span className="font-medium">Upgrade from {upgradeData.current_tier}</span>
<span className="mx-2">→</span>
<span className="font-medium">{upgradeData.suggested_tier}</span>
</div>
<p className="text-xs text-gray-500 mt-1">
Contact sales for personalized pricing
</p>
</div>
)}
{/* Action Buttons */}
<div className="flex items-center space-x-3">
<button
onClick={handleUpgradeClick}
className={`flex items-center space-x-2 px-6 py-2 rounded-lg font-medium transition-colors ${styles.button}`}
>
<ArrowUpIcon className="h-4 w-4" />
<span>{ctaText || `Upgrade to ${upgradeData.suggested_tier}`}</span>
</button>
<button
onClick={onLearnMoreClick}
className="text-gray-600 hover:text-gray-800 font-medium text-sm"
>
Learn More
</button>
</div>
{/* Footer */}
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500">
💡 Upgrade now and see immediate productivity gains
</p>
</div>
</div>
);
};
/**
* Helper function to get default benefits based on tier transition
*/
function getDefaultBenefits(currentTier: string, targetTier: string): string[] {
const benefits = DEFAULT_UPGRADE_BENEFITS as any;
return benefits[currentTier]?.[targetTier] || [
'Access to premium features',
'Higher usage limits',
'Priority support',
'Advanced capabilities'
];
}
export default UpgradePrompt;

View File

@@ -0,0 +1,417 @@
/**
* License Context Provider
*
* This context manages global license state throughout the WHOOSH application.
* It provides license-aware functionality including tier status, feature availability,
* quota monitoring, and upgrade suggestions to all components.
*
* Key Responsibilities:
* - Centralized license state management
* - Automatic license data refresh and caching
* - Feature availability checking for gates
* - Quota monitoring and limit warnings
* - Upgrade suggestion management
* - License status change notifications
*
* Business Integration:
* - Powers revenue optimization through strategic feature gating
* - Enables proactive upgrade suggestions based on usage patterns
* - Provides transparent license information to build user trust
* - Supports self-service upgrade workflows
*
* Technical Implementation:
* - React Context API for global state management
* - Automatic refresh intervals for real-time data
* - Error handling with graceful degradation
* - TypeScript for reliable license operations
*/
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import {
LicenseStatus,
LicenseQuotas,
UpgradeSuggestion,
FeatureAvailability,
AvailableTiers,
licenseApi
} from '../services/licenseApi';
/**
* License Context State Interface
*
* Defines the complete license state available to all components.
* This interface ensures type safety and provides comprehensive license information.
*/
interface LicenseContextState {
// Core license data
licenseStatus: LicenseStatus | null;
quotas: LicenseQuotas | null;
upgradeSuggestions: UpgradeSuggestion[];
availableTiers: AvailableTiers | null;
// Loading and error states
isLoading: boolean;
error: string | null;
// Feature checking methods
hasFeature: (feature: string) => boolean;
checkFeature: (feature: string) => Promise<FeatureAvailability | null>;
// Tier checking methods
isOnTier: (tier: string) => boolean;
hasTierOrHigher: (tier: string) => boolean;
// Quota utilities
getQuotaUsage: (quotaType: keyof LicenseQuotas) => number;
isApproachingLimit: (quotaType: keyof LicenseQuotas, threshold?: number) => boolean;
// Upgrade management
getUrgentSuggestions: () => UpgradeSuggestion[];
refreshLicenseData: () => Promise<void>;
// Cache management
clearCache: () => void;
lastRefresh: Date | null;
}
const LicenseContext = createContext<LicenseContextState | undefined>(undefined);
/**
* License Provider Props
*/
interface LicenseProviderProps {
children: ReactNode;
refreshInterval?: number; // milliseconds, default 5 minutes
enableAutoRefresh?: boolean; // default true
}
/**
* License Provider Component
*
* Wraps the application to provide license context to all child components.
* Manages license data fetching, caching, and automatic refresh cycles.
*
* Features:
* - Automatic license data initialization on mount
* - Periodic refresh for real-time quota updates
* - Intelligent error handling and retry logic
* - Performance optimization through batched API calls
*/
export const LicenseProvider: React.FC<LicenseProviderProps> = ({
children,
refreshInterval = 5 * 60 * 1000, // 5 minutes default
enableAutoRefresh = true,
}) => {
// Core license state
const [licenseStatus, setLicenseStatus] = useState<LicenseStatus | null>(null);
const [quotas, setQuotas] = useState<LicenseQuotas | null>(null);
const [upgradeSuggestions, setUpgradeSuggestions] = useState<UpgradeSuggestion[]>([]);
const [availableTiers, setAvailableTiers] = useState<AvailableTiers | null>(null);
// Loading and error state
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
/**
* Tier hierarchy for comparison operations
* Used for hasTierOrHigher functionality
*/
const tierHierarchy = {
evaluation: 0,
standard: 1,
enterprise: 2,
};
/**
* Fetch all license data from API
*
* Uses batched API calls for optimal performance during initial load
* and refresh operations. Handles errors gracefully to prevent app crashes.
*/
const fetchLicenseData = useCallback(async (showLoading: boolean = true) => {
if (showLoading) {
setIsLoading(true);
}
setError(null);
try {
// Use batched API call for efficiency
const data = await licenseApi.batchFetchLicenseData();
// Update all state at once to prevent multiple re-renders
setLicenseStatus(data.status);
setQuotas(data.quotas);
setUpgradeSuggestions(data.suggestions);
setAvailableTiers(data.tiers);
setLastRefresh(new Date());
// Clear any previous errors
setError(null);
} catch (err) {
console.error('Failed to fetch license data:', err);
setError('Failed to load license information');
// Don't clear existing data on error - allow graceful degradation
if (!licenseStatus) {
// Only show loading error if we have no data at all
setError('Unable to load license information. Some features may be limited.');
}
} finally {
setIsLoading(false);
}
}, [licenseStatus]);
/**
* Initialize license data on component mount
*/
useEffect(() => {
fetchLicenseData();
}, [fetchLicenseData]);
/**
* Set up automatic refresh interval
*
* Keeps license data fresh for real-time quota updates and status changes.
* Respects the enableAutoRefresh prop for environments where it's not needed.
*/
useEffect(() => {
if (!enableAutoRefresh || refreshInterval <= 0) {
return;
}
const interval = setInterval(() => {
// Refresh silently (don't show loading state)
fetchLicenseData(false);
}, refreshInterval);
return () => clearInterval(interval);
}, [fetchLicenseData, refreshInterval, enableAutoRefresh]);
/**
* Feature availability checking
*
* Synchronous feature check based on current license status.
* Falls back to false if license data is not available.
*/
const hasFeature = useCallback((feature: string): boolean => {
if (!licenseStatus) return false;
return licenseStatus.features.includes(feature);
}, [licenseStatus]);
/**
* Asynchronous feature checking with detailed information
*
* Provides detailed feature availability information including upgrade path.
* Uses API call for most current information.
*/
const checkFeature = useCallback(async (feature: string): Promise<FeatureAvailability | null> => {
return await licenseApi.checkFeatureAvailability(feature);
}, []);
/**
* Tier checking methods
*/
const isOnTier = useCallback((tier: string): boolean => {
return licenseStatus?.tier === tier;
}, [licenseStatus]);
const hasTierOrHigher = useCallback((targetTier: string): boolean => {
if (!licenseStatus) return false;
const currentLevel = tierHierarchy[licenseStatus.tier as keyof typeof tierHierarchy] ?? -1;
const targetLevel = tierHierarchy[targetTier as keyof typeof tierHierarchy] ?? 999;
return currentLevel >= targetLevel;
}, [licenseStatus]);
/**
* Quota utility methods
*/
const getQuotaUsage = useCallback((quotaType: keyof LicenseQuotas): number => {
if (!quotas || !quotas[quotaType]) return 0;
return quotas[quotaType].percentage;
}, [quotas]);
const isApproachingLimit = useCallback((
quotaType: keyof LicenseQuotas,
threshold: number = 80
): boolean => {
const usage = getQuotaUsage(quotaType);
return usage >= threshold;
}, [getQuotaUsage]);
/**
* Upgrade suggestion utilities
*/
const getUrgentSuggestions = useCallback((): UpgradeSuggestion[] => {
return upgradeSuggestions.filter(suggestion => suggestion.urgency === 'high');
}, [upgradeSuggestions]);
/**
* Manual refresh function
*
* Allows components to trigger a fresh license data fetch.
* Useful after user actions that might change license status.
*/
const refreshLicenseData = useCallback(async (): Promise<void> => {
await fetchLicenseData();
}, [fetchLicenseData]);
/**
* Clear license cache
*
* Forces fresh data on next API call.
* Useful when license changes are expected.
*/
const clearCache = useCallback((): void => {
licenseApi.clearCache();
}, []);
// Context value object
const contextValue: LicenseContextState = {
// Core data
licenseStatus,
quotas,
upgradeSuggestions,
availableTiers,
// State
isLoading,
error,
// Feature methods
hasFeature,
checkFeature,
// Tier methods
isOnTier,
hasTierOrHigher,
// Quota methods
getQuotaUsage,
isApproachingLimit,
// Upgrade methods
getUrgentSuggestions,
refreshLicenseData,
// Cache management
clearCache,
lastRefresh,
};
return (
<LicenseContext.Provider value={contextValue}>
{children}
</LicenseContext.Provider>
);
};
/**
* License Context Hook
*
* Provides access to license context in functional components.
* Includes helpful error message if used outside of LicenseProvider.
*/
export const useLicense = (): LicenseContextState => {
const context = useContext(LicenseContext);
if (!context) {
throw new Error('useLicense must be used within a LicenseProvider');
}
return context;
};
/**
* Convenience hooks for common license operations
*
* These hooks provide simplified interfaces for the most common license
* checks, making it easier to implement license-aware features.
*/
/**
* Hook for feature availability checking
* Returns boolean for simple feature gates
*/
export const useHasFeature = (feature: string): boolean => {
const { hasFeature } = useLicense();
return hasFeature(feature);
};
/**
* Hook for tier checking
* Returns boolean for tier-based logic
*/
export const useIsOnTier = (tier: string): boolean => {
const { isOnTier } = useLicense();
return isOnTier(tier);
};
/**
* Hook for tier hierarchy checking
* Returns boolean for tier-or-higher logic
*/
export const useHasTierOrHigher = (tier: string): boolean => {
const { hasTierOrHigher } = useLicense();
return hasTierOrHigher(tier);
};
/**
* Hook for quota monitoring
* Returns quota usage percentage for progress bars and warnings
*/
export const useQuotaUsage = (quotaType: keyof LicenseQuotas): number => {
const { getQuotaUsage } = useLicense();
return getQuotaUsage(quotaType);
};
/**
* Hook for approaching limit warnings
* Returns boolean for quota limit warnings
*/
export const useIsApproachingLimit = (
quotaType: keyof LicenseQuotas,
threshold?: number
): boolean => {
const { isApproachingLimit } = useLicense();
return isApproachingLimit(quotaType, threshold);
};
/**
* Hook for urgent upgrade suggestions
* Returns high-priority suggestions for prominent display
*/
export const useUrgentSuggestions = (): UpgradeSuggestion[] => {
const { getUrgentSuggestions } = useLicense();
return getUrgentSuggestions();
};
/**
* Hook for current license status
* Returns complete license status object
*/
export const useLicenseStatus = (): LicenseStatus | null => {
const { licenseStatus } = useLicense();
return licenseStatus;
};
/**
* Hook for license loading state
* Useful for showing loading indicators during license operations
*/
export const useLicenseLoading = (): boolean => {
const { isLoading } = useLicense();
return isLoading;
};
/**
* Hook for license error handling
* Returns current license error state for error boundaries
*/
export const useLicenseError = (): string | null => {
const { error } = useLicense();
return error;
};
export default LicenseContext;

View File

@@ -0,0 +1,467 @@
/**
* License Features Hook
*
* This custom hook provides comprehensive feature availability checking functionality.
* It serves as the primary interface for components to determine what features
* are available to the current user based on their license tier.
*
* Key Features:
* - Comprehensive feature availability checking
* - Tier-based capability limits (search results, storage, etc.)
* - Real-time quota usage monitoring
* - Smart upgrade suggestions based on usage patterns
* - Performance optimized with intelligent caching
*
* Business Logic:
* - Enforces license-based feature restrictions
* - Provides data for upgrade conversion optimization
* - Tracks feature usage for business intelligence
* - Enables tier-appropriate user experiences
*
* Security:
* - Client-side checks are UX optimization only
* - All enforcement happens server-side in API calls
* - Feature flags provide graceful degradation
*/
import { useMemo, useCallback } from 'react';
import { useLicense } from '../contexts/LicenseContext';
import { LicenseQuotas } from '../services/licenseApi';
/**
* Feature capability configuration
* Maps features to their tier requirements and usage limits
*/
const FEATURE_CAPABILITIES = {
'basic-search': {
tiers: ['evaluation', 'standard', 'enterprise'],
maxResults: { evaluation: 50, standard: 1000, enterprise: -1 },
description: 'Basic text search functionality'
},
'advanced-search': {
tiers: ['standard', 'enterprise'],
maxResults: { standard: 1000, enterprise: -1 },
description: 'Advanced search operators, filters, and saved queries'
},
'analytics': {
tiers: ['standard', 'enterprise'],
description: 'Usage analytics and performance metrics'
},
'workflows': {
tiers: ['standard', 'enterprise'],
description: 'Multi-agent workflow orchestration'
},
'bulk-operations': {
tiers: ['enterprise'],
description: 'Batch processing and large-scale operations'
},
'api-access': {
tiers: ['enterprise'],
description: 'Full REST API access and integrations'
},
'enterprise-support': {
tiers: ['enterprise'],
description: 'Priority support and SLA guarantees'
}
} as const;
/**
* Quota thresholds for warnings and restrictions
*/
const QUOTA_THRESHOLDS = {
warning: 80, // Show warning at 80% usage
critical: 95, // Show critical warning at 95% usage
blocked: 100, // Block functionality at 100% usage
} as const;
/**
* Interface for feature availability response
*/
interface FeatureCheck {
available: boolean;
reason?: string;
upgradeRequired?: boolean;
tierRequired?: string;
usageLimit?: number;
currentUsage?: number;
}
/**
* Interface for quota status
*/
interface QuotaStatus {
type: keyof LicenseQuotas;
used: number;
limit: number;
percentage: number;
status: 'normal' | 'warning' | 'critical' | 'exceeded';
daysRemaining?: number;
}
/**
* License Features Hook
*
* Provides comprehensive feature checking and license management functionality.
* Optimized for performance with memoized results and intelligent caching.
*/
export const useLicenseFeatures = () => {
const {
licenseStatus,
quotas,
hasFeature,
isOnTier,
hasTierOrHigher,
getQuotaUsage,
isApproachingLimit,
upgradeSuggestions
} = useLicense();
/**
* Get detailed feature availability information
*
* @param feature - The feature to check
* @returns Detailed feature availability information
*/
const checkFeature = useCallback((feature: string): FeatureCheck => {
// Basic availability check
const available = hasFeature(feature);
if (available) {
return { available: true };
}
// Find which tier is required for this feature
const featureConfig = FEATURE_CAPABILITIES[feature as keyof typeof FEATURE_CAPABILITIES];
if (!featureConfig) {
return {
available: false,
reason: 'Unknown feature',
};
}
// Find the minimum tier that includes this feature
const tierRequired = featureConfig.tiers[0];
return {
available: false,
reason: `Feature requires ${tierRequired} tier or higher`,
upgradeRequired: true,
tierRequired: tierRequired.charAt(0).toUpperCase() + tierRequired.slice(1),
};
}, [hasFeature]);
/**
* Check if user can perform advanced search operations
*/
const canUseAdvancedSearch = useCallback((): FeatureCheck => {
return checkFeature('advanced-search');
}, [checkFeature]);
/**
* Check if user can access analytics features
*/
const canUseAnalytics = useCallback((): FeatureCheck => {
return checkFeature('analytics');
}, [checkFeature]);
/**
* Check if user can use workflow orchestration
*/
const canUseWorkflows = useCallback((): FeatureCheck => {
return checkFeature('workflows');
}, [checkFeature]);
/**
* Check if user can perform bulk operations
*/
const canUseBulkOperations = useCallback((): FeatureCheck => {
const baseCheck = checkFeature('bulk-operations');
if (!baseCheck.available) {
return baseCheck;
}
// Additional check for quota limits
const storageUsage = getQuotaUsage('storage_gb');
if (storageUsage >= QUOTA_THRESHOLDS.critical) {
return {
available: false,
reason: 'Storage quota nearly exceeded. Bulk operations temporarily restricted.',
upgradeRequired: true,
};
}
return baseCheck;
}, [checkFeature, getQuotaUsage]);
/**
* Check if user has API access
*/
const canUseAPI = useCallback((): FeatureCheck => {
return checkFeature('api-access');
}, [checkFeature]);
/**
* Get maximum search results allowed for current tier
*/
const getMaxSearchResults = useCallback((): number => {
if (!licenseStatus) return 50; // Default fallback
const tier = licenseStatus.tier;
// Check tier-specific limits
if (tier === 'enterprise') return -1; // Unlimited
if (tier === 'standard') return 1000;
return 50; // Evaluation tier
}, [licenseStatus]);
/**
* Get maximum API calls per hour for current tier
*/
const getMaxAPICallsPerHour = useCallback((): number => {
if (!licenseStatus) return 100;
const tier = licenseStatus.tier;
if (tier === 'enterprise') return -1; // Unlimited
if (tier === 'standard') return 1000;
return 100; // Evaluation tier
}, [licenseStatus]);
/**
* Get maximum storage in GB for current tier
*/
const getMaxStorageGB = useCallback((): number => {
if (!licenseStatus) return 1;
const tier = licenseStatus.tier;
if (tier === 'enterprise') return 100;
if (tier === 'standard') return 10;
return 1; // Evaluation tier
}, [licenseStatus]);
/**
* Get quota status for all quotas
*/
const getQuotaStatuses = useMemo((): QuotaStatus[] => {
if (!quotas) return [];
return Object.entries(quotas).map(([type, quota]) => {
let status: QuotaStatus['status'] = 'normal';
if (quota.percentage >= QUOTA_THRESHOLDS.blocked) {
status = 'exceeded';
} else if (quota.percentage >= QUOTA_THRESHOLDS.critical) {
status = 'critical';
} else if (quota.percentage >= QUOTA_THRESHOLDS.warning) {
status = 'warning';
}
return {
type: type as keyof LicenseQuotas,
used: quota.used,
limit: quota.limit,
percentage: quota.percentage,
status,
};
});
}, [quotas]);
/**
* Check if any quotas are in critical status
*/
const hasCriticalQuotas = useMemo((): boolean => {
return getQuotaStatuses.some(quota =>
quota.status === 'critical' || quota.status === 'exceeded'
);
}, [getQuotaStatuses]);
/**
* Get features that require upgrade for current usage
*/
const getFeaturesNeedingUpgrade = useMemo((): string[] => {
const needsUpgrade: string[] = [];
// Check search results limit
const maxResults = getMaxSearchResults();
if (maxResults > 0 && maxResults <= 1000) {
needsUpgrade.push('advanced-search');
}
// Check storage usage
const storageQuota = getQuotaStatuses.find(q => q.type === 'storage_gb');
if (storageQuota && storageQuota.status === 'critical') {
needsUpgrade.push('additional-storage');
}
// Check API usage
const apiQuota = getQuotaStatuses.find(q => q.type === 'api_calls');
if (apiQuota && apiQuota.status === 'critical') {
needsUpgrade.push('api-access');
}
return needsUpgrade;
}, [getMaxSearchResults, getQuotaStatuses]);
/**
* Get personalized upgrade recommendation
*/
const getUpgradeRecommendation = useMemo(() => {
if (!licenseStatus) return null;
const currentTier = licenseStatus.tier;
const criticalQuotas = getQuotaStatuses.filter(q => q.status === 'critical' || q.status === 'exceeded');
if (currentTier === 'evaluation') {
return {
targetTier: 'Standard',
reason: criticalQuotas.length > 0
? 'You\'re approaching usage limits'
: 'Unlock advanced features and higher limits',
benefits: [
'20x more search results (1,000 vs 50)',
'Advanced search filters and operators',
'Workflow orchestration capabilities',
'10GB storage (vs 1GB)',
'Analytics dashboard'
],
urgency: criticalQuotas.length > 0 ? 'high' : 'medium'
};
}
if (currentTier === 'standard' && criticalQuotas.length > 0) {
return {
targetTier: 'Enterprise',
reason: 'Scale without limits',
benefits: [
'Unlimited search results and API calls',
'Bulk operations for large datasets',
'100GB storage capacity',
'Priority support with SLA',
'Advanced enterprise integrations'
],
urgency: 'high'
};
}
return null;
}, [licenseStatus, getQuotaStatuses]);
/**
* Check if user can perform a specific action based on quotas
*/
const canPerformAction = useCallback((action: string, quotaType?: keyof LicenseQuotas): boolean => {
// Check feature availability first
const featureCheck = checkFeature(action);
if (!featureCheck.available) {
return false;
}
// Check quota limits if specified
if (quotaType) {
const quotaStatus = getQuotaStatuses.find(q => q.type === quotaType);
if (quotaStatus && quotaStatus.status === 'exceeded') {
return false;
}
}
return true;
}, [checkFeature, getQuotaStatuses]);
/**
* Get usage-based feature warnings
*/
const getUsageWarnings = useMemo(() => {
const warnings: Array<{
type: 'quota' | 'feature' | 'expiration';
severity: 'info' | 'warning' | 'critical';
message: string;
action?: string;
}> = [];
// Quota warnings
getQuotaStatuses.forEach(quota => {
if (quota.status === 'exceeded') {
warnings.push({
type: 'quota',
severity: 'critical',
message: `${quota.type.replace('_', ' ')} quota exceeded (${quota.percentage}%)`,
action: 'upgrade'
});
} else if (quota.status === 'critical') {
warnings.push({
type: 'quota',
severity: 'warning',
message: `${quota.type.replace('_', ' ')} quota nearly full (${quota.percentage}%)`,
action: 'upgrade'
});
}
});
// License expiration warning
if (licenseStatus) {
const expirationDate = new Date(licenseStatus.expires_at);
const daysUntilExpiration = Math.ceil((expirationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
if (daysUntilExpiration <= 7) {
warnings.push({
type: 'expiration',
severity: 'critical',
message: `License expires in ${daysUntilExpiration} days`,
action: 'renew'
});
} else if (daysUntilExpiration <= 30) {
warnings.push({
type: 'expiration',
severity: 'warning',
message: `License expires in ${daysUntilExpiration} days`,
action: 'renew'
});
}
}
return warnings.sort((a, b) => {
const severityOrder = { critical: 3, warning: 2, info: 1 };
return severityOrder[b.severity] - severityOrder[a.severity];
});
}, [getQuotaStatuses, licenseStatus]);
// Return all hook functionality
return {
// Feature checking
checkFeature,
hasFeature,
canUseAdvancedSearch,
canUseAnalytics,
canUseWorkflows,
canUseBulkOperations,
canUseAPI,
canPerformAction,
// Tier checking
isOnTier,
hasTierOrHigher,
// Limits and quotas
getMaxSearchResults,
getMaxAPICallsPerHour,
getMaxStorageGB,
getQuotaUsage,
getQuotaStatuses,
hasCriticalQuotas,
isApproachingLimit,
// Upgrade guidance
getFeaturesNeedingUpgrade,
getUpgradeRecommendation,
upgradeSuggestions,
getUsageWarnings,
// License status
licenseStatus,
quotas,
};
};
export default useLicenseFeatures;

View File

@@ -29,6 +29,7 @@ import {
} from 'recharts';
import { executionApi } from '../services/api';
import { apiConfig } from '../config/api';
import { FeatureGate } from '../components/license/FeatureGate';
interface MetricsData {
timestamp: string;
@@ -287,7 +288,7 @@ export default function Analytics() {
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Execution Trends */}
{/* Execution Trends - Available to all tiers */}
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Execution Trends</h3>
{timeSeriesData && timeSeriesData.length > 0 ? (
@@ -334,9 +335,19 @@ export default function Analytics() {
)}
</div>
{/* System Resource Usage */}
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Resource Usage</h3>
{/* System Resource Usage - Advanced Analytics Feature */}
<FeatureGate
feature="analytics"
upgradeMessage="Unlock detailed resource monitoring with Standard tier"
upgradeBenefits={[
"Real-time CPU and memory usage tracking",
"Historical resource utilization trends",
"Performance bottleneck identification",
"Automated resource alerts and notifications"
]}
>
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Resource Usage</h3>
{timeSeriesData && timeSeriesData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={timeSeriesData}>
@@ -383,7 +394,8 @@ export default function Analytics() {
</div>
</div>
)}
</div>
</div>
</FeatureGate>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
@@ -421,9 +433,14 @@ export default function Analytics() {
)}
</div>
{/* Performance Trends */}
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Weekly Performance</h3>
{/* Performance Trends - Advanced Analytics */}
<FeatureGate
feature="analytics"
compact={true}
upgradeMessage="Unlock advanced performance analytics"
>
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Weekly Performance</h3>
{performanceData.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={performanceData}>
@@ -445,7 +462,8 @@ export default function Analytics() {
</div>
</div>
)}
</div>
</div>
</FeatureGate>
{/* System Alerts */}
<div className="bg-white rounded-lg border p-6">

View File

@@ -0,0 +1,438 @@
/**
* License API Client Service
*
* This service handles all communication with the WHOOSH backend license proxy.
* It implements the secure client-side pattern where no license IDs are exposed
* to the frontend - all license operations are resolved server-side.
*
* Key Features:
* - Secure backend proxy integration (no KACHING direct calls)
* - Comprehensive license status management
* - Feature availability checking for gates
* - Quota monitoring and usage tracking
* - Upgrade suggestion intelligence
*
* Business Logic:
* - All license data flows through WHOOSH backend for security
* - Error handling provides fallback behavior for license failures
* - Caching reduces backend load and improves UX
* - Type safety ensures reliable license status handling
*/
import { apiConfig } from '../config/api';
const API_BASE_URL = apiConfig.baseURL + '/api';
// Types matching backend models
export interface LicenseQuota {
used: number;
limit: number;
percentage: number;
}
export interface LicenseQuotas {
search_requests: LicenseQuota;
storage_gb: LicenseQuota;
api_calls: LicenseQuota;
}
export interface UpgradeSuggestion {
reason: string;
current_tier: string;
suggested_tier: string;
benefits: string[];
roi_estimate?: string;
urgency: 'low' | 'medium' | 'high';
}
export interface LicenseStatus {
status: 'active' | 'suspended' | 'expired' | 'cancelled';
tier: string;
tier_display_name: string;
features: string[];
max_nodes: number;
expires_at: string;
quotas: LicenseQuotas;
upgrade_suggestions: UpgradeSuggestion[];
tier_color: string;
}
export interface FeatureAvailability {
feature: string;
available: boolean;
tier_required?: string;
reason?: string;
}
export interface TierInfo {
display_name: string;
features: string[];
max_search_results: number;
max_storage_gb: number;
color: string;
}
export interface AvailableTiers {
tiers: Record<string, TierInfo>;
}
/**
* License API Client Class
*
* Handles all license-related API calls with proper error handling and caching.
* This client ensures that license operations are performed securely through
* the WHOOSH backend proxy, never exposing license IDs or keys to the frontend.
*/
class LicenseApiClient {
private baseUrl: string;
private cache: Map<string, { data: any; timestamp: number; ttl: number }>;
constructor() {
this.baseUrl = API_BASE_URL;
this.cache = new Map();
}
/**
* Get authenticated headers for API requests
* Uses the same token management as other WHOOSH APIs
*/
private getHeaders(): HeadersInit {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
}
/**
* Generic fetch wrapper with error handling
* Provides consistent error handling and authentication for all license API calls
*/
private async fetchWithAuth<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
...this.getHeaders(),
...options.headers,
},
});
if (!response.ok) {
if (response.status === 401) {
// Token expired or invalid - redirect to login
window.location.href = '/login';
throw new Error('Authentication required');
}
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
console.error(`License API error for ${endpoint}:`, error.message);
throw error;
}
throw new Error('Unknown license API error');
}
}
/**
* Cache management utilities
* Implements intelligent caching to reduce backend load while ensuring data freshness
*/
private getCached<T>(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.data as T;
}
private setCache<T>(key: string, data: T, ttlMs: number = 60000): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttlMs
});
}
/**
* Fetch comprehensive license status for the current user
*
* This is the primary endpoint for license information, providing:
* - Current tier and status
* - Feature availability list
* - Quota usage and limits
* - Personalized upgrade suggestions
*
* Caching: 1 minute TTL to balance freshness with performance
* Error Handling: Returns null on failure, allowing graceful degradation
*/
async getLicenseStatus(): Promise<LicenseStatus | null> {
const cacheKey = 'license-status';
const cached = this.getCached<LicenseStatus>(cacheKey);
if (cached) return cached;
try {
const status = await this.fetchWithAuth<LicenseStatus>('/license/status');
this.setCache(cacheKey, status, 60000); // 1 minute cache
return status;
} catch (error) {
console.error('Failed to fetch license status:', error);
return null;
}
}
/**
* Check if a specific feature is available to the current user
*
* This endpoint supports feature gating throughout the application:
* - Returns availability status with detailed reasoning
* - Provides upgrade path information for unavailable features
* - Enables contextual upgrade prompts
*
* Caching: 5 minutes TTL as feature availability is relatively stable
* Business Logic: Used by FeatureGate components for access control
*/
async checkFeatureAvailability(featureName: string): Promise<FeatureAvailability | null> {
const cacheKey = `feature-${featureName}`;
const cached = this.getCached<FeatureAvailability>(cacheKey);
if (cached) return cached;
try {
const availability = await this.fetchWithAuth<FeatureAvailability>(`/license/features/${featureName}`);
this.setCache(cacheKey, availability, 300000); // 5 minute cache
return availability;
} catch (error) {
console.error(`Failed to check feature availability for ${featureName}:`, error);
return null;
}
}
/**
* Get detailed quota usage information
*
* Provides real-time quota data for usage monitoring:
* - Current usage vs limits for all quotas
* - Percentage calculations for progress bars
* - Historical trend indicators
*
* Caching: 30 seconds TTL for near real-time usage data
* UX: Powers quota usage cards and limit warnings
*/
async getQuotas(): Promise<LicenseQuotas | null> {
const cacheKey = 'license-quotas';
const cached = this.getCached<LicenseQuotas>(cacheKey);
if (cached) return cached;
try {
const quotas = await this.fetchWithAuth<LicenseQuotas>('/license/quotas');
this.setCache(cacheKey, quotas, 30000); // 30 second cache
return quotas;
} catch (error) {
console.error('Failed to fetch quotas:', error);
return null;
}
}
/**
* Get personalized upgrade suggestions
*
* Retrieves AI-powered upgrade recommendations based on:
* - Current usage patterns
* - Tier limitations encountered
* - Business value analysis
* - ROI estimates
*
* Caching: 10 minutes TTL as suggestions don't change frequently
* Revenue Optimization: Powers intelligent upselling throughout the UI
*/
async getUpgradeSuggestions(): Promise<UpgradeSuggestion[]> {
const cacheKey = 'upgrade-suggestions';
const cached = this.getCached<UpgradeSuggestion[]>(cacheKey);
if (cached) return cached;
try {
const suggestions = await this.fetchWithAuth<UpgradeSuggestion[]>('/license/upgrade-suggestions');
this.setCache(cacheKey, suggestions, 600000); // 10 minute cache
return suggestions;
} catch (error) {
console.error('Failed to fetch upgrade suggestions:', error);
return [];
}
}
/**
* Get information about all available license tiers
*
* Provides tier comparison data for upgrade flows:
* - Feature matrices for each tier
* - Capacity limits and capabilities
* - Pricing tier positioning
*
* Caching: 1 hour TTL as tier info is static
* Sales Support: Powers tier comparison tables and upgrade modals
*/
async getAvailableTiers(): Promise<AvailableTiers | null> {
const cacheKey = 'available-tiers';
const cached = this.getCached<AvailableTiers>(cacheKey);
if (cached) return cached;
try {
const tiers = await this.fetchWithAuth<AvailableTiers>('/license/tiers');
this.setCache(cacheKey, tiers, 3600000); // 1 hour cache
return tiers;
} catch (error) {
console.error('Failed to fetch available tiers:', error);
return null;
}
}
/**
* Clear all cached license data
*
* Forces fresh data fetch on next API call.
* Used when license changes are expected (e.g., after upgrade).
*/
clearCache(): void {
this.cache.clear();
}
/**
* Validate license data freshness
*
* Checks if license data needs refreshing based on:
* - Cache expiration
* - Last known license status
* - Time-sensitive quota information
*/
async validateLicenseFreshness(): Promise<boolean> {
try {
// Force a fresh license status check
this.cache.delete('license-status');
const status = await this.getLicenseStatus();
return status !== null && status.status === 'active';
} catch (error) {
console.error('License validation failed:', error);
return false;
}
}
/**
* Batch fetch license data for initial load
*
* Optimizes initial page load by fetching all license data in parallel:
* - License status and tier information
* - Current quota usage
* - Available upgrade suggestions
* - Tier comparison data
*
* Used by LicenseContext during initialization for optimal performance.
*/
async batchFetchLicenseData(): Promise<{
status: LicenseStatus | null;
quotas: LicenseQuotas | null;
suggestions: UpgradeSuggestion[];
tiers: AvailableTiers | null;
}> {
try {
const [status, quotas, suggestions, tiers] = await Promise.all([
this.getLicenseStatus(),
this.getQuotas(),
this.getUpgradeSuggestions(),
this.getAvailableTiers(),
]);
return { status, quotas, suggestions, tiers };
} catch (error) {
console.error('Batch license data fetch failed:', error);
return {
status: null,
quotas: null,
suggestions: [],
tiers: null,
};
}
}
}
// Export singleton instance
export const licenseApi = new LicenseApiClient();
/**
* Convenience functions for common license operations
* These functions provide a simpler interface for common license checks
* while maintaining full type safety and error handling.
*/
/**
* Quick feature availability check
* Returns boolean for simple feature gates without detailed error information
*/
export async function hasFeature(featureName: string): Promise<boolean> {
const availability = await licenseApi.checkFeatureAvailability(featureName);
return availability?.available || false;
}
/**
* Get current license tier
* Returns the tier string for UI display and feature logic
*/
export async function getCurrentTier(): Promise<string | null> {
const status = await licenseApi.getLicenseStatus();
return status?.tier || null;
}
/**
* Check if user is on a specific tier or higher
* Useful for tier-based feature gates with hierarchy
*/
export async function hasTierOrHigher(targetTier: string): Promise<boolean> {
const status = await licenseApi.getLicenseStatus();
if (!status) return false;
// Define tier hierarchy
const tierLevels = { evaluation: 0, standard: 1, enterprise: 2 };
const currentLevel = tierLevels[status.tier as keyof typeof tierLevels] || 0;
const targetLevel = tierLevels[targetTier as keyof typeof tierLevels] || 0;
return currentLevel >= targetLevel;
}
/**
* Get quota usage percentage for a specific quota type
* Useful for progress bars and usage warnings
*/
export async function getQuotaUsagePercentage(quotaType: keyof LicenseQuotas): Promise<number> {
const quotas = await licenseApi.getQuotas();
return quotas?.[quotaType]?.percentage || 0;
}
/**
* Check if any quota is approaching limits
* Returns true if any quota is above the specified threshold (default 80%)
*/
export async function isApproachingLimits(threshold: number = 80): Promise<boolean> {
const quotas = await licenseApi.getQuotas();
if (!quotas) return false;
return Object.values(quotas).some(quota => quota.percentage >= threshold);
}
/**
* Get high-priority upgrade suggestions
* Returns only urgent suggestions for prominent display
*/
export async function getUrgentUpgradeSuggestions(): Promise<UpgradeSuggestion[]> {
const suggestions = await licenseApi.getUpgradeSuggestions();
return suggestions.filter(suggestion => suggestion.urgency === 'high');
}