"use client"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { QUOTA_REQUEST_EMAIL } from "@/lib/plans"; type ExtensionRequest = { id: string; status: string; message: string; requestedAt: string; processedAt: string | null; adminNote: string | null; }; type ExtensionUsage = { used: number; limit: number; remaining: number; requests: ExtensionRequest[]; }; type Props = { initialUsage: ExtensionUsage; }; function openQuotaRequestMailto(reason?: string) { const subject = encodeURIComponent("Songs2VID Quota reset / extension request"); const body = encodeURIComponent( [ "Hi,", "", "I'd like to request a Pro quota reset or temporary extension.", "", reason?.trim() ? `Reason:\n${reason.trim()}` : "Reason: (optional add details here)", "", "Account email: (please keep the address you use to sign in)", "", "Thanks,", ].join("\n"), ); window.location.href = `mailto:${QUOTA_REQUEST_EMAIL}?subject=${subject}&body=${body}`; } function formatEndDate(iso: string | null) { if (!iso) return null; return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric", }); } export function PlanBillingActions({ initialUsage }: Props) { const router = useRouter(); const [usage, setUsage] = useState(initialUsage); const [cancelling, setCancelling] = useState(false); const [requesting, setRequesting] = useState(false); const [openingPortal, setOpeningPortal] = useState(false); const [showCancelChoices, setShowCancelChoices] = useState(false); const [cancelAtPeriodEnd, setCancelAtPeriodEnd] = useState(false); const [endsAt, setEndsAt] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { const res = await fetch("/api/account/cancel-subscription"); if (!res.ok) return; const data = await res.json(); if (cancelled) return; setCancelAtPeriodEnd(Boolean(data.cancelAtPeriodEnd)); setEndsAt(data.endsAt ?? null); } catch { /* ignore */ } })(); return () => { cancelled = true; }; }, []); async function handleOpenBillingPortal() { setOpeningPortal(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/billing-portal", { method: "POST" }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Could not open billing portal"); if (!data.url) throw new Error("No portal URL returned"); window.location.href = data.url; } catch (err) { setError(err instanceof Error ? err.message : "Could not open billing portal"); setOpeningPortal(false); } } async function cancelWithMode(when: "immediate" | "period_end") { setCancelling(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/cancel-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ when }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to cancel subscription"); setShowCancelChoices(false); if (when === "period_end") { setCancelAtPeriodEnd(true); setEndsAt(data.endsAt ?? null); const label = formatEndDate(data.endsAt ?? null); setSuccess( label ? `Cancellation scheduled. You keep Pro until ${label}; no further charges after that.` : "Cancellation scheduled at the end of your current billing period. You keep Pro until then.", ); } else { setCancelAtPeriodEnd(false); setEndsAt(null); setSuccess("Subscription canceled immediately. You are now on the Free plan."); } router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to cancel subscription"); } finally { setCancelling(false); } } async function handleResumeSubscription() { setCancelling(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/cancel-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "resume" }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to resume subscription"); setCancelAtPeriodEnd(false); setEndsAt(data.currentPeriodEnd ?? null); setSuccess("Subscription kept. It will renew as usual."); router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to resume subscription"); } finally { setCancelling(false); } } async function handleQuotaRequest() { if (usage.remaining <= 0) return; const reason = prompt( "Optional: tell us why you need a quota reset or extension (leave blank to skip).", ); if (reason === null) return; setRequesting(true); setError(null); setSuccess(null); try { const res = await fetch("/api/account/quota-extension-request", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: reason }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to submit request"); setUsage({ used: data.used, limit: data.limit, remaining: data.remaining, requests: data.requests ?? usage.requests, }); setSuccess( `Request recorded (${data.used} of ${data.limit} this year). Opening your email client to contact support…`, ); openQuotaRequestMailto(reason); router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to submit request"); } finally { setRequesting(false); } } const hasPending = usage.requests.some((r) => r.status === "PENDING"); const endLabel = formatEndDate(endsAt); return (

Extension requests this year:{" "} {usage.used} / {usage.limit} {usage.remaining > 0 ? ( · {usage.remaining} remaining ) : ( · limit reached )}

{cancelAtPeriodEnd && (

Cancellation scheduled {endLabel ? ( <> {" "} Pro stays active until {endLabel} ) : ( <> at the end of your current billing period )} . You will not be charged again.

)} {error && (
{error}
)} {success && (
{success}
)}
{!cancelAtPeriodEnd && ( )} { if (requesting || usage.remaining <= 0 || hasPending) { e.preventDefault(); return; } e.preventDefault(); void handleQuotaRequest(); }} aria-disabled={requesting || usage.remaining <= 0 || hasPending} className={`inline-flex items-center justify-center rounded border border-accent/40 px-4 py-2 text-sm font-medium text-accent transition-colors hover:bg-accent/10 ${ requesting || usage.remaining <= 0 || hasPending ? "pointer-events-none cursor-not-allowed opacity-50" : "" }`} > {requesting ? "Submitting…" : hasPending ? "Request pending…" : "Request quota reset & extension"}
{showCancelChoices && (

When should Pro end?

This is sent to Stripe. Choose how you want to cancel your subscription.

)} {usage.requests.length > 0 && (

Request history

    {usage.requests.slice(0, 5).map((req) => (
  • {new Date(req.requestedAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", })} {req.status}
    {req.message &&

    {req.message}

    }

    ID: {req.id}

  • ))}
)}

Pro users may request up to 5 manual quota resets or extensions per calendar year by emailing{" "} {QUOTA_REQUEST_EMAIL} . See our{" "} Terms of Service .

); }