Compare commits

..

1 Commits

Author SHA1 Message Date
ي
4e0f176842 Add strict Stripe checkout guard via env flag 2026-03-30 07:51:45 +05:30
3 changed files with 74 additions and 81 deletions

View File

@@ -16,6 +16,10 @@ REQUIRED_STRIPE_PLAN_KEYS = {
}
def _is_truthy_env(var_name: str) -> bool:
return os.getenv(var_name, "").strip().lower() in {"1", "true", "yes", "on"}
def _detect_stripe_mode() -> str:
configured_mode = os.getenv("STRIPE_MODE", "").strip().lower()
if configured_mode in {"test", "live"}:
@@ -98,7 +102,16 @@ class StripeService:
self.db = db
self.api_key = os.getenv("STRIPE_SECRET_KEY")
self.webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
self.require_stripe_checkout = _is_truthy_env("REQUIRE_STRIPE_CHECKOUT")
if not self.api_key:
if self.require_stripe_checkout:
raise HTTPException(
status_code=500,
detail=(
"REQUIRE_STRIPE_CHECKOUT=true but STRIPE_SECRET_KEY is missing. "
"Configure STRIPE_SECRET_KEY to enable Stripe checkout."
),
)
logger.warning("STRIPE_SECRET_KEY is not set. Stripe integration will not work.")
else:
stripe.api_key = self.api_key

View File

@@ -52,27 +52,10 @@ export interface SubscriptionPlan {
}
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 requireStripeCheckout = ['1', 'true', 'yes', 'on'].includes(
(process.env.REACT_APP_REQUIRE_STRIPE_CHECKOUT || '').toLowerCase()
);
const stripePublishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY;
const navigate = useNavigate();
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [loading, setLoading] = useState(true);
@@ -97,11 +80,9 @@ const PricingPage: React.FC = () => {
try {
setLoading(true);
const response = await apiClient.get('/api/subscription/plans');
// Filter out legacy alpha-named plans and enforce tier visibility policy.
// 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') &&
activeTierPolicy.visible.includes(plan.tier)
(plan: SubscriptionPlan) => !plan.name.toLowerCase().includes('alpha')
);
setPlans(filteredPlans);
} catch (err) {
@@ -134,13 +115,10 @@ const PricingPage: React.FC = () => {
return;
}
if (!activeTierPolicy.selectable.includes(plan.tier)) {
// For alpha testing, only allow Free and Basic plans (Pro features not ready)
if (plan.tier !== 'free' && plan.tier !== 'basic') {
console.error('[PricingPage] Plan tier not available:', plan.tier);
setError(
isAlphaMode
? 'This plan is not available during alpha testing'
: 'This plan is currently not available for self-serve checkout'
);
setError('This plan is not available for alpha testing');
return;
}
@@ -199,7 +177,7 @@ const PricingPage: React.FC = () => {
const userId = localStorage.getItem('user_id') || 'anonymous';
// Check if Stripe is configured
if (process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY) {
if (stripePublishableKey) {
console.log('[PricingPage] Initiating Stripe Checkout');
const response = await apiClient.post('/api/subscription/create-checkout-session', {
@@ -213,6 +191,14 @@ const PricingPage: React.FC = () => {
window.location.href = response.data.url;
return;
}
if (requireStripeCheckout) {
throw new Error('Stripe checkout is required but checkout URL was not returned.');
}
} else if (requireStripeCheckout) {
throw new Error(
'Stripe checkout is required but REACT_APP_STRIPE_PUBLISHABLE_KEY is not configured.'
);
}
console.log('[PricingPage] Making legacy subscription API call:', {
@@ -297,7 +283,8 @@ const PricingPage: React.FC = () => {
}, 3000);
} catch (err) {
console.error('Error subscribing:', err);
setError('Failed to process subscription');
const errorMessage = err instanceof Error ? err.message : 'Failed to process subscription';
setError(errorMessage);
setSuccessSnackbar({ open: false, message: '', countdown: 0 });
} finally {
setSubscribing(false);
@@ -377,8 +364,6 @@ const PricingPage: React.FC = () => {
yearlyBilling={yearlyBilling}
selectedPlanId={selectedPlan}
subscribing={subscribing}
canSelectPlan={activeTierPolicy.selectable.includes(plan.tier)}
unavailableLabel={activeTierPolicy.unavailableLabels[plan.tier]}
onSelectPlan={setSelectedPlan}
onSubscribe={handleSubscribe}
openKnowMoreModal={openKnowMoreModal}
@@ -420,48 +405,42 @@ const PricingPage: React.FC = () => {
}}>
<Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ color: 'warning.main' }} />
{isAlphaMode ? 'Alpha Testing Subscription' : 'Confirm Subscription'}
Alpha Testing Subscription
</Typography>
{isAlphaMode ? (
<>
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
⚠️ Alpha Testing Mode - No Payment Required
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
Payment integration is coming soon. For now, subscriptions are activated without charge.
</Typography>
</Alert>
<Typography variant="body1" sx={{ mb: 2 }}>
Thank you for participating in our alpha testing! We&apos;re crediting this plan to your account.
</Typography>
<Box sx={{
p: 2,
mb: 3,
bgcolor: 'info.lighter',
borderRadius: 1,
border: '1px solid',
borderColor: 'info.light'
}}>
<Typography variant="body2" color="info.dark">
<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.
{/* Alpha Testing Notice */}
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
⚠️ Alpha Testing Mode - No Payment Required
</Typography>
)}
<Typography variant="caption" sx={{ display: 'block' }}>
Payment integration is coming soon. For now, subscriptions are activated without charge.
</Typography>
</Alert>
<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.
</Typography>
{/* TODO: Payment Integration Notice */}
<Box sx={{
p: 2,
mb: 3,
bgcolor: 'info.lighter',
borderRadius: 1,
border: '1px solid',
borderColor: 'info.light'
}}>
<Typography variant="body2" color="info.dark">
<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>
{/* Note: Current behavior allows renewal without payment verification */}
{/* This is intentional for alpha testing but will be secured in production */}

View File

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