trptk/app/checkout/return/page.tsx
2026-02-24 17:14:07 +01:00

248 lines
8.8 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import { useCart } from "@/components/cart/CartContext";
import type { MedusaOrder } from "@/lib/medusa";
function formatPrice(amount: number, currencyCode: string) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currencyCode,
minimumFractionDigits: 2,
}).format(amount);
}
interface DownloadItem {
product_title: string;
variant_title: string;
sku: string;
download_url: string;
}
interface UpcomingItem {
product_title: string;
variant_title: string;
sku: string;
release_date: string;
}
export default function CheckoutReturnPage() {
const { cart, resetCart } = useCart();
const [order, setOrder] = useState<MedusaOrder | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
const [upcoming, setUpcoming] = useState<UpcomingItem[]>([]);
const attempted = useRef(false);
useEffect(() => {
if (attempted.current) return;
if (!cart?.id) {
// Cart context still loading — wait
return;
}
attempted.current = true;
async function completeOrder() {
try {
const res = await fetch("/api/checkout/complete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cartId: cart!.id }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? "Failed to complete order");
}
const result = await res.json();
if (result.type === "order" && result.order) {
setOrder(result.order);
resetCart();
// Fetch download links for digital items
try {
const dlRes = await fetch(
`/api/checkout/downloads?orderId=${result.order.id}`,
);
if (dlRes.ok) {
const dlData = await dlRes.json();
if (dlData.downloads?.length) setDownloads(dlData.downloads);
if (dlData.upcoming?.length) setUpcoming(dlData.upcoming);
}
} catch {
// Download links are non-critical — don't block the page
}
} else {
throw new Error("Payment was not completed. Please try again.");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setLoading(false);
}
}
completeOrder();
}, [cart?.id, resetCart]);
if (loading) {
return (
<main className="flex min-h-[60vh] items-center justify-center">
<p className="text-lightsec dark:text-darksec">Completing your order</p>
</main>
);
}
if (error) {
return (
<main className="mx-auto max-w-lg px-6 py-16 text-center">
<h1 className="font-argesta text-3xl">Something went wrong</h1>
<p className="mt-4 text-lightsec dark:text-darksec">{error}</p>
<div className="mt-8 flex justify-center gap-4">
<Link
href="/checkout"
className="rounded-xl border border-lightline px-6 py-3 text-sm transition-all duration-200 hover:border-lightline-hover hover:text-trptkblue dark:border-darkline dark:hover:border-darkline-hover dark:hover:text-white"
>
Try Again
</Link>
<Link
href="/"
className="rounded-xl bg-trptkblue px-6 py-3 text-sm font-silkasb text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
>
Go Home
</Link>
</div>
</main>
);
}
if (!order) return null;
const currencyCode = order.currency_code ?? "eur";
return (
<main className="mx-auto max-w-lg px-6 py-16">
<div className="text-center">
<h1 className="font-argesta text-3xl">Thank you!</h1>
<p className="mt-2 text-lightsec dark:text-darksec">
Your order has been confirmed.
</p>
</div>
<div className="mt-10 rounded-2xl border border-lightline p-6 dark:border-darkline">
<div className="flex items-center justify-between">
<span className="text-sm text-lightsec dark:text-darksec">Order number</span>
<span className="font-silkasb">#{order.display_id}</span>
</div>
{order.email && (
<div className="mt-2 flex items-center justify-between">
<span className="text-sm text-lightsec dark:text-darksec">Confirmation sent to</span>
<span className="text-sm">{order.email}</span>
</div>
)}
<div className="mt-6 space-y-2 border-t border-lightline pt-4 text-sm dark:border-darkline">
<div className="flex justify-between">
<span className="text-lightsec dark:text-darksec">Subtotal</span>
<span>{formatPrice(order.subtotal, currencyCode)}</span>
</div>
{order.shipping_total > 0 && (
<div className="flex justify-between">
<span className="text-lightsec dark:text-darksec">Shipping</span>
<span>{formatPrice(order.shipping_total, currencyCode)}</span>
</div>
)}
{order.tax_total > 0 && (
<div className="flex justify-between">
<span className="text-lightsec dark:text-darksec">Tax</span>
<span>{formatPrice(order.tax_total, currencyCode)}</span>
</div>
)}
<div className="flex justify-between border-t border-lightline pt-2 font-silkasb dark:border-darkline">
<span>Total</span>
<span>{formatPrice(order.total, currencyCode)}</span>
</div>
</div>
</div>
{/* Download links for digital purchases */}
{downloads.length > 0 && (
<div className="mt-6 rounded-2xl border border-lightline p-6 dark:border-darkline">
<h2 className="font-silkasb text-sm uppercase tracking-wider">
Your Downloads
</h2>
<div className="mt-4 space-y-3">
{downloads.map((dl) => (
<div key={dl.sku} className="flex items-center justify-between gap-4">
<div className="min-w-0">
<p className="truncate text-sm font-silkasb">
{dl.product_title}
</p>
<p className="truncate text-xs text-lightsec dark:text-darksec">
{dl.variant_title}
</p>
</div>
<a
href={dl.download_url}
className="shrink-0 rounded-lg bg-trptkblue px-4 py-2 text-xs font-silkasb text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
>
Download
</a>
</div>
))}
</div>
<p className="mt-4 text-xs text-lightsec dark:text-darksec">
Download links expire after 5 minutes. A copy has been sent to your email.
</p>
</div>
)}
{/* Pre-order items not yet available */}
{upcoming.length > 0 && (
<div className="mt-6 rounded-2xl border border-lightline p-6 dark:border-darkline">
<h2 className="font-silkasb text-sm uppercase tracking-wider">
Upcoming Releases
</h2>
<div className="mt-4 space-y-3">
{upcoming.map((item) => (
<div key={item.sku} className="flex items-center justify-between gap-4">
<div className="min-w-0">
<p className="truncate text-sm font-silkasb">
{item.product_title}
</p>
<p className="truncate text-xs text-lightsec dark:text-darksec">
{item.variant_title}
</p>
</div>
<span className="shrink-0 text-xs text-lightsec dark:text-darksec">
Available {new Date(item.release_date).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
})}
</span>
</div>
))}
</div>
<p className="mt-4 text-xs text-lightsec dark:text-darksec">
You&apos;ll receive download links by email when these releases become available.
</p>
</div>
)}
<div className="mt-8 text-center">
<Link
href="/releases"
className="inline-block rounded-xl bg-trptkblue px-6 py-3 font-silkasb text-sm text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
>
Continue Shopping
</Link>
</div>
</main>
);
}