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 */}
+
+
+ );
+};
+
+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 && (
+
+ }
+ onClick={() => {
+ navigate('/pricing');
+ }}
+ >
+ Upgrade Plan
+
+
+ )}
+
+ );
+ }
+
+ 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),
+ };
+};