Pricing Page and Subscription Guard
This commit is contained in:
@@ -14,8 +14,8 @@ from services.database import get_db
|
|||||||
from services.usage_tracking_service import UsageTrackingService
|
from services.usage_tracking_service import UsageTrackingService
|
||||||
from services.pricing_service import PricingService
|
from services.pricing_service import PricingService
|
||||||
from models.subscription_models import (
|
from models.subscription_models import (
|
||||||
APIProvider, SubscriptionPlan, UserSubscription, UsageSummary,
|
APIProvider, SubscriptionPlan, UserSubscription, UsageSummary,
|
||||||
APIProviderPricing, UsageAlert, SubscriptionTier
|
APIProviderPricing, UsageAlert, SubscriptionTier, BillingCycle
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/subscription", tags=["subscription"])
|
router = APIRouter(prefix="/api/subscription", tags=["subscription"])
|
||||||
@@ -209,6 +209,181 @@ async def get_user_subscription(
|
|||||||
logger.error(f"Error getting user subscription: {e}")
|
logger.error(f"Error getting user subscription: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(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")
|
@router.get("/pricing")
|
||||||
async def get_api_pricing(
|
async def get_api_pricing(
|
||||||
provider: Optional[str] = Query(None, description="API provider"),
|
provider: Optional[str] = Query(None, description="API provider"),
|
||||||
|
|||||||
@@ -97,9 +97,8 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add API monitoring middleware
|
# Add API monitoring middleware for subscription enforcement
|
||||||
# Temporarily disabled for Wix testing
|
app.middleware("http")(monitoring_middleware)
|
||||||
# app.middleware("http")(monitoring_middleware)
|
|
||||||
|
|
||||||
# Initialize modular utilities
|
# Initialize modular utilities
|
||||||
health_checker = HealthChecker()
|
health_checker = HealthChecker()
|
||||||
|
|||||||
247
backend/scripts/cleanup_alpha_plans.py
Normal file
247
backend/scripts/cleanup_alpha_plans.py
Normal file
@@ -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)
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1 B After Width: | Height: | Size: 15 KiB |
@@ -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 { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||||
import { CopilotKit } from "@copilotkit/react-core";
|
import { CopilotKit } from "@copilotkit/react-core";
|
||||||
@@ -11,6 +11,7 @@ import ContentPlanningDashboard from './components/ContentPlanningDashboard/Cont
|
|||||||
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
|
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
|
||||||
import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter';
|
import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter';
|
||||||
import BlogWriter from './components/BlogWriter/BlogWriter';
|
import BlogWriter from './components/BlogWriter/BlogWriter';
|
||||||
|
import PricingPage from './components/Pricing/PricingPage';
|
||||||
import WixTestPage from './components/WixTestPage/WixTestPage';
|
import WixTestPage from './components/WixTestPage/WixTestPage';
|
||||||
import WixCallbackPage from './components/WixCallbackPage/WixCallbackPage';
|
import WixCallbackPage from './components/WixCallbackPage/WixCallbackPage';
|
||||||
import WordPressCallbackPage from './components/WordPressCallbackPage/WordPressCallbackPage';
|
import WordPressCallbackPage from './components/WordPressCallbackPage/WordPressCallbackPage';
|
||||||
@@ -20,9 +21,11 @@ import Landing from './components/Landing/Landing';
|
|||||||
import ErrorBoundary from './components/shared/ErrorBoundary';
|
import ErrorBoundary from './components/shared/ErrorBoundary';
|
||||||
import ErrorBoundaryTest from './components/shared/ErrorBoundaryTest';
|
import ErrorBoundaryTest from './components/shared/ErrorBoundaryTest';
|
||||||
import { OnboardingProvider } from './contexts/OnboardingContext';
|
import { OnboardingProvider } from './contexts/OnboardingContext';
|
||||||
|
import { SubscriptionProvider } from './contexts/SubscriptionContext';
|
||||||
|
|
||||||
import { apiClient, setAuthTokenGetter } from './api/client';
|
import { apiClient, setAuthTokenGetter } from './api/client';
|
||||||
import { useOnboarding } from './contexts/OnboardingContext';
|
import { useOnboarding } from './contexts/OnboardingContext';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
// interface OnboardingStatus {
|
// interface OnboardingStatus {
|
||||||
// onboarding_required: boolean;
|
// onboarding_required: boolean;
|
||||||
@@ -41,13 +44,36 @@ const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Component to handle initial routing based on onboarding status
|
// Component to handle initial routing based on subscription and onboarding status
|
||||||
// Now uses OnboardingContext instead of making its own API calls
|
// Flow: Check Subscription → Check Onboarding → Route accordingly
|
||||||
const InitialRouteHandler: React.FC = () => {
|
const InitialRouteHandler: React.FC = () => {
|
||||||
const { loading, error, isOnboardingComplete } = useOnboarding();
|
const { loading, error, isOnboardingComplete } = useOnboarding();
|
||||||
|
const [checkingSubscription, setCheckingSubscription] = useState(true);
|
||||||
|
const [hasActiveSubscription, setHasActiveSubscription] = useState(false);
|
||||||
|
|
||||||
// Loading state
|
useEffect(() => {
|
||||||
if (loading) {
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
@@ -59,7 +85,7 @@ const InitialRouteHandler: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<CircularProgress size={60} />
|
<CircularProgress size={60} />
|
||||||
<Typography variant="h6" color="textSecondary">
|
<Typography variant="h6" color="textSecondary">
|
||||||
Checking onboarding status...
|
{checkingSubscription ? 'Checking subscription...' : 'Checking onboarding status...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -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 <Navigate to="/pricing" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Has subscription, check onboarding
|
||||||
if (isOnboardingComplete) {
|
if (isOnboardingComplete) {
|
||||||
console.log('InitialRouteHandler: Onboarding complete (from context), redirecting to dashboard');
|
console.log('InitialRouteHandler: Subscription active & onboarding complete, redirecting to dashboard');
|
||||||
return <Navigate to="/dashboard" replace />;
|
return <Navigate to="/dashboard" replace />;
|
||||||
} else {
|
} else {
|
||||||
console.log('InitialRouteHandler: Onboarding not complete (from context), redirecting to onboarding');
|
console.log('InitialRouteHandler: Subscription active but onboarding incomplete, redirecting to onboarding');
|
||||||
return <Navigate to="/onboarding" replace />;
|
return <Navigate to="/onboarding" replace />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -255,6 +288,7 @@ const App: React.FC = () => {
|
|||||||
<Route path="/facebook-writer" element={<ProtectedRoute><FacebookWriter /></ProtectedRoute>} />
|
<Route path="/facebook-writer" element={<ProtectedRoute><FacebookWriter /></ProtectedRoute>} />
|
||||||
<Route path="/linkedin-writer" element={<ProtectedRoute><LinkedInWriter /></ProtectedRoute>} />
|
<Route path="/linkedin-writer" element={<ProtectedRoute><LinkedInWriter /></ProtectedRoute>} />
|
||||||
<Route path="/blog-writer" element={<ProtectedRoute><BlogWriter /></ProtectedRoute>} />
|
<Route path="/blog-writer" element={<ProtectedRoute><BlogWriter /></ProtectedRoute>} />
|
||||||
|
<Route path="/pricing" element={<PricingPage />} />
|
||||||
<Route path="/wix-test" element={<WixTestPage />} />
|
<Route path="/wix-test" element={<WixTestPage />} />
|
||||||
<Route path="/wix-test-direct" element={<WixTestPage />} />
|
<Route path="/wix-test-direct" element={<WixTestPage />} />
|
||||||
<Route path="/wix/callback" element={<WixCallbackPage />} />
|
<Route path="/wix/callback" element={<WixCallbackPage />} />
|
||||||
@@ -293,9 +327,11 @@ const App: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClerkProvider publishableKey={clerkPublishableKey}>
|
<ClerkProvider publishableKey={clerkPublishableKey}>
|
||||||
<OnboardingProvider>
|
<SubscriptionProvider>
|
||||||
{renderApp()}
|
<OnboardingProvider>
|
||||||
</OnboardingProvider>
|
{renderApp()}
|
||||||
|
</OnboardingProvider>
|
||||||
|
</SubscriptionProvider>
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -570,6 +570,19 @@ const Landing: React.FC = () => {
|
|||||||
<SolopreneurDilemma />
|
<SolopreneurDilemma />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
{/* Pricing Section - Embedded in Landing */}
|
||||||
|
<Box
|
||||||
|
id="pricing"
|
||||||
|
sx={{
|
||||||
|
py: 8,
|
||||||
|
background: `linear-gradient(180deg, ${alpha(theme.palette.background.default, 0.95)} 0%, ${alpha(theme.palette.background.paper, 0.98)} 100%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
{React.createElement(lazy(() => import('../Pricing/PricingPage')))}
|
||||||
|
</Suspense>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Introducing ALwrity Section with Background - Lazy Loaded */}
|
{/* Introducing ALwrity Section with Background - Lazy Loaded */}
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<IntroducingAlwrity />
|
<IntroducingAlwrity />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import AskAlwrityIcon from '../../assets/images/AskAlwrity-min.ico';
|
import AskAlwrityIcon from '../../assets/images/AskAlwrity-min.ico';
|
||||||
|
import { SubscriptionGuard } from '../SubscriptionGuard';
|
||||||
|
|
||||||
// Shared components
|
// Shared components
|
||||||
import DashboardHeader from '../shared/DashboardHeader';
|
import DashboardHeader from '../shared/DashboardHeader';
|
||||||
@@ -299,8 +300,13 @@ const MainDashboard: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* Content Lifecycle Pillars - First Panel */}
|
{/* Subscription Guard - Protect main dashboard content */}
|
||||||
<ContentLifecyclePillars />
|
<SubscriptionGuard
|
||||||
|
fallbackMessage="Your subscription is not active. Please upgrade to access the dashboard features."
|
||||||
|
showUpgradeButton={true}
|
||||||
|
>
|
||||||
|
{/* Content Lifecycle Pillars - First Panel */}
|
||||||
|
<ContentLifecyclePillars />
|
||||||
|
|
||||||
{/* Side-by-side layout for Areas 2 and 3 */}
|
{/* Side-by-side layout for Areas 2 and 3 */}
|
||||||
<Box sx={{ display: 'flex', gap: 3, mt: 3 }}>
|
<Box sx={{ display: 'flex', gap: 3, mt: 3 }}>
|
||||||
@@ -350,6 +356,7 @@ const MainDashboard: React.FC = () => {
|
|||||||
favorites={favorites}
|
favorites={favorites}
|
||||||
onToggleFavorite={toggleFavorite}
|
onToggleFavorite={toggleFavorite}
|
||||||
/>
|
/>
|
||||||
|
</SubscriptionGuard>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
|||||||
944
frontend/src/components/Pricing/PricingPage.tsx
Normal file
944
frontend/src/components/Pricing/PricingPage.tsx
Normal file
@@ -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<SubscriptionPlan[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [yearlyBilling, setYearlyBilling] = useState(false);
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<number | null>(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 <CheckIcon color="success" />;
|
||||||
|
case 'basic':
|
||||||
|
return <StarIcon color="primary" />;
|
||||||
|
case 'pro':
|
||||||
|
return <PremiumIcon color="secondary" />;
|
||||||
|
case 'enterprise':
|
||||||
|
return <PremiumIcon sx={{ color: theme.palette.warning.main }} />;
|
||||||
|
default:
|
||||||
|
return <CheckIcon />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
|
||||||
|
<CircularProgress size={60} />
|
||||||
|
<Typography variant="h6" sx={{ mt: 2 }}>
|
||||||
|
Loading subscription plans...
|
||||||
|
</Typography>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||||
|
<Alert severity="error" sx={{ mb: 4 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
<Button variant="contained" onClick={fetchPlans}>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||||
|
<Box sx={{ textAlign: 'center', mb: 6 }}>
|
||||||
|
<Typography variant="h3" component="h1" gutterBottom>
|
||||||
|
Choose Your Plan
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
Select the perfect plan for your AI content creation needs
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Billing Toggle */}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={yearlyBilling}
|
||||||
|
onChange={(e) => setYearlyBilling(e.target.checked)}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={yearlyBilling ? "Yearly Billing (Save 20%)" : "Monthly Billing"}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<Grid item key={plan.id} xs={12} sm={6} md={3}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
border: selectedPlan === plan.id ? `2px solid ${theme.palette.primary.main}` : '1px solid #e0e0e0',
|
||||||
|
transform: selectedPlan === plan.id ? 'scale(1.02)' : 'scale(1)',
|
||||||
|
transition: 'all 0.3s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Plan Badge */}
|
||||||
|
{plan.tier === 'pro' && (
|
||||||
|
<Chip
|
||||||
|
label="Most Popular"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -8,
|
||||||
|
right: 16,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardContent sx={{ flexGrow: 1, textAlign: 'center' }}>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
{getPlanIcon(plan.tier)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h5" component="h2" gutterBottom>
|
||||||
|
{plan.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
{plan.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Pricing */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h3" component="span">
|
||||||
|
${yearlyBilling ? plan.price_yearly : plan.price_monthly}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
/{yearlyBilling ? 'year' : 'month'}
|
||||||
|
</Typography>
|
||||||
|
{yearlyBilling && (
|
||||||
|
<Typography variant="caption" color="success.main" sx={{ display: 'block' }}>
|
||||||
|
Save ${(plan.price_monthly * 12 - plan.price_yearly).toFixed(0)} yearly
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<List dense>
|
||||||
|
{/* Platform Access - Free & Basic */}
|
||||||
|
{(plan.tier === 'free' || plan.tier === 'basic') && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
Platform Access:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Edit color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Blog Writer"
|
||||||
|
secondary="AI-powered blog post creation with SEO optimization"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about Blog Writer">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Blog Writer', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>Blog Writer</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Create engaging blog posts with AI assistance. Includes SEO optimization,
|
||||||
|
keyword research, and content structure suggestions.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• SEO-optimized content generation</Typography>
|
||||||
|
<Typography variant="body2">• Keyword research integration</Typography>
|
||||||
|
<Typography variant="body2">• Content structure suggestions</Typography>
|
||||||
|
<Typography variant="body2">• Publishing assistance</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Business color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="LinkedIn Writer"
|
||||||
|
secondary="Professional LinkedIn content creation and posting"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about LinkedIn Writer">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('LinkedIn Writer', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>LinkedIn Writer</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Create professional LinkedIn posts, articles, and carousels that engage
|
||||||
|
your network and showcase your expertise.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Professional post generation</Typography>
|
||||||
|
<Typography variant="body2">• Article writing assistance</Typography>
|
||||||
|
<Typography variant="body2">• Carousel creation</Typography>
|
||||||
|
<Typography variant="body2">• Network engagement optimization</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Group color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Facebook Writer"
|
||||||
|
secondary="Engaging Facebook posts and content creation"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about Facebook Writer">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Facebook Writer', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>Facebook Writer</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Create engaging Facebook posts, stories, and reels that drive
|
||||||
|
engagement and grow your community.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Post and story creation</Typography>
|
||||||
|
<Typography variant="body2">• Reel script generation</Typography>
|
||||||
|
<Typography variant="body2">• Community management</Typography>
|
||||||
|
<Typography variant="body2">• Engagement optimization</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Platform Integrations - Pro & Free */}
|
||||||
|
{(plan.tier === 'free' || plan.tier === 'pro' || plan.tier === 'enterprise') && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
Platform Integrations:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Business color="success" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Wix Integration"
|
||||||
|
secondary="Direct publishing to Wix websites"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about Wix integration">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Wix Integration', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>Wix Integration</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Seamlessly publish your content directly to Wix websites.
|
||||||
|
No manual copying required.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Direct blog post publishing</Typography>
|
||||||
|
<Typography variant="body2">• SEO metadata sync</Typography>
|
||||||
|
<Typography variant="body2">• Image optimization</Typography>
|
||||||
|
<Typography variant="body2">• Publishing queue management</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Edit color="success" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="WordPress Integration"
|
||||||
|
secondary="Publish to WordPress sites with API integration"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about WordPress integration">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('WordPress Integration', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>WordPress Integration</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Connect directly to WordPress sites for seamless content publishing.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• REST API integration</Typography>
|
||||||
|
<Typography variant="body2">• Draft and publish modes</Typography>
|
||||||
|
<Typography variant="body2">• Category and tag management</Typography>
|
||||||
|
<Typography variant="body2">• Featured image handling</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Analytics color="success" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Google Search Console"
|
||||||
|
secondary="SEO performance tracking and insights"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about GSC integration">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Google Search Console', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>Google Search Console</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Monitor your website's SEO performance and get actionable insights
|
||||||
|
for content optimization.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Search performance tracking</Typography>
|
||||||
|
<Typography variant="body2">• Keyword ranking insights</Typography>
|
||||||
|
<Typography variant="body2">• Technical SEO monitoring</Typography>
|
||||||
|
<Typography variant="body2">• Content optimization suggestions</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Social Media & Website Management - Pro & Enterprise */}
|
||||||
|
{(plan.tier === 'pro' || plan.tier === 'enterprise') && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
Social Media & Website Management:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Group color="secondary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="6 Major Social Platforms"
|
||||||
|
secondary="LinkedIn, Facebook, Instagram, Twitter, TikTok, YouTube"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about social media platforms">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('6 Major Social Platforms', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>6 Major Social Platforms</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Comprehensive social media management across all major platforms
|
||||||
|
with AI-powered content optimization.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Platforms:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• LinkedIn (Professional networking)</Typography>
|
||||||
|
<Typography variant="body2">• Facebook (Community building)</Typography>
|
||||||
|
<Typography variant="body2">• Instagram (Visual storytelling)</Typography>
|
||||||
|
<Typography variant="body2">• Twitter (Real-time engagement)</Typography>
|
||||||
|
<Typography variant="body2">• TikTok (Short-form video)</Typography>
|
||||||
|
<Typography variant="body2">• YouTube (Long-form video content)</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Business color="secondary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Website Management"
|
||||||
|
secondary="Blogging platform with content scheduling and SEO tools"
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI Content Creation Capabilities */}
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
AI Content Creation:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Edit color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Text Generation"
|
||||||
|
secondary={plan.tier === 'free' || plan.tier === 'basic'
|
||||||
|
? "AI-powered text content creation"
|
||||||
|
: "Advanced text generation with multimodal capabilities"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about text generation">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Text Generation', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>AI Text Generation</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Generate high-quality text content with AI assistance. From blog posts
|
||||||
|
to social media updates, create engaging content effortlessly.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Capabilities:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Blog posts and articles</Typography>
|
||||||
|
<Typography variant="body2">• Social media content</Typography>
|
||||||
|
<Typography variant="body2">• Email newsletters</Typography>
|
||||||
|
<Typography variant="body2">• Marketing copy</Typography>
|
||||||
|
{plan.tier === 'pro' || plan.tier === 'enterprise' && (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2">• Audio transcription</Typography>
|
||||||
|
<Typography variant="body2">• Video script writing</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Assistant color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
|
<ListItemText
|
||||||
|
primary="Image Generation"
|
||||||
|
secondary={plan.tier === 'free' || plan.tier === 'basic'
|
||||||
|
? "AI image creation for visual content"
|
||||||
|
: "Advanced image generation with video capabilities"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Learn more about image generation">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openKnowMoreModal('Image Generation', (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>AI Image Generation</Typography>
|
||||||
|
<Typography variant="body2" paragraph>
|
||||||
|
Create stunning visuals with AI-powered image generation.
|
||||||
|
Perfect for social media, blog posts, and marketing materials.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Capabilities:</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">• Social media graphics</Typography>
|
||||||
|
<Typography variant="body2">• Blog featured images</Typography>
|
||||||
|
<Typography variant="body2">• Marketing visuals</Typography>
|
||||||
|
<Typography variant="body2">• Custom illustrations</Typography>
|
||||||
|
{plan.tier === 'pro' || plan.tier === 'enterprise' && (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2">• Video thumbnail generation</Typography>
|
||||||
|
<Typography variant="body2">• Animated graphics</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{/* Audio/Video for Pro & Enterprise */}
|
||||||
|
{(plan.tier === 'pro' || plan.tier === 'enterprise') && (
|
||||||
|
<>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Assistant color="secondary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Audio Generation"
|
||||||
|
secondary="AI-powered audio content creation and voice synthesis"
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Assistant color="secondary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Video Generation"
|
||||||
|
secondary="AI video creation with script writing and editing"
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Advanced Features for Higher Tiers */}
|
||||||
|
{plan.tier !== 'free' && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
Support & Analytics:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Support color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Priority Support" />
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{plan.tier === 'pro' && (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Analytics color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Advanced Analytics & Insights" />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plan.tier === 'enterprise' && (
|
||||||
|
<>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Business color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Custom Integrations" />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
<Support color="primary" fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Dedicated Account Manager" />
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Limits */}
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ px: 2 }}>
|
||||||
|
Monthly Limits:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${plan.limits.gemini_calls} AI content generations`}
|
||||||
|
sx={{ '& .MuiListItemText-primary': { fontSize: '0.875rem' } }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${plan.limits.openai_calls} Advanced AI calls`}
|
||||||
|
sx={{ '& .MuiListItemText-primary': { fontSize: '0.875rem' } }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={`${plan.limits.tavily_calls} Research queries`}
|
||||||
|
sx={{ '& .MuiListItemText-primary': { fontSize: '0.875rem' } }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}>
|
||||||
|
{/* For alpha testing: Only Free and Basic are selectable, Pro/Enterprise disabled */}
|
||||||
|
{plan.tier === 'pro' ? (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
disabled
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
Coming Soon
|
||||||
|
</Button>
|
||||||
|
) : plan.tier === 'enterprise' ? (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
disabled
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
Contact Sales
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={selectedPlan === plan.id ? "outlined" : "contained"}
|
||||||
|
color={getPlanColor(plan.tier)}
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
disabled={subscribing}
|
||||||
|
onClick={() => setSelectedPlan(plan.id)}
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
>
|
||||||
|
{selectedPlan === plan.id ? 'Selected' : 'Select Plan'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{selectedPlan === plan.id && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
disabled={subscribing}
|
||||||
|
onClick={() => handleSubscribe(plan.id)}
|
||||||
|
>
|
||||||
|
{subscribing ? <CircularProgress size={20} /> : `Subscribe to ${plan.name}`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 6 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
All plans include our core AI content creation features.
|
||||||
|
<br />
|
||||||
|
Need a custom plan? <Button variant="text" size="small">Contact us</Button>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Payment Modal */}
|
||||||
|
<Modal
|
||||||
|
open={paymentModalOpen}
|
||||||
|
onClose={() => setPaymentModalOpen(false)}
|
||||||
|
closeAfterTransition
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
BackdropProps={{
|
||||||
|
timeout: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fade in={paymentModalOpen}>
|
||||||
|
<Box sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '2px solid #000',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
}}>
|
||||||
|
<Typography variant="h6" component="h2" gutterBottom>
|
||||||
|
Alpha Testing Subscription
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mb: 3 }}>
|
||||||
|
Thank you for participating in our alpha testing! For the Basic plan, we're crediting $29 to your account.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
In production, this would integrate with Stripe/Paddle for real payment processing.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
|
||||||
|
<Button onClick={() => setPaymentModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handlePaymentConfirm}
|
||||||
|
disabled={subscribing}
|
||||||
|
>
|
||||||
|
{subscribing ? <CircularProgress size={20} /> : 'Confirm Subscription'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Know More Modal */}
|
||||||
|
<Dialog
|
||||||
|
open={knowMoreModal.open}
|
||||||
|
onClose={() => setKnowMoreModal({ open: false, title: '', content: null })}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>{knowMoreModal.title}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{knowMoreModal.content}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setKnowMoreModal({ open: false, title: '', content: null })}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingPage;
|
||||||
166
frontend/src/components/SubscriptionGuard.tsx
Normal file
166
frontend/src/components/SubscriptionGuard.tsx
Normal file
@@ -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<SubscriptionGuardProps> = ({
|
||||||
|
children,
|
||||||
|
feature,
|
||||||
|
fallbackMessage,
|
||||||
|
showUpgradeButton = true,
|
||||||
|
showUsageProgress = false,
|
||||||
|
...guardOptions
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
subscription,
|
||||||
|
loading,
|
||||||
|
isGuarded,
|
||||||
|
checkFeatureAccess,
|
||||||
|
getRemainingUsage,
|
||||||
|
checkSubscription
|
||||||
|
} = useSubscriptionGuard(guardOptions);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<LinearProgress />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
Checking subscription...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGuarded) {
|
||||||
|
if (fallbackMessage) {
|
||||||
|
return (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
|
{fallbackMessage}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ maxWidth: 400, mx: 'auto', mt: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ textAlign: 'center', mb: 2 }}>
|
||||||
|
<LockIcon sx={{ fontSize: 48, color: 'warning.main', mb: 1 }} />
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Feature Locked
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
This feature requires an active subscription.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{subscription && (
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
Current Plan: <Chip label={subscription.plan} size="small" />
|
||||||
|
</Typography>
|
||||||
|
{subscription.reason && (
|
||||||
|
<Typography variant="body2" color="error">
|
||||||
|
{subscription.reason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{showUpgradeButton && (
|
||||||
|
<CardActions sx={{ justifyContent: 'center', pb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<UpgradeIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/pricing');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade Plan
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature && !checkFeatureAccess(feature)) {
|
||||||
|
const remaining = getRemainingUsage(feature);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{fallbackMessage || `You've reached your limit for ${feature}. Upgrade to continue using this feature.`}
|
||||||
|
</Typography>
|
||||||
|
{showUpgradeButton && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/pricing');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience component for protecting entire sections
|
||||||
|
export const ProtectedSection: React.FC<{
|
||||||
|
children: ReactNode;
|
||||||
|
feature?: string;
|
||||||
|
title?: string;
|
||||||
|
}> = ({ children, feature, title }) => {
|
||||||
|
return (
|
||||||
|
<SubscriptionGuard feature={feature}>
|
||||||
|
<Box>
|
||||||
|
{title && (
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</SubscriptionGuard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
131
frontend/src/contexts/SubscriptionContext.tsx
Normal file
131
frontend/src/contexts/SubscriptionContext.tsx
Normal file
@@ -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<void>;
|
||||||
|
refreshSubscription: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubscriptionContext = createContext<SubscriptionContextType | undefined>(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<SubscriptionProviderProps> = ({ children }) => {
|
||||||
|
const [subscription, setSubscription] = useState<SubscriptionStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<SubscriptionContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</SubscriptionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
frontend/src/hooks/useSubscriptionGuard.ts
Normal file
101
frontend/src/hooks/useSubscriptionGuard.ts
Normal file
@@ -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),
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user