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
3 changed files with 81 additions and 101 deletions

View File

@@ -8,7 +8,6 @@ IMPORTANT: This is a compatibility layer. For new code, use UserAPIKeyContext di
""" """
import os import os
import time
from fastapi import Request from fastapi import Request
from loguru import logger from loguru import logger
from typing import Callable from typing import Callable
@@ -21,62 +20,9 @@ class APIKeyInjectionMiddleware:
for the duration of each request. for the duration of each request.
""" """
# Shared across middleware instances (module currently instantiates per request)
_missing_keys_log_timestamps = {}
def __init__(self): def __init__(self):
self.original_keys = {} self.original_keys = {}
@staticmethod
def _should_skip_missing_key_warning(request: Request) -> bool:
"""
Optionally suppress missing-key warnings for non-AI/internal routes.
Controlled by API_KEY_INJECTION_SKIP_NON_AI_WARNINGS (default: true).
"""
skip_non_ai_warnings = os.getenv('API_KEY_INJECTION_SKIP_NON_AI_WARNINGS', 'true').lower() in ('1', 'true', 'yes')
if not skip_non_ai_warnings:
return False
path_lower = (request.url.path or '').lower()
return (
path_lower.startswith('/api/subscription/')
or path_lower.startswith('/api/onboarding/')
or path_lower.endswith('/status')
or path_lower.endswith('/health')
or path_lower == '/health'
or path_lower == '/status'
)
def _log_missing_keys_non_blocking(self, request: Request, user_id: str) -> None:
"""
Log missing API keys without interrupting request flow.
- Defaults to debug-level logging.
- Optional warn once-per-user-per-interval via env:
API_KEY_INJECTION_MISSING_KEYS_LOG_MODE=warn_once
API_KEY_INJECTION_MISSING_KEYS_LOG_INTERVAL_SECONDS=900
"""
try:
if self._should_skip_missing_key_warning(request):
logger.debug(f"[API Key Injection] Missing keys for user {user_id} on non-AI route; skipping warning")
return
log_mode = os.getenv('API_KEY_INJECTION_MISSING_KEYS_LOG_MODE', 'debug').lower()
if log_mode != 'warn_once':
logger.debug(f"No API keys found for user {user_id}")
return
interval_seconds = int(os.getenv('API_KEY_INJECTION_MISSING_KEYS_LOG_INTERVAL_SECONDS', '900'))
now = time.time()
last_logged_at = self._missing_keys_log_timestamps.get(user_id, 0)
if (now - last_logged_at) >= max(interval_seconds, 1):
logger.warning(f"No API keys found for user {user_id}")
self._missing_keys_log_timestamps[user_id] = now
else:
logger.debug(f"No API keys found for user {user_id} (warning suppressed by interval)")
except Exception as log_error:
# Logging should never block request processing
logger.debug(f"[API Key Injection] Failed to log missing keys state for user {user_id}: {log_error}")
async def __call__(self, request: Request, call_next: Callable): async def __call__(self, request: Request, call_next: Callable):
""" """
Inject user-specific API keys before processing request, Inject user-specific API keys before processing request,
@@ -122,7 +68,7 @@ class APIKeyInjectionMiddleware:
# Get user-specific API keys from database # Get user-specific API keys from database
with user_api_keys(user_id) as user_keys: with user_api_keys(user_id) as user_keys:
if not user_keys: if not user_keys:
self._log_missing_keys_non_blocking(request, user_id) logger.warning(f"No API keys found for user {user_id}")
return await call_next(request) return await call_next(request)
# Save original environment values # Save original environment values
@@ -174,3 +120,4 @@ async def api_key_injection_middleware(request: Request, call_next: Callable):
""" """
middleware = APIKeyInjectionMiddleware() middleware = APIKeyInjectionMiddleware()
return await middleware(request, call_next) return await middleware(request, call_next)

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,42 +420,48 @@ 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 }}> <>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}> <Alert severity="warning" sx={{ mb: 2 }}>
⚠️ Alpha Testing Mode - No Payment Required <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
</Typography> ⚠️ Alpha Testing Mode - No Payment Required
<Typography variant="caption" sx={{ display: 'block' }}> </Typography>
Payment integration is coming soon. For now, subscriptions are activated without charge. <Typography variant="caption" sx={{ display: 'block' }}>
</Typography> Payment integration is coming soon. For now, subscriptions are activated without charge.
</Alert> </Typography>
</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, bgcolor: 'info.lighter',
bgcolor: 'info.lighter', borderRadius: 1,
borderRadius: 1, border: '1px solid',
border: '1px solid', borderColor: 'info.light'
borderColor: 'info.light' }}>
}}> <Typography variant="body2" color="info.dark">
<Typography variant="body2" color="info.dark"> <strong>Coming in Production:</strong>
<strong>Coming in Production:</strong> </Typography>
<Typography variant="caption" color="info.dark" sx={{ display: 'block', mt: 0.5 }}>
• Secure Stripe/PayPal payment processing<br />
• Automatic renewal management<br />
• Payment verification & receipts<br />
• Upgrade/downgrade options
</Typography>
</Box>
</>
) : (
<Typography variant="body1" sx={{ mb: 3 }}>
Please confirm to continue with your selected subscription plan.
</Typography> </Typography>
<Typography variant="caption" color="info.dark" sx={{ display: 'block', mt: 0.5 }}> )}
Secure Stripe/PayPal payment processing<br />
Automatic renewal management<br />
Payment verification & receipts<br />
Upgrade/downgrade options
</Typography>
</Box>
{/* 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;