import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; import { useAuth } from "@/contexts/AuthContext"; import { useProfile } from "@/contexts/ProfileProvider"; import { useClientTiers } from "@/contexts/ClientTiersProvider"; import { PageMeta } from "@/contexts/MetadataContext"; import { ArrowLeft, BookOpen, CalendarDays, Check, House, Loader2, ShieldCheck, Tag, Zap, LockIcon, } from "lucide-react"; import { useDateFormat } from "@/hooks/useDateFormat"; import api from "@/utils/api.util"; function formatDuration(days, unit) { if (!days) return "Lifetime"; const UNIT_TO_DAYS = { minute: 1 / 1440, hour: 1 / 24, day: 1, month: 30, year: 365 }; const multiplier = UNIT_TO_DAYS[unit] ?? 1; const value = Math.round((days / multiplier) * 1000) / 1000; const label = unit ?? "day"; return `${value} ${label}${value !== 1 ? "s" : ""}`; } function formatCourseDuration(seconds = 0) { if (!seconds) return null; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h && m) return `${h}h ${m}m`; if (h) return `${h}h`; return `${m}m`; } const TIER_STYLES = { free: { badge: "bg-gradient-to-r from-lime-400 to-lime-600 text-white", icon: Tag, label: "Free" }, premium: { badge: "bg-gradient-to-r from-fuchsia-600 to-purple-600 text-white", icon: Zap, label: "Premium" }, exclusive: { badge: "bg-gradient-to-r from-rose-500 to-red-600 text-white", icon: LockIcon, label: "Exclusive" }, }; const CheckoutSkeleton = () => (
); const Checkout = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { fmtCurrency } = useDateFormat(); const planId = searchParams.get("plan_id"); const returnToken = searchParams.get("token"); const wasCancelled = searchParams.get("cancelled") === "true"; const { user } = useAuth(); const { givenName, lastName: lastNameFromProfile, getProfile, profile } = useProfile(); const { plans, plansLoading, myTier, tierLoading, checkoutLoading, promoLoading, getPlans, getMyTier, validatePromo, createOrder, captureOrder, cancelOrder, } = useClientTiers(); const [promoCode, setPromoCode] = useState(""); const [promoResult, setPromoResult] = useState(null); // { valid, code, type, value, discount } const [capturing, setCapturing] = useState(false); const capturingRef = useRef(false); useEffect(() => { getProfile(); }, []); useEffect(() => { getMyTier(); if (!plans.length) getPlans(); }, [getMyTier, getPlans, plans.length]); const plan = useMemo( () => plans.find((p) => String(p.plan_id) === String(planId)) ?? null, [plans, planId] ); // Handle PayPal return after approval useEffect(() => { if (!returnToken || capturingRef.current) return; capturingRef.current = true; setCapturing(true); captureOrder(returnToken).then((result) => { if (result) { navigate("/plans", { replace: true }); } else { setCapturing(false); capturingRef.current = false; } }); }, [returnToken, captureOrder, navigate]); // Handle PayPal return after cancellation useEffect(() => { if (!wasCancelled) return; const orderId = searchParams.get("token"); if (orderId) cancelOrder(orderId); toast("PayPal checkout was cancelled."); navigate(`/plans/checkout?plan_id=${planId}`, { replace: true }); }, [wasCancelled]); // eslint-disable-line react-hooks/exhaustive-deps // const personalInfo = user?.personal_info ?? {}; const firstName = givenName || user?.personal_info?.name?.given_name || ""; const lastName = lastNameFromProfile || user?.personal_info?.name?.last_name || ""; const email = user?.email ?? ""; const isLoading = plansLoading || tierLoading; // A user can hold more than one active tier concurrently — check membership // in the full active set, not just the best one. const isCurrent = !!plan && (myTier?.active_tiers ?? []).some((t) => t.tier === plan.tier); const style = TIER_STYLES[plan?.tier] ?? TIER_STYLES.free; const Icon = style.icon; const effectivePrice = plan ? Number(plan.price) : 0; const effectiveCurrency = plan?.currency ?? 'USD'; const subtotal = effectivePrice; const discount = promoResult?.discount ?? 0; const total = Math.max(subtotal - discount, 0); const duration = formatDuration(plan?.duration_days, plan?.duration_unit); const breadcrumbItems = [ { label: "Home", icon: , to: "/" }, { label: "Plans", to: "/plans" }, { label: "Checkout" }, ]; const handleApplyPromo = async () => { if (!plan) return; const result = await validatePromo(plan.plan_id, promoCode.trim().toUpperCase()); if (result?.valid) { setPromoResult(result); toast("Promo code applied."); } else { toast(result?.reason ?? "Invalid promo code."); } }; const handleRemovePromo = () => { setPromoResult(null); setPromoCode(""); }; const handlePayPal = async () => { const order = await createOrder( plan.plan_id, promoResult?.code ?? null, ); if (!order) return; const approvalUrl = order.approval_url; if (!approvalUrl) { toast("Could not get PayPal approval URL."); return; } window.location.href = approvalUrl; }; if (capturing) { return (

Confirming your payment…

); } if (isLoading) return ; if (!planId || !plan) { return (

No plan selected

Select a subscription plan before continuing to checkout.

); } if (!plan.is_active) { return (

Plan Not Available

The {plan.label} plan is not available for purchase at the moment.

); } return (
Review Plan Confirm the subscription you are about to purchase.
{style.label} {duration}

{plan.label}

{plan.course_count ?? plan.courses?.length ?? 0} course {(plan.course_count ?? plan.courses?.length ?? 0) === 1 ? "" : "s"} included

{fmtCurrency(effectivePrice, effectiveCurrency)}

Bundle

{plan.courses?.length > 0 ? (
{plan.courses.map((course) => (

{course.title}

{course.level && ( {course.level} )} {formatCourseDuration(course.duration_seconds) && ( {formatCourseDuration(course.duration_seconds)} )}
))}
) : (

Access to free course content.

)}
Billing Information This uses the profile details on your account.
Price Breakdown Payment will be processed through PayPal.
Plan Price {fmtCurrency(subtotal, effectiveCurrency)}
{promoResult?.valid && (
Promo ({promoResult.code}) -{fmtCurrency(promoResult.discount, effectiveCurrency)}
)}
{promoResult?.valid ? (
{promoResult.code}
) : (
setPromoCode(e.target.value)} disabled={promoLoading || checkoutLoading} onKeyDown={(e) => e.key === "Enter" && promoCode.trim() && handleApplyPromo()} />
)}
Total {fmtCurrency(total, effectiveCurrency)}
{isCurrent && (

You already have an active {plan.tier} plan — this purchase will extend it by {duration}, instead of starting a new one.

)}

Secure payment powered by PayPal

Access starts after successful payment capture.

); }; export default Checkout;