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 }} >

Complete your purchase

-

+

You're subscribing to the {planDetails.name} plan

+ {/* Billing Cycle Toggle */} +
+
+ + SAVE 30% WITH YEARLY BILLING + +
+
+ + +
+ + {/* Price comparison */} + {billingCycle === 'yearly' && planDetails.name === 'Standard' && ( +
+

+ Save $54 per year compared to monthly billing! +

+

+ That's ${(planDetails.amount / 12).toFixed(2)}/month when billed yearly +

+
+ )} +
+ {paymentError && (
{paymentError} @@ -239,8 +391,18 @@ export default function CheckoutPage() {
- {clientSecret && ( + {paymentLoading && ( +
+
+
+ Updating plan details... +
+
+ )} + + {clientSecret && !paymentLoading && ( + + {/* Debug info - remove in production */} + {process.env.NODE_ENV === 'development' && setupIntentId && ( +
+ Debug: Setup Intent: {setupIntentId.substring(0, 20)}... +
+ Plan: {planDetails.name} {planDetails.cycle} (${planDetails.amount}) +
+ )}
)}
@@ -325,15 +496,30 @@ export default function CheckoutPage() {
- {planDetails.name} Plan + + {planDetails.name} Plan ({planDetails.cycle === 'year' ? 'Yearly' : 'Monthly'}) + ${planDetails.amount}
+ + {/* Show yearly savings */} + {billingCycle === 'yearly' && planDetails.name === 'Standard' && ( +
+ + Yearly Discount (30% off) + + + -$54.00 + +
+ )} + {discountApplied && discountInfo && (
- Discount ({discountInfo.type === 'percent' ? `${discountInfo.value}% off` : `$${discountInfo.value} off`}) + Promo Discount ({discountInfo.type === 'percent' ? `${discountInfo.value}% off` : `$${discountInfo.value} off`}) {discountInfo.type === 'percent' @@ -343,12 +529,26 @@ export default function CheckoutPage() {
)} -

- 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 +

+ )} +
+ + {(discountApplied || billingCycle === 'yearly') && (
-

โœ“ Promo code applied!

+ {discountApplied && ( +

โœ“ Promo code applied!

+ )} + {billingCycle === 'yearly' && !discountApplied && ( +

โœ“ 30% yearly discount applied!

+ )}
)}
@@ -357,19 +557,43 @@ export default function CheckoutPage() {
Total (USD) - {discountApplied && discountInfo ? ( + {(discountApplied && discountInfo) || billingCycle === 'yearly' ? (
- ${discountInfo.type === 'percent' - ? (planDetails.amount - (planDetails.amount * discountInfo.value) / 100).toFixed(2) - : Math.max(0, planDetails.amount - discountInfo.value).toFixed(2) - } + ${(() => { + let total = planDetails.amount + + // Apply promo discount if any + if (discountApplied && discountInfo) { + if (discountInfo.type === 'percent') { + total = total - (total * discountInfo.value) / 100 + } else { + total = Math.max(0, total - discountInfo.value) + } + } + + return total.toFixed(2) + })()} +
+
+ {billingCycle === 'yearly' && planDetails.name === 'Standard' && !discountApplied && ( + 'You save $54.00 with yearly billing!' + )} + {discountApplied && discountInfo && ( + `You save $${ + discountInfo.type === 'percent' + ? ((planDetails.amount * discountInfo.value) / 100).toFixed(2) + : discountInfo.value.toFixed(2) + } with promo code!` + )} + {billingCycle === 'yearly' && planDetails.name === 'Standard' && discountApplied && discountInfo && ( + `Total savings: $${(54 + ( + discountInfo.type === 'percent' + ? (planDetails.amount * discountInfo.value) / 100 + : discountInfo.value + )).toFixed(2)}!` + )}
-
You save ${ - discountInfo.type === 'percent' - ? ((planDetails.amount * discountInfo.value) / 100).toFixed(2) - : discountInfo.value.toFixed(2) - }!
) : ( `$${planDetails.amount}` diff --git a/auto-analyst-frontend/lib/credits-config.ts b/auto-analyst-frontend/lib/credits-config.ts index 8b84ce38..6d4afb55 100644 --- a/auto-analyst-frontend/lib/credits-config.ts +++ b/auto-analyst-frontend/lib/credits-config.ts @@ -49,9 +49,9 @@ export interface TrialConfig { * Trial period configuration - Change here to update across the entire app */ export const TRIAL_CONFIG: TrialConfig = { - duration: 10, + duration: 5, unit: 'minutes', - displayText: '10-Minute Trial', + displayText: '5-Minute Trial', credits: 500 }