diff --git a/backend/api/subscription_api.py b/backend/api/subscription_api.py index 03999ac2..6bd87db4 100644 --- a/backend/api/subscription_api.py +++ b/backend/api/subscription_api.py @@ -14,8 +14,8 @@ from services.database import get_db from services.usage_tracking_service import UsageTrackingService from services.pricing_service import PricingService from models.subscription_models import ( - APIProvider, SubscriptionPlan, UserSubscription, UsageSummary, - APIProviderPricing, UsageAlert, SubscriptionTier + APIProvider, SubscriptionPlan, UserSubscription, UsageSummary, + APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle ) router = APIRouter(prefix="/api/subscription", tags=["subscription"]) @@ -209,6 +209,181 @@ async def get_user_subscription( logger.error(f"Error getting user subscription: {e}") raise HTTPException(status_code=500, detail=str(e)) +@router.get("/status/{user_id}") +async def get_subscription_status( + user_id: str, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """Get simple subscription status for enforcement checks.""" + + try: + subscription = db.query(UserSubscription).filter( + UserSubscription.user_id == user_id, + UserSubscription.is_active == True + ).first() + + if not subscription: + # Check if free tier exists + free_plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.tier == SubscriptionTier.FREE, + SubscriptionPlan.is_active == True + ).first() + + if free_plan: + return { + "success": True, + "data": { + "active": True, + "plan": "free", + "tier": "free", + "can_use_api": True, + "limits": { + "gemini_calls": free_plan.gemini_calls_limit, + "openai_calls": free_plan.openai_calls_limit, + "anthropic_calls": free_plan.anthropic_calls_limit, + "mistral_calls": free_plan.mistral_calls_limit, + "tavily_calls": free_plan.tavily_calls_limit, + "serper_calls": free_plan.serper_calls_limit, + "metaphor_calls": free_plan.metaphor_calls_limit, + "firecrawl_calls": free_plan.firecrawl_calls_limit, + "stability_calls": free_plan.stability_calls_limit, + "monthly_cost": free_plan.monthly_cost_limit + } + } + } + else: + return { + "success": True, + "data": { + "active": False, + "plan": "none", + "tier": "none", + "can_use_api": False, + "reason": "No active subscription or free tier found" + } + } + + # Check if subscription is within valid period + now = datetime.utcnow() + if subscription.current_period_end < now: + return { + "success": True, + "data": { + "active": False, + "plan": subscription.plan.tier.value, + "tier": subscription.plan.tier.value, + "can_use_api": False, + "reason": "Subscription expired" + } + } + + return { + "success": True, + "data": { + "active": True, + "plan": subscription.plan.tier.value, + "tier": subscription.plan.tier.value, + "can_use_api": True, + "limits": { + "gemini_calls": subscription.plan.gemini_calls_limit, + "openai_calls": subscription.plan.openai_calls_limit, + "anthropic_calls": subscription.plan.anthropic_calls_limit, + "mistral_calls": subscription.plan.mistral_calls_limit, + "tavily_calls": subscription.plan.tavily_calls_limit, + "serper_calls": subscription.plan.serper_calls_limit, + "metaphor_calls": subscription.plan.metaphor_calls_limit, + "firecrawl_calls": subscription.plan.firecrawl_calls_limit, + "stability_calls": subscription.plan.stability_calls_limit, + "monthly_cost": subscription.plan.monthly_cost_limit + } + } + } + + except Exception as e: + logger.error(f"Error getting subscription status: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/subscribe/{user_id}") +async def subscribe_to_plan( + user_id: str, + subscription_data: dict, + db: Session = Depends(get_db) +) -> Dict[str, Any]: + """Create or update a user's subscription.""" + + try: + plan_id = subscription_data.get('plan_id') + billing_cycle = subscription_data.get('billing_cycle', 'monthly') + + if not plan_id: + raise HTTPException(status_code=400, detail="plan_id is required") + + # Get the plan + plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.id == plan_id, + SubscriptionPlan.is_active == True + ).first() + + if not plan: + raise HTTPException(status_code=404, detail="Plan not found") + + # Check if user already has an active subscription + existing_subscription = db.query(UserSubscription).filter( + UserSubscription.user_id == user_id, + UserSubscription.is_active == True + ).first() + + now = datetime.utcnow() + + if existing_subscription: + # Update existing subscription + existing_subscription.plan_id = plan_id + existing_subscription.billing_cycle = BillingCycle(billing_cycle) + existing_subscription.current_period_start = now + existing_subscription.current_period_end = now + timedelta( + days=365 if billing_cycle == 'yearly' else 30 + ) + existing_subscription.updated_at = now + + subscription = existing_subscription + else: + # Create new subscription + subscription = UserSubscription( + user_id=user_id, + plan_id=plan_id, + billing_cycle=BillingCycle(billing_cycle), + current_period_start=now, + current_period_end=now + timedelta( + days=365 if billing_cycle == 'yearly' else 30 + ), + status=UsageStatus.ACTIVE, + is_active=True, + auto_renew=True + ) + db.add(subscription) + + db.commit() + + return { + "success": True, + "message": f"Successfully subscribed to {plan.name}", + "data": { + "subscription_id": subscription.id, + "plan_name": plan.name, + "billing_cycle": billing_cycle, + "current_period_start": subscription.current_period_start.isoformat(), + "current_period_end": subscription.current_period_end.isoformat(), + "status": subscription.status.value + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error subscribing to plan: {e}") + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + @router.get("/pricing") async def get_api_pricing( provider: Optional[str] = Query(None, description="API provider"), diff --git a/backend/app.py b/backend/app.py index c5505187..0d2b5b62 100644 --- a/backend/app.py +++ b/backend/app.py @@ -97,9 +97,8 @@ app.add_middleware( allow_headers=["*"], ) -# Add API monitoring middleware -# Temporarily disabled for Wix testing -# app.middleware("http")(monitoring_middleware) +# Add API monitoring middleware for subscription enforcement +app.middleware("http")(monitoring_middleware) # Initialize modular utilities health_checker = HealthChecker() diff --git a/backend/scripts/cleanup_alpha_plans.py b/backend/scripts/cleanup_alpha_plans.py new file mode 100644 index 00000000..59848acb --- /dev/null +++ b/backend/scripts/cleanup_alpha_plans.py @@ -0,0 +1,247 @@ +""" +Script to remove Alpha subscription plans and update limits for production testing. +Only keeps: Free, Basic, Pro, Enterprise with updated feature limits. +""" + +import sys +import os +from pathlib import Path + +# Add the backend directory to Python path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from loguru import logger + +from models.subscription_models import SubscriptionPlan, SubscriptionTier +from services.database import DATABASE_URL + +def cleanup_alpha_plans(): + """Remove alpha subscription plans and update limits.""" + + try: + engine = create_engine(DATABASE_URL, echo=True) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + try: + # Delete all plans with "Alpha" in the name + alpha_plans = db.query(SubscriptionPlan).filter( + SubscriptionPlan.name.like('%Alpha%') + ).all() + + for plan in alpha_plans: + logger.info(f"Deleting Alpha plan: {plan.name}") + db.delete(plan) + + db.commit() + logger.info(f"✅ Deleted {len(alpha_plans)} Alpha plans") + + # Update existing plans with new limits + logger.info("Updating plan limits...") + + # Free Plan - Blog, LinkedIn, Facebook writers + Text & Image only + free_plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.tier == SubscriptionTier.FREE + ).first() + + if free_plan: + free_plan.name = "Free" + free_plan.description = "Perfect for trying ALwrity with Blog, LinkedIn & Facebook writers" + free_plan.gemini_calls_limit = 100 + free_plan.openai_calls_limit = 50 + free_plan.anthropic_calls_limit = 0 + free_plan.mistral_calls_limit = 50 + free_plan.tavily_calls_limit = 20 + free_plan.serper_calls_limit = 20 + free_plan.metaphor_calls_limit = 10 + free_plan.firecrawl_calls_limit = 10 + free_plan.stability_calls_limit = 10 # Image generation + free_plan.gemini_tokens_limit = 100000 + free_plan.monthly_cost_limit = 5.0 + free_plan.features = [ + "blog_writer", + "linkedin_writer", + "facebook_writer", + "text_generation", + "image_generation", + "wix_integration", + "wordpress_integration", + "gsc_integration" + ] + logger.info("✅ Updated Free plan") + + # Basic Plan - Blog, LinkedIn, Facebook writers + Text & Image only + basic_plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.tier == SubscriptionTier.BASIC + ).first() + + if basic_plan: + basic_plan.name = "Basic" + basic_plan.description = "Great for solopreneurs with Blog, LinkedIn & Facebook writers" + basic_plan.price_monthly = 29.0 + basic_plan.price_yearly = 278.0 # ~20% discount + basic_plan.gemini_calls_limit = 500 + basic_plan.openai_calls_limit = 250 + basic_plan.anthropic_calls_limit = 100 + basic_plan.mistral_calls_limit = 250 + basic_plan.tavily_calls_limit = 100 + basic_plan.serper_calls_limit = 100 + basic_plan.metaphor_calls_limit = 50 + basic_plan.firecrawl_calls_limit = 50 + basic_plan.stability_calls_limit = 50 # Image generation + basic_plan.gemini_tokens_limit = 500000 + basic_plan.openai_tokens_limit = 250000 + basic_plan.monthly_cost_limit = 25.0 + basic_plan.features = [ + "blog_writer", + "linkedin_writer", + "facebook_writer", + "text_generation", + "image_generation", + "wix_integration", + "wordpress_integration", + "gsc_integration", + "priority_support" + ] + logger.info("✅ Updated Basic plan") + + # Pro Plan - 6 Social Platforms + Website Management + Text, Image, Audio, Video + pro_plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.tier == SubscriptionTier.PRO + ).first() + + if pro_plan: + pro_plan.name = "Pro" + pro_plan.description = "Perfect for businesses with 6 social platforms & multimodal AI" + pro_plan.price_monthly = 79.0 + pro_plan.price_yearly = 758.0 # ~20% discount + pro_plan.gemini_calls_limit = 2000 + pro_plan.openai_calls_limit = 1000 + pro_plan.anthropic_calls_limit = 500 + pro_plan.mistral_calls_limit = 1000 + pro_plan.tavily_calls_limit = 500 + pro_plan.serper_calls_limit = 500 + pro_plan.metaphor_calls_limit = 250 + pro_plan.firecrawl_calls_limit = 250 + pro_plan.stability_calls_limit = 200 # Image generation + pro_plan.gemini_tokens_limit = 2000000 + pro_plan.openai_tokens_limit = 1000000 + pro_plan.anthropic_tokens_limit = 500000 + pro_plan.monthly_cost_limit = 100.0 + pro_plan.features = [ + "blog_writer", + "linkedin_writer", + "facebook_writer", + "instagram_writer", + "twitter_writer", + "tiktok_writer", + "youtube_writer", + "text_generation", + "image_generation", + "audio_generation", + "video_generation", + "wix_integration", + "wordpress_integration", + "gsc_integration", + "website_management", + "content_scheduling", + "advanced_analytics", + "priority_support" + ] + logger.info("✅ Updated Pro plan") + + # Enterprise Plan - Unlimited with all features + enterprise_plan = db.query(SubscriptionPlan).filter( + SubscriptionPlan.tier == SubscriptionTier.ENTERPRISE + ).first() + + if enterprise_plan: + enterprise_plan.name = "Enterprise" + enterprise_plan.description = "For large teams with unlimited usage & custom integrations" + enterprise_plan.price_monthly = 199.0 + enterprise_plan.price_yearly = 1908.0 # ~20% discount + enterprise_plan.gemini_calls_limit = 0 # Unlimited + enterprise_plan.openai_calls_limit = 0 + enterprise_plan.anthropic_calls_limit = 0 + enterprise_plan.mistral_calls_limit = 0 + enterprise_plan.tavily_calls_limit = 0 + enterprise_plan.serper_calls_limit = 0 + enterprise_plan.metaphor_calls_limit = 0 + enterprise_plan.firecrawl_calls_limit = 0 + enterprise_plan.stability_calls_limit = 0 + enterprise_plan.gemini_tokens_limit = 0 + enterprise_plan.openai_tokens_limit = 0 + enterprise_plan.anthropic_tokens_limit = 0 + enterprise_plan.mistral_tokens_limit = 0 + enterprise_plan.monthly_cost_limit = 0.0 # Unlimited + enterprise_plan.features = [ + "blog_writer", + "linkedin_writer", + "facebook_writer", + "instagram_writer", + "twitter_writer", + "tiktok_writer", + "youtube_writer", + "text_generation", + "image_generation", + "audio_generation", + "video_generation", + "wix_integration", + "wordpress_integration", + "gsc_integration", + "website_management", + "content_scheduling", + "advanced_analytics", + "custom_integrations", + "dedicated_account_manager", + "white_label", + "priority_support" + ] + logger.info("✅ Updated Enterprise plan") + + db.commit() + logger.info("✅ All plans updated successfully!") + + # Display summary + logger.info("\n" + "="*60) + logger.info("SUBSCRIPTION PLANS SUMMARY") + logger.info("="*60) + + all_plans = db.query(SubscriptionPlan).filter( + SubscriptionPlan.is_active == True + ).order_by(SubscriptionPlan.price_monthly).all() + + for plan in all_plans: + logger.info(f"\n{plan.name} ({plan.tier.value})") + logger.info(f" Price: ${plan.price_monthly}/mo, ${plan.price_yearly}/yr") + logger.info(f" Gemini: {plan.gemini_calls_limit if plan.gemini_calls_limit > 0 else 'Unlimited'} calls/month") + logger.info(f" OpenAI: {plan.openai_calls_limit if plan.openai_calls_limit > 0 else 'Unlimited'} calls/month") + logger.info(f" Research: {plan.tavily_calls_limit if plan.tavily_calls_limit > 0 else 'Unlimited'} searches/month") + logger.info(f" Images: {plan.stability_calls_limit if plan.stability_calls_limit > 0 else 'Unlimited'} images/month") + logger.info(f" Features: {', '.join(plan.features or [])}") + + logger.info("\n" + "="*60) + + finally: + db.close() + + except Exception as e: + logger.error(f"❌ Error cleaning up plans: {e}") + import traceback + logger.error(traceback.format_exc()) + raise + +if __name__ == "__main__": + logger.info("🚀 Starting subscription plans cleanup...") + + try: + cleanup_alpha_plans() + logger.info("✅ Cleanup completed successfully!") + + except Exception as e: + logger.error(f"❌ Cleanup failed: {e}") + sys.exit(1) + diff --git a/frontend/build/favicon.ico b/frontend/build/favicon.ico index 8b137891..868a7b8b 100644 Binary files a/frontend/build/favicon.ico and b/frontend/build/favicon.ico differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c34f23d4..3aea1b70 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Box, CircularProgress, Typography } from '@mui/material'; import { CopilotKit } from "@copilotkit/react-core"; @@ -11,6 +11,7 @@ import ContentPlanningDashboard from './components/ContentPlanningDashboard/Cont import FacebookWriter from './components/FacebookWriter/FacebookWriter'; import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter'; import BlogWriter from './components/BlogWriter/BlogWriter'; +import PricingPage from './components/Pricing/PricingPage'; import WixTestPage from './components/WixTestPage/WixTestPage'; import WixCallbackPage from './components/WixCallbackPage/WixCallbackPage'; import WordPressCallbackPage from './components/WordPressCallbackPage/WordPressCallbackPage'; @@ -20,9 +21,11 @@ import Landing from './components/Landing/Landing'; import ErrorBoundary from './components/shared/ErrorBoundary'; import ErrorBoundaryTest from './components/shared/ErrorBoundaryTest'; import { OnboardingProvider } from './contexts/OnboardingContext'; +import { SubscriptionProvider } from './contexts/SubscriptionContext'; import { apiClient, setAuthTokenGetter } from './api/client'; import { useOnboarding } from './contexts/OnboardingContext'; +import { useState, useEffect } from 'react'; // interface OnboardingStatus { // onboarding_required: boolean; @@ -41,13 +44,36 @@ const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ childr return <>{children}; }; -// Component to handle initial routing based on onboarding status -// Now uses OnboardingContext instead of making its own API calls +// Component to handle initial routing based on subscription and onboarding status +// Flow: Check Subscription → Check Onboarding → Route accordingly const InitialRouteHandler: React.FC = () => { const { loading, error, isOnboardingComplete } = useOnboarding(); + const [checkingSubscription, setCheckingSubscription] = useState(true); + const [hasActiveSubscription, setHasActiveSubscription] = useState(false); - // Loading state - if (loading) { + useEffect(() => { + const checkSubscription = async () => { + try { + const userId = localStorage.getItem('user_id') || 'anonymous'; + const response = await apiClient.get(`/api/subscription/status/${userId}`); + const subscriptionData = response.data.data; + + // User has active subscription if plan exists + setHasActiveSubscription(subscriptionData?.active || false); + } catch (err) { + console.error('Error checking subscription:', err); + // On error, assume no subscription (will redirect to pricing) + setHasActiveSubscription(false); + } finally { + setCheckingSubscription(false); + } + }; + + checkSubscription(); + }, []); + + // Loading state - checking both subscription and onboarding + if (loading || checkingSubscription) { return ( { > - Checking onboarding status... + {checkingSubscription ? 'Checking subscription...' : 'Checking onboarding status...'} ); @@ -87,12 +113,19 @@ const InitialRouteHandler: React.FC = () => { ); } - // Redirect based on onboarding status from context + // Decision tree: Subscription → Onboarding → Dashboard + // 1. No subscription? → Pricing page + if (!hasActiveSubscription) { + console.log('InitialRouteHandler: No active subscription, redirecting to pricing'); + return ; + } + + // 2. Has subscription, check onboarding if (isOnboardingComplete) { - console.log('InitialRouteHandler: Onboarding complete (from context), redirecting to dashboard'); + console.log('InitialRouteHandler: Subscription active & onboarding complete, redirecting to dashboard'); return ; } else { - console.log('InitialRouteHandler: Onboarding not complete (from context), redirecting to onboarding'); + console.log('InitialRouteHandler: Subscription active but onboarding incomplete, redirecting to onboarding'); return ; } }; @@ -255,6 +288,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> @@ -293,9 +327,11 @@ const App: React.FC = () => { }} > - - {renderApp()} - + + + {renderApp()} + + ); diff --git a/frontend/src/components/Landing/Landing.tsx b/frontend/src/components/Landing/Landing.tsx index 9957d71c..c2906a82 100644 --- a/frontend/src/components/Landing/Landing.tsx +++ b/frontend/src/components/Landing/Landing.tsx @@ -570,6 +570,19 @@ const Landing: React.FC = () => { + {/* Pricing Section - Embedded in Landing */} + + }> + {React.createElement(lazy(() => import('../Pricing/PricingPage')))} + + + {/* Introducing ALwrity Section with Background - Lazy Loaded */} }> diff --git a/frontend/src/components/MainDashboard/MainDashboard.tsx b/frontend/src/components/MainDashboard/MainDashboard.tsx index 5002d2fa..cdcd249e 100644 --- a/frontend/src/components/MainDashboard/MainDashboard.tsx +++ b/frontend/src/components/MainDashboard/MainDashboard.tsx @@ -9,6 +9,7 @@ import { import { motion, AnimatePresence } from 'framer-motion'; import { useNavigate } from 'react-router-dom'; import AskAlwrityIcon from '../../assets/images/AskAlwrity-min.ico'; +import { SubscriptionGuard } from '../SubscriptionGuard'; // Shared components import DashboardHeader from '../shared/DashboardHeader'; @@ -299,8 +300,13 @@ const MainDashboard: React.FC = () => { /> - {/* Content Lifecycle Pillars - First Panel */} - + {/* Subscription Guard - Protect main dashboard content */} + + {/* Content Lifecycle Pillars - First Panel */} + {/* Side-by-side layout for Areas 2 and 3 */} @@ -350,6 +356,7 @@ const MainDashboard: React.FC = () => { favorites={favorites} onToggleFavorite={toggleFavorite} /> + diff --git a/frontend/src/components/Pricing/PricingPage.tsx b/frontend/src/components/Pricing/PricingPage.tsx new file mode 100644 index 00000000..e9a3341e --- /dev/null +++ b/frontend/src/components/Pricing/PricingPage.tsx @@ -0,0 +1,944 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Container, + Typography, + Card, + CardContent, + CardActions, + Button, + Grid, + Chip, + List, + ListItem, + ListItemIcon, + ListItemText, + Switch, + FormControlLabel, + Divider, + Alert, + CircularProgress, + useTheme, + IconButton, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Modal, + Fade, + Backdrop, +} from '@mui/material'; +import { + Check as CheckIcon, + Close as CloseIcon, + Star as StarIcon, + WorkspacePremium as PremiumIcon, + Info as InfoIcon, + Psychology, + Search, + FactCheck, + Edit, + Assistant, + Verified, + Timeline, + Analytics, + Support, + Business, + Group, +} from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; +import { apiClient } from '../../api/client'; + +interface SubscriptionPlan { + id: number; + name: string; + tier: string; + price_monthly: number; + price_yearly: number; + description: string; + features: string[]; + limits: { + gemini_calls: number; + openai_calls: number; + anthropic_calls: number; + mistral_calls: number; + tavily_calls: number; + serper_calls: number; + metaphor_calls: number; + firecrawl_calls: number; + stability_calls: number; + monthly_cost: number; + }; +} + +const PricingPage: React.FC = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [yearlyBilling, setYearlyBilling] = useState(false); + const [selectedPlan, setSelectedPlan] = useState(null); + const [subscribing, setSubscribing] = useState(false); + const [paymentModalOpen, setPaymentModalOpen] = useState(false); + const [knowMoreModal, setKnowMoreModal] = useState<{ open: boolean; title: string; content: React.ReactNode }>({ + open: false, + title: '', + content: null + }); + + useEffect(() => { + fetchPlans(); + }, []); + + const fetchPlans = async () => { + try { + setLoading(true); + const response = await apiClient.get('/api/subscription/plans'); + // Filter out any alpha plans and ensure we only show the 4 main tiers + const filteredPlans = response.data.data.plans.filter( + (plan: SubscriptionPlan) => !plan.name.toLowerCase().includes('alpha') + ); + setPlans(filteredPlans); + } catch (err) { + console.error('Error fetching plans:', err); + setError('Failed to load subscription plans'); + } finally { + setLoading(false); + } + }; + + const handleSubscribe = async (planId: number) => { + const plan = plans.find(p => p.id === planId); + if (!plan) return; + + // For alpha testing, only allow Free and Basic plans (Pro features not ready) + if (plan.tier !== 'free' && plan.tier !== 'basic') { + setError('This plan is not available for alpha testing'); + return; + } + + if (plan.tier === 'free') { + // For free plan, just create subscription + try { + setSubscribing(true); + const userId = localStorage.getItem('user_id') || 'anonymous'; + + await apiClient.post(`/api/subscription/subscribe/${userId}`, { + plan_id: planId, + billing_cycle: yearlyBilling ? 'yearly' : 'monthly' + }); + + // Refresh subscription status + window.dispatchEvent(new CustomEvent('subscription-updated')); + + // After subscription, check if onboarding is complete + // If not complete, redirect to onboarding; otherwise to dashboard + const onboardingComplete = localStorage.getItem('onboarding_complete') === 'true'; + if (onboardingComplete) { + navigate('/dashboard'); + } else { + navigate('/onboarding'); + } + } catch (err) { + console.error('Error subscribing:', err); + setError('Failed to process subscription'); + } finally { + setSubscribing(false); + } + } else { + // For Basic plan, show payment modal + setPaymentModalOpen(true); + } + }; + + const handlePaymentConfirm = async () => { + if (!selectedPlan) return; + + try { + setSubscribing(true); + const userId = localStorage.getItem('user_id') || 'anonymous'; + + await apiClient.post(`/api/subscription/subscribe/${userId}`, { + plan_id: selectedPlan, + billing_cycle: yearlyBilling ? 'yearly' : 'monthly' + }); + + // Refresh subscription status + window.dispatchEvent(new CustomEvent('subscription-updated')); + + setPaymentModalOpen(false); + + // After subscription, check if onboarding is complete + // If not complete, redirect to onboarding; otherwise to dashboard + const onboardingComplete = localStorage.getItem('onboarding_complete') === 'true'; + if (onboardingComplete) { + navigate('/dashboard'); + } else { + navigate('/onboarding'); + } + } catch (err) { + console.error('Error subscribing:', err); + setError('Failed to process subscription'); + } finally { + setSubscribing(false); + } + }; + + const openKnowMoreModal = (title: string, content: React.ReactNode) => { + setKnowMoreModal({ + open: true, + title, + content + }); + }; + + const getPlanIcon = (tier: string) => { + switch (tier) { + case 'free': + return ; + case 'basic': + return ; + case 'pro': + return ; + case 'enterprise': + return ; + default: + return ; + } + }; + + const getPlanColor = (tier: string) => { + switch (tier) { + case 'free': + return 'success' as const; + case 'basic': + return 'primary' as const; + case 'pro': + return 'secondary' as const; + case 'enterprise': + return 'warning' as const; + default: + return undefined; + } + }; + + if (loading) { + return ( + + + + Loading subscription plans... + + + ); + } + + if (error) { + return ( + + + {error} + + + + ); + } + + return ( + + + + Choose Your Plan + + + Select the perfect plan for your AI content creation needs + + + {/* Billing Toggle */} + setYearlyBilling(e.target.checked)} + color="primary" + /> + } + label={yearlyBilling ? "Yearly Billing (Save 20%)" : "Monthly Billing"} + sx={{ mb: 2 }} + /> + + + + {plans.map((plan) => ( + + + {/* Plan Badge */} + {plan.tier === 'pro' && ( + + )} + + + + {getPlanIcon(plan.tier)} + + + + {plan.name} + + + + {plan.description} + + + {/* Pricing */} + + + ${yearlyBilling ? plan.price_yearly : plan.price_monthly} + + + /{yearlyBilling ? 'year' : 'month'} + + {yearlyBilling && ( + + Save ${(plan.price_monthly * 12 - plan.price_yearly).toFixed(0)} yearly + + )} + + + {/* Features */} + + {/* Platform Access - Free & Basic */} + {(plan.tier === 'free' || plan.tier === 'basic') && ( + <> + + + Platform Access: + + + + + + + + + + openKnowMoreModal('Blog Writer', ( + + Blog Writer + + Create engaging blog posts with AI assistance. Includes SEO optimization, + keyword research, and content structure suggestions. + + + Features: + + • SEO-optimized content generation + • Keyword research integration + • Content structure suggestions + • Publishing assistance + + ))} + > + + + + + + + + + + + + + + openKnowMoreModal('LinkedIn Writer', ( + + LinkedIn Writer + + Create professional LinkedIn posts, articles, and carousels that engage + your network and showcase your expertise. + + + Features: + + • Professional post generation + • Article writing assistance + • Carousel creation + • Network engagement optimization + + ))} + > + + + + + + + + + + + + + + openKnowMoreModal('Facebook Writer', ( + + Facebook Writer + + Create engaging Facebook posts, stories, and reels that drive + engagement and grow your community. + + + Features: + + • Post and story creation + • Reel script generation + • Community management + • Engagement optimization + + ))} + > + + + + + + + )} + + {/* Platform Integrations - Pro & Free */} + {(plan.tier === 'free' || plan.tier === 'pro' || plan.tier === 'enterprise') && ( + <> + + + Platform Integrations: + + + + + + + + + + openKnowMoreModal('Wix Integration', ( + + Wix Integration + + Seamlessly publish your content directly to Wix websites. + No manual copying required. + + + Features: + + • Direct blog post publishing + • SEO metadata sync + • Image optimization + • Publishing queue management + + ))} + > + + + + + + + + + + + + + + openKnowMoreModal('WordPress Integration', ( + + WordPress Integration + + Connect directly to WordPress sites for seamless content publishing. + + + Features: + + • REST API integration + • Draft and publish modes + • Category and tag management + • Featured image handling + + ))} + > + + + + + + + + + + + + + + openKnowMoreModal('Google Search Console', ( + + Google Search Console + + Monitor your website's SEO performance and get actionable insights + for content optimization. + + + Features: + + • Search performance tracking + • Keyword ranking insights + • Technical SEO monitoring + • Content optimization suggestions + + ))} + > + + + + + + + )} + + {/* Social Media & Website Management - Pro & Enterprise */} + {(plan.tier === 'pro' || plan.tier === 'enterprise') && ( + <> + + + Social Media & Website Management: + + + + + + + + + + openKnowMoreModal('6 Major Social Platforms', ( + + 6 Major Social Platforms + + Comprehensive social media management across all major platforms + with AI-powered content optimization. + + + Platforms: + + • LinkedIn (Professional networking) + • Facebook (Community building) + • Instagram (Visual storytelling) + • Twitter (Real-time engagement) + • TikTok (Short-form video) + • YouTube (Long-form video content) + + ))} + > + + + + + + + + + + + + + + )} + + {/* AI Content Creation Capabilities */} + + + AI Content Creation: + + + + + + + + + + openKnowMoreModal('Text Generation', ( + + AI Text Generation + + Generate high-quality text content with AI assistance. From blog posts + to social media updates, create engaging content effortlessly. + + + Capabilities: + + • Blog posts and articles + • Social media content + • Email newsletters + • Marketing copy + {plan.tier === 'pro' || plan.tier === 'enterprise' && ( + <> + • Audio transcription + • Video script writing + + )} + + ))} + > + + + + + + + + + + + + + + openKnowMoreModal('Image Generation', ( + + AI Image Generation + + Create stunning visuals with AI-powered image generation. + Perfect for social media, blog posts, and marketing materials. + + + Capabilities: + + • Social media graphics + • Blog featured images + • Marketing visuals + • Custom illustrations + {plan.tier === 'pro' || plan.tier === 'enterprise' && ( + <> + • Video thumbnail generation + • Animated graphics + + )} + + ))} + > + + + + + + + {/* Audio/Video for Pro & Enterprise */} + {(plan.tier === 'pro' || plan.tier === 'enterprise') && ( + <> + + + + + + + + + + + + + + + )} + + {/* Advanced Features for Higher Tiers */} + {plan.tier !== 'free' && ( + <> + + + Support & Analytics: + + + + + + + + + + {plan.tier === 'pro' && ( + + + + + + + )} + + {plan.tier === 'enterprise' && ( + <> + + + + + + + + + + + + + + )} + + )} + + {/* API Limits */} + + + Monthly Limits: + + + + + + + + + + + + + + + + + + {/* For alpha testing: Only Free and Basic are selectable, Pro/Enterprise disabled */} + {plan.tier === 'pro' ? ( + + ) : plan.tier === 'enterprise' ? ( + + ) : ( + <> + + + {selectedPlan === plan.id && ( + + )} + + )} + + + + ))} + + + + + All plans include our core AI content creation features. +
+ Need a custom plan? +
+
+ + {/* Payment Modal */} + setPaymentModalOpen(false)} + closeAfterTransition + BackdropComponent={Backdrop} + BackdropProps={{ + timeout: 500, + }} + > + + + + Alpha Testing Subscription + + + Thank you for participating in our alpha testing! For the Basic plan, we're crediting $29 to your account. + + + In production, this would integrate with Stripe/Paddle for real payment processing. + + + + + + + + + + {/* Know More Modal */} + setKnowMoreModal({ open: false, title: '', content: null })} + maxWidth="md" + fullWidth + > + {knowMoreModal.title} + + {knowMoreModal.content} + + + + + +
+ ); +}; + +export default PricingPage; diff --git a/frontend/src/components/SubscriptionGuard.tsx b/frontend/src/components/SubscriptionGuard.tsx new file mode 100644 index 00000000..50ad4c66 --- /dev/null +++ b/frontend/src/components/SubscriptionGuard.tsx @@ -0,0 +1,166 @@ +import React, { ReactNode } from 'react'; +import { + Box, + Typography, + Button, + Alert, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Card, + CardContent, + CardActions, + Chip, + LinearProgress +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { useSubscriptionGuard, SubscriptionGuardOptions } from '../hooks/useSubscriptionGuard'; +import { Lock as LockIcon, Upgrade as UpgradeIcon } from '@mui/icons-material'; + +interface SubscriptionGuardProps extends SubscriptionGuardOptions { + children: ReactNode; + feature?: string; + fallbackMessage?: string; + showUpgradeButton?: boolean; + showUsageProgress?: boolean; +} + +export const SubscriptionGuard: React.FC = ({ + children, + feature, + fallbackMessage, + showUpgradeButton = true, + showUsageProgress = false, + ...guardOptions +}) => { + const navigate = useNavigate(); + const { + subscription, + loading, + isGuarded, + checkFeatureAccess, + getRemainingUsage, + checkSubscription + } = useSubscriptionGuard(guardOptions); + + if (loading) { + return ( + + + + Checking subscription... + + + ); + } + + if (isGuarded) { + if (fallbackMessage) { + return ( + + {fallbackMessage} + + ); + } + + return ( + + + + + + Feature Locked + + + This feature requires an active subscription. + + + + {subscription && ( + + + Current Plan: + + {subscription.reason && ( + + {subscription.reason} + + )} + + )} + + + {showUpgradeButton && ( + + + + )} + + ); + } + + if (feature && !checkFeatureAccess(feature)) { + const remaining = getRemainingUsage(feature); + + return ( + + + {fallbackMessage || `You've reached your limit for ${feature}. Upgrade to continue using this feature.`} + + {showUpgradeButton && ( + + )} + + ); + } + + return <>{children}; +}; + +// Convenience component for protecting entire sections +export const ProtectedSection: React.FC<{ + children: ReactNode; + feature?: string; + title?: string; +}> = ({ children, feature, title }) => { + return ( + + + {title && ( + + {title} + + )} + {children} + + + ); +}; + +// Hook for checking if user can perform an action +export const useCanPerformAction = (action: string) => { + const { subscription, isFeatureAvailable } = useSubscriptionGuard(); + + return { + canPerform: subscription?.active && isFeatureAvailable(action), + subscription, + }; +}; diff --git a/frontend/src/contexts/SubscriptionContext.tsx b/frontend/src/contexts/SubscriptionContext.tsx new file mode 100644 index 00000000..5902a254 --- /dev/null +++ b/frontend/src/contexts/SubscriptionContext.tsx @@ -0,0 +1,131 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { apiClient } from '../api/client'; + +export interface SubscriptionLimits { + gemini_calls: number; + openai_calls: number; + anthropic_calls: number; + mistral_calls: number; + tavily_calls: number; + serper_calls: number; + metaphor_calls: number; + firecrawl_calls: number; + stability_calls: number; + monthly_cost: number; +} + +export interface SubscriptionStatus { + active: boolean; + plan: string; + tier: string; + can_use_api: boolean; + reason?: string; + limits: SubscriptionLimits; +} + +interface SubscriptionContextType { + subscription: SubscriptionStatus | null; + loading: boolean; + error: string | null; + checkSubscription: () => Promise; + refreshSubscription: () => Promise; +} + +const SubscriptionContext = createContext(undefined); + +export const useSubscription = () => { + const context = useContext(SubscriptionContext); + if (!context) { + throw new Error('useSubscription must be used within a SubscriptionProvider'); + } + return context; +}; + +interface SubscriptionProviderProps { + children: ReactNode; +} + +export const SubscriptionProvider: React.FC = ({ children }) => { + const [subscription, setSubscription] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const checkSubscription = async () => { + setLoading(true); + setError(null); + + try { + // Get user ID from localStorage or auth context + const userId = localStorage.getItem('user_id') || 'anonymous'; + + const response = await apiClient.get(`/api/subscription/status/${userId}`); + const subscriptionData = response.data.data; + + setSubscription(subscriptionData); + } catch (err) { + console.error('Error checking subscription:', err); + setError(err instanceof Error ? err.message : 'Failed to check subscription'); + + // Default to free tier on error + setSubscription({ + active: true, + plan: 'free', + tier: 'free', + can_use_api: true, + limits: { + gemini_calls: 100, + openai_calls: 100, + anthropic_calls: 100, + mistral_calls: 100, + tavily_calls: 50, + serper_calls: 50, + metaphor_calls: 50, + firecrawl_calls: 50, + stability_calls: 20, + monthly_cost: 5.0 + } + }); + } finally { + setLoading(false); + } + }; + + const refreshSubscription = async () => { + await checkSubscription(); + }; + + useEffect(() => { + // Check subscription on mount + checkSubscription(); + + // Set up periodic refresh (every 5 minutes) + const interval = setInterval(checkSubscription, 5 * 60 * 1000); + + // Listen for subscription updates + const handleSubscriptionUpdate = () => { + console.log('Subscription updated, refreshing...'); + checkSubscription(); + }; + + window.addEventListener('subscription-updated', handleSubscriptionUpdate); + + return () => { + clearInterval(interval); + window.removeEventListener('subscription-updated', handleSubscriptionUpdate); + }; + }, []); + + const value: SubscriptionContextType = { + subscription, + loading, + error, + checkSubscription, + refreshSubscription, + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/hooks/useSubscriptionGuard.ts b/frontend/src/hooks/useSubscriptionGuard.ts new file mode 100644 index 00000000..8d4c21c4 --- /dev/null +++ b/frontend/src/hooks/useSubscriptionGuard.ts @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { useSubscription } from '../contexts/SubscriptionContext'; + +export interface SubscriptionGuardOptions { + requireActive?: boolean; + redirectToPricing?: boolean; + showModal?: boolean; + fallbackComponent?: React.ReactNode; +} + +export const useSubscriptionGuard = (options: SubscriptionGuardOptions = {}) => { + const { subscription, loading, error, checkSubscription } = useSubscription(); + const [isGuarded, setIsGuarded] = useState(false); + + const { + requireActive = true, + redirectToPricing = true, + showModal = true, + fallbackComponent + } = options; + + useEffect(() => { + if (loading || !subscription) return; + + if (requireActive && !subscription.active) { + setIsGuarded(true); + + if (redirectToPricing) { + // Redirect to pricing page or show upgrade modal + console.warn('Subscription not active, redirecting to pricing'); + // For now, just log - in a real app you'd redirect or show modal + } + + if (showModal && !fallbackComponent) { + // Show upgrade modal + console.warn('Showing subscription upgrade modal'); + } + } else { + setIsGuarded(false); + } + }, [subscription, loading, requireActive, redirectToPricing, showModal, fallbackComponent]); + + const checkFeatureAccess = (feature: string, currentUsage?: number, limit?: number): boolean => { + if (!subscription?.active) return false; + + if (limit === undefined) { + // If no limit specified, assume unlimited or check other conditions + return true; + } + + if (currentUsage === undefined) { + // Can't check usage if we don't have current usage data + return true; // Allow for now, middleware will enforce + } + + return currentUsage < limit; + }; + + const getRemainingUsage = (feature: string): number => { + if (!subscription?.active) return 0; + + // This would typically come from usage tracking + // For now, return the limit as remaining usage + switch (feature) { + case 'gemini_calls': + return subscription.limits.gemini_calls; + case 'openai_calls': + return subscription.limits.openai_calls; + case 'anthropic_calls': + return subscription.limits.anthropic_calls; + case 'mistral_calls': + return subscription.limits.mistral_calls; + case 'tavily_calls': + return subscription.limits.tavily_calls; + case 'serper_calls': + return subscription.limits.serper_calls; + case 'metaphor_calls': + return subscription.limits.metaphor_calls; + case 'firecrawl_calls': + return subscription.limits.firecrawl_calls; + case 'stability_calls': + return subscription.limits.stability_calls; + case 'monthly_cost': + return subscription.limits.monthly_cost; + default: + return 0; + } + }; + + return { + subscription, + loading, + error, + isGuarded, + checkSubscription, + checkFeatureAccess, + getRemainingUsage, + canUseFeature: (feature: string) => checkFeatureAccess(feature), + isFeatureAvailable: (feature: string) => subscription?.active && checkFeatureAccess(feature), + }; +};