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:
		
							
								
								
									
										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; | ||||
		Reference in New Issue
	
	Block a user
	 anthonyrawlins
					anthonyrawlins