Compare commits

..

1 Commits

Author SHA1 Message Date
ي
d1ff406d03 Feature-flag pricing tier availability for alpha/demo modes 2026-03-30 07:49:56 +05:30
4 changed files with 112 additions and 105 deletions

View File

@@ -48,9 +48,6 @@ load_dotenv(backend_dir / '.env') # backend/.env
load_dotenv(project_root / '.env') # root .env (fallback) load_dotenv(project_root / '.env') # root .env (fallback)
load_dotenv() # CWD .env (fallback) load_dotenv() # CWD .env (fallback)
# Feature flags (read early so app wiring can rely on a single source of truth)
PODCAST_ONLY_DEMO_MODE = os.getenv("PODCAST_ONLY_DEMO_MODE", "false").lower() == "true"
# Set up clean logging for end users # Set up clean logging for end users
from logging_config import setup_clean_logging from logging_config import setup_clean_logging
setup_clean_logging() setup_clean_logging()
@@ -408,25 +405,9 @@ async def analyze_urls_ai_endpoint(request: AnalyzeURLsRequest, current_user: di
"""Run AI-powered SEO analysis on selected URLs.""" """Run AI-powered SEO analysis on selected URLs."""
return await analyze_urls_ai(request, current_user) return await analyze_urls_ai(request, current_user)
# Centralized mode helpers for router wiring.
# Keep all mode decisions in this section to avoid scattered env checks.
def is_podcast_only_demo_mode() -> bool:
return PODCAST_ONLY_DEMO_MODE
def should_include_non_podcast_routers() -> bool:
return not is_podcast_only_demo_mode()
# Include Podcast Maker router (available in all modes)
from api.podcast.router import router as podcast_router
app.include_router(podcast_router)
if should_include_non_podcast_routers():
# Include platform analytics router # Include platform analytics router
from routers.platform_analytics import router as platform_analytics_router from routers.platform_analytics import router as platform_analytics_router
app.include_router(platform_analytics_router) app.include_router(platform_analytics_router)
# Include Bing Analytics Storage router to expose storage-backed endpoints # Include Bing Analytics Storage router to expose storage-backed endpoints
from routers.bing_analytics_storage import router as bing_analytics_storage_router from routers.bing_analytics_storage import router as bing_analytics_storage_router
app.include_router(bing_analytics_storage_router) app.include_router(bing_analytics_storage_router)
@@ -439,6 +420,10 @@ if should_include_non_podcast_routers():
from api.content_assets.router import router as content_assets_router from api.content_assets.router import router as content_assets_router
app.include_router(content_assets_router) app.include_router(content_assets_router)
# Include Podcast Maker router
from api.podcast.router import router as podcast_router
app.include_router(podcast_router)
# Include YouTube Creator Studio router # Include YouTube Creator Studio router
from api.youtube.router import router as youtube_router from api.youtube.router import router as youtube_router
app.include_router(youtube_router, prefix="/api") app.include_router(youtube_router, prefix="/api")
@@ -462,8 +447,6 @@ if should_include_non_podcast_routers():
# Today workflow routes # Today workflow routes
from api.today_workflow import router as today_workflow_router from api.today_workflow import router as today_workflow_router
app.include_router(today_workflow_router) app.include_router(today_workflow_router)
else:
logger.info("PODCAST_ONLY_DEMO_MODE is enabled: non-podcast routers are not registered.")
# Setup frontend serving using modular utilities # Setup frontend serving using modular utilities
frontend_serving.setup_frontend_serving() frontend_serving.setup_frontend_serving()

View File

@@ -298,11 +298,6 @@ def main():
parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)") parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)")
parser.add_argument("--production", action="store_true", help="Enable production mode (optimized for deployment)") parser.add_argument("--production", action="store_true", help="Enable production mode (optimized for deployment)")
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging for debugging") parser.add_argument("--verbose", action="store_true", help="Enable verbose logging for debugging")
parser.add_argument(
"--podcast-only-demo",
action="store_true",
help="Enable podcast-only demo mode (sets PODCAST_ONLY_DEMO_MODE=true before app startup)",
)
args = parser.parse_args() args = parser.parse_args()
# Determine mode # Determine mode
@@ -312,8 +307,6 @@ def main():
# Set global verbose flag for utilities # Set global verbose flag for utilities
os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false" os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false"
if args.podcast_only_demo:
os.environ["PODCAST_ONLY_DEMO_MODE"] = "true"
print("[*] ALwrity Backend Server") print("[*] ALwrity Backend Server")
print("=" * 40) print("=" * 40)
@@ -321,8 +314,6 @@ def main():
print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}") print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}")
if verbose_mode: if verbose_mode:
print("Verbose logging: ENABLED") print("Verbose logging: ENABLED")
if args.podcast_only_demo:
print("Podcast-only demo mode: ENABLED")
print("=" * 40) print("=" * 40)
# Check if we're in the right directory # Check if we're in the right directory

View File

@@ -52,6 +52,27 @@ export interface SubscriptionPlan {
} }
const PricingPage: React.FC = () => { const PricingPage: React.FC = () => {
const pricingMode = (process.env.REACT_APP_PRICING_MODE || 'alpha').toLowerCase();
const isAlphaMode = pricingMode === 'alpha';
const tierPolicyByMode: Record<string, { visible: string[]; selectable: string[]; unavailableLabels: Record<string, string> }> = {
alpha: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic'],
unavailableLabels: { pro: 'Coming Soon', enterprise: 'Contact Sales' },
},
demo: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic', 'pro'],
unavailableLabels: { enterprise: 'Contact Sales' },
},
production: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic', 'pro'],
unavailableLabels: { enterprise: 'Contact Sales' },
},
};
const activeTierPolicy = tierPolicyByMode[pricingMode] || tierPolicyByMode.alpha;
const navigate = useNavigate(); const navigate = useNavigate();
const [plans, setPlans] = useState<SubscriptionPlan[]>([]); const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -76,9 +97,11 @@ const PricingPage: React.FC = () => {
try { try {
setLoading(true); setLoading(true);
const response = await apiClient.get('/api/subscription/plans'); const response = await apiClient.get('/api/subscription/plans');
// Filter out any alpha plans and ensure we only show the 4 main tiers // Filter out legacy alpha-named plans and enforce tier visibility policy.
const filteredPlans = response.data.data.plans.filter( const filteredPlans = response.data.data.plans.filter(
(plan: SubscriptionPlan) => !plan.name.toLowerCase().includes('alpha') (plan: SubscriptionPlan) =>
!plan.name.toLowerCase().includes('alpha') &&
activeTierPolicy.visible.includes(plan.tier)
); );
setPlans(filteredPlans); setPlans(filteredPlans);
} catch (err) { } catch (err) {
@@ -111,10 +134,13 @@ const PricingPage: React.FC = () => {
return; return;
} }
// For alpha testing, only allow Free and Basic plans (Pro features not ready) if (!activeTierPolicy.selectable.includes(plan.tier)) {
if (plan.tier !== 'free' && plan.tier !== 'basic') {
console.error('[PricingPage] Plan tier not available:', plan.tier); console.error('[PricingPage] Plan tier not available:', plan.tier);
setError('This plan is not available for alpha testing'); setError(
isAlphaMode
? 'This plan is not available during alpha testing'
: 'This plan is currently not available for self-serve checkout'
);
return; return;
} }
@@ -351,6 +377,8 @@ const PricingPage: React.FC = () => {
yearlyBilling={yearlyBilling} yearlyBilling={yearlyBilling}
selectedPlanId={selectedPlan} selectedPlanId={selectedPlan}
subscribing={subscribing} subscribing={subscribing}
canSelectPlan={activeTierPolicy.selectable.includes(plan.tier)}
unavailableLabel={activeTierPolicy.unavailableLabels[plan.tier]}
onSelectPlan={setSelectedPlan} onSelectPlan={setSelectedPlan}
onSubscribe={handleSubscribe} onSubscribe={handleSubscribe}
openKnowMoreModal={openKnowMoreModal} openKnowMoreModal={openKnowMoreModal}
@@ -392,10 +420,11 @@ const PricingPage: React.FC = () => {
}}> }}>
<Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ color: 'warning.main' }} /> <Warning sx={{ color: 'warning.main' }} />
Alpha Testing Subscription {isAlphaMode ? 'Alpha Testing Subscription' : 'Confirm Subscription'}
</Typography> </Typography>
{/* Alpha Testing Notice */} {isAlphaMode ? (
<>
<Alert severity="warning" sx={{ mb: 2 }}> <Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}> <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
⚠️ Alpha Testing Mode - No Payment Required ⚠️ Alpha Testing Mode - No Payment Required
@@ -406,10 +435,9 @@ const PricingPage: React.FC = () => {
</Alert> </Alert>
<Typography variant="body1" sx={{ mb: 2 }}> <Typography variant="body1" sx={{ mb: 2 }}>
Thank you for participating in our alpha testing! We're crediting the Basic plan ($29 value) to your account. Thank you for participating in our alpha testing! We&apos;re crediting this plan to your account.
</Typography> </Typography>
{/* TODO: Payment Integration Notice */}
<Box sx={{ <Box sx={{
p: 2, p: 2,
mb: 3, mb: 3,
@@ -428,6 +456,12 @@ const PricingPage: React.FC = () => {
• Upgrade/downgrade options • Upgrade/downgrade options
</Typography> </Typography>
</Box> </Box>
</>
) : (
<Typography variant="body1" sx={{ mb: 3 }}>
Please confirm to continue with your selected subscription plan.
</Typography>
)}
{/* Note: Current behavior allows renewal without payment verification */} {/* Note: Current behavior allows renewal without payment verification */}
{/* This is intentional for alpha testing but will be secured in production */} {/* This is intentional for alpha testing but will be secured in production */}

View File

@@ -69,6 +69,8 @@ interface PlanCardProps {
yearlyBilling: boolean; yearlyBilling: boolean;
selectedPlanId: number | null; selectedPlanId: number | null;
subscribing: boolean; subscribing: boolean;
canSelectPlan: boolean;
unavailableLabel?: string;
onSelectPlan: (planId: number) => void; onSelectPlan: (planId: number) => void;
onSubscribe: (planId: number) => void; onSubscribe: (planId: number) => void;
openKnowMoreModal: (title: string, content: React.ReactNode) => void; openKnowMoreModal: (title: string, content: React.ReactNode) => void;
@@ -79,6 +81,8 @@ const PlanCard: React.FC<PlanCardProps> = ({
yearlyBilling, yearlyBilling,
selectedPlanId, selectedPlanId,
subscribing, subscribing,
canSelectPlan,
unavailableLabel,
onSelectPlan, onSelectPlan,
onSubscribe, onSubscribe,
openKnowMoreModal, openKnowMoreModal,
@@ -909,13 +913,9 @@ const PlanCard: React.FC<PlanCardProps> = ({
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}> <CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}>
{plan.tier === 'pro' ? ( {!canSelectPlan ? (
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}> <Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
Coming Soon {unavailableLabel || 'Unavailable'}
</Button>
) : plan.tier === 'enterprise' ? (
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
Contact Sales
</Button> </Button>
) : ( ) : (
<> <>
@@ -951,4 +951,3 @@ const PlanCard: React.FC<PlanCardProps> = ({
}; };
export default PlanCard; export default PlanCard;