diff --git a/auto-analyst-frontend/app/api/user/cancel-subscription/route.ts b/auto-analyst-frontend/app/api/user/cancel-subscription/route.ts index af5a5ec9..c6616b18 100644 --- a/auto-analyst-frontend/app/api/user/cancel-subscription/route.ts +++ b/auto-analyst-frontend/app/api/user/cancel-subscription/route.ts @@ -48,11 +48,11 @@ export async function POST(request: NextRequest) { // Only make Stripe API calls for new users with proper subscription IDs if (!isLegacyUser) { - // Cancel the subscription in Stripe - // Using cancel_at_period_end: true to let the user keep access until the end of their current billing period + // Cancel the subscription in Stripe + // Using cancel_at_period_end: true to let the user keep access until the end of their current billing period canceledSubscription = await stripe.subscriptions.update(stripeSubscriptionId, { - cancel_at_period_end: true, - }) + cancel_at_period_end: true, + }) console.log(`Scheduled Stripe cancellation for subscription ${stripeSubscriptionId} for user ${userId}`) } else { console.log(`Legacy user ${userId} - skipping Stripe API calls, updating Redis only`) @@ -85,10 +85,10 @@ export async function POST(request: NextRequest) { if (creditData && creditData.resetDate) { await redis.hset(KEYS.USER_CREDITS(userId), { nextTotalCredits: '0', // No credits after cancellation - pendingDowngrade: 'true', + pendingDowngrade: 'true', lastUpdate: now.toISOString() - }) - } + }) + } } return NextResponse.json({ diff --git a/auto-analyst-frontend/app/checkout/page.tsx b/auto-analyst-frontend/app/checkout/page.tsx index 3ba52096..c923a3e5 100644 --- a/auto-analyst-frontend/app/checkout/page.tsx +++ b/auto-analyst-frontend/app/checkout/page.tsx @@ -21,7 +21,7 @@ export default function CheckoutPage() { const [loadingPlan, setLoadingPlan] = useState(true) const plan = searchParams?.get('plan') - const cycle = searchParams?.get('cycle') + const initialCycle = searchParams?.get('cycle') const [planDetails, setPlanDetails] = useState({ name: '', @@ -30,6 +30,9 @@ export default function CheckoutPage() { priceId: '', }) + // Add state for billing cycle toggle + const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(initialCycle === 'yearly' ? 'yearly' : 'monthly') + const [clientSecret, setClientSecret] = useState('') const [setupIntentId, setSetupIntentId] = useState('') const [isTrialSetup, setIsTrialSetup] = useState(false) @@ -40,11 +43,102 @@ export default function CheckoutPage() { const [discountApplied, setDiscountApplied] = useState(false) const [discountInfo, setDiscountInfo] = useState<{type: string, value: number} | null>(null) + // Plan configurations with both monthly and yearly options + const pricingTiers = [ + { + name: 'Standard', + monthly: { + price: 15, + priceId: process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID, + }, + yearly: { + price: 126, // $15 * 12 months = $180, with 30% discount = $126 + priceId: process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID, + savings: 54, // $180 - $126 = $54 savings + }, + daily: { + price: 0.75, + priceId: process.env.NEXT_PUBLIC_STRIPE_DAILY_PRICE_ID, + }, + }, + { + name: 'Pro', + monthly: { + price: 29, + priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID, + }, + yearly: { + price: 244, + priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID, + }, + }, + ] + + // Function to update plan details when billing cycle changes + const updatePlanForCycle = (newCycle: 'monthly' | 'yearly') => { + if (!plan) return + + const selectedPlan = pricingTiers.find(p => p.name.toLowerCase() === plan) + if (selectedPlan) { + const billing = newCycle === 'yearly' ? 'yearly' : 'monthly' + const planData = { + name: selectedPlan.name, + amount: selectedPlan[billing]?.price || 0, + cycle: billing === 'yearly' ? 'year' : 'month', + priceId: selectedPlan[billing]?.priceId || '', + } + + setPlanDetails(planData) + + // Re-create payment intent with new plan data + createPaymentIntent(planData, promoCode) + } + } + + // Handle billing cycle change + const handleBillingCycleChange = (newCycle: 'monthly' | 'yearly') => { + if (newCycle === billingCycle || paymentLoading) return + + console.log(`๐ Billing cycle change: ${billingCycle} โ ${newCycle}`) + + setPaymentLoading(true) + setBillingCycle(newCycle) + + // Update URL to reflect the new billing cycle + const newUrl = new URL(window.location.href) + newUrl.searchParams.set('cycle', newCycle) + window.history.replaceState({}, '', newUrl.toString()) + + // Clear existing payment intents to force creation of new ones + setClientSecret('') + setSetupIntentId('') + setPaymentError('') + setPromoError('') + + console.log(`๐งน Cleared old setup intent, creating new one for ${newCycle} plan`) + + // Add a small delay to show loading state + setTimeout(() => { + updatePlanForCycle(newCycle) + setPaymentLoading(false) + }, 300) + } + // Create or recreate payment intent const createPaymentIntent = async (planData: any, promoCodeValue: string = '') => { if (!planData.priceId || !session) return setPaymentLoading(true) + + // Clear previous state to avoid stale data + setClientSecret('') + setSetupIntentId('') + setPaymentError('') + if (!promoCodeValue) { + setPromoError('') + setDiscountApplied(false) + setDiscountInfo(null) + } try { const response = await fetch('/api/checkout-sessions', { @@ -69,8 +163,10 @@ export default function CheckoutPage() { } else { setPaymentError(data.message) setClientSecret('') + setSetupIntentId('') } } else { + // Successfully created new setup intent setClientSecret(data.clientSecret) setSetupIntentId(data.setupIntentId) setIsTrialSetup(data.isTrialSetup || false) @@ -83,8 +179,11 @@ export default function CheckoutPage() { } else { setDiscountInfo(null) } + + console.log(`Created new setup intent: ${data.setupIntentId} for ${planData.name} ${planData.cycle} plan ($${planData.amount})`) } } catch (err) { + console.error('Error creating payment intent:', err) if (promoCodeValue) { setPromoError('Failed to validate promo code. Please try again.') setDiscountApplied(false) @@ -92,6 +191,7 @@ export default function CheckoutPage() { } else { setPaymentError('Failed to set up payment. Please try again.') setClientSecret('') + setSetupIntentId('') } } finally { setPaymentLoading(false) @@ -127,7 +227,7 @@ export default function CheckoutPage() { } useEffect(() => { - if (!plan || !cycle || status === 'loading') { + if (!plan || !initialCycle || status === 'loading') { return } @@ -169,7 +269,7 @@ export default function CheckoutPage() { const selectedPlan = pricingTiers.find(p => p.name.toLowerCase() === plan) if (selectedPlan) { - const billing = cycle === 'yearly' ? 'yearly' : cycle === 'daily' ? 'daily' : 'monthly' + const billing = initialCycle === 'yearly' ? 'yearly' : initialCycle === 'daily' ? 'daily' : 'monthly' const planData = { name: selectedPlan.name, amount: selectedPlan[billing]?.price || 0, @@ -187,7 +287,7 @@ export default function CheckoutPage() { } setLoadingPlan(false) - }, [plan, cycle, router, status, session]) + }, [plan, initialCycle, router, status, session]) if (status === 'loading' || loadingPlan || paymentLoading) { return ( @@ -227,10 +327,62 @@ export default function CheckoutPage() { transition={{ duration: 0.4 }} >
+
You're subscribing to the {planDetails.name} plan
+ {/* Billing Cycle Toggle */} ++ Save $54 per year compared to monthly billing! +
++ That's ${(planDetails.amount / 12).toFixed(2)}/month when billed yearly +
+- Billed {planDetails.cycle === 'year' ? 'yearly' : planDetails.cycle === 'day' ? 'daily' : 'monthly'} -
- {discountApplied && ( + ++ Billed {planDetails.cycle === 'year' ? 'yearly' : planDetails.cycle === 'day' ? 'daily' : 'monthly'} +
+ {billingCycle === 'yearly' && ( ++ Credits reset monthly, but you're billed yearly +
+ )} +โ Promo code applied!
+ {discountApplied && ( +โ Promo code applied!
+ )} + {billingCycle === 'yearly' && !discountApplied && ( +โ 30% yearly discount applied!
+ )}