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:
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
403
frontend/src/components/license/FeatureGate.tsx
Normal file
403
frontend/src/components/license/FeatureGate.tsx
Normal 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;
|
||||
492
frontend/src/components/license/LicenseDashboard.tsx
Normal file
492
frontend/src/components/license/LicenseDashboard.tsx
Normal 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;
|
||||
344
frontend/src/components/license/LicenseStatusHeader.tsx
Normal file
344
frontend/src/components/license/LicenseStatusHeader.tsx
Normal 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;
|
||||
335
frontend/src/components/license/UpgradePrompt.tsx
Normal file
335
frontend/src/components/license/UpgradePrompt.tsx
Normal 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;
|
||||
417
frontend/src/contexts/LicenseContext.tsx
Normal file
417
frontend/src/contexts/LicenseContext.tsx
Normal 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;
|
||||
467
frontend/src/hooks/useLicenseFeatures.ts
Normal file
467
frontend/src/hooks/useLicenseFeatures.ts
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
438
frontend/src/services/licenseApi.ts
Normal file
438
frontend/src/services/licenseApi.ts
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user