diff --git a/app/checkout/page.tsx b/app/checkout/page.tsx new file mode 100644 index 0000000..c994104 --- /dev/null +++ b/app/checkout/page.tsx @@ -0,0 +1,235 @@ +'use client' + +import { Suspense, useEffect, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Image from 'next/image' +import Link from 'next/link' +import { motion } from 'framer-motion' +import { useAuth } from '@/lib/auth/context' +import { useSubscription } from '@/lib/swr/dashboard' +import { getSubscription } from '@/lib/api/billing' +import { PLAN_PRICES, TRAFFIC_TIERS } from '@/lib/plans' +import PlanSummary from '@/components/checkout/PlanSummary' +import PaymentForm from '@/components/checkout/PaymentForm' +import pulseLogo from '@/public/pulse_logo_no_margins.png' + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- + +const VALID_PLANS = new Set(Object.keys(PLAN_PRICES)) +const VALID_INTERVALS = new Set(['month', 'year']) +const VALID_LIMITS = new Set(TRAFFIC_TIERS.map((t) => t.value)) + +function isValidCheckoutParams(plan: string | null, interval: string | null, limit: string | null) { + if (!plan || !interval || !limit) return false + const limitNum = Number(limit) + if (!VALID_PLANS.has(plan)) return false + if (!VALID_INTERVALS.has(interval)) return false + if (!VALID_LIMITS.has(limitNum)) return false + if (!PLAN_PRICES[plan]?.[limitNum]) return false + return true +} + +// --------------------------------------------------------------------------- +// Success polling component (post-3DS return) +// --------------------------------------------------------------------------- + +function CheckoutSuccess() { + const router = useRouter() + const [ready, setReady] = useState(false) + const [timedOut, setTimedOut] = useState(false) + + useEffect(() => { + let cancelled = false + const timeout = setTimeout(() => setTimedOut(true), 30000) + + const poll = async () => { + for (let i = 0; i < 15; i++) { + if (cancelled) return + try { + const data = await getSubscription() + if (data.subscription_status === 'active' || data.subscription_status === 'trialing') { + setReady(true) + clearTimeout(timeout) + setTimeout(() => router.push('/'), 2000) + return + } + } catch { + // ignore — keep polling + } + await new Promise((r) => setTimeout(r, 2000)) + } + setTimedOut(true) + } + poll() + + return () => { + cancelled = true + clearTimeout(timeout) + } + }, [router]) + + return ( +
Redirecting to dashboard...
+ > + ) : timedOut ? ( + <> ++ Your payment was received. It may take a moment to activate. +
+ + Go to dashboard + + > + ) : ( + <> + +This usually takes a few seconds.
+ > + )} +